Merge branch 'refs/heads/develop' into redesign

This commit is contained in:
Ruud committed 2014-07-06 22:38:51 +02:00
commit 3986de4ebc
229 files changed
+7700 -8002

No files matched your search

+22 -17
View File
@@ -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
+2
View File
@@ -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)):
+36 -5
View File
@@ -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
+2 -2
View File
@@ -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], ' ')
+1 -1
View File
@@ -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);
}
+18 -4
View File
@@ -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();
}
}
})
)
);
+51 -44
View File
@@ -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)',
},
],
},
],
+14 -1
View File
@@ -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': []
};
+106 -58
View File
@@ -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');
});
+79 -48
View File
@@ -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):
+93 -47
View File
@@ -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,
},
{
+21 -7
View File
@@ -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
+1 -1
View File
@@ -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');