Browse Source

Merge branch 'tv_quality' of https://github.com/fuzeman/CouchPotatoServer into fuzeman-tv_quality

# Conflicts:
#	couchpotato/core/media/movie/quality/main.py
#	couchpotato/core/media/movie/searcher.py
pull/4940/head
Ruud 10 years ago
parent
commit
de5fb285a1
  1. 18
      couchpotato/core/media/_base/matcher/main.py
  2. 21
      couchpotato/core/media/_base/media/main.py
  3. 40
      couchpotato/core/media/_base/providers/base.py
  4. 7
      couchpotato/core/media/_base/quality/__init__.py
  5. 185
      couchpotato/core/media/_base/quality/base.py
  6. 0
      couchpotato/core/media/_base/quality/index.py
  7. 82
      couchpotato/core/media/_base/quality/main.py
  8. 0
      couchpotato/core/media/_base/quality/static/quality.css
  9. 0
      couchpotato/core/media/_base/quality/static/quality.js
  10. 11
      couchpotato/core/media/_base/searcher/main.py
  11. 0
      couchpotato/core/media/movie/quality/__init__.py
  12. 259
      couchpotato/core/media/movie/quality/main.py
  13. 6
      couchpotato/core/media/movie/searcher.py
  14. 4
      couchpotato/core/media/show/_base/episode.py
  15. 4
      couchpotato/core/media/show/_base/main.py
  16. 4
      couchpotato/core/media/show/_base/season.py
  17. 24
      couchpotato/core/media/show/matcher/base.py
  18. 19
      couchpotato/core/media/show/providers/torrent/iptorrents.py
  19. 0
      couchpotato/core/media/show/quality/__init__.py
  20. 196
      couchpotato/core/media/show/quality/main.py
  21. 25
      couchpotato/core/media/show/searcher/episode.py
  22. 26
      couchpotato/core/media/show/searcher/season.py
  23. 3
      couchpotato/core/media/show/searcher/show.py
  24. 5
      couchpotato/core/plugins/quality/__init__.py
  25. 4
      couchpotato/core/plugins/score/scores.py
  26. 2
      couchpotato/templates/index.html

18
couchpotato/core/media/_base/matcher/main.py

@ -21,7 +21,6 @@ class Matcher(MatcherBase):
addEvent('matcher.construct_from_raw', self.constructFromRaw)
addEvent('matcher.correct_title', self.correctTitle)
addEvent('matcher.correct_quality', self.correctQuality)
def parse(self, name, parser='scene'):
return self.caper.parse(name, parser)
@ -70,20 +69,3 @@ class Matcher(MatcherBase):
return True
return False
def correctQuality(self, chain, quality, quality_map):
if quality['identifier'] not in quality_map:
log.info2('Wrong: unknown preferred quality %s', quality['identifier'])
return False
if 'video' not in chain.info:
log.info2('Wrong: no video tags found')
return False
video_tags = quality_map[quality['identifier']]
if not self.chainMatch(chain, 'video', video_tags):
log.info2('Wrong: %s tags not in chain', video_tags)
return False
return True

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

