flexget.utils.qualities
Covered: 172 lines
Missed: 1 lines
Skipped 56 lines
Percent: 99 %
  1
"""
  2
.. warning:: Many methods in this module are not thread safe since they do in place sort for :attr:`qualities`
  3
"""
  5
import re
  6
import copy
  7
import logging
  9
log = logging.getLogger('utils.qualities')
 12
class Quality(object):
 14
    def __init__(self, value, name, all_of=None, none_of=None):
 15
        """
 16
        :param int value:
 17
          numerical value for quality, used for determining order
 18
        :param string name:
 19
          commonly used name for the quality
 20
        :param list all_of:
 21
          list of regexps that all need to match when testing
 22
          whether or not given text matches this quality
 23
        :param list none_of:
 24
          list of regexps that cannot match to this quality
 25
        """
 26
        self.value = value
 27
        self.name = name
 28
        if not all_of:
 29
            all_of = [name]
 30
        self.regexps = []
 31
        self.not_regexps = []
 34
        for r in all_of:
 35
            self.regexps.append(re.compile('(?<![^\W_])' + r + '(?![^\W_])', re.IGNORECASE))
 36
        if none_of:
 37
            for r in none_of:
 38
                self.not_regexps.append(re.compile('(?<![^\W_])' + r + '(?![^\W_])', re.IGNORECASE))
 40
    def matches(self, text):
 41
        """Test if quality matches to text.
 43
        :param string text: data te be tested against
 44
        :returns: tuple (matches, remaining text without quality data)
 45
        """
 49
        for regexp in self.not_regexps:
 50
            match = regexp.search(text)
 51
            if match:
 53
                return False, ""
 56
        for regexp in self.regexps:
 57
            match = regexp.search(text)
 58
            if not match:
 60
                return False, ""
 61
            else:
 63
                text = text[:match.start()] + text[match.end():]
 66
        return True, text
 68
    def __hash__(self):
 69
        return self.value
 71
    def __eq__(self, other):
 72
        if isinstance(other, basestring):
 73
            other = get(other, None)
 74
        if hasattr(other, 'value'):
 75
            return self.value == other.value
 76
        else:
 77
            return NotImplemented
 79
    def __ne__(self, other):
 80
        return not self.__eq__(other)
 82
    def __lt__(self, other):
 83
        if isinstance(other, basestring):
 84
            other = get(other, other)
 85
        if not hasattr(other, 'value'):
 86
            raise TypeError('%r is not a valid quality' % other)
 87
        return self.value < other.value
 89
    def __ge__(self, other):
 90
        return not self.__lt__(other)
 92
    def __le__(self, other):
 93
        return self.__lt__(other) or self.__eq__(other)
 95
    def __gt__(self, other):
 96
        return not self.__le__(other)
 98
    def __repr__(self):
 99
        return '<Quality(name=%s,value=%s)>' % (self.name, self.value)
101
    def __str__(self):
102
        return self.name
104
    def __deepcopy__(self, memo=None):
106
        return copy.copy(self)
