Merge branch 'refs/heads/develop' into redesign
This commit is contained in:
commit
3986de4ebc
229 files changed
+7700
-8002
No files matched your search
@@ -1,3 +1,4 @@
|
||||
from urllib import quote
|
||||
from urlparse import urlparse
|
||||
import glob
|
||||
import inspect
|
||||
@@ -5,7 +6,6 @@ import os.path
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import ss, toSafeString, \
|
||||
@@ -16,7 +16,6 @@ from couchpotato.environment import Env
|
||||
import requests
|
||||
from requests.packages.urllib3 import Timeout
|
||||
from requests.packages.urllib3.exceptions import MaxRetryError
|
||||
from scandir import scandir
|
||||
from tornado import template
|
||||
from tornado.web import StaticFileHandler
|
||||
|
||||
@@ -41,7 +40,6 @@ class Plugin(object):
|
||||
http_time_between_calls = 0
|
||||
http_failed_request = {}
|
||||
http_failed_disabled = {}
|
||||
http_opener = requests.Session()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
new_plugin = super(Plugin, cls).__new__(cls)
|
||||
@@ -141,19 +139,25 @@ class Plugin(object):
|
||||
|
||||
return False
|
||||
|
||||
def deleteEmptyFolder(self, folder, show_error = True):
|
||||
def deleteEmptyFolder(self, folder, show_error = True, only_clean = None):
|
||||
folder = sp(folder)
|
||||
|
||||
for root, dirs, files in scandir.walk(folder):
|
||||
for item in os.listdir(folder):
|
||||
full_folder = os.path.join(folder, item)
|
||||
|
||||
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.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
|
||||
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 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.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
|
||||
|
||||
try:
|
||||
os.rmdir(folder)
|
||||
@@ -163,7 +167,7 @@ class Plugin(object):
|
||||
|
||||
# http request
|
||||
def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True):
|
||||
url = urllib2.quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
|
||||
url = quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
|
||||
|
||||
if not headers: headers = {}
|
||||
if not data: data = {}
|
||||
@@ -179,7 +183,7 @@ class Plugin(object):
|
||||
headers['Connection'] = headers.get('Connection', 'keep-alive')
|
||||
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
|
||||
|
||||
r = self.http_opener
|
||||
r = Env.get('http_opener')
|
||||
|
||||
# Don't try for failed requests
|
||||
if self.http_failed_disabled.get(host, 0) > 0:
|
||||
@@ -201,11 +205,12 @@ class Plugin(object):
|
||||
'data': data if len(data) > 0 else None,
|
||||
'timeout': timeout,
|
||||
'files': files,
|
||||
'verify': False, #verify_ssl, Disable for now as to many wrongly implemented certificates..
|
||||
}
|
||||
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, verify = False, **kwargs)
|
||||
response = r.request(method, url, **kwargs)
|
||||
|
||||
if response.status_code == requests.codes.ok:
|
||||
data = response.content
|
||||
@@ -258,7 +263,7 @@ class Plugin(object):
|
||||
def afterCall(self, handler):
|
||||
self.isRunning('%s.%s' % (self.getName(), handler.__name__), False)
|
||||
|
||||
def doShutdown(self):
|
||||
def doShutdown(self, *args, **kwargs):
|
||||
self.shuttingDown(True)
|
||||
return True
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import string
|
||||
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.helpers.variable import getUserDir
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import six
|
||||
@@ -50,6 +51,7 @@ class FileBrowser(Plugin):
|
||||
path = '/'
|
||||
|
||||
dirs = []
|
||||
path = sp(path)
|
||||
for f in os.listdir(path):
|
||||
p = 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)):
|
||||
|
||||
@@ -5,11 +5,10 @@ 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.variable import md5, getExt
|
||||
from couchpotato.core.helpers.variable import md5, getExt, isSubFolder
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from scandir import scandir
|
||||
from tornado.web import StaticFileHandler
|
||||
|
||||
|
||||
@@ -33,6 +32,8 @@ class FileManager(Plugin):
|
||||
|
||||
fireEvent('schedule.interval', 'file.cleanup', self.cleanup, hours = 24)
|
||||
|
||||
addEvent('app.test', self.doSubfolderTest)
|
||||
|
||||
def cleanup(self):
|
||||
|
||||
# Wait a bit after starting before cleanup
|
||||
@@ -49,9 +50,9 @@ class FileManager(Plugin):
|
||||
for x in file_dict.keys():
|
||||
files.extend(file_dict[x])
|
||||
|
||||
for f in scandir.scandir(cache_dir):
|
||||
if os.path.splitext(f.name)[1] in ['.png', '.jpg', '.jpeg']:
|
||||
file_path = os.path.join(cache_dir, f.name)
|
||||
for f in os.listdir(cache_dir):
|
||||
if os.path.splitext(f)[1] in ['.png', '.jpg', '.jpeg']:
|
||||
file_path = os.path.join(cache_dir, f)
|
||||
if toUnicode(file_path) not in files:
|
||||
os.remove(file_path)
|
||||
except:
|
||||
@@ -77,3 +78,33 @@ class FileManager(Plugin):
|
||||
|
||||
self.createFile(dest, filedata, binary = True)
|
||||
return dest
|
||||
|
||||
def doSubfolderTest(self):
|
||||
|
||||
tests = {
|
||||
('/test/subfolder', '/test/sub'): False,
|
||||
('/test/sub/folder', '/test/sub'): True,
|
||||
('/test/sub/folder', '/test/sub2'): False,
|
||||
('/sub/fold', '/test/sub/fold'): False,
|
||||
('/sub/fold', '/test/sub/folder'): False,
|
||||
('/opt/couchpotato', '/var/opt/couchpotato'): False,
|
||||
('/var/opt', '/var/opt/couchpotato'): False,
|
||||
('/CapItaLs/Are/OK', '/CapItaLs/Are/OK'): True,
|
||||
('/CapItaLs/Are/OK', '/CapItaLs/Are/OK2'): False,
|
||||
('/capitals/are/not/OK', '/capitals/are/NOT'): False,
|
||||
('\\\\Mounted\\Volume\\Test', '\\\\Mounted\\Volume'): True,
|
||||
('C:\\\\test\\path', 'C:\\\\test2'): False
|
||||
}
|
||||
|
||||
failed = 0
|
||||
for x in tests:
|
||||
if isSubFolder(x[0], x[1]) is not tests[x]:
|
||||
log.error('Failed subfolder test %s %s', x)
|
||||
failed += 1
|
||||
|
||||
if failed > 0:
|
||||
log.error('Subfolder test failed %s tests', failed)
|
||||
else:
|
||||
log.info('Subfolder test succeeded')
|
||||
|
||||
return failed == 0
|
||||
@@ -134,8 +134,8 @@ class Logging(Plugin):
|
||||
logs_raw = toUnicode(log_content).split('[0m\n')
|
||||
|
||||
logs = []
|
||||
for log in logs_raw:
|
||||
split = splitString(log, '\x1b')
|
||||
for log_line in logs_raw:
|
||||
split = splitString(log_line, '\x1b')
|
||||
if split:
|
||||
try:
|
||||
date, time, log_type = splitString(split[0], ' ')
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page.log .nav li:hover:not(.active, .filter) {
|
||||
.page.log .nav li:hover:not(.active):not(.filter) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Page.Log = new Class({
|
||||
Movie(s) I have this with: ...\n\
|
||||
Quality of the movie being searched: ...\n\
|
||||
Providers I use: ...\n\
|
||||
Version of CouchPotato: ...\n\
|
||||
Version of CouchPotato: {version}\n\
|
||||
Running on: ...\n\
|
||||
\n\
|
||||
### Logs:\n\
|
||||
@@ -207,7 +207,11 @@ Running on: ...\n\
|
||||
|
||||
showReport: function(text){
|
||||
var self = this,
|
||||
body = self.report_text.replace('{issue}', text);
|
||||
version = Updater.getInfo(),
|
||||
body = self.report_text
|
||||
.replace('{issue}', text)
|
||||
.replace('{version}', version ? version.version.repr : '...'),
|
||||
textarea;
|
||||
|
||||
var overlay = new Element('div.report', {
|
||||
'method': 'post',
|
||||
@@ -240,7 +244,7 @@ Running on: ...\n\
|
||||
'text': ' before posting, then copy the text below'
|
||||
})
|
||||
),
|
||||
new Element('textarea', {
|
||||
textarea = new Element('textarea', {
|
||||
'text': body,
|
||||
'events': {
|
||||
'click': function(){
|
||||
@@ -251,7 +255,17 @@ Running on: ...\n\
|
||||
new Element('a.button', {
|
||||
'target': '_blank',
|
||||
'text': 'Create a new issue on GitHub with the text above',
|
||||
'href': 'https://github.com/RuudBurger/CouchPotatoServer/issues/new?body=' + (body.length < 2000 ? encodeURIComponent(body) : 'Paste the text here')
|
||||
'href': 'https://github.com/RuudBurger/CouchPotatoServer/issues/new',
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).stop();
|
||||
|
||||
var body = textarea.get('value'),
|
||||
bdy = '?body=' + (body.length < 2000 ? encodeURIComponent(body) : 'Paste the text here'),
|
||||
win = window.open(e.target.get('href') + bdy, '_blank');
|
||||
win.focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
@@ -8,7 +6,7 @@ from couchpotato import get_db
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.helpers.variable import splitString, getTitle, tryInt, getIdentifier
|
||||
from couchpotato.core.helpers.variable import splitString, getTitle, tryInt, getIdentifier, getFreeSpace
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
@@ -33,7 +31,7 @@ class Manage(Plugin):
|
||||
# Add files after renaming
|
||||
def after_rename(message = None, group = None):
|
||||
if not group: group = {}
|
||||
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
|
||||
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'], release_download = group['release_download'])
|
||||
addEvent('renamer.after', after_rename, priority = 110)
|
||||
|
||||
addApiView('manage.update', self.updateLibraryView, docs = {
|
||||
@@ -53,6 +51,20 @@ class Manage(Plugin):
|
||||
if not Env.get('dev') and self.conf('startup_scan'):
|
||||
addEvent('app.load', self.updateLibraryQuick)
|
||||
|
||||
addEvent('app.load', self.setCrons)
|
||||
|
||||
# Enable / disable interval
|
||||
addEvent('setting.save.manage.library_refresh_interval.after', self.setCrons)
|
||||
|
||||
def setCrons(self):
|
||||
|
||||
fireEvent('schedule.remove', 'manage.update_library')
|
||||
refresh = tryInt(self.conf('library_refresh_interval'))
|
||||
if refresh > 0:
|
||||
fireEvent('schedule.interval', 'manage.update_library', self.updateLibrary, hours = refresh, single = True)
|
||||
|
||||
return True
|
||||
|
||||
def getProgress(self, **kwargs):
|
||||
return {
|
||||
'progress': self.in_progress
|
||||
@@ -71,7 +83,8 @@ class Manage(Plugin):
|
||||
return self.updateLibrary(full = False)
|
||||
|
||||
def updateLibrary(self, full = True):
|
||||
last_update = float(Env.prop('manage.last_update', default = 0))
|
||||
last_update_key = 'manage.last_update%s' % ('_full' if full else '')
|
||||
last_update = float(Env.prop(last_update_key, default = 0))
|
||||
|
||||
if self.in_progress:
|
||||
log.info('Already updating library: %s', self.in_progress)
|
||||
@@ -122,6 +135,7 @@ class Manage(Plugin):
|
||||
# Get movies with done status
|
||||
total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', release_status = 'done', status_or = True, single = True)
|
||||
|
||||
deleted_releases = []
|
||||
for done_movie in done_movies:
|
||||
if getIdentifier(done_movie) not in added_identifiers:
|
||||
fireEvent('media.delete', media_id = done_movie['_id'], delete_from = 'all')
|
||||
@@ -151,18 +165,24 @@ class Manage(Plugin):
|
||||
already_used = used_files.get(release_file)
|
||||
|
||||
if already_used:
|
||||
# delete current one
|
||||
if already_used.get('last_edit', 0) < release.get('last_edit', 0):
|
||||
fireEvent('release.delete', release['_id'], single = True)
|
||||
# delete previous one
|
||||
else:
|
||||
fireEvent('release.delete', already_used['_id'], single = True)
|
||||
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)
|
||||
break
|
||||
else:
|
||||
used_files[release_file] = release
|
||||
del used_files
|
||||
|
||||
Env.prop('manage.last_update', time.time())
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
if not self.shuttingDown():
|
||||
db = get_db()
|
||||
db.reindex()
|
||||
|
||||
Env.prop(last_update_key, time.time())
|
||||
except:
|
||||
log.error('Failed updating library: %s', (traceback.format_exc()))
|
||||
|
||||
@@ -192,14 +212,14 @@ class Manage(Plugin):
|
||||
'to_go': total_found,
|
||||
})
|
||||
|
||||
self.updateProgress(folder, to_go)
|
||||
|
||||
if group['media'] and group['identifier']:
|
||||
added_identifiers.append(group['identifier'])
|
||||
|
||||
# Add it to release and update the info
|
||||
fireEvent('release.add', group = group, update_info = False)
|
||||
fireEvent('movie.update_info', identifier = group['identifier'], on_complete = self.createAfterUpdate(folder, group['identifier']))
|
||||
else:
|
||||
self.updateProgress(folder)
|
||||
|
||||
return addToLibrary
|
||||
|
||||
@@ -210,7 +230,6 @@ class Manage(Plugin):
|
||||
if not self.in_progress or self.shuttingDown():
|
||||
return
|
||||
|
||||
self.updateProgress(folder)
|
||||
total = self.in_progress[folder]['total']
|
||||
movie_dict = fireEvent('media.get', identifier, single = True)
|
||||
|
||||
@@ -218,10 +237,11 @@ class Manage(Plugin):
|
||||
|
||||
return afterUpdate
|
||||
|
||||
def updateProgress(self, folder):
|
||||
def updateProgress(self, folder, to_go):
|
||||
|
||||
pr = self.in_progress[folder]
|
||||
pr['to_go'] -= 1
|
||||
if to_go < pr['to_go']:
|
||||
pr['to_go'] = to_go
|
||||
|
||||
avg = (time.time() - pr['started']) / (pr['total'] - pr['to_go'])
|
||||
pr['eta'] = tryInt(avg * pr['to_go'])
|
||||
@@ -236,7 +256,7 @@ class Manage(Plugin):
|
||||
|
||||
return []
|
||||
|
||||
def scanFilesToLibrary(self, folder = None, files = None):
|
||||
def scanFilesToLibrary(self, folder = None, files = None, release_download = None):
|
||||
|
||||
folder = os.path.normpath(folder)
|
||||
|
||||
@@ -245,34 +265,13 @@ class Manage(Plugin):
|
||||
if groups:
|
||||
for group in groups.values():
|
||||
if group.get('media'):
|
||||
fireEvent('release.add', group = group)
|
||||
if release_download and release_download.get('release_id'):
|
||||
fireEvent('release.add', group = group, update_id = release_download.get('release_id'))
|
||||
else:
|
||||
fireEvent('release.add', group = group)
|
||||
|
||||
def getDiskSpace(self):
|
||||
|
||||
free_space = {}
|
||||
for folder in self.directories():
|
||||
|
||||
size = None
|
||||
if os.path.isdir(folder):
|
||||
if os.name == 'nt':
|
||||
_, total, free = ctypes.c_ulonglong(), ctypes.c_ulonglong(), \
|
||||
ctypes.c_ulonglong()
|
||||
if sys.version_info >= (3,) or isinstance(folder, unicode):
|
||||
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExW #@UndefinedVariable
|
||||
else:
|
||||
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExA #@UndefinedVariable
|
||||
ret = fun(folder, ctypes.byref(_), ctypes.byref(total), ctypes.byref(free))
|
||||
if ret == 0:
|
||||
raise ctypes.WinError()
|
||||
used = total.value - free.value
|
||||
return [total.value, used, free.value]
|
||||
else:
|
||||
s = os.statvfs(folder)
|
||||
size = [s.f_blocks * s.f_frsize / (1024 * 1024), (s.f_bavail * s.f_frsize) / (1024 * 1024)]
|
||||
|
||||
free_space[folder] = size
|
||||
|
||||
return free_space
|
||||
return getFreeSpace(self.directories())
|
||||
|
||||
|
||||
config = [{
|
||||
@@ -308,6 +307,14 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Do a quick scan on startup. On slow systems better disable this.',
|
||||
},
|
||||
{
|
||||
'label': 'Full library refresh',
|
||||
'name': 'library_refresh_interval',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'advanced': True,
|
||||
'description': 'Do a full scan every X hours. (0 is disabled)',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -38,9 +38,18 @@ class ProfilePlugin(Plugin):
|
||||
|
||||
def forceDefaults(self):
|
||||
|
||||
db = get_db()
|
||||
|
||||
# Fill qualities and profiles if they are empty somehow..
|
||||
if db.count(db.all, 'profile') == 0:
|
||||
|
||||
if db.count(db.all, 'quality') == 0:
|
||||
fireEvent('quality.fill', single = True)
|
||||
|
||||
self.fill()
|
||||
|
||||
# Get all active movies without profile
|
||||
try:
|
||||
db = get_db()
|
||||
medias = fireEvent('media.with_status', 'active', single = True)
|
||||
|
||||
profile_ids = [x.get('_id') for x in self.all()]
|
||||
@@ -79,6 +88,7 @@ class ProfilePlugin(Plugin):
|
||||
'core': kwargs.get('core', False),
|
||||
'qualities': [],
|
||||
'wait_for': [],
|
||||
'stop_after': [],
|
||||
'finish': [],
|
||||
'3d': []
|
||||
}
|
||||
@@ -88,6 +98,7 @@ class ProfilePlugin(Plugin):
|
||||
for type in kwargs.get('types', []):
|
||||
profile['qualities'].append(type.get('quality'))
|
||||
profile['wait_for'].append(tryInt(kwargs.get('wait_for', 0)))
|
||||
profile['stop_after'].append(tryInt(kwargs.get('stop_after', 0)))
|
||||
profile['finish'].append((tryInt(type.get('finish')) == 1) if order > 0 else True)
|
||||
profile['3d'].append(tryInt(type.get('3d')))
|
||||
order += 1
|
||||
@@ -208,6 +219,7 @@ class ProfilePlugin(Plugin):
|
||||
'qualities': profile.get('qualities'),
|
||||
'finish': [],
|
||||
'wait_for': [],
|
||||
'stop_after': [],
|
||||
'3d': []
|
||||
}
|
||||
|
||||
@@ -215,6 +227,7 @@ class ProfilePlugin(Plugin):
|
||||
for q in profile.get('qualities'):
|
||||
pro['finish'].append(True)
|
||||
pro['wait_for'].append(0)
|
||||
pro['stop_after'].append(0)
|
||||
pro['3d'].append(threed.pop() if threed else False)
|
||||
|
||||
db.insert(pro)
|
||||
|
||||
@@ -43,9 +43,8 @@
|
||||
}
|
||||
|
||||
.profile .wait_for {
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
top: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.profile .wait_for input {
|
||||
@@ -159,9 +158,6 @@
|
||||
}
|
||||
|
||||
#profile_ordering li {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
padding: 0 5px;
|
||||
}
|
||||
@@ -183,6 +179,9 @@
|
||||
background: url('../../images/handle.png') center;
|
||||
width: 20px;
|
||||
float: right;
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
#profile_ordering .formHint {
|
||||
|
||||
@@ -37,20 +37,28 @@ var Profile = new Class({
|
||||
'placeholder': 'Profile name'
|
||||
})
|
||||
),
|
||||
new Element('div.wait_for.ctrlHolder').adopt(
|
||||
new Element('span', {'text':'Wait'}),
|
||||
new Element('input.inlay.xsmall', {
|
||||
'type':'text',
|
||||
'value': data.wait_for && data.wait_for.length > 0 ? data.wait_for[0] : 0
|
||||
}),
|
||||
new Element('span', {'text':'day(s) for a better quality.'})
|
||||
),
|
||||
new Element('div.qualities.ctrlHolder').adopt(
|
||||
new Element('label', {'text': 'Search for'}),
|
||||
self.type_container = new Element('ol.types'),
|
||||
new Element('div.formHint', {
|
||||
'html': "Search these qualities (2 minimum), from top to bottom. Use the checkbox, to stop searching after it found this quality."
|
||||
})
|
||||
),
|
||||
new Element('div.wait_for.ctrlHolder').adopt(
|
||||
// "Wait the entered number of days for a checked quality, before downloading a lower quality release."
|
||||
new Element('span', {'text':'Wait'}),
|
||||
new Element('input.inlay.wait_for_input.xsmall', {
|
||||
'type':'text',
|
||||
'value': data.wait_for && data.wait_for.length > 0 ? data.wait_for[0] : 0
|
||||
}),
|
||||
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.'})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -116,7 +124,8 @@ var Profile = new Class({
|
||||
var data = {
|
||||
'id' : self.data._id,
|
||||
'label' : self.el.getElement('.quality_label input').get('value'),
|
||||
'wait_for' : self.el.getElement('.wait_for input').get('value'),
|
||||
'wait_for' : self.el.getElement('.wait_for_input').get('value'),
|
||||
'stop_after' : self.el.getElement('.stop_after_input').get('value'),
|
||||
'types': []
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import traceback
|
||||
import re
|
||||
|
||||
from CodernityDB.database import RecordNotFound
|
||||
from couchpotato import get_db
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||
from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt
|
||||
from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.quality.index import QualityIndex
|
||||
@@ -21,23 +22,23 @@ class QualityPlugin(Plugin):
|
||||
}
|
||||
|
||||
qualities = [
|
||||
{'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], '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'], 'tags': ['m2ts', 'x264', 'h264']},
|
||||
{'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'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
|
||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], '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': [], 'allow': [], 'ext':[], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||
{'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'], 'ext':[]},
|
||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':[]},
|
||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':[]},
|
||||
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]}
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr', '720p'], 'ext':[]},
|
||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': ['720p'], 'ext':[]},
|
||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': ['720p'], 'ext':[]},
|
||||
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': ['720p'], 'ext':[]}
|
||||
]
|
||||
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
|
||||
threed_tags = {
|
||||
'sbs': [('half', 'sbs'), 'hsbs', ('full', 'sbs'), 'fsbs'],
|
||||
'ou': [('half', 'ou'), 'hou', ('full', 'ou'), 'fou'],
|
||||
'3d': ['2d3d', '3d2d'],
|
||||
'3d': ['2d3d', '3d2d', '3d'],
|
||||
}
|
||||
|
||||
cached_qualities = None
|
||||
@@ -51,6 +52,7 @@ class QualityPlugin(Plugin):
|
||||
addEvent('quality.order', self.getOrder)
|
||||
addEvent('quality.ishigher', self.isHigher)
|
||||
addEvent('quality.isfinish', self.isFinish)
|
||||
addEvent('quality.fill', self.fill)
|
||||
|
||||
addApiView('quality.size.save', self.saveSize)
|
||||
addApiView('quality.list', self.allView, docs = {
|
||||
@@ -93,15 +95,14 @@ class QualityPlugin(Plugin):
|
||||
|
||||
db = get_db()
|
||||
|
||||
qualities = db.all('quality', with_doc = True)
|
||||
|
||||
temp = []
|
||||
for quality in qualities:
|
||||
quality = quality['doc']
|
||||
q = mergeDicts(self.getQuality(quality.get('identifier')), quality)
|
||||
for quality in self.qualities:
|
||||
quality_doc = db.get('quality', quality.get('identifier'), with_doc = True)['doc']
|
||||
q = mergeDicts(quality, quality_doc)
|
||||
temp.append(q)
|
||||
|
||||
self.cached_qualities = temp
|
||||
if len(temp) == len(self.qualities):
|
||||
self.cached_qualities = temp
|
||||
|
||||
return temp
|
||||
|
||||
@@ -152,24 +153,31 @@ class QualityPlugin(Plugin):
|
||||
order = 0
|
||||
for q in self.qualities:
|
||||
|
||||
db.insert({
|
||||
'_t': 'quality',
|
||||
'order': order,
|
||||
'identifier': q.get('identifier'),
|
||||
'size_min': tryInt(q.get('size')[0]),
|
||||
'size_max': tryInt(q.get('size')[1]),
|
||||
})
|
||||
existing = None
|
||||
try:
|
||||
existing = db.get('quality', q.get('identifier'))
|
||||
except RecordNotFound:
|
||||
pass
|
||||
|
||||
log.info('Creating profile: %s', q.get('label'))
|
||||
db.insert({
|
||||
'_t': 'profile',
|
||||
'order': order + 20, # Make sure it goes behind other profiles
|
||||
'core': True,
|
||||
'qualities': [q.get('identifier')],
|
||||
'label': toUnicode(q.get('label')),
|
||||
'finish': [True],
|
||||
'wait_for': [0],
|
||||
})
|
||||
if not existing:
|
||||
db.insert({
|
||||
'_t': 'quality',
|
||||
'order': order,
|
||||
'identifier': q.get('identifier'),
|
||||
'size_min': tryInt(q.get('size')[0]),
|
||||
'size_max': tryInt(q.get('size')[1]),
|
||||
})
|
||||
|
||||
log.info('Creating profile: %s', q.get('label'))
|
||||
db.insert({
|
||||
'_t': 'profile',
|
||||
'order': order + 20, # Make sure it goes behind other profiles
|
||||
'core': True,
|
||||
'qualities': [q.get('identifier')],
|
||||
'label': toUnicode(q.get('label')),
|
||||
'finish': [True],
|
||||
'wait_for': [0],
|
||||
})
|
||||
|
||||
order += 1
|
||||
|
||||
@@ -200,22 +208,34 @@ class QualityPlugin(Plugin):
|
||||
|
||||
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)
|
||||
threed_words = words
|
||||
if name_year and name_year.get('name'):
|
||||
split_name = splitString(name_year.get('name'), ' ')
|
||||
threed_words = [x for x in words if x not in split_name]
|
||||
|
||||
for quality in qualities:
|
||||
contains_score = self.containsTagScore(quality, words, cur_file)
|
||||
threedscore = self.contains3D(quality, words, cur_file) if quality.get('allow_3d') else (0, None)
|
||||
threedscore = self.contains3D(quality, threed_words, cur_file) if quality.get('allow_3d') else (0, None)
|
||||
|
||||
self.calcScore(score, quality, contains_score, threedscore)
|
||||
|
||||
# Evaluate score based on size
|
||||
size_scores = []
|
||||
for quality in qualities:
|
||||
size_score = self.guessSizeScore(quality, size = size)
|
||||
self.calcScore(score, quality, size_score, penalty = False)
|
||||
|
||||
# Try again with loose testing
|
||||
for quality in qualities:
|
||||
# Evaluate score based on size
|
||||
size_score = self.guessSizeScore(quality, size = size)
|
||||
loose_score = self.guessLooseScore(quality, extra = extra)
|
||||
self.calcScore(score, quality, loose_score, penalty = False)
|
||||
|
||||
if size_score > 0:
|
||||
size_scores.append(quality)
|
||||
|
||||
self.calcScore(score, quality, size_score + loose_score, penalty = False)
|
||||
|
||||
# Add additional size score if only 1 size validated
|
||||
if len(size_scores) == 1:
|
||||
self.calcScore(score, size_scores[0], 10, penalty = False)
|
||||
del size_scores
|
||||
|
||||
# Return nothing if all scores are <= 0
|
||||
has_non_zero = 0
|
||||
@@ -241,6 +261,9 @@ class QualityPlugin(Plugin):
|
||||
cur_file = ss(cur_file)
|
||||
score = 0
|
||||
|
||||
extension = words[-1]
|
||||
words = words[:-1]
|
||||
|
||||
points = {
|
||||
'identifier': 10,
|
||||
'label': 10,
|
||||
@@ -260,7 +283,7 @@ 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 cur_file.lower():
|
||||
if isinstance(alt, (str, unicode)) and ss(alt.lower()) in 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) / 2
|
||||
|
||||
@@ -270,8 +293,8 @@ class QualityPlugin(Plugin):
|
||||
|
||||
# Check extention
|
||||
for ext in quality.get('ext', []):
|
||||
if ext == words[-1]:
|
||||
log.debug('Found %s extension in %s', (ext, cur_file))
|
||||
if ext == extension:
|
||||
log.debug('Found %s with .%s extension in %s', (quality['identifier'], ext, cur_file))
|
||||
score += points['ext']
|
||||
|
||||
return score
|
||||
@@ -283,14 +306,14 @@ class QualityPlugin(Plugin):
|
||||
tags = self.threed_tags.get(key, [])
|
||||
|
||||
for tag in tags:
|
||||
if (isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words)) or (isinstance(tag, (str, unicode)) and ss(tag.lower()) in cur_file.lower()):
|
||||
if isinstance(tag, tuple):
|
||||
if len(set(words) & set(tag)) == len(tag):
|
||||
log.debug('Found %s in %s', (tag, cur_file))
|
||||
return 1, key
|
||||
elif tag in words:
|
||||
log.debug('Found %s in %s', (tag, cur_file))
|
||||
return 1, key
|
||||
|
||||
if list(set([key]) & set(words)):
|
||||
log.debug('Found %s in %s', (key, cur_file))
|
||||
return 1, key
|
||||
|
||||
return 0, None
|
||||
|
||||
def guessLooseScore(self, quality, extra = None):
|
||||
@@ -325,6 +348,8 @@ class QualityPlugin(Plugin):
|
||||
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
|
||||
else:
|
||||
score -= 5
|
||||
|
||||
return score
|
||||
|
||||
@@ -351,28 +376,34 @@ class QualityPlugin(Plugin):
|
||||
|
||||
# Give panelty for all lower qualities
|
||||
for q in self.qualities[self.order.index(quality.get('identifier'))+1:]:
|
||||
score[q.get('identifier')]['score'] -= 1
|
||||
if score.get(q.get('identifier')):
|
||||
score[q.get('identifier')]['score'] -= 1
|
||||
|
||||
def isFinish(self, quality, profile):
|
||||
def isFinish(self, quality, profile, release_age = 0):
|
||||
if not isinstance(profile, dict) or not profile.get('qualities'):
|
||||
return False
|
||||
# No profile so anything (scanned) is good enough
|
||||
return True
|
||||
|
||||
try:
|
||||
quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0]
|
||||
return profile['finish'][quality_order]
|
||||
index = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else False) == bool(quality.get('is_3d', False))][0]
|
||||
|
||||
if index == 0 or (profile['finish'][index] and int(release_age) >= int(profile.get('stop_after', [0])[0])):
|
||||
return True
|
||||
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
def isHigher(self, quality, compare_with, profile = None):
|
||||
if not isinstance(profile, dict) or not profile.get('qualities'):
|
||||
profile = {'qualities': self.order}
|
||||
profile = fireEvent('profile.default', single = True)
|
||||
|
||||
# Try to find quality in profile, if not found: a quality we do not want is lower than anything else
|
||||
try:
|
||||
quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0]
|
||||
except:
|
||||
log.debug('Quality %s not found in profile identifiers %s', (quality['identifier'] + (' 3D' if quality.get('is_3d', 0) else ''), \
|
||||
[identifier + ('3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])]))
|
||||
[identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])]))
|
||||
return 'lower'
|
||||
|
||||
# Try to find compare quality in profile, if not found: anything is higher than a not wanted quality
|
||||
@@ -412,6 +443,20 @@ class QualityPlugin(Plugin):
|
||||
'Movie Name (2013) 2D + 3D': {'size': 49000, 'quality': 'bd50', 'is_3d': True},
|
||||
'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},
|
||||
'/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'},
|
||||
'/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},
|
||||
'Moviename 2014 720p HDCAM XviD DualAudio': {'size': 4000, 'quality': 'cam'},
|
||||
'Moviename (2014) - 720p CAM x264': {'size': 2250, 'quality': 'cam'},
|
||||
'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'}
|
||||
}
|
||||
|
||||
correct = 0
|
||||
@@ -419,7 +464,10 @@ class QualityPlugin(Plugin):
|
||||
test_quality = self.guess(files = [name], extra = tests[name].get('extra', None), size = tests[name].get('size', None)) 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', (name, self.guess([name]).get('identifier')))
|
||||
log.error('%s failed check, thinks it\'s "%s" expecting "%s"', (name,
|
||||
test_quality.get('identifier') + (' 3D' if test_quality.get('is_3d') else ''),
|
||||
tests[name]['quality'] + (' 3D' if tests[name].get('is_3d') else '')
|
||||
))
|
||||
|
||||
correct += success
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ var QualityBase = new Class({
|
||||
|
||||
self.qualities = data.qualities;
|
||||
|
||||
self.profiles_list = null;
|
||||
self.profiles = [];
|
||||
Array.each(data.profiles, self.createProfilesClass.bind(self));
|
||||
|
||||
@@ -29,9 +30,14 @@ var QualityBase = new Class({
|
||||
},
|
||||
|
||||
getQuality: function(identifier){
|
||||
return this.qualities.filter(function(q){
|
||||
return q.identifier == identifier;
|
||||
}).pick();
|
||||
try {
|
||||
return this.qualities.filter(function(q){
|
||||
return q.identifier == identifier;
|
||||
}).pick();
|
||||
}
|
||||
catch(e){}
|
||||
|
||||
return {}
|
||||
},
|
||||
|
||||
addSettings: function(){
|
||||
@@ -101,14 +107,13 @@ var QualityBase = new Class({
|
||||
createProfileOrdering: function(){
|
||||
var self = this;
|
||||
|
||||
var profile_list;
|
||||
self.settings.createGroup({
|
||||
'label': 'Profile Defaults',
|
||||
'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
|
||||
'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
|
||||
}).adopt(
|
||||
new Element('.ctrlHolder#profile_ordering').adopt(
|
||||
new Element('label[text=Order]'),
|
||||
profile_list = new Element('ul'),
|
||||
self.profiles_list = new Element('ul'),
|
||||
new Element('p.formHint', {
|
||||
'html': 'Change the order the profiles are in the dropdown list. Uncheck to hide it completely.<br />First one will be default.'
|
||||
})
|
||||
@@ -128,29 +133,37 @@ var QualityBase = new Class({
|
||||
'text': profile.data.label
|
||||
}),
|
||||
new Element('span.handle')
|
||||
).inject(profile_list);
|
||||
).inject(self.profiles_list);
|
||||
|
||||
new Form.Check(check);
|
||||
|
||||
});
|
||||
|
||||
// Sortable
|
||||
self.profile_sortable = new Sortables(profile_list, {
|
||||
var sorted_changed = false;
|
||||
self.profile_sortable = new Sortables(self.profiles_list, {
|
||||
'revert': true,
|
||||
'handle': '',
|
||||
'handle': '.handle',
|
||||
'opacity': 0.5,
|
||||
'onComplete': self.saveProfileOrdering.bind(self)
|
||||
'onSort': function(){
|
||||
sorted_changed = true;
|
||||
},
|
||||
'onComplete': function(){
|
||||
if(sorted_changed){
|
||||
self.saveProfileOrdering();
|
||||
sorted_changed = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
saveProfileOrdering: function(){
|
||||
var self = this;
|
||||
var self = this,
|
||||
ids = [],
|
||||
hidden = [];
|
||||
|
||||
var ids = [];
|
||||
var hidden = [];
|
||||
|
||||
self.profile_sortable.list.getElements('li').each(function(el, nr){
|
||||
self.profiles_list.getElements('li').each(function(el, nr){
|
||||
ids.include(el.get('data-id'));
|
||||
hidden[nr] = +!el.getElement('input[type=checkbox]').get('checked');
|
||||
});
|
||||
|
||||
@@ -3,11 +3,11 @@ import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from CodernityDB.database import RecordDeleted
|
||||
from CodernityDB.database import RecordDeleted, RecordNotFound
|
||||
from couchpotato import md5, get_db
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import ss, toUnicode, sp
|
||||
from couchpotato.core.helpers.encoding import toUnicode, sp
|
||||
from couchpotato.core.helpers.variable import getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -58,7 +58,7 @@ class Release(Plugin):
|
||||
addEvent('release.for_media', self.forMedia)
|
||||
|
||||
# Clean releases that didn't have activity in the last week
|
||||
addEvent('app.load', self.cleanDone)
|
||||
addEvent('app.load', self.cleanDone, priority = 1000)
|
||||
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 12)
|
||||
|
||||
def cleanDone(self):
|
||||
@@ -79,6 +79,13 @@ class Release(Plugin):
|
||||
try:
|
||||
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'])
|
||||
except:
|
||||
log.error('Failed fixing mis-status tag: %s', traceback.format_exc())
|
||||
except RecordDeleted:
|
||||
db.delete(release['doc'])
|
||||
log.debug('Deleted orphaned release: %s', release['doc'])
|
||||
@@ -87,9 +94,6 @@ class Release(Plugin):
|
||||
|
||||
del media_exist
|
||||
|
||||
# Reindex statuses
|
||||
db.reindex_index('media_status')
|
||||
|
||||
# get movies last_edit more than a week ago
|
||||
medias = fireEvent('media.with_status', 'done', single = True)
|
||||
|
||||
@@ -103,11 +107,13 @@ class Release(Plugin):
|
||||
if rel['status'] in ['available']:
|
||||
self.delete(rel['_id'])
|
||||
|
||||
# Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the move
|
||||
# Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the media
|
||||
elif rel['status'] in ['snatched', 'downloaded']:
|
||||
self.updateStatus(rel['_id'], status = 'ignore')
|
||||
self.updateStatus(rel['_id'], status = 'ignored')
|
||||
|
||||
def add(self, group, update_info = True):
|
||||
fireEvent('media.untag', media.get('_id'), 'recent', single = True)
|
||||
|
||||
def add(self, group, update_info = True, update_id = None):
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
@@ -123,33 +129,49 @@ class Release(Plugin):
|
||||
'profile_id': None,
|
||||
}, search_after = False, update_after = update_info, notify_after = False, status = 'done', single = True)
|
||||
|
||||
# Add Release
|
||||
release = {
|
||||
'_t': 'release',
|
||||
'media_id': media['_id'],
|
||||
'identifier': release_identifier,
|
||||
'quality': group['meta_data']['quality'].get('identifier'),
|
||||
'is_3d': group['meta_data']['quality'].get('is_3d', 0),
|
||||
'last_edit': int(time.time()),
|
||||
'status': 'done'
|
||||
}
|
||||
try:
|
||||
r = db.get('release_identifier', release_identifier, with_doc = True)['doc']
|
||||
r['media_id'] = media['_id']
|
||||
except:
|
||||
r = db.insert(release)
|
||||
release = None
|
||||
if update_id:
|
||||
try:
|
||||
release = db.get('id', update_id)
|
||||
release.update({
|
||||
'identifier': release_identifier,
|
||||
'last_edit': int(time.time()),
|
||||
'status': 'done',
|
||||
})
|
||||
except:
|
||||
log.error('Failed updating existing release: %s', traceback.format_exc())
|
||||
else:
|
||||
|
||||
# Update with ref and _id
|
||||
release.update({
|
||||
'_id': r['_id'],
|
||||
'_rev': r['_rev'],
|
||||
})
|
||||
# Add Release
|
||||
if not release:
|
||||
release = {
|
||||
'_t': 'release',
|
||||
'media_id': media['_id'],
|
||||
'identifier': release_identifier,
|
||||
'quality': group['meta_data']['quality'].get('identifier'),
|
||||
'is_3d': group['meta_data']['quality'].get('is_3d', 0),
|
||||
'last_edit': int(time.time()),
|
||||
'status': 'done'
|
||||
}
|
||||
|
||||
try:
|
||||
r = db.get('release_identifier', release_identifier, with_doc = True)['doc']
|
||||
r['media_id'] = media['_id']
|
||||
except:
|
||||
log.debug('Failed updating release by identifier "%s". Inserting new.', release_identifier)
|
||||
r = db.insert(release)
|
||||
|
||||
# Update with ref and _id
|
||||
release.update({
|
||||
'_id': r['_id'],
|
||||
'_rev': r['_rev'],
|
||||
})
|
||||
|
||||
# Empty out empty file groups
|
||||
release['files'] = dict((k, v) for k, v in group['files'].items() if v)
|
||||
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'])
|
||||
fireEvent('media.restatus', media['_id'], single = True)
|
||||
|
||||
return True
|
||||
except:
|
||||
@@ -170,6 +192,9 @@ class Release(Plugin):
|
||||
rel = db.get('id', release_id)
|
||||
db.delete(rel)
|
||||
return True
|
||||
except RecordDeleted:
|
||||
log.debug('Already deleted: %s', release_id)
|
||||
return True
|
||||
except:
|
||||
log.error('Failed: %s', traceback.format_exc())
|
||||
|
||||
@@ -302,7 +327,7 @@ class Release(Plugin):
|
||||
log_movie = '%s (%s) in %s' % (getTitle(media), media['info']['year'], rls['quality'])
|
||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||
log.info(snatch_message)
|
||||
fireEvent('%s.snatched' % data['type'], message = snatch_message, data = rls)
|
||||
fireEvent('%s.snatched' % data['type'], message = snatch_message, data = media)
|
||||
|
||||
# Mark release as snatched
|
||||
if renamer_enabled:
|
||||
@@ -313,22 +338,14 @@ class Release(Plugin):
|
||||
|
||||
if media['status'] == 'active':
|
||||
profile = db.get('id', media['profile_id'])
|
||||
finished = False
|
||||
if rls['quality'] in profile['qualities']:
|
||||
nr = profile['qualities'].index(rls['quality'])
|
||||
finished = profile['finish'][nr]
|
||||
|
||||
if finished:
|
||||
if fireEvent('quality.isfinish', {'identifier': rls['quality'], 'is_3d': rls.get('is_3d', False)}, profile, single = True):
|
||||
log.info('Renamer disabled, marking media as finished: %s', log_movie)
|
||||
|
||||
# Mark release done
|
||||
self.updateStatus(rls['_id'], status = 'done')
|
||||
|
||||
# Mark media done
|
||||
mdia = db.get('id', media['_id'])
|
||||
mdia['status'] = 'done'
|
||||
mdia['last_edit'] = int(time.time())
|
||||
db.update(mdia)
|
||||
fireEvent('media.restatus', media['_id'], single = True)
|
||||
|
||||
return True
|
||||
|
||||
@@ -341,7 +358,7 @@ class Release(Plugin):
|
||||
|
||||
return True
|
||||
|
||||
def tryDownloadResult(self, results, media, quality_custom, manual = False):
|
||||
def tryDownloadResult(self, results, media, quality_custom):
|
||||
|
||||
wait_for = False
|
||||
let_through = False
|
||||
@@ -355,7 +372,11 @@ class Release(Plugin):
|
||||
continue
|
||||
|
||||
if rel['score'] <= 0:
|
||||
log.info('Ignored, score to low: %s', rel['name'])
|
||||
log.info('Ignored, score "%s" to low: %s', (rel['score'], rel['name']))
|
||||
continue
|
||||
|
||||
if rel['size'] <= 50:
|
||||
log.info('Ignored, size "%sMB" to low: %s', (rel['size'], rel['name']))
|
||||
continue
|
||||
|
||||
rel['wait_for'] = False
|
||||
@@ -375,7 +396,7 @@ class Release(Plugin):
|
||||
wait_for = True
|
||||
continue
|
||||
|
||||
downloaded = fireEvent('release.download', data = rel, media = media, manual = manual, single = True)
|
||||
downloaded = fireEvent('release.download', data = rel, media = media, single = True)
|
||||
if downloaded is True:
|
||||
return True
|
||||
elif downloaded != 'try_next':
|
||||
@@ -453,7 +474,7 @@ class Release(Plugin):
|
||||
rel = db.get('id', release_id)
|
||||
if rel and rel.get('status') != status:
|
||||
|
||||
release_name = rel['info'].get('name')
|
||||
release_name = None
|
||||
if rel.get('files'):
|
||||
for file_type in rel.get('files', {}):
|
||||
if file_type == 'movie':
|
||||
@@ -461,6 +482,9 @@ class Release(Plugin):
|
||||
release_name = os.path.basename(release_file)
|
||||
break
|
||||
|
||||
if not release_name and rel.get('info'):
|
||||
release_name = rel['info'].get('name')
|
||||
|
||||
#update status in Db
|
||||
log.debug('Marking release %s as %s', (release_name, status))
|
||||
rel['status'] = status
|
||||
@@ -484,8 +508,15 @@ class Release(Plugin):
|
||||
status = list(status if isinstance(status, (list, tuple)) else [status])
|
||||
|
||||
for s in status:
|
||||
for ms in db.get_many('release_status', s, with_doc = with_doc):
|
||||
yield ms['doc'] if with_doc else ms
|
||||
for ms in db.get_many('release_status', s):
|
||||
if with_doc:
|
||||
try:
|
||||
doc = db.get('id', ms['_id'])
|
||||
yield doc
|
||||
except RecordNotFound:
|
||||
log.debug('Record not found, skipping: %s', ms['_id'])
|
||||
else:
|
||||
yield ms
|
||||
|
||||
def forMedia(self, media_id):
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from scandir import scandir
|
||||
from unrar2 import RarFile
|
||||
import six
|
||||
from six.moves import filter
|
||||
@@ -124,11 +123,6 @@ class Renamer(Plugin):
|
||||
no_process = [to_folder]
|
||||
cat_list = fireEvent('category.all', single = True) or []
|
||||
no_process.extend([item['destination'] for item in cat_list])
|
||||
try:
|
||||
if Env.setting('library', section = 'manage').strip():
|
||||
no_process.extend([sp(manage_folder) for manage_folder in splitString(Env.setting('library', section = 'manage'), '::')])
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check to see if the no_process folders are inside the "from" folder.
|
||||
if not os.path.isdir(base_folder) or not os.path.isdir(to_folder):
|
||||
@@ -137,7 +131,7 @@ class Renamer(Plugin):
|
||||
else:
|
||||
for item in no_process:
|
||||
if isSubFolder(item, base_folder):
|
||||
log.error('To protect your data, the media libraries can\'t be inside of or the same as the "from" folder.')
|
||||
log.error('To protect your data, the media libraries can\'t be inside of or the same as the "from" folder. "%s" in "%s"', (item, base_folder))
|
||||
return
|
||||
|
||||
# Check to see if the no_process folders are inside the provided media_folder
|
||||
@@ -169,7 +163,7 @@ class Renamer(Plugin):
|
||||
if media_folder:
|
||||
for item in no_process:
|
||||
if isSubFolder(item, media_folder):
|
||||
log.error('To protect your data, the media libraries can\'t be inside of or the same as the provided media folder.')
|
||||
log.error('To protect your data, the media libraries can\'t be inside of or the same as the provided media folder. "%s" in "%s"', (item, media_folder))
|
||||
return
|
||||
|
||||
# Make sure a checkSnatched marked all downloads/seeds as such
|
||||
@@ -195,7 +189,7 @@ class Renamer(Plugin):
|
||||
else:
|
||||
# Get all files from the specified folder
|
||||
try:
|
||||
for root, folders, names in scandir.walk(media_folder):
|
||||
for root, folders, names in os.walk(media_folder):
|
||||
files.extend([sp(os.path.join(root, name)) for name in names])
|
||||
except:
|
||||
log.error('Failed getting files from %s: %s', (media_folder, traceback.format_exc()))
|
||||
@@ -203,14 +197,18 @@ class Renamer(Plugin):
|
||||
db = get_db()
|
||||
|
||||
# Extend the download info with info stored in the downloaded release
|
||||
keep_original = self.moveTypeIsLinked()
|
||||
is_torrent = False
|
||||
if release_download:
|
||||
release_download = self.extendReleaseDownload(release_download)
|
||||
is_torrent = self.downloadIsTorrent(release_download)
|
||||
keep_original = True if is_torrent and self.conf('file_action') not in ['move'] else keep_original
|
||||
|
||||
# Unpack any archives
|
||||
extr_files = None
|
||||
if self.conf('unrar'):
|
||||
folder, media_folder, files, extr_files = self.extractFiles(folder = folder, media_folder = media_folder, files = files,
|
||||
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(release_download))
|
||||
cleanup = self.conf('cleanup') and not keep_original)
|
||||
|
||||
groups = fireEvent('scanner.scan', folder = folder if folder else base_folder,
|
||||
files = files, release_download = release_download, return_ignored = False, single = True) or []
|
||||
@@ -228,6 +226,7 @@ class Renamer(Plugin):
|
||||
for group_identifier in groups:
|
||||
|
||||
group = groups[group_identifier]
|
||||
group['release_download'] = None
|
||||
rename_files = {}
|
||||
remove_files = []
|
||||
remove_releases = []
|
||||
@@ -326,7 +325,7 @@ class Renamer(Plugin):
|
||||
if file_type is 'nfo' and not self.conf('rename_nfo'):
|
||||
log.debug('Skipping, renaming of %s disabled', file_type)
|
||||
for current_file in group['files'][file_type]:
|
||||
if self.conf('cleanup') and (not self.downloadIsTorrent(release_download) or self.fileIsAdded(current_file, group)):
|
||||
if self.conf('cleanup') and (not keep_original or self.fileIsAdded(current_file, group)):
|
||||
remove_files.append(current_file)
|
||||
continue
|
||||
|
||||
@@ -446,19 +445,22 @@ class Renamer(Plugin):
|
||||
# Before renaming, remove the lower quality files
|
||||
remove_leftovers = True
|
||||
|
||||
# Mark movie "done" once it's found the quality with the finish check
|
||||
# Get media quality profile
|
||||
profile = None
|
||||
try:
|
||||
if media.get('status') == 'active' and media.get('profile_id'):
|
||||
if media.get('profile_id'):
|
||||
try:
|
||||
profile = db.get('id', media['profile_id'])
|
||||
if fireEvent('quality.isfinish', group['meta_data']['quality'], profile, single = True):
|
||||
mdia = db.get('id', media['_id'])
|
||||
mdia['status'] = 'done'
|
||||
mdia['last_edit'] = int(time.time())
|
||||
db.update(mdia)
|
||||
except:
|
||||
# Set profile to None as it does not exist anymore
|
||||
mdia = db.get('id', media['_id'])
|
||||
mdia['profile_id'] = None
|
||||
db.update(mdia)
|
||||
log.error('Error getting quality profile for %s: %s', (media_title, traceback.format_exc()))
|
||||
else:
|
||||
log.debug('Media has no quality profile: %s', media_title)
|
||||
|
||||
except Exception as e:
|
||||
log.error('Failed marking movie finished: %s', (traceback.format_exc()))
|
||||
# Mark media for dashboard
|
||||
mark_as_recent = False
|
||||
|
||||
# Go over current movie releases
|
||||
for release in fireEvent('release.for_media', media['_id'], single = True):
|
||||
@@ -468,7 +470,7 @@ class Renamer(Plugin):
|
||||
|
||||
# This is where CP removes older, lesser quality releases or releases that are not wanted anymore
|
||||
is_higher = fireEvent('quality.ishigher', \
|
||||
group['meta_data']['quality'], {'identifier': release['quality'], 'is_3d': release.get('is_3d', 0)}, profile, single = True)
|
||||
group['meta_data']['quality'], {'identifier': release['quality'], 'is_3d': release.get('is_3d', False)}, profile, single = True)
|
||||
|
||||
if is_higher == 'higher':
|
||||
log.info('Removing lesser or not wanted quality %s for %s.', (media_title, release.get('quality')))
|
||||
@@ -493,7 +495,7 @@ class Renamer(Plugin):
|
||||
self.tagRelease(group = group, tag = 'exists')
|
||||
|
||||
# Notify on rename fail
|
||||
download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (media_title, group['meta_data']['quality']['label'], release.get('identifier'))
|
||||
download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (media_title, group['meta_data']['quality']['label'], release.get('quality'))
|
||||
fireEvent('movie.renaming.canceled', message = download_message, data = group)
|
||||
remove_leftovers = False
|
||||
|
||||
@@ -505,13 +507,22 @@ class Renamer(Plugin):
|
||||
if release_download['status'] == 'completed':
|
||||
# Set the release to downloaded
|
||||
fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True)
|
||||
group['release_download'] = release_download
|
||||
mark_as_recent = True
|
||||
elif release_download['status'] == 'seeding':
|
||||
# Set the release to seeding
|
||||
fireEvent('release.update_status', release['_id'], status = 'seeding', single = True)
|
||||
mark_as_recent = True
|
||||
|
||||
elif release.get('identifier') == group['meta_data']['quality']['identifier']:
|
||||
# Set the release to downloaded
|
||||
fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True)
|
||||
elif release.get('quality') == group['meta_data']['quality']['identifier']:
|
||||
# Set the release to downloaded
|
||||
fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True)
|
||||
group['release_download'] = release_download
|
||||
mark_as_recent = True
|
||||
|
||||
# Mark media for dashboard
|
||||
if mark_as_recent:
|
||||
fireEvent('media.tag', group['media'].get('_id'), 'recent', single = True)
|
||||
|
||||
# Remove leftover files
|
||||
if not remove_leftovers: # Don't remove anything
|
||||
@@ -520,7 +531,7 @@ class Renamer(Plugin):
|
||||
log.debug('Removing leftover files')
|
||||
for current_file in group['files']['leftover']:
|
||||
if self.conf('cleanup') and not self.conf('move_leftover') and \
|
||||
(not self.downloadIsTorrent(release_download) or self.fileIsAdded(current_file, group)):
|
||||
(not keep_original or self.fileIsAdded(current_file, group)):
|
||||
remove_files.append(current_file)
|
||||
|
||||
# Remove files
|
||||
@@ -567,7 +578,7 @@ class Renamer(Plugin):
|
||||
self.makeDir(os.path.dirname(dst))
|
||||
|
||||
try:
|
||||
self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(release_download) or self.fileIsAdded(src, group))
|
||||
self.moveFile(src, dst, use_default = not is_torrent or self.fileIsAdded(src, group))
|
||||
group['renamed_files'].append(dst)
|
||||
except:
|
||||
log.error('Failed renaming the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
|
||||
@@ -583,7 +594,7 @@ class Renamer(Plugin):
|
||||
self.untagRelease(group = group, tag = 'failed_rename')
|
||||
|
||||
# Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent
|
||||
if self.movieInFromFolder(media_folder) and self.downloadIsTorrent(release_download):
|
||||
if self.movieInFromFolder(media_folder) and keep_original:
|
||||
self.tagRelease(group = group, tag = 'renamed_already')
|
||||
|
||||
# Remove matching releases
|
||||
@@ -594,7 +605,7 @@ class Renamer(Plugin):
|
||||
except:
|
||||
log.error('Failed removing %s: %s', (release, traceback.format_exc()))
|
||||
|
||||
if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(release_download):
|
||||
if group['dirname'] and group['parentdir'] and not keep_original:
|
||||
if media_folder:
|
||||
# Delete the movie folder
|
||||
group_folder = media_folder
|
||||
@@ -609,7 +620,7 @@ class Renamer(Plugin):
|
||||
log.error('Failed removing %s: %s', (group_folder, traceback.format_exc()))
|
||||
|
||||
# Notify on download, search for trailers etc
|
||||
download_message = 'Downloaded %s (%s)' % (media_title, replacements['quality'])
|
||||
download_message = 'Downloaded %s (%s%s)' % (media_title, replacements['quality'], (' ' + replacements['3d']) if replacements['3d'] else '')
|
||||
try:
|
||||
fireEvent('renamer.after', message = download_message, group = group, in_order = True)
|
||||
except:
|
||||
@@ -664,7 +675,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
|
||||
# Tag all files in release folder
|
||||
elif release_download['folder']:
|
||||
for root, folders, names in scandir.walk(sp(release_download['folder'])):
|
||||
for root, folders, names in os.walk(sp(release_download['folder'])):
|
||||
tag_files.extend([os.path.join(root, name) for name in names])
|
||||
|
||||
for filename in tag_files:
|
||||
@@ -704,7 +715,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
|
||||
# Untag all files in release folder
|
||||
else:
|
||||
for root, folders, names in scandir.walk(folder):
|
||||
for root, folders, names in os.walk(folder):
|
||||
tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore'])
|
||||
|
||||
if not folder:
|
||||
@@ -712,7 +723,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
|
||||
# Find all .ignore files in folder
|
||||
ignore_files = []
|
||||
for root, dirnames, filenames in scandir.walk(folder):
|
||||
for root, dirnames, filenames in os.walk(folder):
|
||||
ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag))
|
||||
|
||||
# Match all found ignore files with the tag_files and delete if found
|
||||
@@ -741,11 +752,11 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
|
||||
# Find tag on all files in release folder
|
||||
else:
|
||||
for root, folders, names in scandir.walk(folder):
|
||||
for root, folders, names in os.walk(folder):
|
||||
tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore'])
|
||||
|
||||
# Find all .ignore files in folder
|
||||
for root, dirnames, filenames in scandir.walk(folder):
|
||||
for root, dirnames, filenames in os.walk(folder):
|
||||
ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag))
|
||||
|
||||
# Match all found ignore files with the tag_files and return True found
|
||||
@@ -756,10 +767,15 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
|
||||
return False
|
||||
|
||||
def moveFile(self, old, dest, forcemove = False):
|
||||
def moveFile(self, old, dest, use_default = False):
|
||||
dest = sp(dest)
|
||||
try:
|
||||
if forcemove or self.conf('file_action') not in ['copy', 'link']:
|
||||
|
||||
move_type = self.conf('file_action')
|
||||
if use_default:
|
||||
move_type = self.conf('default_file_action')
|
||||
|
||||
if move_type not in ['copy', 'link']:
|
||||
try:
|
||||
shutil.move(old, dest)
|
||||
except:
|
||||
@@ -768,16 +784,16 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
os.unlink(old)
|
||||
else:
|
||||
raise
|
||||
elif self.conf('file_action') == 'copy':
|
||||
elif move_type == 'copy':
|
||||
shutil.copy(old, dest)
|
||||
elif self.conf('file_action') == 'link':
|
||||
else:
|
||||
# First try to hardlink
|
||||
try:
|
||||
log.debug('Hardlinking file "%s" to "%s"...', (old, dest))
|
||||
link(old, dest)
|
||||
except:
|
||||
# Try to simlink next
|
||||
log.debug('Couldn\'t hardlink file "%s" to "%s". Simlinking instead. Error: %s.', (old, dest, traceback.format_exc()))
|
||||
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')
|
||||
@@ -1077,6 +1093,9 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
return False
|
||||
return src in group['before_rename']
|
||||
|
||||
def moveTypeIsLinked(self):
|
||||
return self.conf('default_file_action') in ['copy', 'link']
|
||||
|
||||
def statusInfoComplete(self, release_download):
|
||||
return release_download.get('id') and release_download.get('downloader') and release_download.get('folder')
|
||||
|
||||
@@ -1102,7 +1121,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
check_file_date = False
|
||||
|
||||
if not files:
|
||||
for root, folders, names in scandir.walk(folder):
|
||||
for root, folders, names in os.walk(folder):
|
||||
files.extend([sp(os.path.join(root, name)) for name in names])
|
||||
|
||||
# Find all archive files
|
||||
@@ -1128,14 +1147,20 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
|
||||
log.info('Archive %s found. Extracting...', os.path.basename(archive['file']))
|
||||
try:
|
||||
rar_handle = RarFile(archive['file'])
|
||||
rar_handle = RarFile(archive['file'], custom_path = self.conf('unrar_path'))
|
||||
extr_path = os.path.join(from_folder, os.path.relpath(os.path.dirname(archive['file']), folder))
|
||||
self.makeDir(extr_path)
|
||||
for packedinfo in rar_handle.infolist():
|
||||
if not packedinfo.isdir and not os.path.isfile(sp(os.path.join(extr_path, os.path.basename(packedinfo.filename)))):
|
||||
extr_file_path = sp(os.path.join(extr_path, os.path.basename(packedinfo.filename)))
|
||||
if not packedinfo.isdir and not os.path.isfile(extr_file_path):
|
||||
log.debug('Extracting %s...', packedinfo.filename)
|
||||
rar_handle.extract(condition = [packedinfo.index], path = extr_path, withSubpath = False, overwrite = False)
|
||||
extr_files.append(sp(os.path.join(extr_path, os.path.basename(packedinfo.filename))))
|
||||
if self.conf('unrar_modify_date'):
|
||||
try:
|
||||
os.utime(extr_file_path, (os.path.getatime(archive['file']), os.path.getmtime(archive['file'])))
|
||||
except:
|
||||
log.error('Rar modify date enabled, but failed: %s', traceback.format_exc())
|
||||
extr_files.append(extr_file_path)
|
||||
del rar_handle
|
||||
except Exception as e:
|
||||
log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc()))
|
||||
@@ -1270,6 +1295,18 @@ config = [{
|
||||
'description': 'Extract rar files if found.',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
'name': 'unrar_path',
|
||||
'description': 'Custom path to unrar bin',
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
'name': 'unrar_modify_date',
|
||||
'type': 'bool',
|
||||
'description': ('Set modify date of unrar-ed files to the rar-file\'s date.', 'This will allow XBMC to recognize extracted files as recently added even if the movie was released some time ago.'),
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'cleanup',
|
||||
'type': 'bool',
|
||||
@@ -1320,14 +1357,23 @@ config = [{
|
||||
'label': 'Folder-Separator',
|
||||
'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'),
|
||||
},
|
||||
{
|
||||
'name': 'default_file_action',
|
||||
'label': 'Default File Action',
|
||||
'default': 'move',
|
||||
'type': 'dropdown',
|
||||
'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
|
||||
'description': ('<strong>Link</strong>, <strong>Copy</strong> or <strong>Move</strong> after download completed.',
|
||||
'Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy.'),
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'file_action',
|
||||
'label': 'Torrent File Action',
|
||||
'default': 'link',
|
||||
'type': 'dropdown',
|
||||
'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
|
||||
'description': ('<strong>Link</strong>, <strong>Copy</strong> or <strong>Move</strong> after download completed.',
|
||||
'Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy. It is perfered to use link when downloading torrents as it will save you space, while still beeing able to seed.'),
|
||||
'description': 'See above. It is prefered to use link when downloading torrents as it will save you space, while still beeing able to seed.',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,7 +13,6 @@ 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 scandir import scandir
|
||||
from subliminal.videos import Video
|
||||
import enzyme
|
||||
from six.moves import filter, map, zip
|
||||
@@ -106,7 +105,7 @@ class Scanner(Plugin):
|
||||
'HDTV': ['hdtv']
|
||||
}
|
||||
|
||||
clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|ou|extended.cut|directors.cut|french|fr|swedisch|sw|danish|dutch|nl|swesub|subs|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \
|
||||
clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|half.sbs|full.sbs|ou|half.ou|full.ou|extended|extended.cut|directors.cut|french|fr|swedisch|sw|danish|dutch|nl|swesub|subs|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \
|
||||
'|hdtvrip|webdl|web.dl|webrip|web.rip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|hc|\[.*\])(?=[ _\,\.\(\)\[\]\-]|$)'
|
||||
multipart_regex = [
|
||||
'[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
|
||||
@@ -150,7 +149,7 @@ class Scanner(Plugin):
|
||||
check_file_date = True
|
||||
try:
|
||||
files = []
|
||||
for root, dirs, walk_files in scandir.walk(folder, followlinks=True):
|
||||
for root, dirs, walk_files in os.walk(folder, followlinks=True):
|
||||
files.extend([sp(os.path.join(sp(root), ss(filename))) for filename in walk_files])
|
||||
|
||||
# Break if CP wants to shut down
|
||||
@@ -365,6 +364,7 @@ class Scanner(Plugin):
|
||||
|
||||
if return_ignored is False and identifier in ignored_identifiers:
|
||||
log.debug('Ignore file found, ignoring release: %s', identifier)
|
||||
total_found -= 1
|
||||
continue
|
||||
|
||||
# Group extra (and easy) files first
|
||||
@@ -385,6 +385,7 @@ class Scanner(Plugin):
|
||||
|
||||
if len(group['files']['movie']) == 0:
|
||||
log.error('Couldn\'t find any movie files for %s', identifier)
|
||||
total_found -= 1
|
||||
continue
|
||||
|
||||
log.debug('Getting metadata for %s', identifier)
|
||||
@@ -430,7 +431,7 @@ class Scanner(Plugin):
|
||||
|
||||
# Notify parent & progress on something found
|
||||
if on_found:
|
||||
on_found(group, total_found, total_found - len(processed_movies))
|
||||
on_found(group, total_found, len(valid_files))
|
||||
|
||||
# Wait for all the async events calm down a bit
|
||||
while threading.activeCount() > 100 and not self.shuttingDown():
|
||||
@@ -472,7 +473,7 @@ class Scanner(Plugin):
|
||||
data['size'] = data.get('size', 0) + self.getFileSize(cur_file)
|
||||
|
||||
data['quality'] = None
|
||||
quality = fireEvent('quality.guess', size = data['size'], files = files, extra = data, single = True)
|
||||
quality = fireEvent('quality.guess', size = data.get('size'), files = files, extra = data, single = True)
|
||||
|
||||
# Use the quality that we snatched but check if it matches our guess
|
||||
if release_download and release_download.get('quality'):
|
||||
@@ -633,7 +634,14 @@ class Scanner(Plugin):
|
||||
|
||||
name_year = self.getReleaseNameYear(identifier, file_name = filename if not group['is_dvd'] else None)
|
||||
if name_year.get('name') and name_year.get('year'):
|
||||
movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1)
|
||||
search_q = '%(name)s %(year)s' % name_year
|
||||
movie = fireEvent('movie.search', q = search_q, merge = True, limit = 1)
|
||||
|
||||
# Try with other
|
||||
if len(movie) == 0 and name_year.get('other') and name_year['other'].get('name') and name_year['other'].get('year'):
|
||||
search_q2 = '%(name)s %(year)s' % name_year.get('other')
|
||||
if search_q2 != search_q:
|
||||
movie = fireEvent('movie.search', q = search_q2, merge = True, limit = 1)
|
||||
|
||||
if len(movie) > 0:
|
||||
imdb_id = movie[0].get('imdb')
|
||||
@@ -851,7 +859,9 @@ class Scanner(Plugin):
|
||||
if key in filename.lower() and key != 'default':
|
||||
return self.resolutions[key]
|
||||
except:
|
||||
return self.resolutions['default']
|
||||
pass
|
||||
|
||||
return self.resolutions['default']
|
||||
|
||||
def getGroup(self, file):
|
||||
try:
|
||||
@@ -900,6 +910,7 @@ class Scanner(Plugin):
|
||||
log.debug('Could not detect via guessit "%s": %s', (file_name, traceback.format_exc()))
|
||||
|
||||
# Backup to simple
|
||||
release_name = os.path.basename(release_name.replace('\\', '/'))
|
||||
cleaned = ' '.join(re.split('\W+', simplifyString(release_name)))
|
||||
cleaned = re.sub(self.clean, ' ', cleaned)
|
||||
|
||||
@@ -934,8 +945,11 @@ class Scanner(Plugin):
|
||||
pass
|
||||
|
||||
if cp_guess.get('year') == guess.get('year') and len(cp_guess.get('name', '')) > len(guess.get('name', '')):
|
||||
cp_guess['other'] = guess
|
||||
return cp_guess
|
||||
elif guess == {}:
|
||||
cp_guess['other'] = guess
|
||||
return cp_guess
|
||||
|
||||
guess['other'] = cp_guess
|
||||
return guess
|
||||
@@ -32,7 +32,7 @@ class Trailer(Plugin):
|
||||
destination = os.path.join(group['destination_dir'], filename)
|
||||
if not os.path.isfile(destination):
|
||||
trailer_file = fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
|
||||
if os.path.getsize(trailer_file) < (1024 * 1024): # Don't trust small trailers (1MB), try next one
|
||||
if trailer_file and os.path.getsize(trailer_file) < (1024 * 1024): # Don't trust small trailers (1MB), try next one
|
||||
os.unlink(trailer_file)
|
||||
continue
|
||||
else:
|
||||
|
||||
@@ -90,7 +90,7 @@ Page.Wizard = new Class({
|
||||
self.parent(action, params);
|
||||
|
||||
self.addEvent('create', function(){
|
||||
self.order();
|
||||
self.orderGroups();
|
||||
});
|
||||
|
||||
self.initialized = true;
|
||||
@@ -106,7 +106,7 @@ Page.Wizard = new Class({
|
||||
}).delay(1)
|
||||
},
|
||||
|
||||
order: function(){
|
||||
orderGroups: function(){
|
||||
var self = this;
|
||||
|
||||
var form = self.el.getElement('.uniForm');
|
||||
|
||||
Reference in new issue
Block a user