@ -198,14 +198,25 @@ class MediaPlugin(MediaBase):
else:
yield ms
def withIdentifiers(self, identifiers, with_doc = False):
def withIdentifiers(self, identifiers, with_doc = False, types = None):
if types and not with_doc:
raise ValueError("Unable to filter types without with_doc = True")
db = get_db()
for x in identifiers:
try:
return db.get('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc)
except:
pass
items = db.get_many('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc)
if not items:
# No items found, move to next identifier
continue
for item in items:
if types and item['doc'].get('type') not in types:
# Type doesn't match request, move to next item
continue
return item
log.debug('No media found with identifiers: %s', identifiers)
return False

40
couchpotato/core/media/_base/providers/base.py

@ -266,8 +266,8 @@ class YarrProvider(Provider):
if quality.get('custom'):
want_3d = quality['custom'].get('3d')
for ids, qualities in self.cat_ids:
if identifier in qualities or (want_3d and '3d' in qualities):
for ids, value in self.cat_ids:
if self.categoryMatch(value, quality, identifier, want_3d):
return ids
if self.cat_backup_id:
@ -275,6 +275,42 @@ class YarrProvider(Provider):
return []
def categoryMatch(self, value, quality, identifier, want_3d):
if type(value) is list:
# Basic identifier matching
if identifier in value:
return True
if want_3d and '3d' in value:
return True
return False
if type(value) is dict:
if not value:
# Wildcard category
return True
# Property matching
for key in ['codec', 'resolution', 'source']:
if key not in quality:
continue
for required in quality.get(key):
# Ensure category contains property list
if key not in value:
return False
# Ensure required property is in category
if required not in value[key]:
return False
# Valid
return True
# Unknown failure
return False
class ResultList(list):

7
couchpotato/core/media/_base/quality/__init__.py

@ -0,0 +1,7 @@
from .main import Quality
def autoload():
return Quality()
config = []

185
couchpotato/core/media/_base/quality/base.py

@ -0,0 +1,185 @@
import traceback
from CodernityDB.database import RecordNotFound
from couchpotato import get_db
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, ss
from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
class QualityBase(Plugin):
type = None
properties = {}
qualities = []
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
threed_tags = {
'sbs': [('half', 'sbs'), 'hsbs', ('full', 'sbs'), 'fsbs'],
'ou': [('half', 'ou'), 'hou', ('full', 'ou'), 'fou'],
'3d': ['2d3d', '3d2d', '3d'],
}
cached_qualities = None
cached_order = None
def __init__(self):
addEvent('quality.pre_releases', self.preReleases)
addEvent('quality.get', self.get)
addEvent('quality.all', self.all)
addEvent('quality.reset_cache', self.resetCache)
addEvent('quality.fill', self.fill)
addEvent('quality.isfinish', self.isFinish)
addEvent('quality.ishigher', self.isHigher)
addEvent('app.initialize', self.fill, priority = 10)
self.order = []
for q in self.qualities:
self.order.append(q.get('identifier'))
def preReleases(self, types = None):
if types and self.type not in types:
return
return self.pre_releases
def get(self, identifier, types = None):
if types and self.type not in types:
return
for q in self.qualities:
if identifier == q.get('identifier'):
return q
def all(self, types = None):
if types and self.type not in types:
return
if self.cached_qualities:
return self.cached_qualities
db = get_db()
temp = []
for quality in self.qualities:
quality_doc = db.get('quality', quality.get('identifier'), with_doc = True)['doc']
q = mergeDicts(quality, quality_doc)
temp.append(q)
if len(temp) == len(self.qualities):
self.cached_qualities = temp
return temp
def expand(self, quality):
for key, options in self.properties.items():
if key not in quality:
continue
quality[key] = [self.getProperty(key, identifier) for identifier in quality[key]]
return quality
def getProperty(self, key, identifier):
if key not in self.properties:
return
for item in self.properties[key]:
if item.get('identifier') == identifier:
return item
def resetCache(self):
self.cached_qualities = None
def fill(self):
try:
db = get_db()
order = 0
for q in self.qualities:
existing = None
try:
existing = db.get('quality', q.get('identifier'))
except RecordNotFound:
pass
if not existing:
db.insert({
'_t': 'quality',
'order': order,
'identifier': q.get('identifier'),
'size_min': tryInt(q.get('size')[0]),
'size_max': tryInt(q.get('size')[1]),
})
log.info('Creating profile: %s', q.get('label'))
db.insert({
'_t': 'profile',
'order': order + 20, # Make sure it goes behind other profiles
'core': True,
'qualities': [q.get('identifier')],
'label': toUnicode(q.get('label')),
'finish': [True],
'wait_for': [0],
})
order += 1
return True
except:
log.error('Failed: %s', traceback.format_exc())
return False
def isFinish(self, quality, profile, release_age = 0):
if not isinstance(profile, dict) or not profile.get('qualities'):
# No profile so anything (scanned) is good enough
return True
try:
index = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else False) == bool(quality.get('is_3d', False))][0]
if index == 0 or (profile['finish'][index] and int(release_age) >= int(profile.get('stop_after', [0])[0])):
return True
return False
except:
return False
def isHigher(self, quality, compare_with, profile = None):
if not isinstance(profile, dict) or not profile.get('qualities'):
profile = fireEvent('profile.default', single = True)
# Try to find quality in profile, if not found: a quality we do not want is lower than anything else
try:
quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0]
except:
log.debug('Quality %s not found in profile identifiers %s', (quality['identifier'] + (' 3D' if quality.get('is_3d', 0) else ''), \
[identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])]))
return 'lower'
# Try to find compare quality in profile, if not found: anything is higher than a not wanted quality
try:
compare_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == compare_with['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(compare_with.get('is_3d', 0))][0]
except:
log.debug('Compare quality %s not found in profile identifiers %s', (compare_with['identifier'] + (' 3D' if compare_with.get('is_3d', 0) else ''), \
[identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])]))
return 'higher'
# Note to self: a lower number means higher quality
if quality_order > compare_order:
return 'lower'
elif quality_order == compare_order:
return 'equal'
else:
return 'higher'

0
couchpotato/core/plugins/quality/index.py → couchpotato/core/media/_base/quality/index.py

82
couchpotato/core/media/_base/quality/main.py