108
UNKNOWN = Quality(0, 'unknown')
111
re_rc_or_r5 = 'rc|r5'
112
re_webdl = 'web[\W_]?dl'
113
re_720p = '(?:1280x)?720p?'
114
re_1080p = '(?:1920x)?1080p?'
115
re_bluray = '(?:b[dr][\W_]?rip|bluray(?:[\W_]?rip)?)'
116
re_10bit = '(10.?bit|hi10p)'
119
qualities = [Quality(1200, '1080p bluray 10bit', [re_1080p, re_bluray, re_10bit], none_of=[re_rc_or_r5]),
120
             Quality(1100, '1080p bluray', [re_1080p, re_bluray], none_of=[re_rc_or_r5]),
121
             Quality(1000, '1080p web-dl', [re_1080p, re_webdl]),
122
             Quality(850, '1080p 10bit', [re_1080p, re_10bit], none_of=[re_bluray, re_rc_or_r5]),
123
             Quality(800, '1080p', [re_1080p], none_of=[re_bluray, re_rc_or_r5]),
124
             Quality(750, '1080i'),
125
             Quality(670, '720p bluray 10bit', [re_720p, re_bluray, re_10bit], none_of=[re_rc_or_r5]),
126
             Quality(650, '720p bluray', [re_720p, re_bluray], none_of=[re_rc_or_r5, re_10bit]),
127
             Quality(600, '720p web-dl', [re_720p, re_webdl]),
128
             Quality(520, '720p 10bit', [re_720p, re_10bit], none_of=[re_bluray, re_rc_or_r5]),
129
             Quality(500, '720p', [re_720p], none_of=[re_bluray, re_rc_or_r5, re_10bit]),
130
             Quality(450, '720i'),
131
             Quality(430, '1080p bluray rc', [re_1080p, re_bluray, re_rc_or_r5]),
132
             Quality(420, '720p bluray rc', [re_720p, re_bluray, re_rc_or_r5]),
133
             Quality(400, 'hr'),
134
             Quality(380, 'bdrip', [re_bluray], none_of=[re_rc_or_r5]),
135
             Quality(350, 'dvdrip', ['dvd(?:[\W_]?rip)?'], none_of=[re_rc_or_r5]),
136
             Quality(320, 'web-dl', [re_webdl]),
137
             Quality(315, '576p', ['576p?']),
138
             Quality(310, '480p 10bit', ['480p?', re_10bit]),
139
             Quality(300, '480p', ['480p?'], none_of=[re_10bit]),
140
             Quality(290, '368p', ['368p?']),
141
             Quality(280, '360p'), # I don't think we want to make trailing p optional here (ie. xbox 360)
142
             Quality(270, 'hdtv', ['hdtv(?:[\W_]?rip)?']),
143
             Quality(260, 'dvdrip r5', ['dvd(?:[\W_]?rip)?', re_rc_or_r5]),
144
             Quality(250, 'bdscr'),
145
             Quality(240, 'dvdscr'),
146
             Quality(100, 'sdtv', ['(?:[sp]dtv|dvb)(?:[\W_]?rip)?|(?:t|pp)v[\W_]?rip']),
147
             Quality(80, 'dsr', ['dsr|(?:ds|web)[\W_]?rip']),
148
             Quality(50, 'r5'),
149
             Quality(40, 'tc'),
150
             Quality(30, 'preair'),
151
             Quality(25, 'ts', ['ts|telesync']),
152
             Quality(20, 'cam'),
153
             Quality(10, 'workprint')]
155
registry = dict([(qual.name.lower(), qual) for qual in qualities])
156
registry['unknown'] = UNKNOWN
159
def all():
160
    """Return all Qualities in order of best to worst"""
161
    return sorted(qualities, reverse=True) + [UNKNOWN]
164
def get(name, default=None):
165
    """
166
    Return Quality object for :name: (case insensitive)
167
    :param name: Quality name
168
    :return: Found :class:`Quality` / UNKNOWN or *default* if given and nothing was found.
169
    """
170
    name = name.lower()
171
    if name in registry:
172
        return registry[name]
173
    q = parse_quality(name)
174
    if q.value:
175
        return q
176
    return default if default is not None else UNKNOWN
179
def value(name):
180
    """
181
    :param str name: case insensitive quality name
182
    :return: Return value of quality with given *name* or 0 if unknown
183
    """
184
    return get(name).value
187
def min():
188
    """Return lowest known Quality excluding unknown."""
189
    qualities.sort()
190
    return qualities[0]
193
def max():
194
    """Return highest known Quality."""
195
    qualities.sort(reverse=True)
196
    return qualities[0]
199
def common_name(name):
200
    """Return `common name` for *name* (case insensitive).
202
    :param string name: Name to be converted in the common form.
203
    :returns: common name, eg. 1280x720, 720 and 720p will all return 720p
204
    :rtype: string
205
    """
206
    return get(name).name
209
def quality_match(title):
210
    """Search best quality from title
212
    :param string title: text to search from
213
    :returns: tuple (:class:`Quality` which can be unknown, remaining title without quality)
214
    """
215
    qualities.sort(reverse=True)
216
    for quality in qualities:
217
        result, remaining = quality.matches(title)
218
        if result:
219
            return quality, remaining
220
    return UNKNOWN, title
223
def parse_quality(title):
224
    """Find the highest know quality in a given string :title:
226
    :returns: :class:`Quality` object or False
227
    """
228
    return quality_match(title)[0]