Merge branch 'develop' into tv
This commit is contained in:
@@ -46,7 +46,8 @@ class Automation(Plugin):
|
||||
break
|
||||
|
||||
movie_dict = fireEvent('media.get', movie_id, single = True)
|
||||
fireEvent('movie.searcher.single', movie_dict)
|
||||
if movie_dict:
|
||||
fireEvent('movie.searcher.single', movie_dict)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import threading
|
||||
from urllib import quote
|
||||
from urlparse import urlparse
|
||||
import glob
|
||||
@@ -10,7 +11,8 @@ import traceback
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import ss, toSafeString, \
|
||||
toUnicode, sp
|
||||
from couchpotato.core.helpers.variable import getExt, md5, isLocalIP, scanForPassword, tryInt, getIdentifier
|
||||
from couchpotato.core.helpers.variable import getExt, md5, isLocalIP, scanForPassword, tryInt, getIdentifier, \
|
||||
randomString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
import requests
|
||||
@@ -35,6 +37,8 @@ class Plugin(object):
|
||||
_needs_shutdown = False
|
||||
_running = None
|
||||
|
||||
_locks = {}
|
||||
|
||||
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20130519 Firefox/24.0'
|
||||
http_last_use = {}
|
||||
http_time_between_calls = 0
|
||||
@@ -118,15 +122,31 @@ class Plugin(object):
|
||||
if os.path.exists(path):
|
||||
log.debug('%s already exists, overwriting file with new version', path)
|
||||
|
||||
try:
|
||||
f = open(path, 'w+' if not binary else 'w+b')
|
||||
f.write(content)
|
||||
f.close()
|
||||
os.chmod(path, Env.getPermission('file'))
|
||||
except:
|
||||
log.error('Unable writing to file "%s": %s', (path, traceback.format_exc()))
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
write_type = 'w+' if not binary else 'w+b'
|
||||
|
||||
# Stream file using response object
|
||||
if isinstance(content, requests.models.Response):
|
||||
|
||||
# Write file to temp
|
||||
with open('%s.tmp' % path, write_type) as f:
|
||||
for chunk in content.iter_content(chunk_size = 1048576):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
f.write(chunk)
|
||||
f.flush()
|
||||
|
||||
# Rename to destination
|
||||
os.rename('%s.tmp' % path, path)
|
||||
|
||||
else:
|
||||
try:
|
||||
f = open(path, write_type)
|
||||
f.write(content)
|
||||
f.close()
|
||||
os.chmod(path, Env.getPermission('file'))
|
||||
except:
|
||||
log.error('Unable writing to file "%s": %s', (path, traceback.format_exc()))
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
|
||||
def makeDir(self, path):
|
||||
path = sp(path)
|
||||
@@ -143,21 +163,17 @@ class Plugin(object):
|
||||
folder = sp(folder)
|
||||
|
||||
for item in os.listdir(folder):
|
||||
full_folder = os.path.join(folder, item)
|
||||
full_folder = sp(os.path.join(folder, item))
|
||||
|
||||
if not only_clean or (item in only_clean and os.path.isdir(full_folder)):
|
||||
|
||||
for root, dirs, files in os.walk(full_folder):
|
||||
for subfolder, dirs, files in os.walk(full_folder, topdown = False):
|
||||
|
||||
for dir_name in dirs:
|
||||
full_path = os.path.join(root, dir_name)
|
||||
|
||||
if len(os.listdir(full_path)) == 0:
|
||||
try:
|
||||
os.rmdir(full_path)
|
||||
except:
|
||||
if show_error:
|
||||
log.info2('Couldn\'t remove directory %s: %s', (full_path, traceback.format_exc()))
|
||||
try:
|
||||
os.rmdir(subfolder)
|
||||
except:
|
||||
if show_error:
|
||||
log.info2('Couldn\'t remove directory %s: %s', (subfolder, traceback.format_exc()))
|
||||
|
||||
try:
|
||||
os.rmdir(folder)
|
||||
@@ -166,7 +182,7 @@ class Plugin(object):
|
||||
log.error('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
|
||||
|
||||
# http request
|
||||
def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True):
|
||||
def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True, stream = False):
|
||||
url = quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
|
||||
|
||||
if not headers: headers = {}
|
||||
@@ -177,10 +193,10 @@ class Plugin(object):
|
||||
host = '%s%s' % (parsed_url.hostname, (':' + str(parsed_url.port) if parsed_url.port else ''))
|
||||
|
||||
headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
|
||||
headers['Host'] = headers.get('Host', host)
|
||||
headers['Host'] = headers.get('Host', None)
|
||||
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
headers['Connection'] = headers.get('Connection', 'keep-alive')
|
||||
headers['Connection'] = headers.get('Connection', 'close')
|
||||
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
|
||||
|
||||
r = Env.get('http_opener')
|
||||
@@ -198,6 +214,7 @@ class Plugin(object):
|
||||
del self.http_failed_disabled[host]
|
||||
|
||||
self.wait(host)
|
||||
status_code = None
|
||||
try:
|
||||
|
||||
kwargs = {
|
||||
@@ -206,14 +223,16 @@ class Plugin(object):
|
||||
'timeout': timeout,
|
||||
'files': files,
|
||||
'verify': False, #verify_ssl, Disable for now as to many wrongly implemented certificates..
|
||||
'stream': stream,
|
||||
}
|
||||
method = 'post' if len(data) > 0 or files else 'get'
|
||||
|
||||
log.info('Opening url: %s %s, data: %s', (method, url, [x for x in data.keys()] if isinstance(data, dict) else 'with data'))
|
||||
response = r.request(method, url, **kwargs)
|
||||
|
||||
status_code = response.status_code
|
||||
if response.status_code == requests.codes.ok:
|
||||
data = response.content
|
||||
data = response if stream else response.content
|
||||
else:
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -224,6 +243,12 @@ class Plugin(object):
|
||||
|
||||
# Save failed requests by hosts
|
||||
try:
|
||||
|
||||
# To many requests
|
||||
if status_code in [429]:
|
||||
self.http_failed_request[host] = 1
|
||||
self.http_failed_disabled[host] = time.time()
|
||||
|
||||
if not self.http_failed_request.get(host):
|
||||
self.http_failed_request[host] = 1
|
||||
else:
|
||||
@@ -254,8 +279,8 @@ class Plugin(object):
|
||||
wait = (last_use - now) + self.http_time_between_calls
|
||||
|
||||
if wait > 0:
|
||||
log.debug('Waiting for %s, %d seconds', (self.getName(), wait))
|
||||
time.sleep(wait)
|
||||
log.debug('Waiting for %s, %d seconds', (self.getName(), max(1, wait)))
|
||||
time.sleep(min(wait, 30))
|
||||
|
||||
def beforeCall(self, handler):
|
||||
self.isRunning('%s.%s' % (self.getName(), handler.__name__))
|
||||
@@ -322,9 +347,9 @@ class Plugin(object):
|
||||
Env.get('cache').set(cache_key_md5, value, timeout)
|
||||
return value
|
||||
|
||||
def createNzbName(self, data, media):
|
||||
def createNzbName(self, data, media, unique_tag = False):
|
||||
release_name = data.get('name')
|
||||
tag = self.cpTag(media)
|
||||
tag = self.cpTag(media, unique_tag = unique_tag)
|
||||
|
||||
# Check if password is filename
|
||||
name_password = scanForPassword(data.get('name'))
|
||||
@@ -337,18 +362,26 @@ class Plugin(object):
|
||||
max_length = 127 - len(tag) # Some filesystems don't support 128+ long filenames
|
||||
return '%s%s' % (toSafeString(toUnicode(release_name)[:max_length]), tag)
|
||||
|
||||
def createFileName(self, data, filedata, media):
|
||||
name = self.createNzbName(data, media)
|
||||
def createFileName(self, data, filedata, media, unique_tag = False):
|
||||
name = self.createNzbName(data, media, unique_tag = unique_tag)
|
||||
if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
||||
return '%s.%s' % (name, 'rar')
|
||||
return '%s.%s' % (name, data.get('protocol'))
|
||||
|
||||
def cpTag(self, media):
|
||||
if Env.setting('enabled', 'renamer'):
|
||||
identifier = getIdentifier(media)
|
||||
return '.cp(' + identifier + ')' if identifier else ''
|
||||
def cpTag(self, media, unique_tag = False):
|
||||
|
||||
return ''
|
||||
tag = ''
|
||||
if Env.setting('enabled', 'renamer') or unique_tag:
|
||||
identifier = getIdentifier(media) or ''
|
||||
unique_tag = ', ' + randomString() if unique_tag else ''
|
||||
|
||||
tag = '.cp('
|
||||
tag += identifier
|
||||
tag += ', ' if unique_tag and identifier else ''
|
||||
tag += randomString() if unique_tag else ''
|
||||
tag += ')'
|
||||
|
||||
return tag if len(tag) > 7 else ''
|
||||
|
||||
def checkFilesChanged(self, files, unchanged_for = 60):
|
||||
now = time.time()
|
||||
@@ -393,3 +426,19 @@ class Plugin(object):
|
||||
|
||||
def isEnabled(self):
|
||||
return self.conf(self.enabled_option) or self.conf(self.enabled_option) is None
|
||||
|
||||
def acquireLock(self, key):
|
||||
|
||||
lock = self._locks.get(key)
|
||||
if not lock:
|
||||
self._locks[key] = threading.RLock()
|
||||
|
||||
log.debug('Acquiring lock: %s', key)
|
||||
self._locks.get(key).acquire()
|
||||
|
||||
def releaseLock(self, key):
|
||||
|
||||
lock = self._locks.get(key)
|
||||
if lock:
|
||||
log.debug('Releasing lock: %s', key)
|
||||
self._locks.get(key).release()
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import ctypes
|
||||
import os
|
||||
import string
|
||||
import traceback
|
||||
import time
|
||||
|
||||
from couchpotato import CPLog
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import sp, ss, toUnicode
|
||||
from couchpotato.core.helpers.variable import getUserDir
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
if os.name == 'nt':
|
||||
@@ -53,9 +59,9 @@ class FileBrowser(Plugin):
|
||||
dirs = []
|
||||
path = sp(path)
|
||||
for f in os.listdir(path):
|
||||
p = os.path.join(path, f)
|
||||
p = sp(os.path.join(path, f))
|
||||
if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)):
|
||||
dirs.append(p + os.path.sep)
|
||||
dirs.append(toUnicode('%s%s' % (p, os.path.sep)))
|
||||
|
||||
return sorted(dirs)
|
||||
|
||||
@@ -66,8 +72,8 @@ class FileBrowser(Plugin):
|
||||
|
||||
driveletters = []
|
||||
for drive in string.ascii_uppercase:
|
||||
if win32file.GetDriveType(drive + ":") in [win32file.DRIVE_FIXED, win32file.DRIVE_REMOTE, win32file.DRIVE_RAMDISK, win32file.DRIVE_REMOVABLE]:
|
||||
driveletters.append(drive + ":\\")
|
||||
if win32file.GetDriveType(drive + ':') in [win32file.DRIVE_FIXED, win32file.DRIVE_REMOTE, win32file.DRIVE_RAMDISK, win32file.DRIVE_REMOVABLE]:
|
||||
driveletters.append(drive + ':\\')
|
||||
|
||||
return driveletters
|
||||
|
||||
@@ -100,14 +106,19 @@ class FileBrowser(Plugin):
|
||||
|
||||
|
||||
def is_hidden(self, filepath):
|
||||
name = os.path.basename(os.path.abspath(filepath))
|
||||
name = ss(os.path.basename(os.path.abspath(filepath)))
|
||||
return name.startswith('.') or self.has_hidden_attribute(filepath)
|
||||
|
||||
def has_hidden_attribute(self, filepath):
|
||||
|
||||
result = False
|
||||
try:
|
||||
attrs = ctypes.windll.kernel32.GetFileAttributesW(six.text_type(filepath)) #@UndefinedVariable
|
||||
attrs = ctypes.windll.kernel32.GetFileAttributesW(sp(filepath)) #@UndefinedVariable
|
||||
assert attrs != -1
|
||||
result = bool(attrs & 2)
|
||||
except (AttributeError, AssertionError):
|
||||
result = False
|
||||
pass
|
||||
except:
|
||||
log.error('Failed getting hidden attribute: %s', traceback.format_exc())
|
||||
|
||||
return result
|
||||
|
||||
@@ -27,7 +27,7 @@ class CategoryPlugin(Plugin):
|
||||
'desc': 'List all available categories',
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'list': array, categories
|
||||
'categories': array, categories
|
||||
}"""}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import date
|
||||
import random as rndm
|
||||
import time
|
||||
from CodernityDB.database import RecordDeleted
|
||||
|
||||
from couchpotato import get_db
|
||||
from couchpotato.api import addApiView
|
||||
@@ -48,7 +48,6 @@ class Dashboard(Plugin):
|
||||
active_ids = [x['_id'] for x in fireEvent('media.with_status', 'active', with_doc = False, single = True)]
|
||||
|
||||
medias = []
|
||||
now_year = date.today().year
|
||||
|
||||
if len(active_ids) > 0:
|
||||
|
||||
@@ -60,7 +59,11 @@ class Dashboard(Plugin):
|
||||
rndm.shuffle(active_ids)
|
||||
|
||||
for media_id in active_ids:
|
||||
media = db.get('id', media_id)
|
||||
try:
|
||||
media = db.get('id', media_id)
|
||||
except RecordDeleted:
|
||||
log.debug('Record already deleted: %s', media_id)
|
||||
continue
|
||||
|
||||
pp = profile_pre.get(media.get('profile_id'))
|
||||
if not pp: continue
|
||||
@@ -69,23 +72,26 @@ class Dashboard(Plugin):
|
||||
coming_soon = False
|
||||
|
||||
# Theater quality
|
||||
if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, media['info'].get('year'), single = True):
|
||||
coming_soon = True
|
||||
elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, media['info'].get('year'), single = True):
|
||||
coming_soon = True
|
||||
if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, media['info']['year'], single = True):
|
||||
coming_soon = 'theater'
|
||||
elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, media['info']['year'], single = True):
|
||||
coming_soon = 'dvd'
|
||||
|
||||
if coming_soon:
|
||||
|
||||
# Don't list older movies
|
||||
if ((not late and (media['info'].get('year') >= now_year - 1) and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or
|
||||
(late and (media['info'].get('year') < now_year - 1 or (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200)))):
|
||||
eta_date = eta.get(coming_soon)
|
||||
eta_3month_passed = eta_date < (now - 7862400) # Release was more than 3 months ago
|
||||
|
||||
if (not late and not eta_3month_passed) or \
|
||||
(late and eta_3month_passed):
|
||||
|
||||
add = True
|
||||
|
||||
# Check if it doesn't have any releases
|
||||
if late:
|
||||
media['releases'] = fireEvent('release.for_media', media['_id'], single = True)
|
||||
|
||||
|
||||
for release in media.get('releases'):
|
||||
if release.get('status') in ['snatched', 'available', 'seeding', 'downloaded']:
|
||||
add = False
|
||||
|
||||
@@ -4,7 +4,7 @@ import traceback
|
||||
from couchpotato import get_db
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss, sp
|
||||
from couchpotato.core.helpers.variable import md5, getExt, isSubFolder
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -59,13 +59,18 @@ class FileManager(Plugin):
|
||||
log.error('Failed removing unused file: %s', traceback.format_exc())
|
||||
|
||||
def showCacheFile(self, route, **kwargs):
|
||||
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': Env.get('cache_dir')})])
|
||||
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': toUnicode(Env.get('cache_dir'))})])
|
||||
|
||||
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = None):
|
||||
if not urlopen_kwargs: urlopen_kwargs = {}
|
||||
|
||||
# Return response object to stream download
|
||||
urlopen_kwargs['stream'] = True
|
||||
|
||||
if not dest: # to Cache
|
||||
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
|
||||
dest = os.path.join(Env.get('cache_dir'), ss('%s.%s' % (md5(url), getExt(url))))
|
||||
|
||||
dest = sp(dest)
|
||||
|
||||
if not overwrite and os.path.isfile(dest):
|
||||
return dest
|
||||
@@ -107,4 +112,4 @@ class FileManager(Plugin):
|
||||
else:
|
||||
log.info('Subfolder test succeeded')
|
||||
|
||||
return failed == 0
|
||||
return failed == 0
|
||||
|
||||
@@ -241,7 +241,7 @@ Running on: ...\n\
|
||||
'href': 'https://github.com/RuudBurger/CouchPotatoServer/blob/develop/contributing.md'
|
||||
}),
|
||||
new Element('span', {
|
||||
'text': ' before posting (kittens die if you don\'t), then copy the text below.'
|
||||
'html': ' before posting, then copy the text below and <strong>FILL IN</strong> the dots.'
|
||||
})
|
||||
),
|
||||
textarea = new Element('textarea', {
|
||||
|
||||
@@ -123,7 +123,7 @@ class Manage(Plugin):
|
||||
fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder)
|
||||
|
||||
onFound = self.createAddToLibrary(folder, added_identifiers)
|
||||
fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, on_found = onFound, single = True)
|
||||
fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, check_file_date = False, on_found = onFound, single = True)
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
@@ -165,7 +165,7 @@ class Manage(Plugin):
|
||||
already_used = used_files.get(release_file)
|
||||
|
||||
if already_used:
|
||||
release_id = release['_id'] if already_used.get('last_edit', 0) < release.get('last_edit', 0) else already_used['_id']
|
||||
release_id = release['_id'] if already_used.get('last_edit', 0) > release.get('last_edit', 0) else already_used['_id']
|
||||
if release_id not in deleted_releases:
|
||||
fireEvent('release.delete', release_id, single = True)
|
||||
deleted_releases.append(release_id)
|
||||
@@ -190,6 +190,7 @@ class Manage(Plugin):
|
||||
|
||||
delete_me = {}
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
for folder in self.in_progress:
|
||||
if self.in_progress[folder]['to_go'] <= 0:
|
||||
delete_me[folder] = True
|
||||
@@ -233,7 +234,8 @@ class Manage(Plugin):
|
||||
total = self.in_progress[folder]['total']
|
||||
movie_dict = fireEvent('media.get', identifier, single = True)
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict))
|
||||
if movie_dict:
|
||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict))
|
||||
|
||||
return afterUpdate
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ class ProfilePlugin(Plugin):
|
||||
'label': toUnicode(kwargs.get('label')),
|
||||
'order': tryInt(kwargs.get('order', 999)),
|
||||
'core': kwargs.get('core', False),
|
||||
'minimum_score': tryInt(kwargs.get('minimum_score', 1)),
|
||||
'qualities': [],
|
||||
'wait_for': [],
|
||||
'stop_after': [],
|
||||
@@ -217,6 +218,7 @@ class ProfilePlugin(Plugin):
|
||||
'label': toUnicode(profile.get('label')),
|
||||
'order': order,
|
||||
'qualities': profile.get('qualities'),
|
||||
'minimum_score': 1,
|
||||
'finish': [],
|
||||
'wait_for': [],
|
||||
'stop_after': [],
|
||||
|
||||
@@ -51,6 +51,11 @@
|
||||
margin: 0 5px !important;
|
||||
}
|
||||
|
||||
.profile .wait_for .minimum_score_input {
|
||||
width: 40px !important;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.profile .types {
|
||||
padding: 0;
|
||||
margin: 0 20px 0 -4px;
|
||||
|
||||
@@ -53,12 +53,21 @@ var Profile = new Class({
|
||||
}),
|
||||
new Element('span', {'text':'day(s) for a better quality '}),
|
||||
new Element('span.advanced', {'text':'and keep searching'}),
|
||||
|
||||
// "After a checked quality is found and downloaded, continue searching for even better quality releases for the entered number of days."
|
||||
new Element('input.inlay.xsmall.stop_after_input.advanced', {
|
||||
'type':'text',
|
||||
'value': data.stop_after && data.stop_after.length > 0 ? data.stop_after[0] : 0
|
||||
}),
|
||||
new Element('span.advanced', {'text':'day(s) for a better (checked) quality.'})
|
||||
new Element('span.advanced', {'text':'day(s) for a better (checked) quality.'}),
|
||||
|
||||
// Minimum score of
|
||||
new Element('span.advanced', {'html':'<br/>Releases need a minimum score of'}),
|
||||
new Element('input.advanced.inlay.xsmall.minimum_score_input', {
|
||||
'size': 4,
|
||||
'type':'text',
|
||||
'value': data.minimum_score || 1
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -126,6 +135,7 @@ var Profile = new Class({
|
||||
'label' : self.el.getElement('.quality_label input').get('value'),
|
||||
'wait_for' : self.el.getElement('.wait_for_input').get('value'),
|
||||
'stop_after' : self.el.getElement('.stop_after_input').get('value'),
|
||||
'minimum_score' : self.el.getElement('.minimum_score_input').get('value'),
|
||||
'types': []
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from math import fabs, ceil
|
||||
import traceback
|
||||
import re
|
||||
|
||||
@@ -6,7 +7,7 @@ from couchpotato import get_db
|
||||
from couchpotato.api import addApiView
|
||||
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.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
|
||||
@@ -22,17 +23,17 @@ class QualityPlugin(Plugin):
|
||||
}
|
||||
|
||||
qualities = [
|
||||
{'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':[]},
|
||||
{'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']},
|
||||
{'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']},
|
||||
{'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
|
||||
@@ -205,14 +206,15 @@ class QualityPlugin(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def guess(self, files, extra = None, size = None):
|
||||
def guess(self, files, extra = None, size = None, use_cache = True):
|
||||
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
|
||||
if use_cache:
|
||||
cached = self.getCache(cache_key)
|
||||
if cached and len(extra) == 0:
|
||||
return cached
|
||||
|
||||
qualities = self.all()
|
||||
|
||||
@@ -224,6 +226,10 @@ class QualityPlugin(Plugin):
|
||||
'3d': {}
|
||||
}
|
||||
|
||||
# Use metadata titles as extra check
|
||||
if extra and extra.get('titles'):
|
||||
files.extend(extra.get('titles'))
|
||||
|
||||
for cur_file in files:
|
||||
words = re.split('\W+', cur_file.lower())
|
||||
name_year = fireEvent('scanner.name_year', cur_file, file_name = cur_file, single = True)
|
||||
@@ -236,7 +242,7 @@ class QualityPlugin(Plugin):
|
||||
contains_score = self.containsTagScore(quality, words, cur_file)
|
||||
threedscore = self.contains3D(quality, threed_words, cur_file) if quality.get('allow_3d') else (0, None)
|
||||
|
||||
self.calcScore(score, quality, contains_score, threedscore)
|
||||
self.calcScore(score, quality, contains_score, threedscore, penalty = contains_score)
|
||||
|
||||
size_scores = []
|
||||
for quality in qualities:
|
||||
@@ -248,11 +254,11 @@ class QualityPlugin(Plugin):
|
||||
if size_score > 0:
|
||||
size_scores.append(quality)
|
||||
|
||||
self.calcScore(score, quality, size_score + loose_score, penalty = False)
|
||||
self.calcScore(score, quality, size_score + loose_score)
|
||||
|
||||
# Add additional size score if only 1 size validated
|
||||
if len(size_scores) == 1:
|
||||
self.calcScore(score, size_scores[0], 10, penalty = False)
|
||||
self.calcScore(score, size_scores[0], 8)
|
||||
del size_scores
|
||||
|
||||
# Return nothing if all scores are <= 0
|
||||
@@ -277,19 +283,21 @@ class QualityPlugin(Plugin):
|
||||
|
||||
def containsTagScore(self, quality, words, cur_file = ''):
|
||||
cur_file = ss(cur_file)
|
||||
score = 0
|
||||
score = 0.0
|
||||
|
||||
extension = words[-1]
|
||||
words = words[:-1]
|
||||
|
||||
points = {
|
||||
'identifier': 10,
|
||||
'label': 10,
|
||||
'alternative': 9,
|
||||
'tags': 9,
|
||||
'ext': 3,
|
||||
'identifier': 20,
|
||||
'label': 20,
|
||||
'alternative': 20,
|
||||
'tags': 11,
|
||||
'ext': 5,
|
||||
}
|
||||
|
||||
scored_on = []
|
||||
|
||||
# Check alt and tags
|
||||
for tag_type in ['identifier', 'alternative', 'tags', 'label']:
|
||||
qualities = quality.get(tag_type, [])
|
||||
@@ -301,13 +309,12 @@ class QualityPlugin(Plugin):
|
||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||
score += points.get(tag_type)
|
||||
|
||||
if isinstance(alt, (str, unicode)) and ss(alt.lower()) in words:
|
||||
if isinstance(alt, (str, unicode)) and ss(alt.lower()) in words and ss(alt.lower()) not in scored_on:
|
||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||
score += points.get(tag_type) / 2
|
||||
score += points.get(tag_type)
|
||||
|
||||
if list(set(qualities) & set(words)):
|
||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||
score += points.get(tag_type)
|
||||
# Don't score twice on same tag
|
||||
scored_on.append(ss(alt).lower())
|
||||
|
||||
# Check extention
|
||||
for ext in quality.get('ext', []):
|
||||
@@ -343,7 +350,7 @@ class QualityPlugin(Plugin):
|
||||
# Check width resolution, range 20
|
||||
if quality.get('width') and (quality.get('width') - 20) <= extra.get('resolution_width', 0) <= (quality.get('width') + 20):
|
||||
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width'), extra.get('resolution_width', 0)))
|
||||
score += 5
|
||||
score += 10
|
||||
|
||||
# Check height resolution, range 20
|
||||
if quality.get('height') and (quality.get('height') - 20) <= extra.get('resolution_height', 0) <= (quality.get('height') + 20):
|
||||
@@ -363,15 +370,28 @@ class QualityPlugin(Plugin):
|
||||
|
||||
if size:
|
||||
|
||||
if tryInt(quality['size_min']) <= tryInt(size) <= tryInt(quality['size_max']):
|
||||
log.debug('Found %s via release size: %s MB < %s MB < %s MB', (quality['identifier'], quality['size_min'], size, quality['size_max']))
|
||||
score += 5
|
||||
size = tryFloat(size)
|
||||
size_min = tryFloat(quality['size_min'])
|
||||
size_max = tryFloat(quality['size_max'])
|
||||
|
||||
if size_min <= size <= size_max:
|
||||
log.debug('Found %s via release size: %s MB < %s MB < %s MB', (quality['identifier'], size_min, size, size_max))
|
||||
|
||||
proc_range = size_max - size_min
|
||||
size_diff = size - size_min
|
||||
size_proc = (size_diff / proc_range)
|
||||
|
||||
median_diff = quality['median_size'] - size_min
|
||||
median_proc = (median_diff / proc_range)
|
||||
|
||||
max_points = 8
|
||||
score += ceil(max_points - (fabs(size_proc - median_proc) * max_points))
|
||||
else:
|
||||
score -= 5
|
||||
|
||||
return score
|
||||
|
||||
def calcScore(self, score, quality, add_score, threedscore = (0, None), penalty = True):
|
||||
def calcScore(self, score, quality, add_score, threedscore = (0, None), penalty = 0):
|
||||
|
||||
score[quality['identifier']]['score'] += add_score
|
||||
|
||||
@@ -390,11 +410,11 @@ class QualityPlugin(Plugin):
|
||||
|
||||
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
|
||||
score[allow]['score'] -= ((penalty * 2) if self.cached_order[allow] < self.cached_order[quality['identifier']] else penalty) * 2
|
||||
|
||||
# Give panelty for all lower qualities
|
||||
for q in self.qualities[self.order.index(quality.get('identifier'))+1:]:
|
||||
if score.get(q.get('identifier')):
|
||||
# Give panelty for all other qualities
|
||||
for q in self.qualities:
|
||||
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):
|
||||
@@ -462,10 +482,12 @@ class QualityPlugin(Plugin):
|
||||
'Movie Monuments 2013 BrRip 1080p': {'size': 1800, 'quality': 'brrip'},
|
||||
'Movie Monuments 2013 BrRip 720p': {'size': 1300, 'quality': 'brrip'},
|
||||
'The.Movie.2014.3D.1080p.BluRay.AVC.DTS-HD.MA.5.1-GroupName': {'size': 30000, 'quality': 'bd50', 'is_3d': True},
|
||||
'/home/namehou/Movie Monuments (2013)/Movie Monuments.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': False},
|
||||
'/home/namehou/Movie Monuments (2013)/Movie Monuments Full-OU.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': True},
|
||||
'/home/namehou/Movie Monuments (2012)/Movie Monuments.mkv': {'size': 5500, 'quality': '720p', 'is_3d': False},
|
||||
'/home/namehou/Movie Monuments (2012)/Movie Monuments Full-OU.mkv': {'size': 5500, 'quality': '720p', 'is_3d': True},
|
||||
'/home/namehou/Movie Monuments (2013)/Movie Monuments.mkv': {'size': 10000, 'quality': '1080p', 'is_3d': False},
|
||||
'/home/namehou/Movie Monuments (2013)/Movie Monuments Full-OU.mkv': {'size': 10000, 'quality': '1080p', 'is_3d': True},
|
||||
'/volume1/Public/3D/Moviename/Moviename (2009).3D.SBS.ts': {'size': 7500, 'quality': '1080p', 'is_3d': True},
|
||||
'/volume1/Public/Moviename/Moviename (2009).ts': {'size': 5500, 'quality': '1080p'},
|
||||
'/volume1/Public/Moviename/Moviename (2009).ts': {'size': 7500, 'quality': '1080p'},
|
||||
'/movies/BluRay HDDVD H.264 MKV 720p EngSub/QuiQui le fou (criterion collection #123, 1915)/QuiQui le fou (1915) 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p'},
|
||||
'C:\\movies\QuiQui le fou (collection #123, 1915)\QuiQui le fou (1915) 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p'},
|
||||
'C:\\movies\QuiQui le fou (collection #123, 1915)\QuiQui le fou (1915) half-sbs 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p', 'is_3d': True},
|
||||
@@ -474,12 +496,24 @@ class QualityPlugin(Plugin):
|
||||
'Movie Name (2014).mp4': {'size': 750, 'quality': 'brrip'},
|
||||
'Moviename.2014.720p.R6.WEB-DL.x264.AC3-xyz': {'size': 750, 'quality': 'r5'},
|
||||
'Movie name 2014 New Source 720p HDCAM x264 AC3 xyz': {'size': 750, 'quality': 'cam'},
|
||||
'Movie.Name.2014.720p.HD.TS.AC3.x264': {'size': 750, 'quality': 'ts'}
|
||||
'Movie.Name.2014.720p.HD.TS.AC3.x264': {'size': 750, 'quality': 'ts'},
|
||||
'Movie.Name.2014.1080p.HDrip.x264.aac-ReleaseGroup': {'size': 7000, 'quality': 'brrip'},
|
||||
'Movie.Name.2014.HDCam.Chinese.Subs-ReleaseGroup': {'size': 15000, 'quality': 'cam'},
|
||||
'Movie Name 2014 HQ DVDRip X264 AC3 (bla)': {'size': 0, 'quality': 'dvdrip'},
|
||||
'Movie Name1 (2012).mkv': {'size': 4500, 'quality': '720p'},
|
||||
'Movie Name (2013).mkv': {'size': 8500, 'quality': '1080p'},
|
||||
'Movie Name (2014).mkv': {'size': 4500, 'quality': '720p', 'extra': {'titles': ['Movie Name 2014 720p Bluray']}},
|
||||
'Movie Name (2015).mkv': {'size': 500, 'quality': '1080p', 'extra': {'resolution_width': 1920}},
|
||||
'Movie Name (2015).mp4': {'size': 6500, 'quality': 'brrip'},
|
||||
'Movie Name (2015).mp4': {'size': 6500, 'quality': 'brrip'},
|
||||
'Movie Name.2014.720p Web-Dl Aac2.0 h264-ReleaseGroup': {'size': 3800, 'quality': 'brrip'},
|
||||
'Movie Name.2014.720p.WEBRip.x264.AC3-ReleaseGroup': {'size': 3000, 'quality': 'scr'},
|
||||
'Movie.Name.2014.1080p.HDCAM.-.ReleaseGroup': {'size': 5300, 'quality': 'cam'},
|
||||
}
|
||||
|
||||
correct = 0
|
||||
for name in tests:
|
||||
test_quality = self.guess(files = [name], extra = tests[name].get('extra', None), size = tests[name].get('size', None)) or {}
|
||||
test_quality = self.guess(files = [name], extra = tests[name].get('extra', None), size = tests[name].get('size', None), use_cache = False) or {}
|
||||
success = test_quality.get('identifier') == tests[name]['quality'] and test_quality.get('is_3d') == tests[name].get('is_3d', False)
|
||||
if not success:
|
||||
log.error('%s failed check, thinks it\'s "%s" expecting "%s"', (name,
|
||||
|
||||
@@ -8,7 +8,7 @@ from couchpotato import md5, get_db
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, sp
|
||||
from couchpotato.core.helpers.variable import getTitle
|
||||
from couchpotato.core.helpers.variable import getTitle, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from .index import ReleaseIndex, ReleaseStatusIndex, ReleaseIDIndex, ReleaseDownloadIndex
|
||||
@@ -65,43 +65,58 @@ class Release(Plugin):
|
||||
log.debug('Removing releases from dashboard')
|
||||
|
||||
now = time.time()
|
||||
week = 262080
|
||||
week = 604800
|
||||
|
||||
db = get_db()
|
||||
|
||||
# Get (and remove) parentless releases
|
||||
releases = db.all('release', with_doc = True)
|
||||
releases = db.all('release', with_doc = False)
|
||||
media_exist = []
|
||||
reindex = 0
|
||||
for release in releases:
|
||||
if release.get('key') in media_exist:
|
||||
continue
|
||||
|
||||
try:
|
||||
|
||||
try:
|
||||
doc = db.get('id', release.get('_id'))
|
||||
except RecordDeleted:
|
||||
reindex += 1
|
||||
continue
|
||||
|
||||
db.get('id', release.get('key'))
|
||||
media_exist.append(release.get('key'))
|
||||
|
||||
try:
|
||||
if release['doc'].get('status') == 'ignore':
|
||||
release['doc']['status'] = 'ignored'
|
||||
db.update(release['doc'])
|
||||
if doc.get('status') == 'ignore':
|
||||
doc['status'] = 'ignored'
|
||||
db.update(doc)
|
||||
except:
|
||||
log.error('Failed fixing mis-status tag: %s', traceback.format_exc())
|
||||
except ValueError:
|
||||
fireEvent('database.delete_corrupted', release.get('key'), traceback_error = traceback.format_exc(0))
|
||||
reindex += 1
|
||||
except RecordDeleted:
|
||||
db.delete(release['doc'])
|
||||
log.debug('Deleted orphaned release: %s', release['doc'])
|
||||
db.delete(doc)
|
||||
log.debug('Deleted orphaned release: %s', doc)
|
||||
reindex += 1
|
||||
except:
|
||||
log.debug('Failed cleaning up orphaned releases: %s', traceback.format_exc())
|
||||
|
||||
if reindex > 0:
|
||||
db.reindex()
|
||||
|
||||
del media_exist
|
||||
|
||||
# get movies last_edit more than a week ago
|
||||
medias = fireEvent('media.with_status', 'done', single = True)
|
||||
medias = fireEvent('media.with_status', ['done', 'active'], single = True)
|
||||
|
||||
for media in medias:
|
||||
if media.get('last_edit', 0) > (now - week):
|
||||
continue
|
||||
|
||||
for rel in fireEvent('release.for_media', media['_id'], single = True):
|
||||
for rel in self.forMedia(media['_id']):
|
||||
|
||||
# Remove all available releases
|
||||
if rel['status'] in ['available']:
|
||||
@@ -111,7 +126,8 @@ class Release(Plugin):
|
||||
elif rel['status'] in ['snatched', 'downloaded']:
|
||||
self.updateStatus(rel['_id'], status = 'ignored')
|
||||
|
||||
fireEvent('media.untag', media.get('_id'), 'recent', single = True)
|
||||
if 'recent' in media.get('tags', []):
|
||||
fireEvent('media.untag', media.get('_id'), 'recent', single = True)
|
||||
|
||||
def add(self, group, update_info = True, update_id = None):
|
||||
|
||||
@@ -171,7 +187,7 @@ class Release(Plugin):
|
||||
release['files'] = dict((k, [toUnicode(x) for x in v]) for k, v in group['files'].items() if v)
|
||||
db.update(release)
|
||||
|
||||
fireEvent('media.restatus', media['_id'], single = True)
|
||||
fireEvent('media.restatus', media['_id'], allowed_restatus = ['done'], single = True)
|
||||
|
||||
return True
|
||||
except:
|
||||
@@ -364,6 +380,7 @@ class Release(Plugin):
|
||||
wait_for = False
|
||||
let_through = False
|
||||
filtered_results = []
|
||||
minimum_seeders = tryInt(Env.setting('minimum_seeders', section = 'torrent', default = 1))
|
||||
|
||||
# Filter out ignored and other releases we don't want
|
||||
for rel in results:
|
||||
@@ -372,16 +389,16 @@ class Release(Plugin):
|
||||
log.info('Ignored: %s', rel['name'])
|
||||
continue
|
||||
|
||||
if rel['score'] <= 0:
|
||||
log.info('Ignored, score "%s" to low: %s', (rel['score'], rel['name']))
|
||||
if rel['score'] < quality_custom.get('minimum_score'):
|
||||
log.info('Ignored, score "%s" to low, need at least "%s": %s', (rel['score'], quality_custom.get('minimum_score'), rel['name']))
|
||||
continue
|
||||
|
||||
if rel['size'] <= 50:
|
||||
log.info('Ignored, size "%sMB" to low: %s', (rel['size'], rel['name']))
|
||||
continue
|
||||
|
||||
if 'seeders' in rel and rel.get('seeders') <= 0:
|
||||
log.info('Ignored, no seeders: %s', (rel['name']))
|
||||
if 'seeders' in rel and rel.get('seeders') < minimum_seeders:
|
||||
log.info('Ignored, not enough seeders, has %s needs %s: %s', (rel.get('seeders'), minimum_seeders, rel['name']))
|
||||
continue
|
||||
|
||||
# If a single release comes through the "wait for", let through all
|
||||
@@ -424,7 +441,6 @@ class Release(Plugin):
|
||||
for rel in search_results:
|
||||
|
||||
rel_identifier = md5(rel['url'])
|
||||
found_releases.append(rel_identifier)
|
||||
|
||||
release = {
|
||||
'_t': 'release',
|
||||
@@ -465,6 +481,9 @@ class Release(Plugin):
|
||||
# Update release in search_results
|
||||
rel['status'] = rls.get('status')
|
||||
|
||||
if rel['status'] == 'available':
|
||||
found_releases.append(rel_identifier)
|
||||
|
||||
return found_releases
|
||||
except:
|
||||
log.error('Failed: %s', traceback.format_exc())
|
||||
@@ -527,11 +546,15 @@ class Release(Plugin):
|
||||
def forMedia(self, media_id):
|
||||
|
||||
db = get_db()
|
||||
raw_releases = list(db.get_many('release', media_id, with_doc = True))
|
||||
raw_releases = db.get_many('release', media_id)
|
||||
|
||||
releases = []
|
||||
for r in raw_releases:
|
||||
releases.append(r['doc'])
|
||||
try:
|
||||
doc = db.get('id', r.get('_id'))
|
||||
releases.append(doc)
|
||||
except RecordDeleted:
|
||||
pass
|
||||
|
||||
releases = sorted(releases, key = lambda k: k.get('info', {}).get('score', 0), reverse = True)
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss, sp
|
||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||
getImdb, link, symlink, tryInt, splitString, fnEscape, isSubFolder, getIdentifier
|
||||
getImdb, link, symlink, tryInt, splitString, fnEscape, isSubFolder, \
|
||||
getIdentifier, randomString, getFreeSpace, getSize
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
@@ -219,6 +220,16 @@ class Renamer(Plugin):
|
||||
nfo_name = self.conf('nfo_name')
|
||||
separator = self.conf('separator')
|
||||
|
||||
if len(file_name) == 0:
|
||||
log.error('Please fill in the filename option under renamer settings. Forcing it on <original>.<ext> to keep the same name as source file.')
|
||||
file_name = '<original>.<ext>'
|
||||
|
||||
cd_keys = ['<cd>','<cd_nr>', '<original>']
|
||||
if not any(x in folder_name for x in cd_keys) and not any(x in file_name for x in cd_keys):
|
||||
log.error('Missing `cd` or `cd_nr` in the renamer. This will cause multi-file releases of being renamed to the same file. '
|
||||
'Please add it in the renamer settings. Force adding it for now.')
|
||||
file_name = '%s %s' % ('<cd>', file_name)
|
||||
|
||||
# Tag release folder as failed_rename in case no groups were found. This prevents check_snatched from removing the release from the downloader.
|
||||
if not groups and self.statusInfoComplete(release_download):
|
||||
self.tagRelease(release_download = release_download, tag = 'failed_rename')
|
||||
@@ -266,13 +277,14 @@ class Renamer(Plugin):
|
||||
category_label = category['label']
|
||||
|
||||
if category['destination'] and len(category['destination']) > 0 and category['destination'] != 'None':
|
||||
destination = category['destination']
|
||||
destination = sp(category['destination'])
|
||||
log.debug('Setting category destination for "%s": %s' % (media_title, destination))
|
||||
else:
|
||||
log.debug('No category destination found for "%s"' % media_title)
|
||||
except:
|
||||
log.error('Failed getting category label: %s', traceback.format_exc())
|
||||
|
||||
|
||||
# Find subtitle for renaming
|
||||
group['before_rename'] = []
|
||||
fireEvent('renamer.before', group)
|
||||
@@ -344,6 +356,9 @@ class Renamer(Plugin):
|
||||
replacements['original'] = os.path.splitext(os.path.basename(current_file))[0]
|
||||
replacements['original_folder'] = fireEvent('scanner.remove_cptag', group['dirname'], single = True)
|
||||
|
||||
if not replacements['original_folder'] or len(replacements['original_folder']) == 0:
|
||||
replacements['original_folder'] = replacements['original']
|
||||
|
||||
# Extension
|
||||
replacements['ext'] = getExt(current_file)
|
||||
|
||||
@@ -362,10 +377,6 @@ class Renamer(Plugin):
|
||||
elif file_type is 'nfo':
|
||||
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True)
|
||||
|
||||
# Seperator replace
|
||||
if separator:
|
||||
final_file_name = final_file_name.replace(' ', separator)
|
||||
|
||||
# Move DVD files (no structure renaming)
|
||||
if group['is_dvd'] and file_type is 'movie':
|
||||
found = False
|
||||
@@ -526,7 +537,7 @@ class Renamer(Plugin):
|
||||
|
||||
# Remove leftover files
|
||||
if not remove_leftovers: # Don't remove anything
|
||||
break
|
||||
continue
|
||||
|
||||
log.debug('Removing leftover files')
|
||||
for current_file in group['files']['leftover']:
|
||||
@@ -534,6 +545,14 @@ class Renamer(Plugin):
|
||||
(not keep_original or self.fileIsAdded(current_file, group)):
|
||||
remove_files.append(current_file)
|
||||
|
||||
if self.conf('check_space'):
|
||||
total_space, available_space = getFreeSpace(destination)
|
||||
renaming_size = getSize(rename_files.keys())
|
||||
if renaming_size > available_space:
|
||||
log.error('Not enough space left, need %s MB but only %s MB available', (renaming_size, available_space))
|
||||
self.tagRelease(group = group, tag = 'not_enough_space')
|
||||
continue
|
||||
|
||||
# Remove files
|
||||
delete_folders = []
|
||||
for src in remove_files:
|
||||
@@ -549,9 +568,9 @@ class Renamer(Plugin):
|
||||
os.remove(src)
|
||||
|
||||
parent_dir = os.path.dirname(src)
|
||||
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and \
|
||||
if parent_dir not in delete_folders and os.path.isdir(parent_dir) and \
|
||||
not isSubFolder(destination, parent_dir) and not isSubFolder(media_folder, parent_dir) and \
|
||||
not isSubFolder(parent_dir, base_folder):
|
||||
isSubFolder(parent_dir, base_folder):
|
||||
|
||||
delete_folders.append(parent_dir)
|
||||
|
||||
@@ -560,6 +579,7 @@ class Renamer(Plugin):
|
||||
self.tagRelease(group = group, tag = 'failed_remove')
|
||||
|
||||
# Delete leftover folder from older releases
|
||||
delete_folders = sorted(delete_folders, key = len, reverse = True)
|
||||
for delete_folder in delete_folders:
|
||||
try:
|
||||
self.deleteEmptyFolder(delete_folder, show_error = False)
|
||||
@@ -572,7 +592,10 @@ class Renamer(Plugin):
|
||||
for src in rename_files:
|
||||
if rename_files[src]:
|
||||
dst = rename_files[src]
|
||||
log.info('Renaming "%s" to "%s"', (src, dst))
|
||||
|
||||
if dst in group['renamed_files']:
|
||||
log.error('File "%s" already renamed once, adding random string at the end to prevent data loss', dst)
|
||||
dst = '%s.random-%s' % (dst, randomString())
|
||||
|
||||
# Create dir
|
||||
self.makeDir(os.path.dirname(dst))
|
||||
@@ -614,8 +637,9 @@ class Renamer(Plugin):
|
||||
group_folder = sp(os.path.join(base_folder, os.path.relpath(group['parentdir'], base_folder).split(os.path.sep)[0]))
|
||||
|
||||
try:
|
||||
log.info('Deleting folder: %s', group_folder)
|
||||
self.deleteEmptyFolder(group_folder)
|
||||
if self.conf('cleanup') or self.conf('move_leftover'):
|
||||
log.info('Deleting folder: %s', group_folder)
|
||||
self.deleteEmptyFolder(group_folder)
|
||||
except:
|
||||
log.error('Failed removing %s: %s', (group_folder, traceback.format_exc()))
|
||||
|
||||
@@ -771,22 +795,32 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
dest = sp(dest)
|
||||
try:
|
||||
|
||||
if os.path.exists(dest) and os.path.isfile(dest):
|
||||
raise Exception('Destination "%s" already exists' % dest)
|
||||
|
||||
move_type = self.conf('file_action')
|
||||
if use_default:
|
||||
move_type = self.conf('default_file_action')
|
||||
|
||||
if move_type not in ['copy', 'link']:
|
||||
try:
|
||||
log.info('Moving "%s" to "%s"', (old, dest))
|
||||
shutil.move(old, dest)
|
||||
except:
|
||||
if os.path.exists(dest):
|
||||
exists = os.path.exists(dest)
|
||||
if exists and os.path.getsize(old) == os.path.getsize(dest):
|
||||
log.error('Successfully moved file "%s", but something went wrong: %s', (dest, traceback.format_exc()))
|
||||
os.unlink(old)
|
||||
else:
|
||||
# remove faultly copied file
|
||||
if exists:
|
||||
os.unlink(dest)
|
||||
raise
|
||||
elif move_type == 'copy':
|
||||
log.info('Copying "%s" to "%s"', (old, dest))
|
||||
shutil.copy(old, dest)
|
||||
else:
|
||||
log.info('Linking "%s" to "%s"', (old, dest))
|
||||
# First try to hardlink
|
||||
try:
|
||||
log.debug('Hardlinking file "%s" to "%s"...', (old, dest))
|
||||
@@ -796,9 +830,10 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
log.debug('Couldn\'t hardlink file "%s" to "%s". Symlinking instead. Error: %s.', (old, dest, traceback.format_exc()))
|
||||
shutil.copy(old, dest)
|
||||
try:
|
||||
symlink(dest, old + '.link')
|
||||
old_link = '%s.link' % sp(old)
|
||||
symlink(dest, old_link)
|
||||
os.unlink(old)
|
||||
os.rename(old + '.link', old)
|
||||
os.rename(old_link, old)
|
||||
except:
|
||||
log.error('Couldn\'t symlink file "%s" to "%s". Copied instead. Error: %s. ', (old, dest, traceback.format_exc()))
|
||||
|
||||
@@ -841,7 +876,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced)
|
||||
|
||||
sep = self.conf('foldersep') if folder else self.conf('separator')
|
||||
return replaced.replace(' ', ' ' if not sep else sep)
|
||||
return ss(replaced.replace(' ', ' ' if not sep else sep))
|
||||
|
||||
def replaceDoubles(self, string):
|
||||
|
||||
@@ -854,6 +889,8 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
reg, replace_with = r
|
||||
string = re.sub(reg, replace_with, string)
|
||||
|
||||
string = string.rstrip(',_-/\\ ')
|
||||
|
||||
return string
|
||||
|
||||
def checkSnatched(self, fire_scan = True):
|
||||
@@ -1187,7 +1224,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
except Exception as e:
|
||||
log.error('Failed moving left over file %s to %s: %s %s', (leftoverfile, move_to, e, traceback.format_exc()))
|
||||
# As we probably tried to overwrite the nfo file, check if it exists and then remove the original
|
||||
if os.path.isfile(move_to):
|
||||
if os.path.isfile(move_to) and os.path.getsize(leftoverfile) == os.path.getsize(move_to):
|
||||
if cleanup:
|
||||
log.info('Deleting left over file %s instead...', leftoverfile)
|
||||
os.unlink(leftoverfile)
|
||||
@@ -1357,6 +1394,14 @@ config = [{
|
||||
'label': 'Folder-Separator',
|
||||
'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'),
|
||||
},
|
||||
{
|
||||
'name': 'check_space',
|
||||
'label': 'Check space',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'description': ('Check if there\'s enough available space to rename the files', 'Disable when the filesystem doesn\'t return the proper value'),
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'default_file_action',
|
||||
'label': 'Default File Action',
|
||||
|
||||
@@ -11,7 +11,6 @@ from couchpotato.core.helpers.variable import getExt, getImdb, tryInt, \
|
||||
splitString, getIdentifier
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from enzyme.exceptions import NoParserError, ParseError
|
||||
from guessit import guess_movie_info
|
||||
from subliminal.videos import Video
|
||||
import enzyme
|
||||
@@ -121,7 +120,7 @@ class Scanner(Plugin):
|
||||
'()([ab])(\.....?)$' #*a.mkv
|
||||
]
|
||||
|
||||
cp_imdb = '(.cp.(?P<id>tt[0-9{7}]+).)'
|
||||
cp_imdb = '\.cp\((?P<id>tt[0-9]+),?\s?(?P<random>[A-Za-z0-9]+)?\)'
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -132,7 +131,7 @@ class Scanner(Plugin):
|
||||
addEvent('scanner.name_year', self.getReleaseNameYear)
|
||||
addEvent('scanner.partnumber', self.getPartNumber)
|
||||
|
||||
def scan(self, folder = None, files = None, release_download = None, simple = False, newer_than = 0, return_ignored = True, on_found = None):
|
||||
def scan(self, folder = None, files = None, release_download = None, simple = False, newer_than = 0, return_ignored = True, check_file_date = True, on_found = None):
|
||||
|
||||
folder = sp(folder)
|
||||
|
||||
@@ -146,7 +145,6 @@ class Scanner(Plugin):
|
||||
|
||||
# Scan all files of the folder if no files are set
|
||||
if not files:
|
||||
check_file_date = True
|
||||
try:
|
||||
files = []
|
||||
for root, dirs, walk_files in os.walk(folder, followlinks=True):
|
||||
@@ -457,6 +455,7 @@ class Scanner(Plugin):
|
||||
meta = self.getMeta(cur_file)
|
||||
|
||||
try:
|
||||
data['titles'] = meta.get('titles', [])
|
||||
data['video'] = meta.get('video', self.getCodec(cur_file, self.codecs['video']))
|
||||
data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio']))
|
||||
data['audio_channels'] = meta.get('audio_channels', 2.0)
|
||||
@@ -492,7 +491,7 @@ class Scanner(Plugin):
|
||||
|
||||
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
|
||||
|
||||
filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
|
||||
filename = re.sub(self.cp_imdb, '', files[0])
|
||||
data['group'] = self.getGroup(filename[len(folder):])
|
||||
data['source'] = self.getSourceMedia(filename)
|
||||
if data['quality'].get('is_3d', 0):
|
||||
@@ -527,16 +526,33 @@ class Scanner(Plugin):
|
||||
try: ac = self.audio_codec_map.get(p.audio[0].codec)
|
||||
except: pass
|
||||
|
||||
# Find title in video headers
|
||||
titles = []
|
||||
|
||||
try:
|
||||
if p.title and self.findYear(p.title):
|
||||
titles.append(ss(p.title))
|
||||
except:
|
||||
log.error('Failed getting title from meta: %s', traceback.format_exc())
|
||||
|
||||
for video in p.video:
|
||||
try:
|
||||
if video.title and self.findYear(video.title):
|
||||
titles.append(ss(video.title))
|
||||
except:
|
||||
log.error('Failed getting title from meta: %s', traceback.format_exc())
|
||||
|
||||
return {
|
||||
'titles': list(set(titles)),
|
||||
'video': vc,
|
||||
'audio': ac,
|
||||
'resolution_width': tryInt(p.video[0].width),
|
||||
'resolution_height': tryInt(p.video[0].height),
|
||||
'audio_channels': p.audio[0].channels,
|
||||
}
|
||||
except ParseError:
|
||||
except enzyme.exceptions.ParseError:
|
||||
log.debug('Failed to parse meta for %s', filename)
|
||||
except NoParserError:
|
||||
except enzyme.exceptions.NoParserError:
|
||||
log.debug('No parser found for %s', filename)
|
||||
except:
|
||||
log.debug('Failed parsing %s', filename)
|
||||
@@ -677,7 +693,7 @@ class Scanner(Plugin):
|
||||
|
||||
def removeCPTag(self, name):
|
||||
try:
|
||||
return re.sub(self.cp_imdb, '', name)
|
||||
return re.sub(self.cp_imdb, '', name).strip()
|
||||
except:
|
||||
pass
|
||||
return name
|
||||
|
||||
@@ -33,33 +33,43 @@ name_scores = [
|
||||
def nameScore(name, year, preferred_words):
|
||||
""" Calculate score for words in the NZB name """
|
||||
|
||||
score = 0
|
||||
name = name.lower()
|
||||
try:
|
||||
score = 0
|
||||
name = name.lower()
|
||||
|
||||
# give points for the cool stuff
|
||||
for value in name_scores:
|
||||
v = value.split(':')
|
||||
add = int(v.pop())
|
||||
if v.pop() in name:
|
||||
score += add
|
||||
# give points for the cool stuff
|
||||
for value in name_scores:
|
||||
v = value.split(':')
|
||||
add = int(v.pop())
|
||||
if v.pop() in name:
|
||||
score += add
|
||||
|
||||
# points if the year is correct
|
||||
if year and str(year) in name:
|
||||
score += 5
|
||||
# points if the year is correct
|
||||
if str(year) in name:
|
||||
score += 5
|
||||
|
||||
# Contains preferred word
|
||||
nzb_words = re.split('\W+', simplifyString(name))
|
||||
score += 100 * len(list(set(nzb_words) & set(preferred_words)))
|
||||
# Contains preferred word
|
||||
nzb_words = re.split('\W+', simplifyString(name))
|
||||
score += 100 * len(list(set(nzb_words) & set(preferred_words)))
|
||||
|
||||
return score
|
||||
return score
|
||||
except:
|
||||
log.error('Failed doing nameScore: %s', traceback.format_exc())
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def nameRatioScore(nzb_name, movie_name):
|
||||
nzb_words = re.split('\W+', fireEvent('scanner.create_file_identifier', nzb_name, single = True))
|
||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||
try:
|
||||
nzb_words = re.split('\W+', fireEvent('scanner.create_file_identifier', nzb_name, single = True))
|
||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||
|
||||
left_over = set(nzb_words) - set(movie_words)
|
||||
return 10 - len(left_over)
|
||||
left_over = set(nzb_words) - set(movie_words)
|
||||
return 10 - len(left_over)
|
||||
except:
|
||||
log.error('Failed doing nameRatioScore: %s', traceback.format_exc())
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def namePositionScore(nzb_name, movie_name):
|
||||
@@ -134,38 +144,53 @@ def providerScore(provider):
|
||||
|
||||
def duplicateScore(nzb_name, movie_name):
|
||||
|
||||
nzb_words = re.split('\W+', simplifyString(nzb_name))
|
||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||
try:
|
||||
nzb_words = re.split('\W+', simplifyString(nzb_name))
|
||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||
|
||||
# minus for duplicates
|
||||
duplicates = [x for i, x in enumerate(nzb_words) if nzb_words[i:].count(x) > 1]
|
||||
# minus for duplicates
|
||||
duplicates = [x for i, x in enumerate(nzb_words) if nzb_words[i:].count(x) > 1]
|
||||
|
||||
return len(list(set(duplicates) - set(movie_words))) * -4
|
||||
return len(list(set(duplicates) - set(movie_words))) * -4
|
||||
except:
|
||||
log.error('Failed doing duplicateScore: %s', traceback.format_exc())
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def partialIgnoredScore(nzb_name, movie_name, ignored_words):
|
||||
|
||||
nzb_name = nzb_name.lower()
|
||||
movie_name = movie_name.lower()
|
||||
try:
|
||||
nzb_name = nzb_name.lower()
|
||||
movie_name = movie_name.lower()
|
||||
|
||||
score = 0
|
||||
for ignored_word in ignored_words:
|
||||
if ignored_word in nzb_name and ignored_word not in movie_name:
|
||||
score -= 5
|
||||
score = 0
|
||||
for ignored_word in ignored_words:
|
||||
if ignored_word in nzb_name and ignored_word not in movie_name:
|
||||
score -= 5
|
||||
|
||||
return score
|
||||
return score
|
||||
except:
|
||||
log.error('Failed doing partialIgnoredScore: %s', traceback.format_exc())
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def halfMultipartScore(nzb_name):
|
||||
|
||||
wrong_found = 0
|
||||
for nr in [1, 2, 3, 4, 5, 'i', 'ii', 'iii', 'iv', 'v', 'a', 'b', 'c', 'd', 'e']:
|
||||
for wrong in ['cd', 'part', 'dis', 'disc', 'dvd']:
|
||||
if '%s%s' % (wrong, nr) in nzb_name.lower():
|
||||
wrong_found += 1
|
||||
try:
|
||||
wrong_found = 0
|
||||
for nr in [1, 2, 3, 4, 5, 'i', 'ii', 'iii', 'iv', 'v', 'a', 'b', 'c', 'd', 'e']:
|
||||
for wrong in ['cd', 'part', 'dis', 'disc', 'dvd']:
|
||||
if '%s%s' % (wrong, nr) in nzb_name.lower():
|
||||
wrong_found += 1
|
||||
|
||||
if wrong_found == 1:
|
||||
return -30
|
||||
if wrong_found == 1:
|
||||
return -30
|
||||
|
||||
return 0
|
||||
except:
|
||||
log.error('Failed doing halfMultipartScore: %s', traceback.format_exc())
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user