@ -0,0 +1,82 @@
import traceback
from couchpotato import fireEvent, get_db, tryInt, CPLog
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import splitString, mergeDicts
from couchpotato.core.media._base.quality.index import QualityIndex
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
class Quality(Plugin):
_database = {
'quality': QualityIndex
}
def __init__(self):
addEvent('quality.single', self.single)
addApiView('quality.list', self.allView, docs = {
'desc': 'List all available qualities',
'params': {
'type': {'type': 'string', 'desc': 'Media type to filter on.'},
},
'return': {'type': 'object', 'example': """{
'success': True,
'list': array, qualities
}"""}
})
addApiView('quality.size.save', self.saveSize)
def single(self, identifier = '', types = None):
db = get_db()
quality = db.get('quality', identifier, with_doc = True)['doc']
if quality:
return mergeDicts(
fireEvent(
'quality.get',
quality['identifier'],
types = types,
single = True
),
quality
)
return {}
def allView(self, **kwargs):
return {
'success': True,
'list': fireEvent(
'quality.all',
types = splitString(kwargs.get('type')),
merge = True
)
}
def saveSize(self, **kwargs):
try:
db = get_db()
quality = db.get('quality', kwargs.get('identifier'), with_doc = True)
if quality:
quality['doc'][kwargs.get('value_type')] = tryInt(kwargs.get('value'))
db.update(quality['doc'])
fireEvent('quality.reset_cache')
return {
'success': True
}
except:
log.error('Failed: %s', traceback.format_exc())
return {
'success': False
}

0
couchpotato/core/plugins/quality/static/quality.css → couchpotato/core/media/_base/quality/static/quality.css

0
couchpotato/core/plugins/quality/static/quality.js → couchpotato/core/media/_base/quality/static/quality.js

11
couchpotato/core/media/_base/searcher/main.py

@ -84,13 +84,14 @@ class Searcher(SearcherBase):
return search_protocols
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = None):
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = None, types = None):
if not preferred_quality: preferred_quality = {}
found = {}
# Try guessing via quality tags
guess = fireEvent('quality.guess', files = [nzb.get('name')], size = nzb.get('size', None), single = True)
guess = fireEvent('quality.guess', files = [nzb.get('name')], size = nzb.get('size', None), types = types, single = True)
if guess:
found[guess['identifier']] = True
@ -111,7 +112,7 @@ class Searcher(SearcherBase):
found['dvdrip'] = True
# Allow other qualities
for allowed in preferred_quality.get('allow'):
for allowed in preferred_quality.get('allow', []):
if found.get(allowed):
del found[allowed]
@ -120,14 +121,14 @@ class Searcher(SearcherBase):
return found
def correct3D(self, nzb, preferred_quality = None):
def correct3D(self, nzb, preferred_quality = None, types = None):
if not preferred_quality: preferred_quality = {}
if not preferred_quality.get('custom'): return
threed = preferred_quality['custom'].get('3d')
# Try guessing via quality tags
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
guess = fireEvent('quality.guess', [nzb.get('name')], types = types, single = True)
if guess:
return threed == guess.get('is_3d')

0
couchpotato/core/media/movie/quality/__init__.py

259
couchpotato/core/plugins/quality/main.py → couchpotato/core/media/movie/quality/main.py

