Update libs
This commit is contained in:
+62
-48
@@ -20,7 +20,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__version__ = '0.7-dev'
|
||||
__version__ = '0.6.2'
|
||||
__all__ = ['Guess', 'Language',
|
||||
'guess_file_info', 'guess_video_info',
|
||||
'guess_movie_info', 'guess_episode_info']
|
||||
@@ -76,6 +76,7 @@ from guessit.language import Language
|
||||
from guessit.matcher import IterativeMatcher
|
||||
from guessit.textutils import clean_string
|
||||
import logging
|
||||
import json
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -105,17 +106,74 @@ def _guess_filename(filename, filetype):
|
||||
|
||||
mtree = IterativeMatcher(filename, filetype=filetype)
|
||||
|
||||
m = mtree.matched()
|
||||
|
||||
second_pass_opts = []
|
||||
second_pass_transfo_opts = {}
|
||||
|
||||
# if there are multiple possible years found, we assume the first one is
|
||||
# part of the title, reparse the tree taking this into account
|
||||
years = set(n.value for n in find_nodes(mtree.match_tree, 'year'))
|
||||
if len(years) >= 2:
|
||||
mtree = IterativeMatcher(filename, filetype=filetype,
|
||||
opts=['skip_first_year'])
|
||||
second_pass_opts.append('skip_first_year')
|
||||
|
||||
to_skip_language_nodes = []
|
||||
|
||||
title_nodes = set(n for n in find_nodes(mtree.match_tree, ['title', 'series']))
|
||||
title_spans = {}
|
||||
for title_node in title_nodes:
|
||||
title_spans[title_node.span[0]] = title_node
|
||||
title_spans[title_node.span[1]] = title_node
|
||||
|
||||
for lang_key in ('language', 'subtitleLanguage'):
|
||||
langs = {}
|
||||
lang_nodes = set(n for n in find_nodes(mtree.match_tree, lang_key))
|
||||
|
||||
for lang_node in lang_nodes:
|
||||
lang = lang_node.guess.get(lang_key, None)
|
||||
if len(lang_node.value) > 3 and (lang_node.span[0] in title_spans.keys() or lang_node.span[1] in title_spans.keys()):
|
||||
# Language is next or before title, and is not a language code. Add to skip for 2nd pass.
|
||||
|
||||
# if filetype is subtitle and the language appears last, just before
|
||||
# the extension, then it is likely a subtitle language
|
||||
parts = clean_string(lang_node.root.value).split()
|
||||
if m['type'] in ['moviesubtitle', 'episodesubtitle'] and (parts.index(lang_node.value) == len(parts) - 2):
|
||||
continue
|
||||
|
||||
to_skip_language_nodes.append(lang_node)
|
||||
elif not lang in langs:
|
||||
langs[lang] = lang_node
|
||||
else:
|
||||
# The same language was found. Keep the more confident one, and add others to skip for 2nd pass.
|
||||
existing_lang_node = langs[lang]
|
||||
to_skip = None
|
||||
if existing_lang_node.guess.confidence('language') >= lang_node.guess.confidence('language'):
|
||||
# lang_node is to remove
|
||||
to_skip = lang_node
|
||||
else:
|
||||
# existing_lang_node is to remove
|
||||
langs[lang] = lang_node
|
||||
to_skip = existing_lang_node
|
||||
to_skip_language_nodes.append(to_skip)
|
||||
|
||||
|
||||
if to_skip_language_nodes:
|
||||
second_pass_transfo_opts['guess_language'] = (
|
||||
((), { 'skip': [ { 'node_idx': node.parent.node_idx,
|
||||
'span': node.span }
|
||||
for node in to_skip_language_nodes ] }))
|
||||
|
||||
if second_pass_opts or second_pass_transfo_opts:
|
||||
# 2nd pass is needed
|
||||
log.info("Running 2nd pass with options: %s" % second_pass_opts)
|
||||
log.info("Transfo options: %s" % second_pass_transfo_opts)
|
||||
mtree = IterativeMatcher(filename, filetype=filetype,
|
||||
opts=second_pass_opts,
|
||||
transfo_opts=second_pass_transfo_opts)
|
||||
|
||||
m = mtree.matched()
|
||||
|
||||
if 'language' not in m and 'subtitleLanguage' not in m:
|
||||
if 'language' not in m and 'subtitleLanguage' not in m or 'title' not in m:
|
||||
return m
|
||||
|
||||
# if we found some language, make sure we didn't cut a title or sth...
|
||||
@@ -123,51 +181,10 @@ def _guess_filename(filename, filetype):
|
||||
opts=['nolanguage', 'nocountry'])
|
||||
m2 = mtree2.matched()
|
||||
|
||||
|
||||
if m.get('title') is None:
|
||||
return m
|
||||
|
||||
if m.get('title') != m2.get('title'):
|
||||
title = next(find_nodes(mtree.match_tree, 'title'))
|
||||
title2 = next(find_nodes(mtree2.match_tree, 'title'))
|
||||
|
||||
langs = list(find_nodes(mtree.match_tree, ['language', 'subtitleLanguage']))
|
||||
if not langs:
|
||||
return warning('A weird error happened with language detection')
|
||||
|
||||
# find the language that is likely more relevant
|
||||
for lng in langs:
|
||||
if lng.value in title2.value:
|
||||
# if the language was detected as part of a potential title,
|
||||
# look at this one in particular
|
||||
lang = lng
|
||||
break
|
||||
else:
|
||||
# pick the first one if we don't have a better choice
|
||||
lang = langs[0]
|
||||
|
||||
|
||||
# language code are rarely part of a title, and those
|
||||
# should be handled by the Language exceptions anyway
|
||||
if len(lang.value) <= 3:
|
||||
return m
|
||||
|
||||
|
||||
# if filetype is subtitle and the language appears last, just before
|
||||
# the extension, then it is likely a subtitle language
|
||||
parts = clean_string(title.root.value).split()
|
||||
if (m['type'] in ['moviesubtitle', 'episodesubtitle'] and
|
||||
parts.index(lang.value) == len(parts) - 2):
|
||||
return m
|
||||
|
||||
# if the language was in the middle of the other potential title,
|
||||
# keep the other title (eg: The Italian Job), except if it is at the
|
||||
# very beginning, in which case we consider it an error
|
||||
if m2['title'].startswith(lang.value):
|
||||
return m
|
||||
elif lang.value in title2.value:
|
||||
return m2
|
||||
|
||||
# if a node is in an explicit group, then the correct title is probably
|
||||
# the other one
|
||||
if title.root.node_at(title.node_idx[:2]).is_explicit():
|
||||
@@ -175,9 +192,6 @@ def _guess_filename(filename, filetype):
|
||||
elif title2.root.node_at(title2.node_idx[:2]).is_explicit():
|
||||
return m
|
||||
|
||||
return warning('Not sure of the title because of the language position')
|
||||
|
||||
|
||||
return m
|
||||
|
||||
|
||||
|
||||
@@ -24,16 +24,19 @@ from guessit import u
|
||||
from guessit import slogging, guess_file_info
|
||||
from optparse import OptionParser
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import locale
|
||||
|
||||
|
||||
def detect_filename(filename, filetype, info=['filename']):
|
||||
def detect_filename(filename, filetype, info=['filename'], advanced = False):
|
||||
filename = u(filename)
|
||||
|
||||
print('For:', filename)
|
||||
print('GuessIt found:', guess_file_info(filename, filetype, info).nice_string())
|
||||
print('GuessIt found:', guess_file_info(filename, filetype, info).nice_string(advanced))
|
||||
|
||||
|
||||
def run_demo(episodes=True, movies=True):
|
||||
def run_demo(episodes=True, movies=True, advanced=False):
|
||||
# NOTE: tests should not be added here but rather in the tests/ folder
|
||||
# this is just intended as a quick example
|
||||
if episodes:
|
||||
@@ -50,7 +53,7 @@ def run_demo(episodes=True, movies=True):
|
||||
|
||||
for f in testeps:
|
||||
print('-'*80)
|
||||
detect_filename(f, filetype='episode')
|
||||
detect_filename(f, filetype='episode', advanced=advanced)
|
||||
|
||||
|
||||
if movies:
|
||||
@@ -77,12 +80,17 @@ def run_demo(episodes=True, movies=True):
|
||||
|
||||
for f in testmovies:
|
||||
print('-'*80)
|
||||
detect_filename(f, filetype = 'movie')
|
||||
detect_filename(f, filetype = 'movie', advanced = advanced)
|
||||
|
||||
|
||||
def main():
|
||||
slogging.setupLogging()
|
||||
|
||||
# see http://bugs.python.org/issue2128
|
||||
if sys.version_info.major < 3 and os.name == 'nt':
|
||||
for i, a in enumerate(sys.argv):
|
||||
sys.argv[i] = a.decode(locale.getpreferredencoding())
|
||||
|
||||
parser = OptionParser(usage = 'usage: %prog [options] file1 [file2...]')
|
||||
parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False,
|
||||
help = 'display debug output')
|
||||
@@ -92,6 +100,8 @@ def main():
|
||||
'them, comma-separated')
|
||||
parser.add_option('-t', '--type', dest = 'filetype', default = 'autodetect',
|
||||
help = 'the suggested file type: movie, episode or autodetect')
|
||||
parser.add_option('-a', '--advanced', dest = 'advanced', action='store_true', default = False,
|
||||
help = 'display advanced information for filename guesses, as json output')
|
||||
parser.add_option('-d', '--demo', action='store_true', dest='demo', default=False,
|
||||
help = 'run a few builtin tests instead of analyzing a file')
|
||||
|
||||
@@ -100,13 +110,14 @@ def main():
|
||||
logging.getLogger('guessit').setLevel(logging.DEBUG)
|
||||
|
||||
if options.demo:
|
||||
run_demo(episodes=True, movies=True)
|
||||
run_demo(episodes=True, movies=True, advanced=options.advanced)
|
||||
else:
|
||||
if args:
|
||||
for filename in args:
|
||||
detect_filename(filename,
|
||||
filetype = options.filetype,
|
||||
info = options.info.split(','))
|
||||
info = options.info.split(','),
|
||||
advanced = options.advanced)
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
@@ -44,13 +44,14 @@ def split_path(path):
|
||||
result = []
|
||||
while True:
|
||||
head, tail = os.path.split(path)
|
||||
headlen = len(head)
|
||||
|
||||
# on Unix systems, the root folder is '/'
|
||||
if head == '/' and tail == '':
|
||||
if head and head == '/'*headlen and tail == '':
|
||||
return ['/'] + result
|
||||
|
||||
# on Windows, the root folder is a drive letter (eg: 'C:\') or for shares \\
|
||||
if ((len(head) == 3 and head[1:] == ':\\') or (len(head) == 2 and head == '\\\\')) and tail == '':
|
||||
if ((headlen == 3 and head[1:] == ':\\') or (headlen == 2 and head == '\\\\')) and tail == '':
|
||||
return [head] + result
|
||||
|
||||
if head == '' and tail == '':
|
||||
@@ -61,6 +62,7 @@ def split_path(path):
|
||||
path = head
|
||||
continue
|
||||
|
||||
# otherwise, add the last path fragment and keep splitting
|
||||
result = [tail] + result
|
||||
path = head
|
||||
|
||||
|
||||
+46
-19
@@ -41,15 +41,21 @@ class Guess(UnicodeMixin, dict):
|
||||
confidence = kwargs.pop('confidence')
|
||||
except KeyError:
|
||||
confidence = 0
|
||||
|
||||
try:
|
||||
raw = kwargs.pop('raw')
|
||||
except KeyError:
|
||||
raw = None
|
||||
|
||||
dict.__init__(self, *args, **kwargs)
|
||||
|
||||
self._confidence = {}
|
||||
self._raw = {}
|
||||
for prop in self:
|
||||
self._confidence[prop] = confidence
|
||||
|
||||
|
||||
def to_dict(self):
|
||||
self._raw[prop] = raw
|
||||
|
||||
def to_dict(self, advanced=False):
|
||||
data = dict(self)
|
||||
for prop, value in data.items():
|
||||
if isinstance(value, datetime.date):
|
||||
@@ -58,46 +64,65 @@ class Guess(UnicodeMixin, dict):
|
||||
data[prop] = u(value)
|
||||
elif isinstance(value, list):
|
||||
data[prop] = [u(x) for x in value]
|
||||
if advanced:
|
||||
data[prop] = {"value": data[prop], "raw": self.raw(prop), "confidence": self.confidence(prop)}
|
||||
|
||||
return data
|
||||
|
||||
def nice_string(self):
|
||||
data = self.to_dict()
|
||||
|
||||
parts = json.dumps(data, indent=4).split('\n')
|
||||
for i, p in enumerate(parts):
|
||||
if p[:5] != ' "':
|
||||
continue
|
||||
|
||||
prop = p.split('"')[1]
|
||||
parts[i] = (' [%.2f] "' % self.confidence(prop)) + p[5:]
|
||||
|
||||
return '\n'.join(parts)
|
||||
def nice_string(self, advanced=False):
|
||||
if advanced:
|
||||
data = self.to_dict(advanced)
|
||||
return json.dumps(data, indent=4)
|
||||
else:
|
||||
data = self.to_dict()
|
||||
|
||||
parts = json.dumps(data, indent=4).split('\n')
|
||||
for i, p in enumerate(parts):
|
||||
if p[:5] != ' "':
|
||||
continue
|
||||
|
||||
prop = p.split('"')[1]
|
||||
parts[i] = (' [%.2f] "' % self.confidence(prop)) + p[5:]
|
||||
|
||||
return '\n'.join(parts)
|
||||
|
||||
def __unicode__(self):
|
||||
return u(self.to_dict())
|
||||
|
||||
def confidence(self, prop):
|
||||
return self._confidence.get(prop, -1)
|
||||
|
||||
def raw(self, prop):
|
||||
return self._raw.get(prop, None)
|
||||
|
||||
def set(self, prop, value, confidence=None):
|
||||
def set(self, prop, value, confidence=None, raw=None):
|
||||
self[prop] = value
|
||||
if confidence is not None:
|
||||
self._confidence[prop] = confidence
|
||||
if raw is not None:
|
||||
self._raw[prop] = raw
|
||||
|
||||
def set_confidence(self, prop, value):
|
||||
self._confidence[prop] = value
|
||||
|
||||
def set_raw(self, prop, value):
|
||||
self._raw[prop] = value
|
||||
|
||||
def update(self, other, confidence=None):
|
||||
def update(self, other, confidence=None, raw=None):
|
||||
dict.update(self, other)
|
||||
if isinstance(other, Guess):
|
||||
for prop in other:
|
||||
self._confidence[prop] = other.confidence(prop)
|
||||
self._raw[prop] = other.raw(prop)
|
||||
|
||||
if confidence is not None:
|
||||
for prop in other:
|
||||
self._confidence[prop] = confidence
|
||||
|
||||
if raw is not None:
|
||||
for prop in other:
|
||||
self._raw[prop] = raw
|
||||
|
||||
def update_highest_confidence(self, other):
|
||||
"""Update this guess with the values from the given one. In case
|
||||
there is property present in both, only the one with the highest one
|
||||
@@ -110,6 +135,7 @@ class Guess(UnicodeMixin, dict):
|
||||
continue
|
||||
self[prop] = other[prop]
|
||||
self._confidence[prop] = other.confidence(prop)
|
||||
self._raw[prop] = other.raw(prop)
|
||||
|
||||
|
||||
def choose_int(g1, g2):
|
||||
@@ -181,7 +207,7 @@ def choose_string(g1, g2):
|
||||
elif v1l in v2l:
|
||||
return (v1, combined_prob)
|
||||
|
||||
# in case of conflict, return the one with highest priority
|
||||
# in case of conflict, return the one with highest confidence
|
||||
else:
|
||||
if c1 > c2:
|
||||
return (v1, c1 - c2)
|
||||
@@ -288,7 +314,8 @@ def merge_all(guesses, append=None):
|
||||
result.set(prop, result.get(prop, []) + [g[prop]],
|
||||
# TODO: what to do with confidence here? maybe an
|
||||
# arithmetic mean...
|
||||
confidence=g.confidence(prop))
|
||||
confidence=g.confidence(prop),
|
||||
raw=g.raw(prop))
|
||||
|
||||
del g[prop]
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ UNDETERMINED = Language('und')
|
||||
ALL_LANGUAGES = frozenset(Language(lng) for lng in lng_all_names) - frozenset([UNDETERMINED])
|
||||
ALL_LANGUAGES_NAMES = lng_all_names
|
||||
|
||||
def search_language(string, lang_filter=None):
|
||||
def search_language(string, lang_filter=None, skip=None):
|
||||
"""Looks for language patterns, and if found return the language object,
|
||||
its group span and an associated confidence.
|
||||
|
||||
@@ -345,6 +345,16 @@ def search_language(string, lang_filter=None):
|
||||
|
||||
if pos != -1:
|
||||
end = pos + len(lang)
|
||||
|
||||
# skip if span in in skip list
|
||||
while skip and (pos - 1, end - 1) in skip:
|
||||
pos = slow.find(lang, end)
|
||||
if pos == -1:
|
||||
continue
|
||||
end = pos + len(lang)
|
||||
if pos == -1:
|
||||
continue
|
||||
|
||||
# make sure our word is always surrounded by separators
|
||||
if slow[pos - 1] not in sep or slow[end] not in sep:
|
||||
continue
|
||||
|
||||
+37
-14
@@ -21,14 +21,14 @@
|
||||
from __future__ import unicode_literals
|
||||
from guessit import PY3, u, base_text_type
|
||||
from guessit.matchtree import MatchTree
|
||||
from guessit.textutils import normalize_unicode
|
||||
from guessit.textutils import normalize_unicode, clean_string
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IterativeMatcher(object):
|
||||
def __init__(self, filename, filetype='autodetect', opts=None):
|
||||
def __init__(self, filename, filetype='autodetect', opts=None, transfo_opts=None):
|
||||
"""An iterative matcher tries to match different patterns that appear
|
||||
in the filename.
|
||||
|
||||
@@ -38,7 +38,8 @@ class IterativeMatcher(object):
|
||||
a movie.
|
||||
|
||||
The recognized 'filetype' values are:
|
||||
[ autodetect, subtitle, movie, moviesubtitle, episode, episodesubtitle ]
|
||||
[ autodetect, subtitle, info, movie, moviesubtitle, movieinfo, episode,
|
||||
episodesubtitle, episodeinfo ]
|
||||
|
||||
|
||||
The IterativeMatcher works mainly in 2 steps:
|
||||
@@ -61,15 +62,20 @@ class IterativeMatcher(object):
|
||||
it corresponds to a video codec, denoted by the letter'v' in the 4th line.
|
||||
(for more info, see guess.matchtree.to_string)
|
||||
|
||||
Second, it tries to merge all this information into a single object
|
||||
containing all the found properties, and does some (basic) conflict
|
||||
resolution when they arise.
|
||||
|
||||
Second, it tries to merge all this information into a single object
|
||||
containing all the found properties, and does some (basic) conflict
|
||||
resolution when they arise.
|
||||
|
||||
When you create the Matcher, you can pass it:
|
||||
- a list 'opts' of option names, that act as global flags
|
||||
- a dict 'transfo_opts' of { transfo_name: (transfo_args, transfo_kwargs) }
|
||||
with which to call the transfo.process() function.
|
||||
"""
|
||||
|
||||
valid_filetypes = ('autodetect', 'subtitle', 'video',
|
||||
'movie', 'moviesubtitle',
|
||||
'episode', 'episodesubtitle')
|
||||
valid_filetypes = ('autodetect', 'subtitle', 'info', 'video',
|
||||
'movie', 'moviesubtitle', 'movieinfo',
|
||||
'episode', 'episodesubtitle', 'episodeinfo')
|
||||
if filetype not in valid_filetypes:
|
||||
raise ValueError("filetype needs to be one of %s" % valid_filetypes)
|
||||
if not PY3 and not isinstance(filename, unicode):
|
||||
@@ -80,10 +86,22 @@ class IterativeMatcher(object):
|
||||
|
||||
if opts is None:
|
||||
opts = []
|
||||
elif isinstance(opts, base_text_type):
|
||||
opts = opts.split()
|
||||
if not isinstance(opts, list):
|
||||
raise ValueError('opts must be a list of option names! Received: type=%s val=%s',
|
||||
type(opts), opts)
|
||||
|
||||
if transfo_opts is None:
|
||||
transfo_opts = {}
|
||||
if not isinstance(transfo_opts, dict):
|
||||
raise ValueError('transfo_opts must be a dict of { transfo_name: (args, kwargs) }. '+
|
||||
'Received: type=%s val=%s', type(transfo_opts), transfo_opts)
|
||||
|
||||
self.match_tree = MatchTree(filename)
|
||||
|
||||
# sanity check: make sure we don't process a (mostly) empty string
|
||||
if clean_string(filename) == '':
|
||||
return
|
||||
|
||||
mtree = self.match_tree
|
||||
mtree.guess.set('type', filetype, confidence=1.0)
|
||||
|
||||
@@ -91,7 +109,11 @@ class IterativeMatcher(object):
|
||||
transfo = __import__('guessit.transfo.' + transfo_name,
|
||||
globals=globals(), locals=locals(),
|
||||
fromlist=['process'], level=0)
|
||||
transfo.process(mtree, *args, **kwargs)
|
||||
default_args, default_kwargs = transfo_opts.get(transfo_name, ((), {}))
|
||||
all_args = args or default_args
|
||||
all_kwargs = dict(default_kwargs)
|
||||
all_kwargs.update(kwargs) # keep all kwargs merged together
|
||||
transfo.process(mtree, *all_args, **all_kwargs)
|
||||
|
||||
# 1- first split our path into dirs + basename + ext
|
||||
apply_transfo('split_path_components')
|
||||
@@ -111,7 +133,7 @@ class IterativeMatcher(object):
|
||||
# - language before episodes_rexps
|
||||
# - properties before language (eg: he-aac vs hebrew)
|
||||
# - release_group before properties (eg: XviD-?? vs xvid)
|
||||
if mtree.guess['type'] in ('episode', 'episodesubtitle'):
|
||||
if mtree.guess['type'] in ('episode', 'episodesubtitle', 'episodeinfo'):
|
||||
strategy = [ 'guess_date', 'guess_website', 'guess_release_group',
|
||||
'guess_properties', 'guess_language',
|
||||
'guess_video_rexps',
|
||||
@@ -124,6 +146,7 @@ class IterativeMatcher(object):
|
||||
if 'nolanguage' in opts:
|
||||
strategy.remove('guess_language')
|
||||
|
||||
|
||||
for name in strategy:
|
||||
apply_transfo(name)
|
||||
|
||||
@@ -143,7 +166,7 @@ class IterativeMatcher(object):
|
||||
|
||||
# 5- try to identify the remaining unknown groups by looking at their
|
||||
# position relative to other known elements
|
||||
if mtree.guess['type'] in ('episode', 'episodesubtitle'):
|
||||
if mtree.guess['type'] in ('episode', 'episodesubtitle', 'episodeinfo'):
|
||||
apply_transfo('guess_episode_info_from_position')
|
||||
else:
|
||||
apply_transfo('guess_movie_title_from_position')
|
||||
|
||||
@@ -25,6 +25,8 @@ import re
|
||||
|
||||
subtitle_exts = [ 'srt', 'idx', 'sub', 'ssa' ]
|
||||
|
||||
info_exts = [ 'nfo' ]
|
||||
|
||||
video_exts = ['3g2', '3gp', '3gp2', 'asf', 'avi', 'divx', 'flv', 'm4v', 'mk2',
|
||||
'mka', 'mkv', 'mov', 'mp4', 'mp4a', 'mpeg', 'mpg', 'ogg', 'ogm',
|
||||
'ogv', 'qt', 'ra', 'ram', 'rm', 'ts', 'wav', 'webm', 'wma', 'wmv']
|
||||
@@ -32,7 +34,7 @@ video_exts = ['3g2', '3gp', '3gp2', 'asf', 'avi', 'divx', 'flv', 'm4v', 'mk2',
|
||||
group_delimiters = [ '()', '[]', '{}' ]
|
||||
|
||||
# separator character regexp
|
||||
sep = r'[][)(}{+ /\._-]' # regexp art, hehe :D
|
||||
sep = r'[][,)(}{+ /\._-]' # regexp art, hehe :D
|
||||
|
||||
# character used to represent a deleted char (when matching groups)
|
||||
deleted = '_'
|
||||
@@ -49,7 +51,7 @@ episode_rexps = [ # ... Season 2 ...
|
||||
#(r'[Ss](?P<season>[0-9]{1,3})[^0-9]?(?P<bonusNumber>(?:-?[xX-][0-9]{1,3})+)[^0-9]', 1.0, (0, -1)),
|
||||
|
||||
# ... 2x13 ...
|
||||
(r'[^0-9](?P<season>[0-9]{1,2})[^0-9]?(?P<episodeNumber>(?:-?[xX][0-9]{1,3})+)[^0-9]', 1.0, (1, -1)),
|
||||
(r'[^0-9](?P<season>[0-9]{1,2})[^0-9 .-]?(?P<episodeNumber>(?:-?[xX][0-9]{1,3})+)[^0-9]', 1.0, (1, -1)),
|
||||
|
||||
# ... s02 ...
|
||||
#(sep + r's(?P<season>[0-9]{1,2})' + sep, 0.6, (1, -1)),
|
||||
@@ -122,9 +124,12 @@ prop_multi = { 'format': { 'DVD': [ 'DVD', 'DVD-Rip', 'VIDEO-TS', 'DVDivX' ],
|
||||
'VHS': [ 'VHS' ],
|
||||
'WEB-DL': [ 'WEB-DL' ] },
|
||||
|
||||
'is3D': { True: [ '3D' ] },
|
||||
|
||||
'screenSize': { '480p': [ '480[pi]?' ],
|
||||
'720p': [ '720[pi]?' ],
|
||||
'1080p': [ '1080[pi]?' ] },
|
||||
'1080i': [ '1080i' ],
|
||||
'1080p': [ '1080p', '1080[^i]' ] },
|
||||
|
||||
'videoCodec': { 'XviD': [ 'Xvid' ],
|
||||
'DivX': [ 'DVDivX', 'DivX' ],
|
||||
@@ -140,7 +145,7 @@ prop_multi = { 'format': { 'DVD': [ 'DVD', 'DVD-Rip', 'VIDEO-TS', 'DVDivX' ],
|
||||
'DTS': [ 'DTS' ],
|
||||
'AAC': [ 'He-AAC', 'AAC-He', 'AAC' ] },
|
||||
|
||||
'audioChannels': { '5.1': [ r'5\.1', 'DD5[\._ ]1', '5ch' ] },
|
||||
'audioChannels': { '5.1': [ r'5\.1', 'DD5[._ ]1', '5ch' ] },
|
||||
|
||||
'episodeFormat': { 'Minisode': [ 'Minisodes?' ] }
|
||||
|
||||
@@ -170,7 +175,7 @@ prop_single = { 'releaseGroup': [ 'ESiR', 'WAF', 'SEPTiC', r'\[XCT\]', 'iNT', 'P
|
||||
}
|
||||
|
||||
_dash = '-'
|
||||
_psep = '[-\. _]?'
|
||||
_psep = '[-. _]?'
|
||||
|
||||
def _to_rexp(prop):
|
||||
return re.compile(prop.replace(_dash, _psep), re.IGNORECASE)
|
||||
@@ -237,8 +242,9 @@ def canonical_form(string):
|
||||
def compute_canonical_form(property_name, value):
|
||||
"""Return the canonical form of a property given its type if it is a valid
|
||||
one, None otherwise."""
|
||||
for canonical_form, rexps in properties_rexps[property_name].items():
|
||||
for rexp in rexps:
|
||||
if rexp.match(value):
|
||||
return canonical_form
|
||||
if isinstance(value, basestring):
|
||||
for canonical_form, rexps in properties_rexps[property_name].items():
|
||||
for rexp in rexps:
|
||||
if rexp.match(value):
|
||||
return canonical_form
|
||||
return None
|
||||
|
||||
@@ -31,14 +31,15 @@ RED_FONT = "\x1B[0;31m"
|
||||
RESET_FONT = "\x1B[0m"
|
||||
|
||||
|
||||
def setupLogging(colored=True, with_time=False, with_thread=False, filename=None):
|
||||
def setupLogging(colored=True, with_time=False, with_thread=False, filename=None, with_lineno=False):
|
||||
"""Set up a nice colored logger as the main application logger."""
|
||||
|
||||
class SimpleFormatter(logging.Formatter):
|
||||
def __init__(self, with_time, with_thread):
|
||||
self.fmt = (('%(asctime)s ' if with_time else '') +
|
||||
'%(levelname)-8s ' +
|
||||
'[%(name)s:%(funcName)s]' +
|
||||
'[%(name)s:%(funcName)s' +
|
||||
(':%(lineno)s' if with_lineno else '') + ']' +
|
||||
('[%(threadName)s]' if with_thread else '') +
|
||||
' -- %(message)s')
|
||||
logging.Formatter.__init__(self, self.fmt)
|
||||
@@ -47,7 +48,8 @@ def setupLogging(colored=True, with_time=False, with_thread=False, filename=None
|
||||
def __init__(self, with_time, with_thread):
|
||||
self.fmt = (('%(asctime)s ' if with_time else '') +
|
||||
'-CC-%(levelname)-8s ' +
|
||||
BLUE_FONT + '[%(name)s:%(funcName)s]' +
|
||||
BLUE_FONT + '[%(name)s:%(funcName)s' +
|
||||
(':%(lineno)s' if with_lineno else '') + ']' +
|
||||
RESET_FONT + ('[%(threadName)s]' if with_thread else '') +
|
||||
' -- %(message)s')
|
||||
|
||||
|
||||
@@ -43,10 +43,13 @@ def strip_brackets(s):
|
||||
return s
|
||||
|
||||
|
||||
def clean_string(s):
|
||||
for c in sep[:-2]: # do not remove dashes ('-')
|
||||
s = s.replace(c, ' ')
|
||||
parts = s.split()
|
||||
def clean_string(st):
|
||||
for c in sep:
|
||||
# do not remove certain chars
|
||||
if c in ['-', ',']:
|
||||
continue
|
||||
st = st.replace(c, ' ')
|
||||
parts = st.split()
|
||||
result = ' '.join(p for p in parts if p != '')
|
||||
|
||||
# now also remove dashes on the outer part of the string
|
||||
|
||||
@@ -28,7 +28,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def found_property(node, name, confidence):
|
||||
node.guess = Guess({name: node.clean_value}, confidence=confidence)
|
||||
node.guess = Guess({name: node.clean_value}, confidence=confidence, raw=node.value)
|
||||
log.debug('Found with confidence %.2f: %s' % (confidence, node.guess))
|
||||
|
||||
|
||||
@@ -52,11 +52,17 @@ def format_guess(guess):
|
||||
|
||||
def find_and_split_node(node, strategy, logger):
|
||||
string = ' %s ' % node.value # add sentinels
|
||||
for matcher, confidence in strategy:
|
||||
for matcher, confidence, args, kwargs in strategy:
|
||||
all_args = [string]
|
||||
if getattr(matcher, 'use_node', False):
|
||||
result, span = matcher(string, node)
|
||||
all_args.append(node)
|
||||
if args:
|
||||
all_args.append(args)
|
||||
|
||||
if kwargs:
|
||||
result, span = matcher(*all_args, **kwargs)
|
||||
else:
|
||||
result, span = matcher(string)
|
||||
result, span = matcher(*all_args)
|
||||
|
||||
if result:
|
||||
# readjust span to compensate for sentinels
|
||||
@@ -69,7 +75,7 @@ def find_and_split_node(node, strategy, logger):
|
||||
if confidence is None:
|
||||
confidence = 1.0
|
||||
|
||||
guess = format_guess(Guess(result, confidence=confidence))
|
||||
guess = format_guess(Guess(result, confidence=confidence, raw=string[span[0] + 1:span[1] + 1]))
|
||||
msg = 'Found with confidence %.2f: %s' % (confidence, guess)
|
||||
(logger or log).debug(msg)
|
||||
|
||||
@@ -84,10 +90,12 @@ def find_and_split_node(node, strategy, logger):
|
||||
|
||||
|
||||
class SingleNodeGuesser(object):
|
||||
def __init__(self, guess_func, confidence, logger=None):
|
||||
def __init__(self, guess_func, confidence, logger, *args, **kwargs):
|
||||
self.guess_func = guess_func
|
||||
self.confidence = confidence
|
||||
self.logger = logger
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def process(self, mtree):
|
||||
# strategy is a list of pairs (guesser, confidence)
|
||||
@@ -95,7 +103,7 @@ class SingleNodeGuesser(object):
|
||||
# it will override it, otherwise it will leave the guess confidence
|
||||
# - if the guesser returns a simple dict as a guess and confidence is
|
||||
# specified, it will use it, or 1.0 otherwise
|
||||
strategy = [ (self.guess_func, self.confidence) ]
|
||||
strategy = [ (self.guess_func, self.confidence, self.args, self.kwargs) ]
|
||||
|
||||
for node in mtree.unidentified_leaves():
|
||||
find_and_split_node(node, strategy, self.logger)
|
||||
|
||||
@@ -45,4 +45,4 @@ def process(mtree):
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
node.guess = Guess(country=country, confidence=1.0)
|
||||
node.guess = Guess(country=country, confidence=1.0, raw=c)
|
||||
|
||||
@@ -40,27 +40,22 @@ def guess_episodes_rexps(string):
|
||||
for rexp, confidence, span_adjust in episode_rexps:
|
||||
match = re.search(rexp, string, re.IGNORECASE)
|
||||
if match:
|
||||
guess = Guess(match.groupdict(), confidence=confidence)
|
||||
span = (match.start() + span_adjust[0],
|
||||
span = (match.start() + span_adjust[0],
|
||||
match.end() + span_adjust[1])
|
||||
|
||||
# episodes which have a season > 30 are most likely errors
|
||||
# (Simpsons is at 24!)
|
||||
if int(guess.get('season', 0)) > 30:
|
||||
continue
|
||||
guess = Guess(match.groupdict(), confidence=confidence, raw=string[span[0]:span[1]])
|
||||
|
||||
# decide whether we have only a single episode number or an
|
||||
# episode list
|
||||
if guess.get('episodeNumber'):
|
||||
eplist = number_list(guess['episodeNumber'])
|
||||
guess.set('episodeNumber', eplist[0], confidence=confidence)
|
||||
guess.set('episodeNumber', eplist[0], confidence=confidence, raw=string[span[0]:span[1]])
|
||||
|
||||
if len(eplist) > 1:
|
||||
guess.set('episodeList', eplist, confidence=confidence)
|
||||
guess.set('episodeList', eplist, confidence=confidence, raw=string[span[0]:span[1]])
|
||||
|
||||
if guess.get('bonusNumber'):
|
||||
eplist = number_list(guess['bonusNumber'])
|
||||
guess.set('bonusNumber', eplist[0], confidence=confidence)
|
||||
guess.set('bonusNumber', eplist[0], confidence=confidence, raw=string[span[0]:span[1]])
|
||||
|
||||
return guess, span
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from guessit import Guess
|
||||
from guessit.patterns import (subtitle_exts, video_exts, episode_rexps,
|
||||
from guessit.patterns import (subtitle_exts, info_exts, video_exts, episode_rexps,
|
||||
find_properties, compute_canonical_form)
|
||||
from guessit.date import valid_year
|
||||
from guessit.textutils import clean_string
|
||||
@@ -53,12 +53,16 @@ def guess_filetype(mtree, filetype):
|
||||
filetype_container[0] = 'episode'
|
||||
elif filetype_container[0] == 'subtitle':
|
||||
filetype_container[0] = 'episodesubtitle'
|
||||
elif filetype_container[0] == 'info':
|
||||
filetype_container[0] = 'episodeinfo'
|
||||
|
||||
def upgrade_movie():
|
||||
if filetype_container[0] == 'video':
|
||||
filetype_container[0] = 'movie'
|
||||
elif filetype_container[0] == 'subtitle':
|
||||
filetype_container[0] = 'moviesubtitle'
|
||||
elif filetype_container[0] == 'info':
|
||||
filetype_container[0] = 'movieinfo'
|
||||
|
||||
def upgrade_subtitle():
|
||||
if 'movie' in filetype_container[0]:
|
||||
@@ -68,6 +72,14 @@ def guess_filetype(mtree, filetype):
|
||||
else:
|
||||
filetype_container[0] = 'subtitle'
|
||||
|
||||
def upgrade_info():
|
||||
if 'movie' in filetype_container[0]:
|
||||
filetype_container[0] = 'movieinfo'
|
||||
elif 'episode' in filetype_container[0]:
|
||||
filetype_container[0] = 'episodeinfo'
|
||||
else:
|
||||
filetype_container[0] = 'info'
|
||||
|
||||
def upgrade(type='unknown'):
|
||||
if filetype_container[0] == 'autodetect':
|
||||
filetype_container[0] = type
|
||||
@@ -78,6 +90,9 @@ def guess_filetype(mtree, filetype):
|
||||
if fileext in subtitle_exts:
|
||||
upgrade_subtitle()
|
||||
other = { 'container': fileext }
|
||||
elif fileext in info_exts:
|
||||
upgrade_info()
|
||||
other = { 'container': fileext }
|
||||
elif fileext in video_exts:
|
||||
upgrade(type='video')
|
||||
other = { 'container': fileext }
|
||||
@@ -104,17 +119,20 @@ def guess_filetype(mtree, filetype):
|
||||
fname = clean_string(filename).lower()
|
||||
for m in MOVIES:
|
||||
if m in fname:
|
||||
log.debug('Found in exception list of movies -> type = movie')
|
||||
upgrade_movie()
|
||||
for s in SERIES:
|
||||
if s in fname:
|
||||
log.debug('Found in exception list of series -> type = episode')
|
||||
upgrade_episode()
|
||||
|
||||
# now look whether there are some specific hints for episode vs movie
|
||||
if filetype_container[0] in ('video', 'subtitle'):
|
||||
if filetype_container[0] in ('video', 'subtitle', 'info'):
|
||||
# if we have an episode_rexp (eg: s02e13), it is an episode
|
||||
for rexp, _, _ in episode_rexps:
|
||||
match = re.search(rexp, filename, re.IGNORECASE)
|
||||
if match:
|
||||
log.debug('Found matching regexp: "%s" (string = "%s") -> type = episode', rexp, match.group())
|
||||
upgrade_episode()
|
||||
break
|
||||
|
||||
@@ -133,24 +151,29 @@ def guess_filetype(mtree, filetype):
|
||||
possible = False
|
||||
|
||||
if possible:
|
||||
log.debug('Found possible episode number: %s (from string "%s") -> type = episode', epnumber, match.group())
|
||||
upgrade_episode()
|
||||
|
||||
# if we have certain properties characteristic of episodes, it is an ep
|
||||
for prop, value, _, _ in find_properties(filename):
|
||||
log.debug('prop: %s = %s' % (prop, value))
|
||||
if prop == 'episodeFormat':
|
||||
log.debug('Found characteristic property of episodes: %s = "%s"', prop, value)
|
||||
upgrade_episode()
|
||||
break
|
||||
|
||||
elif compute_canonical_form('format', value) == 'DVB':
|
||||
log.debug('Found characteristic property of episodes: %s = "%s"', prop, value)
|
||||
upgrade_episode()
|
||||
break
|
||||
|
||||
# origin-specific type
|
||||
if 'tvu.org.ru' in filename:
|
||||
log.debug('Found characteristic property of episodes: %s = "%s"', prop, value)
|
||||
upgrade_episode()
|
||||
|
||||
# if no episode info found, assume it's a movie
|
||||
log.debug('Nothing characteristic found, assuming type = movie')
|
||||
upgrade_movie()
|
||||
|
||||
filetype = filetype_container[0]
|
||||
|
||||
@@ -22,22 +22,34 @@ from __future__ import unicode_literals
|
||||
from guessit import Guess
|
||||
from guessit.transfo import SingleNodeGuesser
|
||||
from guessit.language import search_language
|
||||
from guessit.textutils import clean_string, find_words
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def guess_language(string):
|
||||
language, span, confidence = search_language(string)
|
||||
def guess_language(string, node, skip=None):
|
||||
if skip:
|
||||
relative_skip = []
|
||||
for entry in skip:
|
||||
node_idx = entry['node_idx']
|
||||
span = entry['span']
|
||||
if node_idx == node.node_idx[:len(node_idx)]:
|
||||
relative_span = (span[0] - node.offset + 1, span[1] - node.offset + 1)
|
||||
relative_skip.append(relative_span)
|
||||
skip = relative_skip
|
||||
|
||||
language, span, confidence = search_language(string, skip=skip)
|
||||
if language:
|
||||
return (Guess({'language': language},
|
||||
confidence=confidence),
|
||||
confidence=confidence,
|
||||
raw= string[span[0]:span[1]]),
|
||||
span)
|
||||
|
||||
return None, None
|
||||
|
||||
guess_language.use_node = True
|
||||
|
||||
def process(mtree):
|
||||
SingleNodeGuesser(guess_language, None, log).process(mtree)
|
||||
|
||||
def process(mtree, *args, **kwargs):
|
||||
SingleNodeGuesser(guess_language, None, log, *args, **kwargs).process(mtree)
|
||||
# Note: 'language' is promoted to 'subtitleLanguage' in the post_process transfo
|
||||
|
||||
@@ -29,7 +29,8 @@ log = logging.getLogger(__name__)
|
||||
def process(mtree):
|
||||
def found_property(node, name, value, confidence):
|
||||
node.guess = Guess({ name: value },
|
||||
confidence=confidence)
|
||||
confidence=confidence,
|
||||
raw=value)
|
||||
log.debug('Found with confidence %.2f: %s' % (confidence, node.guess))
|
||||
|
||||
def found_title(node, confidence):
|
||||
|
||||
@@ -38,9 +38,10 @@ def guess_video_rexps(string):
|
||||
# the soonest that we can catch it)
|
||||
if metadata.get('cdNumberTotal', -1) is None:
|
||||
del metadata['cdNumberTotal']
|
||||
return (Guess(metadata, confidence=confidence),
|
||||
(match.start() + span_adjust[0],
|
||||
match.end() + span_adjust[1] - 2))
|
||||
span = (match.start() + span_adjust[0],
|
||||
match.end() + span_adjust[1] - 2)
|
||||
return (Guess(metadata, confidence=confidence, raw=string[span[0]:span[1]]),
|
||||
span)
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ def guess_weak_episodes_rexps(string, node):
|
||||
continue
|
||||
return Guess({ 'season': season,
|
||||
'episodeNumber': epnum },
|
||||
confidence=0.6), span
|
||||
confidence=0.6, raw=string[span[0]:span[1]]), span
|
||||
else:
|
||||
return Guess(metadata, confidence=0.3), span
|
||||
return Guess(metadata, confidence=0.3, raw=string[span[0]:span[1]]), span
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user