@ -1,212 +1,44 @@
from math import fabs, ceil
import traceback
import re
from CodernityDB.database import RecordNotFound
from couchpotato import get_db
from couchpotato.api import addApiView
from couchpotato import CPLog
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, ss
from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt, splitString, tryFloat
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.quality.index import QualityIndex
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import getExt, splitString, tryInt
from couchpotato.core.media._base.quality.base import QualityBase
log = CPLog(__name__)
autoload = 'MovieQuality'
class QualityPlugin(Plugin):
_database = {
'quality': QualityIndex
}
class MovieQuality(QualityBase):
type = 'movie'
qualities = [
{'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'median_size': 40000, 'label': 'BR-Disk', 'alternative': ['bd25', ('br', 'disk')], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']},
{'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'median_size': 10000, 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts'], 'tags': ['m2ts', 'x264', 'h264', '1080']},
{'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'median_size': 5500, 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264', '720']},
{'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'median_size': 2000, 'label': 'BR-Rip', 'alternative': ['bdrip', ('br', 'rip'), 'hdtv', 'hdrip'], 'allow': ['720p', '1080p'], 'ext':['mp4', 'avi'], 'tags': ['webdl', ('web', 'dl')]},
{'identifier': 'dvdr', 'size': (3000, 10000), 'median_size': 4500, 'label': 'DVD-R', 'alternative': ['br2dvd', ('dvd', 'r')], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'median_size': 1500, 'label': 'DVD-Rip', 'width': 720, 'alternative': [('dvd', 'rip')], 'allow': [], 'ext':['avi'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
{'identifier': 'scr', 'size': (600, 1600), 'median_size': 700, 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr', 'webrip', ('web', 'rip')], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': []},
{'identifier': 'r5', 'size': (600, 1000), 'median_size': 700, 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr', '720p', '1080p'], 'ext':[]},
{'identifier': 'tc', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': ['720p', '1080p'], 'ext':[]},
{'identifier': 'ts', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': ['720p', '1080p'], 'ext':[]},
{'identifier': 'cam', 'size': (600, 1000), 'median_size': 700, 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': ['720p', '1080p'], 'ext':[]},
# TODO come back to this later, think this could be handled better, this is starting to get out of hand....
# BluRay
{'identifier': 'bluray_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BluRay - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'bluray_720p', 'hd': True, 'size': (800, 5000), 'label': 'BluRay - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# BDRip
{'identifier': 'bdrip_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BDRip - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'bdrip_720p', 'hd': True, 'size': (800, 5000), 'label': 'BDRip - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# BRRip
{'identifier': 'brrip_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BRRip - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'brrip_720p', 'hd': True, 'size': (800, 5000), 'label': 'BRRip - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# WEB-DL
{'identifier': 'webdl_1080p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'webdl_720p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'webdl_480p', 'hd': True, 'size': (100, 5000), 'label': 'WEB-DL - 480p', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# HDTV
{'identifier': 'hdtv_720p', 'hd': True, 'size': (800, 5000), 'label': 'HDTV - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'hdtv_sd', 'hd': False, 'size': (100, 1000), 'label': 'HDTV - SD', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'mp4', 'avi']},
{'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25', ('br', 'disk')], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']},
{'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts'], 'tags': ['m2ts', 'x264', 'h264']},
{'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']},
{'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip', ('br', 'rip')], 'allow': ['720p', '1080p'], 'ext':['mp4', 'avi'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd', ('dvd', 'r')], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [('dvd', 'rip')], 'allow': [], 'ext':['avi'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': ['webrip', ('web', 'rip')]},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr', '720p'], 'ext':[]},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': ['720p'], 'ext':[]},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': ['720p'], 'ext':[]},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': ['720p'], 'ext':[]},
]
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
threed_tags = {
'sbs': [('half', 'sbs'), 'hsbs', ('full', 'sbs'), 'fsbs'],
'ou': [('half', 'ou'), 'hou', ('full', 'ou'), 'fou'],
'3d': ['2d3d', '3d2d', '3d'],
}
cached_qualities = None
cached_order = None
def __init__(self):
addEvent('quality.all', self.all)
addEvent('quality.single', self.single)
super(MovieQuality, self).__init__()
addEvent('quality.guess', self.guess)
addEvent('quality.pre_releases', self.preReleases)
addEvent('quality.order', self.getOrder)
addEvent('quality.ishigher', self.isHigher)
addEvent('quality.isfinish', self.isFinish)
addEvent('quality.fill', self.fill)
addApiView('quality.size.save', self.saveSize)
addApiView('quality.list', self.allView, docs = {
'desc': 'List all available qualities',
'return': {'type': 'object', 'example': """{
'success': True,
'list': array, qualities
}"""}
})
addEvent('app.initialize', self.fill, priority = 10)
addEvent('app.test', self.doTest)
self.order = []
self.addOrder()
def addOrder(self):
self.order = []
for q in self.qualities:
self.order.append(q.get('identifier'))
def getOrder(self):
return self.order
def preReleases(self):
return self.pre_releases
def allView(self, **kwargs):
return {
'success': True,
'list': self.all()
}
def all(self):
if self.cached_qualities:
return self.cached_qualities
db = get_db()
temp = []
for quality in self.qualities:
quality_doc = db.get('quality', quality.get('identifier'), with_doc = True)['doc']
q = mergeDicts(quality, quality_doc)
temp.append(q)
if len(temp) == len(self.qualities):
self.cached_qualities = temp
return temp
def single(self, identifier = ''):
db = get_db()
quality_dict = {}
quality = db.get('quality', identifier, with_doc = True)['doc']
if quality:
quality_dict = mergeDicts(self.getQuality(quality['identifier']), quality)
return quality_dict
def getQuality(self, identifier):
for q in self.qualities:
if identifier == q.get('identifier'):
return q
def saveSize(self, **kwargs):
try:
db = get_db()
quality = db.get('quality', kwargs.get('identifier'), with_doc = True)
if quality:
quality['doc'][kwargs.get('value_type')] = tryInt(kwargs.get('value'))
db.update(quality['doc'])
self.cached_qualities = None
return {
'success': True
}
except:
log.error('Failed: %s', traceback.format_exc())
return {
'success': False
}
def fill(self):
try:
db = get_db()
order = 0
for q in self.qualities:
existing = None
try:
existing = db.get('quality', q.get('identifier'))
except RecordNotFound:
pass
if not existing:
db.insert({
'_t': 'quality',
'order': order,
'identifier': q.get('identifier'),
'size_min': tryInt(q.get('size')[0]),
'size_max': tryInt(q.get('size')[1]),
})
log.info('Creating profile: %s', q.get('label'))
db.insert({
'_t': 'profile',
'order': order + 20, # Make sure it goes behind other profiles
'core': True,
'qualities': [q.get('identifier')],
'label': toUnicode(q.get('label')),
'finish': [True],
'wait_for': [0],
})
order += 1
def guess(self, files, extra = None, size = None, types = None):
if types and self.type not in types:
return
return True
except:
log.error('Failed: %s', traceback.format_exc())
return False
def guess(self, files, extra = None, size = None, use_cache = True):
if not extra: extra = {}
# Create hash for cache
@ -417,49 +249,6 @@ class QualityPlugin(Plugin):
if quality.get('identifier') != q.get('identifier') and score.get(q.get('identifier')):
score[q.get('identifier')]['score'] -= 1
def isFinish(self, quality, profile, release_age = 0):
if not isinstance(profile, dict) or not profile.get('qualities'):
# No profile so anything (scanned) is good enough
return True
try:
index = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else False) == bool(quality.get('is_3d', False))][0]
if index == 0 or (profile['finish'][index] and int(release_age) >= int(profile.get('stop_after', [0])[0])):
return True
return False
except:
return False
def isHigher(self, quality, compare_with, profile = None):
if not isinstance(profile, dict) or not profile.get('qualities'):
profile = fireEvent('profile.default', single = True)
# Try to find quality in profile, if not found: a quality we do not want is lower than anything else
try:
quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0]
except:
log.debug('Quality %s not found in profile identifiers %s', (quality['identifier'] + (' 3D' if quality.get('is_3d', 0) else ''), \
[identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])]))
return 'lower'
# Try to find compare quality in profile, if not found: anything is higher than a not wanted quality
try:
compare_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == compare_with['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(compare_with.get('is_3d', 0))][0]
except:
log.debug('Compare quality %s not found in profile identifiers %s', (compare_with['identifier'] + (' 3D' if compare_with.get('is_3d', 0) else ''), \
[identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])]))
return 'higher'
# Note to self: a lower number means higher quality
if quality_order > compare_order:
return 'lower'
elif quality_order == compare_order:
return 'equal'
else:
return 'higher'
def doTest(self):
tests = {
@ -530,5 +319,3 @@ class QualityPlugin(Plugin):
return True
else:
log.error('Quality test failed: %s out of %s succeeded', (correct, len(tests)))

6
couchpotato/core/media/movie/searcher.py

@ -277,13 +277,13 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string
contains_other = fireEvent('searcher.contains_other_quality', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, single = True)
if contains_other and isinstance(contains_other, dict):
contains_other = fireEvent('searcher.contains_other_quality', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, types = [self._type], single = True)
if contains_other != False:
log.info2('Wrong: %s, looking for %s, found %s', (nzb['name'], quality['label'], [x for x in contains_other] if contains_other else 'no quality'))
return False
# Contains lower quality string
if not fireEvent('searcher.correct_3d', nzb, preferred_quality = preferred_quality, single = True):
if not fireEvent('searcher.correct_3d', nzb, preferred_quality = preferred_quality, types = [self._type], single = True):
log.info2('Wrong: %s, %slooking for %s in 3D', (nzb['name'], ('' if preferred_quality['custom'].get('3d') else 'NOT '), quality['label']))
return False

4
couchpotato/core/media/show/_base/episode.py

@ -12,6 +12,8 @@ autoload = 'Episode'
class Episode(MediaBase):
_type = 'show.episode'
def __init__(self):
addEvent('show.episode.add', self.add)
addEvent('show.episode.update', self.update)
@ -37,7 +39,7 @@ class Episode(MediaBase):
}
# Check if season already exists
existing_episode = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True)
existing_episode = fireEvent('media.with_identifiers', identifiers, with_doc = True, types = [self._type], single = True)
db = get_db()

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

@ -105,7 +105,7 @@ class ShowBase(MediaBase):
# Update media with info
self.updateInfo(media, info)
existing_show = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True)
existing_show = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True, types = [self._type], single = True)
db = get_db()
@ -233,8 +233,6 @@ class ShowBase(MediaBase):
return {}
def updateInfo(self, media, info):
db = get_db()
# Remove season info for later use (save separately)
info.pop('in_wanted', None)
info.pop('in_library', None)

4
couchpotato/core/media/show/_base/season.py

@ -12,6 +12,8 @@ autoload = 'Season'
class Season(MediaBase):
_type = 'show.season'
def __init__(self):
addEvent('show.season.add', self.add)
addEvent('show.season.update', self.update)
@ -34,7 +36,7 @@ class Season(MediaBase):
}
# Check if season already exists
existing_season = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True)
existing_season = fireEvent('media.with_identifiers', identifiers, with_doc = True, types = [self._type], single = True)
db = get_db()

24
couchpotato/core/media/show/matcher/base.py

@ -6,26 +6,6 @@ log = CPLog(__name__)
class Base(MatcherBase):
# TODO come back to this later, think this could be handled better, this is starting to get out of hand....
quality_map = {
'bluray_1080p': {'resolution': ['1080p'], 'source': ['bluray']},
'bluray_720p': {'resolution': ['720p'], 'source': ['bluray']},
'bdrip_1080p': {'resolution': ['1080p'], 'source': ['BDRip']},
'bdrip_720p': {'resolution': ['720p'], 'source': ['BDRip']},
'brrip_1080p': {'resolution': ['1080p'], 'source': ['BRRip']},
'brrip_720p': {'resolution': ['720p'], 'source': ['BRRip']},
'webdl_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]},
'webdl_720p': {'resolution': ['720p'], 'source': ['webdl', ['web', 'dl']]},
'webdl_480p': {'resolution': ['480p'], 'source': ['webdl', ['web', 'dl']]},
'hdtv_720p': {'resolution': ['720p'], 'source': ['hdtv']},
'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']},
}
def __init__(self):
super(Base, self).__init__()
@ -35,10 +15,6 @@ class Base(MatcherBase):
log.info("Checking if '%s' is valid", release['name'])
log.info2('Release parsed as: %s', chain.info)
if not fireEvent('matcher.correct_quality', chain, quality, self.quality_map, single = True):
log.info('Wrong: %s, quality does not match', release['name'])
return False
if not fireEvent('%s.matcher.correct_identifier' % self.type, chain, media):
log.info('Wrong: %s, identifier does not match', release['name'])
return False

19
couchpotato/core/media/show/providers/torrent/iptorrents.py

@ -9,29 +9,20 @@ autoload = 'IPTorrents'
class IPTorrents(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Season(SeasonProvider, Base):
# TODO come back to this later, a better quality system needs to be created
cat_ids = [
([65], [
'bluray_1080p', 'bluray_720p',
'bdrip_1080p', 'bdrip_720p',
'brrip_1080p', 'brrip_720p',
'webdl_1080p', 'webdl_720p', 'webdl_480p',
'hdtv_720p', 'hdtv_sd'
]),
([65], {}),
]
class Episode(EpisodeProvider, Base):
# TODO come back to this later, a better quality system needs to be created
cat_ids = [
([5], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([4, 78, 79], ['hdtv_sd'])
([4], {'codec': ['mp4-asp'], 'resolution': ['sd'], 'source': ['hdtv', 'web']}),
([5], {'codec': ['mp4-avc'], 'resolution': ['720p', '1080p'], 'source': ['hdtv', 'web']}),
([78], {'codec': ['mp4-avc'], 'resolution': ['480p'], 'source': ['hdtv', 'web']}),
([79], {'codec': ['mp4-avc'], 'resolution': ['sd'], 'source': ['hdtv', 'web']})
]

0
couchpotato/core/media/show/quality/__init__.py

196
couchpotato/core/media/show/quality/main.py

@ -0,0 +1,196 @@
from caper import Caper
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.quality.base import QualityBase
log = CPLog(__name__)
autoload = 'ShowQuality'
class ShowQuality(QualityBase):
type = 'show'
properties = {
'codec': [
{'identifier': 'mp2', 'label': 'MPEG-2/H.262', 'value': ['mpeg2']},
{'identifier': 'mp4-asp', 'label': 'MPEG-4 ASP', 'value': ['divx', 'xvid']},
{'identifier': 'mp4-avc', 'label': 'MPEG-4 AVC/H.264', 'value': ['avc', 'h264', 'x264', ('h', '264')]},
],
'container': [
{'identifier': 'avi', 'label': 'AVI', 'value': ['avi']},
{'identifier': 'mov', 'label': 'QuickTime Movie', 'value': ['mov']},
{'identifier': 'mpeg-4', 'label': 'MPEG-4', 'value': ['m4v', 'mp4']},
{'identifier': 'mpeg-ts', 'label': 'MPEG-TS', 'value': ['m2ts', 'ts']},
{'identifier': 'mkv', 'label': 'Matroska', 'value': ['mkv']},
{'identifier': 'wmv', 'label': 'Windows Media Video', 'value': ['wmv']}
],
'resolution': [
# TODO interlaced resolutions (auto-fill these options?)
{'identifier': 'sd'},
{'identifier': '480p', 'width': 853, 'height': 480},
{'identifier': '576p', 'width': 1024, 'height': 576},
{'identifier': '720p', 'width': 1280, 'height': 720},
{'identifier': '1080p', 'width': 1920, 'height': 1080}
],
'source': [
{'identifier': 'cam', 'label': 'Cam', 'value': ['camrip', 'hdcam']},
{'identifier': 'hdtv', 'label': 'HDTV', 'value': ['hdtv']},
{'identifier': 'screener', 'label': 'Screener', 'value': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr']},
{'identifier': 'web', 'label': 'Web', 'value': ['webrip', ('web', 'rip'), 'webdl', ('web', 'dl')]}
]
}
qualities = [
# TODO sizes will need to be adjusted for season packs
# resolutions
{'identifier': '1080p', 'label': '1080p', 'size': (1000, 25000), 'codec': ['mp4-avc'], 'container': ['mpeg-ts', 'mkv'], 'resolution': ['1080p']},
{'identifier': '720p', 'label': '720p', 'size': (1000, 5000), 'codec': ['mp4-avc'], 'container': ['mpeg-ts', 'mkv'], 'resolution': ['720p']},
{'identifier': '480p', 'label': '480p', 'size': (800, 5000), 'codec': ['mp4-avc'], 'container': ['mpeg-ts', 'mkv'], 'resolution': ['480p']},
# sources
{'identifier': 'cam', 'label': 'Cam', 'size': (800, 5000), 'source': ['cam']},
{'identifier': 'hdtv', 'label': 'HDTV', 'size': (800, 5000), 'source': ['hdtv']},
{'identifier': 'screener', 'label': 'Screener', 'size': (800, 5000), 'source': ['screener']},
{'identifier': 'web', 'label': 'Web', 'size': (800, 5000), 'source': ['web']},
]
def __init__(self):
super(ShowQuality, self).__init__()
addEvent('quality.guess', self.guess)
self.caper = Caper()
def guess(self, files, extra = None, size = None, types = None):
if types and self.type not in types:
return
log.debug('Trying to guess quality of: %s', files)
if not extra: extra = {}
# Create hash for cache
cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files])
cached = self.getCache(cache_key)
if cached and len(extra) == 0:
return cached
qualities = self.all()
# Score files against each quality
score = self.score(files, qualities = qualities)
if score is None:
return None
# Return nothing if all scores are <= 0
has_non_zero = 0
for s in score:
if score[s]['score'] > 0:
has_non_zero += 1
if not has_non_zero:
return None
heighest_quality = max(score, key = lambda p: score[p]['score'])
if heighest_quality:
for quality in qualities:
if quality.get('identifier') == heighest_quality:
quality['is_3d'] = False
if score[heighest_quality].get('3d'):
quality['is_3d'] = True
return self.setCache(cache_key, quality)
return None
def score(self, files, qualities = None, types = None):
if types and self.type not in types:
return None
if not qualities:
qualities = self.all()
qualities_expanded = [self.expand(q.copy()) for q in qualities]
# Start with 0
score = {}
for quality in qualities:
score[quality.get('identifier')] = {
'score': 0,
'3d': {}
}
for cur_file in files:
match = self.caper.parse(cur_file, 'scene')
if len(match.chains) < 1:
log.info2('Unable to parse "%s", ignoring file')
continue
chain = match.chains[0]
for quality in qualities_expanded:
property_score = self.propertyScore(quality, chain)
self.calcScore(score, quality, property_score)
return score
def propertyScore(self, quality, chain):
score = 0
if 'video' not in chain.info:
return 0
info = fireEvent('matcher.flatten_info', chain.info['video'], single = True)
for key in ['codec', 'resolution', 'source']:
if key not in quality:
# No specific property required
score += 5
continue
available = list(self.getInfo(info, key))
found = False
for property in quality[key]:
required = property['value'] if 'value' in property else [property['identifier']]
if set(available) & set(required):
score += 10
found = True
break
if not found:
score -= 10
return score
def getInfo(self, info, key):
for value in info.get(key, []):
if isinstance(value, list):
yield tuple([x.lower() for x in value])
else:
yield value.lower()
def calcScore(self, score, quality, add_score, threedscore = (0, None), penalty = True):
score[quality['identifier']]['score'] += add_score
# Set order for allow calculation (and cache)
if not self.cached_order:
self.cached_order = {}
for q in self.qualities:
self.cached_order[q.get('identifier')] = self.qualities.index(q)
if penalty and add_score != 0:
for allow in quality.get('allow', []):
score[allow]['score'] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5
# Give panelty for all lower qualities
for q in self.qualities[self.order.index(quality.get('identifier'))+1:]:
if score.get(q.get('identifier')):
score[q.get('identifier')]['score'] -= 1

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

@ -47,7 +47,7 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase):
'result': fireEvent('%s.searcher.single' % self.getType(), media, single = True)
}
def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False):
def single(self, media, profile = None, search_protocols = None, manual = False):
db = get_db()
related = fireEvent('library.related', media, single = True)
@ -63,9 +63,6 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase):
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)
# 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']):
@ -93,14 +90,19 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase):
# 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']:
if release['status'] not in ['available', 'ignored', 'failed']:
is_higher = fireEvent('quality.ishigher', \
{'identifier': q_identifier, 'is_3d': quality_custom.get('3d', 0)}, \
{'identifier': release['quality'], 'is_3d': release.get('is_3d', 0)}, \
profile, single = True)
if is_higher != 'higher':
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 = fireEvent('quality.single', identifier = q_identifier, types = ['show'], single = True)
quality['custom'] = quality_custom
results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
@ -131,8 +133,7 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase):
log.info2('Too early to search for %s, %s', (too_early_to_search, query))
def correctRelease(self, release = None, media = None, quality = None, **kwargs):
if media.get('type') != 'show.episode':
return
if media.get('type') != 'show.episode': return
retention = Env.setting('retention', section = 'nzb')
@ -144,6 +145,14 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase):
if not fireEvent('searcher.correct_words', release['name'], media, single = True):
return False
preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string
contains_other = fireEvent('searcher.contains_other_quality', release, preferred_quality = preferred_quality, types = [self._type], single = True)
if contains_other != False:
log.info2('Wrong: %s, looking for %s, found %s', (release['name'], quality['label'], [x for x in contains_other] if contains_other else 'no quality'))
return False
# 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:

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

@ -37,7 +37,7 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
def searchAll(self, manual = False):
pass
def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False):
def single(self, media, profile = None, search_protocols = None, manual = False):
db = get_db()
related = fireEvent('library.related', media, single = True)
@ -53,9 +53,6 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
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)
# Find 'active' episodes
episodes = related['episodes']
episodes_active = []
@ -68,7 +65,7 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
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):
if self.search(media, profile, search_protocols):
# Success, end season search
return True
else:
@ -76,14 +73,14 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
# Search for each episode individually
for episode in episodes_active:
fireEvent('show.episode.searcher.single', episode, profile, quality_order, search_protocols, manual)
fireEvent('show.episode.searcher.single', episode, profile, search_protocols, manual)
# TODO (testing) only grab one episode
return True
return True
def search(self, media, profile, quality_order, search_protocols):
def search(self, media, profile, 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']):
@ -111,7 +108,12 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
# 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']:
if release['status'] not in ['available', 'ignored', 'failed']:
is_higher = fireEvent('quality.ishigher', \
{'identifier': q_identifier, 'is_3d': quality_custom.get('3d', 0)}, \
{'identifier': release['quality'], 'is_3d': release.get('is_3d', 0)}, \
profile, single = True)
if is_higher != 'higher':
has_better_quality += 1
# Don't search for quality lower then already available.
@ -164,6 +166,14 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
if not fireEvent('searcher.correct_words', release['name'], media, single = True):
return False
preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string
contains_other = fireEvent('searcher.contains_other_quality', release, preferred_quality = preferred_quality, types = [self._type], single = True)
if contains_other != False:
log.info2('Wrong: %s, looking for %s, found %s', (release['name'], quality['label'], [x for x in contains_other] if contains_other else 'no quality'))
return False
# 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:

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

@ -59,7 +59,6 @@ class ShowSearcher(SearcherBase, ShowTypeBase):
db = get_db()
profile = db.get('id', media['profile_id'])
quality_order = fireEvent('quality.order', single = True)
for season in show_tree.get('seasons', []):
if not season.get('info'):
@ -71,7 +70,7 @@ class ShowSearcher(SearcherBase, ShowTypeBase):
continue
# Check if full season can be downloaded
fireEvent('show.season.searcher.single', season, profile, quality_order, search_protocols, manual)
fireEvent('show.season.searcher.single', season, profile, search_protocols, manual)
# TODO (testing) only snatch one season
return

5
couchpotato/core/plugins/quality/__init__.py

@ -1,5 +0,0 @@
from .main import QualityPlugin
def autoload():
return QualityPlugin()

4
couchpotato/core/plugins/score/scores.py

@ -76,7 +76,7 @@ def namePositionScore(nzb_name, movie_name):
score = 0
nzb_words = re.split('\W+', simplifyString(nzb_name))
qualities = fireEvent('quality.all', single = True)
qualities = fireEvent('quality.all', merge = True)
try:
nzb_name = re.search(r'([\'"])[^\1]*\1', nzb_name).group(0)
@ -108,7 +108,7 @@ def namePositionScore(nzb_name, movie_name):
found_quality = quality['identifier']
# Alt in words
for alt in quality['alternative']:
for alt in quality.get('alternative', []):
if alt in nzb_words:
found_quality = alt
break

2
couchpotato/templates/index.html

@ -66,7 +66,7 @@
Quality.setup({
'profiles': {{ json_encode(fireEvent('profile.all', single = True)) }},
'qualities': {{ json_encode(fireEvent('quality.all', single = True)) }}
'qualities': {{ json_encode(fireEvent('quality.all', merge = True)) }}
});
CategoryList.setup({{ json_encode(fireEvent('category.all', single = True)) }});

Loading…
Cancel
Save