Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop
Conflicts: couchpotato/core/_base/_core/__init__.py
@@ -33,7 +33,7 @@ def start():
|
|||||||
new_environ[key] = value.encode('iso-8859-1')
|
new_environ[key] = value.encode('iso-8859-1')
|
||||||
|
|
||||||
subprocess.call(args, env = new_environ)
|
subprocess.call(args, env = new_environ)
|
||||||
return os.path.isfile(os.path.join(options.data_dir, 'restart'))
|
return os.path.isfile(os.path.join(base_path, 'restart'))
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
log.critical(e)
|
log.critical(e)
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ def index():
|
|||||||
return jsonified({'routes': routes})
|
return jsonified({'routes': routes})
|
||||||
|
|
||||||
addApiView('', index)
|
addApiView('', index)
|
||||||
|
addApiView('default', index)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ config = [{
|
|||||||
{
|
{
|
||||||
'tab': 'general',
|
'tab': 'general',
|
||||||
'name': 'advanced',
|
'name': 'advanced',
|
||||||
'description': "For those who know what the're doing",
|
'description': "For those who know what they're doing",
|
||||||
'advanced': True,
|
'advanced': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
@@ -67,7 +67,7 @@ config = [{
|
|||||||
'name': 'data_dir',
|
'name': 'data_dir',
|
||||||
'label': 'Data dir',
|
'label': 'Data dir',
|
||||||
'type': 'directory',
|
'type': 'directory',
|
||||||
'description': 'Where cache/logs/etc are stored.',
|
'description': 'Where cache/logs/etc are stored. Keep empty for <strong>./_data</strong>.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'url_base',
|
'name': 'url_base',
|
||||||
@@ -79,13 +79,13 @@ config = [{
|
|||||||
'name': 'permission_folder',
|
'name': 'permission_folder',
|
||||||
'default': 0755,
|
'default': 0755,
|
||||||
'label': 'Folder CHMOD',
|
'label': 'Folder CHMOD',
|
||||||
'description': 'Permission for creating/copying folders',
|
'description': 'Permission (octal) for creating/copying folders.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'permission_file',
|
'name': 'permission_file',
|
||||||
'default': 0755,
|
'default': 0755,
|
||||||
'label': 'File CHMOD',
|
'label': 'File CHMOD',
|
||||||
'description': 'Permission for creating/copying files',
|
'description': 'Permission (octal) for creating/copying files',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
@@ -43,14 +43,13 @@ class Core(Plugin):
|
|||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
if restart:
|
if restart:
|
||||||
self.createFile(self.restartFilePath(), 'This is the most suckiest way to register if CP is restarted. Ever...')
|
self.createFile(self.restartFilePath(), 'This is the most suckiest way to register if CP is restarted. Ever...')
|
||||||
|
|
||||||
func = request.environ.get('werkzeug.server.shutdown')
|
try:
|
||||||
if func is None:
|
request.environ.get('werkzeug.server.shutdown')()
|
||||||
|
except:
|
||||||
log.error('Failed shutting down the server')
|
log.error('Failed shutting down the server')
|
||||||
func()
|
|
||||||
|
|
||||||
def removeRestartFile(self):
|
def removeRestartFile(self):
|
||||||
try:
|
try:
|
||||||
@@ -59,4 +58,4 @@ class Core(Plugin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def restartFilePath(self):
|
def restartFilePath(self):
|
||||||
return os.path.join(Env.get('data_dir'), 'restart')
|
return os.path.join(Env.get('app_dir'), 'restart')
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -16,7 +17,10 @@ class Downloader(Plugin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def cpTag(self, movie):
|
def cpTag(self, movie):
|
||||||
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
|
if Env.setting('enabled', 'renamer'):
|
||||||
|
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
def isDisabled(self):
|
def isDisabled(self):
|
||||||
return not self.isEnabled()
|
return not self.isEnabled()
|
||||||
|
|||||||
@@ -27,18 +27,16 @@ class Blackhole(Downloader):
|
|||||||
try:
|
try:
|
||||||
if not os.path.isfile(fullPath):
|
if not os.path.isfile(fullPath):
|
||||||
log.info('Downloading %s to %s.' % (data.get('type'), fullPath))
|
log.info('Downloading %s to %s.' % (data.get('type'), fullPath))
|
||||||
if isfunction(data.get('download')):
|
|
||||||
file = data.get('download')()
|
|
||||||
else:
|
|
||||||
file = self.urlopen(data.get('url'))
|
|
||||||
|
|
||||||
if not file or file == '':
|
try:
|
||||||
|
file = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||||
|
|
||||||
|
with open(fullPath, 'wb') as f:
|
||||||
|
f.write(file)
|
||||||
|
except:
|
||||||
log.debug('Failed download file: %s' % data.get('name'))
|
log.debug('Failed download file: %s' % data.get('name'))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with open(fullPath, 'wb') as f:
|
|
||||||
f.write(file)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
log.info('File %s already exists.' % fullPath)
|
log.info('File %s already exists.' % fullPath)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from urllib import urlencode
|
|||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -37,15 +38,11 @@ class Sabnzbd(Downloader):
|
|||||||
params = {
|
params = {
|
||||||
'apikey': self.conf('api_key'),
|
'apikey': self.conf('api_key'),
|
||||||
'cat': self.conf('category'),
|
'cat': self.conf('category'),
|
||||||
'mode': 'addurl',
|
'mode': 'addfile',
|
||||||
'name': data.get('url'),
|
|
||||||
'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)),
|
'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)),
|
||||||
}
|
}
|
||||||
|
|
||||||
# sabNzbd complains about "invalid archive file" for newzbin urls
|
nzb_file = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||||
# added using addurl, works fine with addid
|
|
||||||
if data.get('addbyid'):
|
|
||||||
params['mode'] = 'addid'
|
|
||||||
|
|
||||||
if pp:
|
if pp:
|
||||||
params['script'] = pp_script_fn
|
params['script'] = pp_script_fn
|
||||||
@@ -53,9 +50,9 @@ class Sabnzbd(Downloader):
|
|||||||
url = cleanHost(self.conf('host')) + "api?" + urlencode(params)
|
url = cleanHost(self.conf('host')) + "api?" + urlencode(params)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = self.urlopen(url)
|
data = self.urlopen(url, params = {"nzbfile": (params['nzbname'] + ".nzb", nzb_file)}, multipart = True)
|
||||||
except Exception, e:
|
except Exception:
|
||||||
log.error("Unable to connect to SAB: %s" % e)
|
log.error("Unable to connect to SAB: %s" % traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
result = data.strip()
|
result = data.strip()
|
||||||
@@ -63,7 +60,7 @@ class Sabnzbd(Downloader):
|
|||||||
log.error("SABnzbd didn't return anything.")
|
log.error("SABnzbd didn't return anything.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
log.debug("Result text from SAB: " + result)
|
log.debug("Result text from SAB: " + result[:40])
|
||||||
if result == "ok":
|
if result == "ok":
|
||||||
log.info("NZB sent to SAB successfully.")
|
log.info("NZB sent to SAB successfully.")
|
||||||
return True
|
return True
|
||||||
@@ -71,7 +68,7 @@ class Sabnzbd(Downloader):
|
|||||||
log.error("Incorrect username/password.")
|
log.error("Incorrect username/password.")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
log.error("Unknown error: " + result)
|
log.error("Unknown error: " + result[:40])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def buildPp(self, imdb_id):
|
def buildPp(self, imdb_id):
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ log = CPLog(__name__)
|
|||||||
events = {}
|
events = {}
|
||||||
|
|
||||||
|
|
||||||
def addEvent(name, handler):
|
def addEvent(name, handler, priority = 0):
|
||||||
|
|
||||||
if events.get(name):
|
if events.get(name):
|
||||||
e = events[name]
|
e = events[name]
|
||||||
@@ -27,7 +27,7 @@ def addEvent(name, handler):
|
|||||||
|
|
||||||
return h
|
return h
|
||||||
|
|
||||||
e += createHandle
|
e.handle(createHandle, priority = priority)
|
||||||
|
|
||||||
def removeEvent(name, handler):
|
def removeEvent(name, handler):
|
||||||
e = events[name]
|
e = events[name]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from couchpotato.core.helpers.variable import natcmp
|
||||||
from flask.globals import current_app
|
from flask.globals import current_app
|
||||||
from flask.helpers import json
|
from flask.helpers import json
|
||||||
from libs.werkzeug.urls import url_decode
|
from libs.werkzeug.urls import url_decode
|
||||||
@@ -42,7 +43,7 @@ def dictToList(params):
|
|||||||
new = {}
|
new = {}
|
||||||
for x, value in params.iteritems():
|
for x, value in params.iteritems():
|
||||||
try:
|
try:
|
||||||
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys())]
|
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys(), cmp = natcmp)]
|
||||||
except:
|
except:
|
||||||
new_value = value
|
new_value = value
|
||||||
|
|
||||||
@@ -70,4 +71,3 @@ def jsonified(*args, **kwargs):
|
|||||||
return padded_jsonify(callback, *args, **kwargs)
|
return padded_jsonify(callback, *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
return jsonify('text/javascript' if Env.doDebug() else 'application/json', *args, **kwargs)
|
return jsonify('text/javascript' if Env.doDebug() else 'application/json', *args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import os.path
|
import os.path
|
||||||
|
import re
|
||||||
|
|
||||||
def isDict(object):
|
def isDict(object):
|
||||||
return isinstance(object, dict)
|
return isinstance(object, dict)
|
||||||
|
|
||||||
|
|
||||||
def mergeDicts(a, b):
|
def mergeDicts(a, b):
|
||||||
assert isDict(a), isDict(b)
|
assert isDict(a), isDict(b)
|
||||||
dst = a.copy()
|
dst = a.copy()
|
||||||
@@ -16,7 +16,7 @@ def mergeDicts(a, b):
|
|||||||
if key not in current_dst:
|
if key not in current_dst:
|
||||||
current_dst[key] = current_src[key]
|
current_dst[key] = current_src[key]
|
||||||
else:
|
else:
|
||||||
if isDict(current_src[key]) and isDict(current_dst[key]) :
|
if isDict(current_src[key]) and isDict(current_dst[key]):
|
||||||
stack.append((current_dst[key], current_src[key]))
|
stack.append((current_dst[key], current_src[key]))
|
||||||
else:
|
else:
|
||||||
current_dst[key] = current_src[key]
|
current_dst[key] = current_src[key]
|
||||||
@@ -42,3 +42,13 @@ def cleanHost(host):
|
|||||||
host += '/'
|
host += '/'
|
||||||
|
|
||||||
return host
|
return host
|
||||||
|
|
||||||
|
def tryInt(s):
|
||||||
|
try: return int(s)
|
||||||
|
except: return s
|
||||||
|
|
||||||
|
def natsortKey(s):
|
||||||
|
return map(tryInt, re.findall(r'(\d+|\D+)', s))
|
||||||
|
|
||||||
|
def natcmp(a, b):
|
||||||
|
return cmp(natsortKey(a), natsortKey(b))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import re
|
|||||||
class CPLog():
|
class CPLog():
|
||||||
|
|
||||||
context = ''
|
context = ''
|
||||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username']
|
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h']
|
||||||
|
|
||||||
def __init__(self, context = ''):
|
def __init__(self, context = ''):
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class Notification(Plugin):
|
|||||||
data = {}
|
data = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
#return jsonified({'success': success})
|
return jsonified({'success': success})
|
||||||
|
|
||||||
def testNotifyName(self):
|
def testNotifyName(self):
|
||||||
return 'notify.%s.test' % self.getName().lower()
|
return 'notify.%s.test' % self.getName().lower()
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.core.event import addEvent
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from couchpotato.core.settings.model import History as Hist
|
from couchpotato.core.settings.model import History as Hist
|
||||||
from couchpotato.environment import Env
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -13,12 +11,6 @@ class History(Notification):
|
|||||||
|
|
||||||
listen_to = ['movie.downloaded', 'movie.snatched', 'renamer.canceled']
|
listen_to = ['movie.downloaded', 'movie.snatched', 'renamer.canceled']
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(Notification, self).__init__()
|
|
||||||
|
|
||||||
if Env.doDebug():
|
|
||||||
addEvent('app.load', self.test)
|
|
||||||
|
|
||||||
def notify(self, message = '', data = {}):
|
def notify(self, message = '', data = {}):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class Synoindex(Notification):
|
|||||||
addEvent('renamer.after', self.addToLibrary)
|
addEvent('renamer.after', self.addToLibrary)
|
||||||
|
|
||||||
def addToLibrary(self, group = {}):
|
def addToLibrary(self, group = {}):
|
||||||
|
if self.isDisabled(): return
|
||||||
|
|
||||||
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
|
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
|
||||||
log.info(u'Executing synoindex command: %s ' % command)
|
log.info(u'Executing synoindex command: %s ' % command)
|
||||||
|
|||||||
@@ -1,75 +1 @@
|
|||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
def start():
|
|
||||||
pass
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'core',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'general',
|
|
||||||
'name': 'basics',
|
|
||||||
'description': 'Needs restart before changes take effect.',
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
'default': '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'password',
|
|
||||||
'default': '',
|
|
||||||
'type': 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'advanced': True,
|
|
||||||
'default': '0.0.0.0',
|
|
||||||
'label': 'IP',
|
|
||||||
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'port',
|
|
||||||
'default': 5000,
|
|
||||||
'type': 'int',
|
|
||||||
'description': 'The port I should listen to.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'launch_browser',
|
|
||||||
'default': 1,
|
|
||||||
'type': 'bool',
|
|
||||||
'label': 'Launch Browser',
|
|
||||||
'description': 'Launch the browser when I start.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tab': 'general',
|
|
||||||
'name': 'advanced',
|
|
||||||
'description': "For those who know what the're doing",
|
|
||||||
'advanced': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'api_key',
|
|
||||||
'default': uuid4().hex,
|
|
||||||
'readonly': 1,
|
|
||||||
'label': 'Api Key',
|
|
||||||
'description': "This is top-secret! Don't share this!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'debug',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'label': 'Debug',
|
|
||||||
'description': 'Enable debugging.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'url_base',
|
|
||||||
'default': '',
|
|
||||||
'label': 'Url Base',
|
|
||||||
'description': 'When using mod_proxy use this to append the url with this.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ from couchpotato.core.helpers.variable import getExt
|
|||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from flask.helpers import send_from_directory
|
from flask.helpers import send_from_directory
|
||||||
|
from libs.multipartpost import MultipartPostHandler
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
import cookielib
|
||||||
import glob
|
import glob
|
||||||
import math
|
import math
|
||||||
import os.path
|
import os.path
|
||||||
@@ -73,11 +75,14 @@ class Plugin(object):
|
|||||||
try:
|
try:
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
os.makedirs(path, Env.getPermission('folder'))
|
os.makedirs(path, Env.getPermission('folder'))
|
||||||
|
return True
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
log.error('Unable to create folder "%s": %s' % (path, e))
|
log.error('Unable to create folder "%s": %s' % (path, e))
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
# http request
|
# http request
|
||||||
def urlopen(self, url, timeout = 10, params = {}, headers = {}):
|
def urlopen(self, url, timeout = 10, params = {}, headers = {}, multipart = False):
|
||||||
|
|
||||||
socket.setdefaulttimeout(timeout)
|
socket.setdefaulttimeout(timeout)
|
||||||
|
|
||||||
@@ -85,15 +90,24 @@ class Plugin(object):
|
|||||||
self.wait(host)
|
self.wait(host)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
log.info('Opening url: %s, params: %s' % (url, params))
|
|
||||||
|
|
||||||
data = urllib.urlencode(params) if len(params) > 0 else None
|
if multipart:
|
||||||
request = urllib2.Request(url, data, headers)
|
log.info('Opening multipart url: %s, params: %s' % (url, params.iterkeys()))
|
||||||
|
request = urllib2.Request(url, params, headers)
|
||||||
|
|
||||||
data = urllib2.urlopen(request).read()
|
cookies = cookielib.CookieJar()
|
||||||
|
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
|
||||||
|
|
||||||
|
data = opener.open(request).read()
|
||||||
|
else:
|
||||||
|
log.info('Opening url: %s, params: %s' % (url, params))
|
||||||
|
data = urllib.urlencode(params) if len(params) > 0 else None
|
||||||
|
request = urllib2.Request(url, data, headers)
|
||||||
|
|
||||||
|
data = urllib2.urlopen(request).read()
|
||||||
except IOError, e:
|
except IOError, e:
|
||||||
log.error('Failed opening url, %s: %s' % (url, e))
|
log.error('Failed opening url, %s: %s' % (url, e))
|
||||||
data = ''
|
raise
|
||||||
|
|
||||||
self.http_last_use[host] = time.time()
|
self.http_last_use[host] = time.time()
|
||||||
|
|
||||||
@@ -111,7 +125,7 @@ class Plugin(object):
|
|||||||
time.sleep(last_use - now + self.http_time_between_calls)
|
time.sleep(last_use - now + self.http_time_between_calls)
|
||||||
|
|
||||||
def beforeCall(self, handler):
|
def beforeCall(self, handler):
|
||||||
log.debug('Calling %s.%s' % (self.getName(), handler.__name__))
|
#log.debug('Calling %s.%s' % (self.getName(), handler.__name__))
|
||||||
self.isRunning('%s.%s' % (self.getName(), handler.__name__))
|
self.isRunning('%s.%s' % (self.getName(), handler.__name__))
|
||||||
|
|
||||||
def afterCall(self, handler):
|
def afterCall(self, handler):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.helpers.request import getParam, jsonified
|
from couchpotato.core.helpers.request import getParam, jsonified
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
import ctypes
|
||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ class FileBrowser(Plugin):
|
|||||||
dirs = []
|
dirs = []
|
||||||
for f in os.listdir(path):
|
for f in os.listdir(path):
|
||||||
p = os.path.join(path, f)
|
p = os.path.join(path, f)
|
||||||
if(os.path.isdir(p)):
|
if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)):
|
||||||
dirs.append(p + '/')
|
dirs.append(p + '/')
|
||||||
|
|
||||||
return dirs
|
return dirs
|
||||||
@@ -48,6 +49,21 @@ class FileBrowser(Plugin):
|
|||||||
dirs = []
|
dirs = []
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
|
'is_root': getParam('path', '/') == '/',
|
||||||
'empty': len(dirs) == 0,
|
'empty': len(dirs) == 0,
|
||||||
'dirs': dirs,
|
'dirs': dirs,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def is_hidden(self, filepath):
|
||||||
|
name = os.path.basename(os.path.abspath(filepath))
|
||||||
|
return name.startswith('.') or self.has_hidden_attribute(filepath)
|
||||||
|
|
||||||
|
def has_hidden_attribute(self, filepath):
|
||||||
|
try:
|
||||||
|
attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(filepath))
|
||||||
|
assert attrs != -1
|
||||||
|
result = bool(attrs & 2)
|
||||||
|
except (AttributeError, AssertionError):
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|||||||
@@ -42,17 +42,11 @@ class FileManager(Plugin):
|
|||||||
if not dest: # to Cache
|
if not dest: # to Cache
|
||||||
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
|
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
|
||||||
|
|
||||||
if overwrite or not os.path.exists(dest):
|
if overwrite or not os.path.isfile(dest):
|
||||||
log.debug('Writing file to: %s' % dest)
|
self.createFile(dest, file)
|
||||||
output = open(dest, 'wb')
|
|
||||||
output.write(file)
|
|
||||||
output.close()
|
|
||||||
else:
|
|
||||||
log.debug('File already exists: %s' % dest)
|
|
||||||
|
|
||||||
return dest
|
return dest
|
||||||
|
|
||||||
|
|
||||||
def add(self, path = '', part = 1, type = (), available = 1, properties = {}):
|
def add(self, path = '', part = 1, type = (), available = 1, properties = {}):
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ from couchpotato import get_session
|
|||||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
from couchpotato.core.settings.model import Library, LibraryTitle, File, \
|
||||||
|
LibraryGenre
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
class LibraryPlugin(Plugin):
|
class LibraryPlugin(Plugin):
|
||||||
|
|
||||||
default_dict = {'titles': {}, 'files':{}, 'info':{}}
|
default_dict = {'titles': {}, 'files':{}, 'info':{}, 'genres':{}}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('library.add', self.add)
|
addEvent('library.add', self.add)
|
||||||
@@ -51,7 +52,9 @@ class LibraryPlugin(Plugin):
|
|||||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||||
done_status = fireEvent('status.get', 'done', single = True)
|
done_status = fireEvent('status.get', 'done', single = True)
|
||||||
|
|
||||||
library_dict = library.to_dict(self.default_dict)
|
if library:
|
||||||
|
library_dict = library.to_dict(self.default_dict)
|
||||||
|
|
||||||
do_update = True
|
do_update = True
|
||||||
|
|
||||||
if library.status_id == done_status.get('id') and not force:
|
if library.status_id == done_status.get('id') and not force:
|
||||||
@@ -60,7 +63,7 @@ class LibraryPlugin(Plugin):
|
|||||||
info = fireEvent('provider.movie.info', merge = True, identifier = identifier)
|
info = fireEvent('provider.movie.info', merge = True, identifier = identifier)
|
||||||
if not info or len(info) == 0:
|
if not info or len(info) == 0:
|
||||||
log.error('Could not update, no movie info to work with: %s' % identifier)
|
log.error('Could not update, no movie info to work with: %s' % identifier)
|
||||||
do_update = False
|
return False
|
||||||
|
|
||||||
# Main info
|
# Main info
|
||||||
if do_update:
|
if do_update:
|
||||||
@@ -75,7 +78,6 @@ class LibraryPlugin(Plugin):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
titles = info.get('titles', [])
|
titles = info.get('titles', [])
|
||||||
|
|
||||||
log.debug('Adding titles: %s' % titles)
|
log.debug('Adding titles: %s' % titles)
|
||||||
for title in titles:
|
for title in titles:
|
||||||
t = LibraryTitle(
|
t = LibraryTitle(
|
||||||
@@ -86,6 +88,20 @@ class LibraryPlugin(Plugin):
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
# Genres
|
||||||
|
[db.delete(genre) for genre in library.genres]
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
genres = info.get('genres', [])
|
||||||
|
log.debug('Adding genres: %s' % genres)
|
||||||
|
for genre in genres:
|
||||||
|
g = LibraryGenre(
|
||||||
|
name = genre
|
||||||
|
)
|
||||||
|
library.genres.append(g)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
# Files
|
# Files
|
||||||
images = info.get('images', [])
|
images = info.get('images', [])
|
||||||
for type in images:
|
for type in images:
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ log = CPLog(__name__)
|
|||||||
class MetaData(Plugin):
|
class MetaData(Plugin):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('renaming.after', self.add)
|
addEvent('renamer.after', self.add)
|
||||||
|
|
||||||
addEvent('app.load', self.add)
|
|
||||||
|
|
||||||
def add(self, data = {}):
|
def add(self, data = {}):
|
||||||
log.info('Getting meta data')
|
log.info('Getting meta data')
|
||||||
|
|||||||
@@ -5,11 +5,20 @@ from couchpotato.core.helpers.request import getParams, jsonified
|
|||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Movie
|
from couchpotato.core.settings.model import Movie
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
|
from sqlalchemy.sql.expression import or_
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
|
|
||||||
|
|
||||||
class MoviePlugin(Plugin):
|
class MoviePlugin(Plugin):
|
||||||
|
|
||||||
|
default_dict = {
|
||||||
|
'profile': {'types': {'quality': {}}},
|
||||||
|
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
||||||
|
'library': {'titles': {}, 'files':{}},
|
||||||
|
'files': {},
|
||||||
|
'status': {}
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addApiView('movie.search', self.search)
|
addApiView('movie.search', self.search)
|
||||||
addApiView('movie.list', self.list)
|
addApiView('movie.list', self.list)
|
||||||
@@ -24,18 +33,16 @@ class MoviePlugin(Plugin):
|
|||||||
params = getParams()
|
params = getParams()
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
results = db.query(Movie).filter(
|
# Make a list from string
|
||||||
Movie.status.has(identifier = params.get('status', 'active'))
|
status = params.get('status', ['active'])
|
||||||
).all()
|
if not isinstance(status, (list, tuple)):
|
||||||
|
status = [status]
|
||||||
|
|
||||||
|
results = db.query(Movie).filter(or_(*[Movie.status.has(identifier = s) for s in status])).all()
|
||||||
|
|
||||||
movies = []
|
movies = []
|
||||||
for movie in results:
|
for movie in results:
|
||||||
temp = movie.to_dict(deep = {
|
temp = movie.to_dict(self.default_dict)
|
||||||
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
|
||||||
'library': {'titles': {}, 'files':{}},
|
|
||||||
'files': {}
|
|
||||||
})
|
|
||||||
|
|
||||||
movies.append(temp)
|
movies.append(temp)
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
@@ -59,12 +66,7 @@ class MoviePlugin(Plugin):
|
|||||||
if movie:
|
if movie:
|
||||||
#addEvent('library.update.after', )
|
#addEvent('library.update.after', )
|
||||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
|
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
|
||||||
fireEventAsync('searcher.single', movie.to_dict(deep = {
|
fireEventAsync('searcher.single', movie.to_dict(self.default_dict))
|
||||||
'profile': {'types': {'quality': {}}},
|
|
||||||
'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}},
|
|
||||||
'library': {'titles': {}, 'files':{}},
|
|
||||||
'files': {}
|
|
||||||
}))
|
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -116,13 +118,14 @@ class MoviePlugin(Plugin):
|
|||||||
if release.status_id == status_snatched.get('id'):
|
if release.status_id == status_snatched.get('id'):
|
||||||
release.delete()
|
release.delete()
|
||||||
|
|
||||||
|
m.profile_id = params.get('profile_id')
|
||||||
|
|
||||||
m.status_id = status_active.get('id')
|
m.status_id = status_active.get('id')
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
movie_dict = m.to_dict(deep = {
|
movie_dict = m.to_dict(self.default_dict)
|
||||||
'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}},
|
|
||||||
'library': {'titles': {}}
|
fireEventAsync('searcher.single', movie_dict)
|
||||||
})
|
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': True,
|
'success': True,
|
||||||
|
|||||||
@@ -27,12 +27,17 @@ var MovieList = new Class({
|
|||||||
self.createNavigation();
|
self.createNavigation();
|
||||||
|
|
||||||
Object.each(self.movies, function(info){
|
Object.each(self.movies, function(info){
|
||||||
|
|
||||||
|
// Attach proper actions
|
||||||
|
var a = self.options.actions
|
||||||
|
var actions = a[info.status.identifier.capitalize()] || a.Wanted || {};
|
||||||
|
|
||||||
var m = new Movie(self, {
|
var m = new Movie(self, {
|
||||||
'actions': self.options.actions
|
'actions': actions
|
||||||
}, info);
|
}, info);
|
||||||
$(m).inject(self.el);
|
$(m).inject(self.el);
|
||||||
m.fireEvent('injected');
|
m.fireEvent('injected');
|
||||||
|
|
||||||
if(self.options.navigation){
|
if(self.options.navigation){
|
||||||
var first_char = m.getTitle().substr(0, 1);
|
var first_char = m.getTitle().substr(0, 1);
|
||||||
self.activateLetter(first_char);
|
self.activateLetter(first_char);
|
||||||
@@ -71,7 +76,7 @@ var MovieList = new Class({
|
|||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
activateLetter: function(letter){
|
activateLetter: function(letter){
|
||||||
this.letters[letter].addClass('active');
|
this.letters[letter].addClass('active');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
/* @override http://localhost:5000/static/movie_plugin/movie.css */
|
/* @override
|
||||||
|
http://localhost:5000/static/movie_plugin/movie.css
|
||||||
|
http://192.168.1.20:5000/static/movie_plugin/movie.css
|
||||||
|
http://127.0.0.1:5000/static/movie_plugin/movie.css
|
||||||
|
*/
|
||||||
|
|
||||||
.movies {
|
.movies {
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
@@ -79,13 +83,21 @@
|
|||||||
float: left;
|
float: left;
|
||||||
width: 5%;
|
width: 5%;
|
||||||
padding: 0 0 0 3%;
|
padding: 0 0 0 3%;
|
||||||
background: url('../images/rating.png') no-repeat left center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .info .description {
|
.movies .info .description {
|
||||||
clear: both;
|
clear: both;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.movies .data .quality span {
|
||||||
|
padding: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movies .data .quality .available { color: orange; }
|
||||||
|
.movies .data .quality .snatched { color: lightgreen; }
|
||||||
|
|
||||||
.movies .data .actions {
|
.movies .data .actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 15px;
|
right: 15px;
|
||||||
@@ -96,17 +108,14 @@
|
|||||||
.movies .data:hover .action:hover { opacity: 1; }
|
.movies .data:hover .action:hover { opacity: 1; }
|
||||||
|
|
||||||
.movies .data .action {
|
.movies .data .action {
|
||||||
background: no-repeat center;
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.movies .data .action.refresh { background-image: url('../images/reload.png'); }
|
|
||||||
.movies .data .action.delete { background-image: url('../images/delete.png'); }
|
|
||||||
.movies .data .action.edit { background-image: url('../images/edit.png'); }
|
|
||||||
.movies .data .action.imdb { background-image: url('../images/imdb.png'); }
|
|
||||||
|
|
||||||
.movies .delete_container {
|
.movies .delete_container {
|
||||||
clear: both;
|
clear: both;
|
||||||
@@ -142,6 +151,65 @@
|
|||||||
padding: 2%;
|
padding: 2%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.movies .options .releases {
|
||||||
|
height: 157px;
|
||||||
|
overflow: auto;
|
||||||
|
margin: -20px -20px -20px 110px;
|
||||||
|
padding: 15px 0 5px;
|
||||||
|
}
|
||||||
|
.movies .options .releases .item {
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.movies .options .releases .item:last-child { border: 0; }
|
||||||
|
.movies .options .releases .item:nth-child(even) {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
.movies .options .releases .item:not(.head):hover {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.movies .options .releases .item > * {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 5px;
|
||||||
|
width: 50px;
|
||||||
|
min-height: 24px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
-moz-text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: top;
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.movies .options .releases .item > *:first-child {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.movies .options .releases .provider {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
.movies .options .releases .name {
|
||||||
|
width: 360px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movies .options .releases a {
|
||||||
|
width: 16px !important;
|
||||||
|
height: 16px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.movies .options .releases a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movies .options .releases .head > * {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.movies .alph_nav ul {
|
.movies .alph_nav ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
var Movie = new Class({
|
var Movie = new Class({
|
||||||
|
|
||||||
Extends: BlockBase,
|
Extends: BlockBase,
|
||||||
|
|
||||||
action: {},
|
action: {},
|
||||||
|
|
||||||
initialize: function(self, options, data){
|
initialize: function(self, options, data){
|
||||||
@@ -32,7 +32,7 @@ var Movie = new Class({
|
|||||||
self.year = new Element('div.year', {
|
self.year = new Element('div.year', {
|
||||||
'text': self.data.library.year || 'Unknown'
|
'text': self.data.library.year || 'Unknown'
|
||||||
}),
|
}),
|
||||||
self.rating = new Element('div.rating', {
|
self.rating = new Element('div.rating.icon', {
|
||||||
'text': self.data.library.rating
|
'text': self.data.library.rating
|
||||||
}),
|
}),
|
||||||
self.description = new Element('div.description', {
|
self.description = new Element('div.description', {
|
||||||
@@ -45,13 +45,20 @@ var Movie = new Class({
|
|||||||
self.actions = new Element('div.actions')
|
self.actions = new Element('div.actions')
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
self.profile.get('types').each(function(type){
|
self.profile.get('types').each(function(type){
|
||||||
|
|
||||||
|
// Check if quality is snatched
|
||||||
|
var is_snatched = self.data.releases.filter(function(release){
|
||||||
|
return release.quality_id == type.quality_id && release.status.identifier == 'snatched'
|
||||||
|
}).pick();
|
||||||
|
|
||||||
var q = Quality.getQuality(type.quality_id);
|
var q = Quality.getQuality(type.quality_id);
|
||||||
new Element('span', {
|
new Element('span', {
|
||||||
'text': ' '+q.label
|
'text': q.label,
|
||||||
|
'class': is_snatched ? 'snatched' : ''
|
||||||
}).inject(self.quality);
|
}).inject(self.quality);
|
||||||
})
|
});
|
||||||
|
|
||||||
Object.each(self.options.actions, function(action, key){
|
Object.each(self.options.actions, function(action, key){
|
||||||
self.actions.adopt(
|
self.actions.adopt(
|
||||||
@@ -127,7 +134,7 @@ var Movie = new Class({
|
|||||||
|
|
||||||
var MovieAction = new Class({
|
var MovieAction = new Class({
|
||||||
|
|
||||||
class_name: 'action',
|
class_name: 'action icon',
|
||||||
|
|
||||||
initialize: function(movie){
|
initialize: function(movie){
|
||||||
var self = this;
|
var self = this;
|
||||||
@@ -193,7 +200,7 @@ var ReleaseAction = new Class({
|
|||||||
|
|
||||||
self.id = self.movie.get('identifier');
|
self.id = self.movie.get('identifier');
|
||||||
|
|
||||||
self.el = new Element('a.releases', {
|
self.el = new Element('a.releases.icon.download', {
|
||||||
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
|
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.show.bind(self)
|
'click': self.show.bind(self)
|
||||||
@@ -211,16 +218,78 @@ var ReleaseAction = new Class({
|
|||||||
$(self.movie.thumbnail).clone(),
|
$(self.movie.thumbnail).clone(),
|
||||||
self.release_container = new Element('div.releases')
|
self.release_container = new Element('div.releases')
|
||||||
).inject(self.movie, 'top');
|
).inject(self.movie, 'top');
|
||||||
|
|
||||||
|
// Header
|
||||||
|
new Element('div.item.head').adopt(
|
||||||
|
new Element('span.name', {'text': 'Release name'}),
|
||||||
|
new Element('span.quality', {'text': 'Quality'}),
|
||||||
|
new Element('span.size', {'text': 'Size (MB)'}),
|
||||||
|
new Element('span.age', {'text': 'Age'}),
|
||||||
|
new Element('span.score', {'text': 'Score'}),
|
||||||
|
new Element('span.provider', {'text': 'Provider'})
|
||||||
|
).inject(self.release_container)
|
||||||
|
|
||||||
Array.each(self.movie.data.releases, function(release){
|
Array.each(self.movie.data.releases, function(release){
|
||||||
p(release);
|
|
||||||
new Element('div', {
|
new Element('div', {
|
||||||
'text': release.title
|
'class': 'item ' + release.status.identifier
|
||||||
}).inject(self.release_container)
|
}).adopt(
|
||||||
|
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
|
||||||
|
new Element('span.quality', {'text': release.quality.label}),
|
||||||
|
new Element('span.size', {'text': (self.get(release, 'size') || 'unknown')}),
|
||||||
|
new Element('span.age', {'text': self.get(release, 'age')}),
|
||||||
|
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||||
|
new Element('span.provider', {'text': self.get(release, 'provider')}),
|
||||||
|
new Element('a.download.icon', {
|
||||||
|
'events': {
|
||||||
|
'click': function(e){
|
||||||
|
(e).stop();
|
||||||
|
self.download(release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Element('a.delete.icon', {
|
||||||
|
'events': {
|
||||||
|
'click': function(e){
|
||||||
|
(e).stop();
|
||||||
|
self.del(release);
|
||||||
|
this.getParent('.item').destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).inject(self.release_container)
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
self.movie.slide('in');
|
self.movie.slide('in');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get: function(release, type){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return (release.info.filter(function(info){
|
||||||
|
return type == info.identifier
|
||||||
|
}).pick() || {}).value
|
||||||
|
},
|
||||||
|
|
||||||
|
download: function(release){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
Api.request('release.download', {
|
||||||
|
'data': {
|
||||||
|
'id': release.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
del: function(release){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
Api.request('release.delete', {
|
||||||
|
'data': {
|
||||||
|
'id': release.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
/* @override http://localhost:5000/static/movie_plugin/search.css */
|
/* @override
|
||||||
|
http://localhost:5000/static/movie_plugin/search.css
|
||||||
|
http://192.168.1.20:5000/static/movie_plugin/search.css
|
||||||
|
*/
|
||||||
|
|
||||||
.search_form {
|
.search_form {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -86,7 +89,7 @@
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.search_form .results .movie .options select[name=title] { width: 180px; }
|
.search_form .results .movie .options select[name=title] { width: 180px; }
|
||||||
.search_form .results .movie .options select[name=quality] { width: 90px; }
|
.search_form .results .movie .options select[name=profile] { width: 90px; }
|
||||||
|
|
||||||
.search_form .results .movie .options .button {
|
.search_form .results .movie .options .button {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Block.Search = new Class({
|
|||||||
self.el = new Element('div.search_form').adopt(
|
self.el = new Element('div.search_form').adopt(
|
||||||
new Element('div.input').adopt(
|
new Element('div.input').adopt(
|
||||||
self.input = new Element('input.inlay', {
|
self.input = new Element('input.inlay', {
|
||||||
|
'placeholder': 'Search for new movies',
|
||||||
'events': {
|
'events': {
|
||||||
'keyup': self.keyup.bind(self),
|
'keyup': self.keyup.bind(self),
|
||||||
'focus': self.hideResults.bind(self, false)
|
'focus': self.hideResults.bind(self, false)
|
||||||
@@ -28,7 +29,7 @@ Block.Search = new Class({
|
|||||||
}).adopt(
|
}).adopt(
|
||||||
new Element('div.pointer'),
|
new Element('div.pointer'),
|
||||||
self.results = new Element('div.results')
|
self.results = new Element('div.results')
|
||||||
).fade('hide')
|
).hide()
|
||||||
);
|
);
|
||||||
|
|
||||||
self.spinner = new Spinner(self.result_container);
|
self.spinner = new Spinner(self.result_container);
|
||||||
@@ -51,7 +52,7 @@ Block.Search = new Class({
|
|||||||
|
|
||||||
if(self.hidden == bool) return;
|
if(self.hidden == bool) return;
|
||||||
|
|
||||||
self.result_container.fade(bool ? 0 : 1)
|
self.result_container[bool ? 'hide' : 'show']();
|
||||||
|
|
||||||
if(bool){
|
if(bool){
|
||||||
History.removeEvent('change', self.hideResults.bind(self, !bool));
|
History.removeEvent('change', self.hideResults.bind(self, !bool));
|
||||||
@@ -302,7 +303,7 @@ Block.Search.Item = new Class({
|
|||||||
}).inject(self.title_select)
|
}).inject(self.title_select)
|
||||||
})
|
})
|
||||||
|
|
||||||
Object.each(Quality.profiles, function(profile){
|
Object.each(Quality.getActiveProfiles(), function(profile){
|
||||||
new Element('option', {
|
new Element('option', {
|
||||||
'value': profile.id ? profile.id : profile.data.id,
|
'value': profile.id ? profile.id : profile.data.id,
|
||||||
'text': profile.label ? profile.label : profile.data.label
|
'text': profile.label ? profile.label : profile.data.label
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.request import jsonified, getParams, getParam
|
from couchpotato.core.helpers.request import jsonified, getParams, getParam
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
@@ -15,8 +16,11 @@ class ProfilePlugin(Plugin):
|
|||||||
addEvent('profile.all', self.all)
|
addEvent('profile.all', self.all)
|
||||||
|
|
||||||
addApiView('profile.save', self.save)
|
addApiView('profile.save', self.save)
|
||||||
|
addApiView('profile.save_order', self.saveOrder)
|
||||||
addApiView('profile.delete', self.delete)
|
addApiView('profile.delete', self.delete)
|
||||||
|
|
||||||
|
addEvent('app.initialize', self.fill, priority = 90)
|
||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
@@ -50,7 +54,7 @@ class ProfilePlugin(Plugin):
|
|||||||
for type in params.get('types', []):
|
for type in params.get('types', []):
|
||||||
t = ProfileType(
|
t = ProfileType(
|
||||||
order = order,
|
order = order,
|
||||||
finish = type.get('finish'),
|
finish = type.get('finish') if order > 0 else 1,
|
||||||
wait_for = params.get('wait_for'),
|
wait_for = params.get('wait_for'),
|
||||||
quality_id = type.get('quality_id')
|
quality_id = type.get('quality_id')
|
||||||
)
|
)
|
||||||
@@ -67,6 +71,25 @@ class ProfilePlugin(Plugin):
|
|||||||
'profile': profile_dict
|
'profile': profile_dict
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def saveOrder(self):
|
||||||
|
|
||||||
|
params = getParams()
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
order = 0
|
||||||
|
for profile in params.get('ids', []):
|
||||||
|
p = db.query(Profile).filter_by(id = profile).first()
|
||||||
|
p.hide = params.get('hidden')[order]
|
||||||
|
p.order = order
|
||||||
|
|
||||||
|
order += 1
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return jsonified({
|
||||||
|
'success': True
|
||||||
|
})
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
|
|
||||||
id = getParam('id')
|
id = getParam('id')
|
||||||
@@ -90,3 +113,44 @@ class ProfilePlugin(Plugin):
|
|||||||
'success': success,
|
'success': success,
|
||||||
'message': message
|
'message': message
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def fill(self):
|
||||||
|
|
||||||
|
db = get_session();
|
||||||
|
|
||||||
|
profiles = [{
|
||||||
|
'label': 'Best',
|
||||||
|
'qualities': ['720p', '1080p', 'brrip', 'dvdrip']
|
||||||
|
}, {
|
||||||
|
'label': 'HD',
|
||||||
|
'qualities': ['720p', '1080p']
|
||||||
|
}]
|
||||||
|
|
||||||
|
# Create default quality profile
|
||||||
|
order = -2
|
||||||
|
for profile in profiles:
|
||||||
|
log.info('Creating default profile: %s' % profile.get('label'))
|
||||||
|
p = Profile(
|
||||||
|
label = toUnicode(profile.get('label')),
|
||||||
|
order = order
|
||||||
|
)
|
||||||
|
db.add(p)
|
||||||
|
|
||||||
|
quality_order = 0
|
||||||
|
for quality in profile.get('qualities'):
|
||||||
|
quality = fireEvent('quality.single', identifier = quality, single = True)
|
||||||
|
profile_type = ProfileType(
|
||||||
|
quality_id = quality.get('id'),
|
||||||
|
profile = p,
|
||||||
|
finish = True,
|
||||||
|
wait_for = 0,
|
||||||
|
order = quality_order
|
||||||
|
)
|
||||||
|
p.types.append(profile_type)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
quality_order += 1
|
||||||
|
|
||||||
|
order += 1
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
|
After Width: | Height: | Size: 160 B |
@@ -1,18 +1,134 @@
|
|||||||
.profile > .delete {
|
/* @override http://192.168.1.20:5000/static/profile_plugin/profile.css */
|
||||||
background-position: center;
|
|
||||||
height: 20px;
|
.add_new_profile {
|
||||||
width: 20px;
|
padding: 20px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile .types .type .handle {
|
.profile { border-bottom: 1px solid rgba(255,255,255,0.2) }
|
||||||
background: url('../../images/handle.png') center;
|
|
||||||
display: inline-block;
|
.profile > .delete {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
position: absolute;
|
||||||
|
margin-left: 690px;
|
||||||
|
padding: 14px;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .qualities {
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .formHint {
|
||||||
|
width: 250px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .wait_for {
|
||||||
|
position: absolute;
|
||||||
|
margin: -45px 0 0 437px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .wait_for input {
|
||||||
|
margin: 0 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .types {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 20px 0 -4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .types li {
|
||||||
|
padding: 3px 5px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.profile .types li:last-child { border: 0; }
|
||||||
|
|
||||||
|
.profile .types li > * {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .quality_type select {
|
||||||
|
width: 186px;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .types li.is_empty .check, .profile .types li.is_empty .delete, .profile .types li.is_empty .handle {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .types .type .handle {
|
||||||
|
background: url('./handle.png') center;
|
||||||
|
display: inline-block;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
cursor: grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .types .type .delete {
|
||||||
|
background-position: left center;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
visibility: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .types .type:hover:not(.is_empty) .delete {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile_ordering {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile .types .type .delete {
|
#profile_ordering ul {
|
||||||
background-position: center;
|
float: left;
|
||||||
height: 20px;
|
margin: 0;
|
||||||
width: 20px;
|
width: 275px;
|
||||||
}
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile_ordering li {
|
||||||
|
cursor: grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
#profile_ordering li:last-child { border: 0; }
|
||||||
|
|
||||||
|
#profile_ordering li .check {
|
||||||
|
margin: 2px 10px 0 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile_ordering li > span {
|
||||||
|
display: inline-block;
|
||||||
|
height: 20px;
|
||||||
|
vertical-align: top;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile_ordering li .handle {
|
||||||
|
background: url('./handle.png') center;
|
||||||
|
width: 20px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profile_ordering .formHint {
|
||||||
|
clear: none;
|
||||||
|
float: right;
|
||||||
|
width: 250px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -24,47 +24,32 @@ var Profile = new Class({
|
|||||||
var data = self.data;
|
var data = self.data;
|
||||||
|
|
||||||
self.el = new Element('div.profile').adopt(
|
self.el = new Element('div.profile').adopt(
|
||||||
self.header = new Element('h4', {'text': data.label}),
|
|
||||||
new Element('span.delete.icon', {
|
new Element('span.delete.icon', {
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.del.bind(self)
|
'click': self.del.bind(self)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new Element('div', {
|
new Element('.quality_label.ctrlHolder').adopt(
|
||||||
'class': 'ctrlHolder'
|
|
||||||
}).adopt(
|
|
||||||
new Element('label', {'text':'Name'}),
|
new Element('label', {'text':'Name'}),
|
||||||
new Element('input.label.textInput.large', {
|
new Element('input.inlay', {
|
||||||
'type':'text',
|
'type':'text',
|
||||||
'value': data.label,
|
'value': data.label,
|
||||||
'events': {
|
'placeholder': 'Profile name'
|
||||||
'keyup': function(){
|
|
||||||
self.header.set('text', this.get('value'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
new Element('div.ctrlHolder').adopt(
|
new Element('div.wait_for.ctrlHolder').adopt(
|
||||||
new Element('label', {'text':'Wait'}),
|
new Element('span', {'text':'Wait'}),
|
||||||
new Element('input.wait_for.textInput.xsmall', {
|
new Element('input.inlay.xsmall', {
|
||||||
'type':'text',
|
'type':'text',
|
||||||
'value': data.types && data.types.length > 0 ? data.types[0].wait_for : 0
|
'value': data.types && data.types.length > 0 ? data.types[0].wait_for : 0
|
||||||
}),
|
}),
|
||||||
new Element('span', {'text':' day(s) for better quality.'})
|
new Element('span', {'text':'day(s) for a better quality.'})
|
||||||
),
|
),
|
||||||
new Element('div.ctrlHolder').adopt(
|
new Element('div.qualities.ctrlHolder').adopt(
|
||||||
new Element('label', {'text': 'Qualities'}),
|
new Element('label', {'text': 'Search for'}),
|
||||||
new Element('div.head').adopt(
|
|
||||||
new Element('span.quality_type', {'text': 'Search for'}),
|
|
||||||
new Element('span.finish', {'html': '<acronym title="Won\'t download anything else if it has found this quality.">Finish</acronym>'})
|
|
||||||
),
|
|
||||||
self.type_container = new Element('ol.types'),
|
self.type_container = new Element('ol.types'),
|
||||||
new Element('a.addType', {
|
new Element('div.formHint', {
|
||||||
'text': 'Add another quality to search for.',
|
'html': "Search these qualities (2 minimum), from top to bottom. Use the checkbox, to stop searching after it found this quality."
|
||||||
'href': '#',
|
|
||||||
'events': {
|
|
||||||
'click': self.addType.bind(self)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -73,6 +58,8 @@ var Profile = new Class({
|
|||||||
|
|
||||||
if(data.types)
|
if(data.types)
|
||||||
Object.each(data.types, self.addType.bind(self))
|
Object.each(data.types, self.addType.bind(self))
|
||||||
|
|
||||||
|
self.addType();
|
||||||
},
|
},
|
||||||
|
|
||||||
save: function(delay){
|
save: function(delay){
|
||||||
@@ -81,6 +68,8 @@ var Profile = new Class({
|
|||||||
if(self.save_timer) clearTimeout(self.save_timer);
|
if(self.save_timer) clearTimeout(self.save_timer);
|
||||||
self.save_timer = (function(){
|
self.save_timer = (function(){
|
||||||
|
|
||||||
|
self.addType();
|
||||||
|
|
||||||
var data = self.getData();
|
var data = self.getData();
|
||||||
if(data.types.length < 2) return;
|
if(data.types.length < 2) return;
|
||||||
|
|
||||||
@@ -96,6 +85,7 @@ var Profile = new Class({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}).delay(delay, self)
|
}).delay(delay, self)
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -105,8 +95,8 @@ var Profile = new Class({
|
|||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
'id' : self.data.id,
|
'id' : self.data.id,
|
||||||
'label' : self.el.getElement('.label').get('value'),
|
'label' : self.el.getElement('.quality_label input').get('value'),
|
||||||
'wait_for' : self.el.getElement('.wait_for').get('value'),
|
'wait_for' : self.el.getElement('.wait_for input').get('value'),
|
||||||
'types': []
|
'types': []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +114,19 @@ var Profile = new Class({
|
|||||||
addType: function(data){
|
addType: function(data){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var t = new Profile.Type(data);
|
var has_empty = false;
|
||||||
|
self.types.each(function(type){
|
||||||
|
if($(type).hasClass('is_empty'))
|
||||||
|
has_empty = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(has_empty) return;
|
||||||
|
|
||||||
|
var t = new Profile.Type(data, {
|
||||||
|
'onChange': self.save.bind(self, 0)
|
||||||
|
});
|
||||||
$(t).inject(self.type_container);
|
$(t).inject(self.type_container);
|
||||||
|
|
||||||
self.sortable.addItems($(t));
|
self.sortable.addItems($(t));
|
||||||
|
|
||||||
self.types.include(t);
|
self.types.include(t);
|
||||||
@@ -135,23 +136,35 @@ var Profile = new Class({
|
|||||||
del: function(){
|
del: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if(!confirm('Are you sure you want to delete this profile?')) return
|
var label = self.el.getElement('.quality_label input').get('value');
|
||||||
|
new Question('Are you sure you want to delete <strong>"'+label+'"</strong>?', 'Items using this profile, will be set to the default quality.', [{
|
||||||
Api.request('profile.delete', {
|
'text': 'Delete "'+label+'"',
|
||||||
'data': {
|
'class': 'delete',
|
||||||
'id': self.data.id
|
'events': {
|
||||||
},
|
'click': function(e){
|
||||||
'useSpinner': true,
|
(e).stop();
|
||||||
'spinnerOptions': {
|
Api.request('profile.delete', {
|
||||||
'target': self.el
|
'data': {
|
||||||
},
|
'id': self.data.id
|
||||||
'onComplete': function(json){
|
},
|
||||||
if(json.success)
|
'useSpinner': true,
|
||||||
self.el.destroy();
|
'spinnerOptions': {
|
||||||
else
|
'target': self.el
|
||||||
alert(json.message)
|
},
|
||||||
|
'onComplete': function(json){
|
||||||
|
if(json.success)
|
||||||
|
self.el.destroy();
|
||||||
|
else
|
||||||
|
alert(json.message)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}, {
|
||||||
|
'text': 'Cancel',
|
||||||
|
'cancel': true
|
||||||
|
}]);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
makeSortable: function(){
|
makeSortable: function(){
|
||||||
@@ -180,16 +193,24 @@ var Profile = new Class({
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Profile.Type = Class({
|
Profile.Type = new Class({
|
||||||
|
|
||||||
|
Implements: [Events, Options],
|
||||||
|
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
|
||||||
initialize: function(data){
|
initialize: function(data, options){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
self.setOptions(options);
|
||||||
|
|
||||||
self.data = data;
|
self.data = data || {};
|
||||||
self.create();
|
self.create();
|
||||||
|
|
||||||
|
self.addEvent('change', function(){
|
||||||
|
self.el[self.qualities.get('value') == '-1' ? 'addClass' : 'removeClass']('is_empty');
|
||||||
|
self.deleted = self.qualities.get('value') == '-1';
|
||||||
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
create: function(){
|
create: function(){
|
||||||
@@ -201,10 +222,11 @@ Profile.Type = Class({
|
|||||||
self.fillQualities()
|
self.fillQualities()
|
||||||
),
|
),
|
||||||
new Element('span.finish').adopt(
|
new Element('span.finish').adopt(
|
||||||
self.finish = new Element('input', {
|
self.finish = new Element('input.inlay.finish[type=checkbox]', {
|
||||||
'type':'checkbox',
|
'checked': data.finish,
|
||||||
'class':'finish',
|
'events': {
|
||||||
'checked': data.finish
|
'change': self.fireEvent.bind(self, 'change')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
new Element('span.delete.icon', {
|
new Element('span.delete.icon', {
|
||||||
@@ -213,14 +235,27 @@ Profile.Type = Class({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new Element('span.handle')
|
new Element('span.handle')
|
||||||
)
|
);
|
||||||
|
|
||||||
|
self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
|
||||||
|
|
||||||
|
new Form.Check(self.finish);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
fillQualities: function(){
|
fillQualities: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.qualities = new Element('select');
|
self.qualities = new Element('select', {
|
||||||
|
'events': {
|
||||||
|
'change': self.fireEvent.bind(self, 'change')
|
||||||
|
}
|
||||||
|
}).adopt(
|
||||||
|
new Element('option', {
|
||||||
|
'text': '+ Add another quality',
|
||||||
|
'value': -1
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
Object.each(Quality.qualities, function(q){
|
Object.each(Quality.qualities, function(q){
|
||||||
new Element('option', {
|
new Element('option', {
|
||||||
@@ -250,6 +285,8 @@ Profile.Type = Class({
|
|||||||
self.el.addClass('deleted');
|
self.el.addClass('deleted');
|
||||||
self.el.hide();
|
self.el.hide();
|
||||||
self.deleted = true;
|
self.deleted = true;
|
||||||
|
|
||||||
|
self.fireEvent('change');
|
||||||
},
|
},
|
||||||
|
|
||||||
toElement: function(){
|
toElement: function(){
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ log = CPLog(__name__)
|
|||||||
class QualityPlugin(Plugin):
|
class QualityPlugin(Plugin):
|
||||||
|
|
||||||
qualities = [
|
qualities = [
|
||||||
{'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['x264', 'h264', 'bluray']},
|
{'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate']},
|
||||||
{'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
|
{'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
|
||||||
{'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
|
{'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
|
||||||
{'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
|
{'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
|
||||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc']},
|
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
||||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['dvdscr'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['dvdscr', 'ppvrip'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
@@ -31,7 +31,8 @@ class QualityPlugin(Plugin):
|
|||||||
addEvent('quality.all', self.all)
|
addEvent('quality.all', self.all)
|
||||||
addEvent('quality.single', self.single)
|
addEvent('quality.single', self.single)
|
||||||
addEvent('quality.guess', self.guess)
|
addEvent('quality.guess', self.guess)
|
||||||
addEvent('app.load', self.fill)
|
|
||||||
|
addEvent('app.initialize', self.fill, priority = 10)
|
||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
|
|
||||||
@@ -112,40 +113,44 @@ class QualityPlugin(Plugin):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def guess(self, files, extra = {}):
|
def guess(self, files, extra = {}, loose = False):
|
||||||
found = False
|
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
size = (os.path.getsize(file) / 1024 / 1024)
|
size = (os.path.getsize(file) / 1024 / 1024)
|
||||||
words = re.split('\W+', file.lower())
|
words = re.split('\W+', file.lower())
|
||||||
for quality in self.all():
|
|
||||||
correctSize = False
|
|
||||||
|
|
||||||
if size >= quality['size_min'] and size <= quality['size_max']:
|
for quality in self.all():
|
||||||
correctSize = True
|
|
||||||
|
|
||||||
# Check tags
|
# Check tags
|
||||||
if type in words:
|
if quality['identifier'] in words:
|
||||||
found = True
|
log.debug('Found via identifier "%s" in %s' % (quality['identifier'], file))
|
||||||
|
|
||||||
for alt in quality.get('alternative'):
|
|
||||||
if alt in words:
|
|
||||||
found = True
|
|
||||||
|
|
||||||
for tag in quality.get('tags', []):
|
|
||||||
if tag in words:
|
|
||||||
found = True
|
|
||||||
|
|
||||||
# Check extension + filesize
|
|
||||||
for ext in quality.get('ext'):
|
|
||||||
if ext in words and correctSize:
|
|
||||||
found = True
|
|
||||||
|
|
||||||
# Last check on resolution only
|
|
||||||
if quality.get('width', 480) == extra.get('resolution_width', 0):
|
|
||||||
found = True
|
|
||||||
|
|
||||||
if found:
|
|
||||||
return quality
|
return quality
|
||||||
|
|
||||||
return ''
|
if list(set(quality.get('alternative', [])) & set(words)):
|
||||||
|
log.debug('Found %s via alt %s in %s' % (quality['identifier'], quality.get('alternative'), file))
|
||||||
|
return quality
|
||||||
|
|
||||||
|
if list(set(quality.get('tags', [])) & set(words)):
|
||||||
|
log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), file))
|
||||||
|
return quality
|
||||||
|
|
||||||
|
# Check on unreliable stuff
|
||||||
|
if loose:
|
||||||
|
# Check extension + filesize
|
||||||
|
if list(set(quality.get('ext', [])) & set(words)) and size >= quality['size_min'] and size <= quality['size_max']:
|
||||||
|
log.debug('Found %s via ext %s in %s' % (quality['identifier'], quality.get('ext'), words))
|
||||||
|
return quality
|
||||||
|
|
||||||
|
# Last check on resolution only
|
||||||
|
if quality.get('width', 480) == extra.get('resolution_width', 0):
|
||||||
|
log.debug('Found %s via resolution_width: %s == %s' % (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0)))
|
||||||
|
return quality
|
||||||
|
|
||||||
|
|
||||||
|
# Try again with loose testing
|
||||||
|
quality = self.guess(files, extra = extra, loose = True)
|
||||||
|
if quality:
|
||||||
|
return quality
|
||||||
|
|
||||||
|
log.error('Could not identify quality for: %s' % files)
|
||||||
|
return {}
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ var QualityBase = new Class({
|
|||||||
return this.profiles[id]
|
return this.profiles[id]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Hide items when getting profiles
|
||||||
|
getActiveProfiles: function(){
|
||||||
|
return Object.filter(this.profiles, function(profile){
|
||||||
|
return !profile.data.hide
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
getQuality: function(id){
|
getQuality: function(id){
|
||||||
return this.qualities.filter(function(q){
|
return this.qualities.filter(function(q){
|
||||||
return q.id == id;
|
return q.id == id;
|
||||||
@@ -31,7 +38,7 @@ var QualityBase = new Class({
|
|||||||
self.settings = App.getPage('Settings')
|
self.settings = App.getPage('Settings')
|
||||||
self.settings.addEvent('create', function(){
|
self.settings.addEvent('create', function(){
|
||||||
var tab = self.settings.createTab('profile', {
|
var tab = self.settings.createTab('profile', {
|
||||||
'label': 'Profile',
|
'label': 'Quality',
|
||||||
'name': 'profile'
|
'name': 'profile'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,6 +46,7 @@ var QualityBase = new Class({
|
|||||||
self.content = tab.content;
|
self.content = tab.content;
|
||||||
|
|
||||||
self.createProfiles();
|
self.createProfiles();
|
||||||
|
self.createProfileOrdering();
|
||||||
self.createSizes();
|
self.createSizes();
|
||||||
|
|
||||||
})
|
})
|
||||||
@@ -50,42 +58,104 @@ var QualityBase = new Class({
|
|||||||
*/
|
*/
|
||||||
createProfiles: function(){
|
createProfiles: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
var non_core_profiles = Object.filter(self.profiles, function(profile){ return !profile.isCore() });
|
||||||
|
var count = Object.getLength(non_core_profiles);
|
||||||
|
|
||||||
self.settings.createGroup({
|
self.settings.createGroup({
|
||||||
'label': 'Custom',
|
'label': 'Quality Profiles',
|
||||||
'description': 'Discriptions'
|
'description': 'Create your own profiles with multiple qualities.'
|
||||||
}).inject(self.content).adopt(
|
}).inject(self.content).adopt(
|
||||||
new Element('a.add_new', {
|
self.profile_container = new Element('div.container'),
|
||||||
'text': 'Create a new quality profile',
|
new Element('a.add_new_profile', {
|
||||||
|
'text': count > 0 ? 'Create another quality profile' : 'Click here to create a quality profile.',
|
||||||
'events': {
|
'events': {
|
||||||
'click': function(){
|
'click': function(){
|
||||||
var profile = self.createProfilesClass();
|
var profile = self.createProfilesClass();
|
||||||
$(profile).inject(self.profile_container, 'top')
|
$(profile).inject(self.profile_container)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
self.profile_container = new Element('div.container')
|
);
|
||||||
)
|
|
||||||
|
|
||||||
Object.each(self.profiles, function(profile){
|
// Add profiles, that aren't part of the core (for editing)
|
||||||
if(!profile.isCore())
|
Object.each(non_core_profiles, function(profile){
|
||||||
$(profile).inject(self.profile_container, 'top')
|
$(profile).inject(self.profile_container)
|
||||||
})
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
createProfilesClass: function(data){
|
createProfilesClass: function(data){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if(data){
|
var data = data || {'id': randomString()}
|
||||||
return self.profiles[data.id] = new Profile(data);
|
|
||||||
}
|
return self.profiles[data.id] = new Profile(data);
|
||||||
else {
|
},
|
||||||
var data = {
|
|
||||||
'id': randomString()
|
createProfileOrdering: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var profile_list;
|
||||||
|
var group = self.settings.createGroup({
|
||||||
|
'label': 'Profile Defaults'
|
||||||
|
}).adopt(
|
||||||
|
new Element('.ctrlHolder#profile_ordering').adopt(
|
||||||
|
new Element('label[text=Order]'),
|
||||||
|
profile_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.'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).inject(self.content)
|
||||||
|
|
||||||
|
Object.each(self.profiles, function(profile){
|
||||||
|
var check;
|
||||||
|
new Element('li', {'data-id': profile.data.id}).adopt(
|
||||||
|
check = new Element('input.inlay[type=checkbox]', {
|
||||||
|
'checked': !profile.data.hide,
|
||||||
|
'events': {
|
||||||
|
'change': self.saveProfileOrdering.bind(self)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Element('span.profile_label', {
|
||||||
|
'text': profile.data.label
|
||||||
|
}),
|
||||||
|
new Element('span.handle')
|
||||||
|
).inject(profile_list);
|
||||||
|
|
||||||
|
new Form.Check(check);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sortable
|
||||||
|
self.profile_sortable = new Sortables(profile_list, {
|
||||||
|
'revert': true,
|
||||||
|
'handle': '',
|
||||||
|
'opacity': 0.5,
|
||||||
|
'onComplete': self.saveProfileOrdering.bind(self)
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
saveProfileOrdering: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var ids = [];
|
||||||
|
var hidden = [];
|
||||||
|
|
||||||
|
self.profile_sortable.list.getElements('li').each(function(el, nr){
|
||||||
|
ids.include(el.get('data-id'));
|
||||||
|
hidden[nr] = +!el.getElement('input[type=checkbox]').get('checked');
|
||||||
|
});
|
||||||
|
|
||||||
|
Api.request('profile.save_order', {
|
||||||
|
'data': {
|
||||||
|
'ids': ids,
|
||||||
|
'hidden': hidden
|
||||||
}
|
}
|
||||||
return self.profiles[data.id] = new Profile(data);
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,24 +166,25 @@ var QualityBase = new Class({
|
|||||||
|
|
||||||
var group = self.settings.createGroup({
|
var group = self.settings.createGroup({
|
||||||
'label': 'Sizes',
|
'label': 'Sizes',
|
||||||
'description': 'Discriptions',
|
'description': 'Edit the minimal and maximum sizes (in MB) for each quality.',
|
||||||
'advanced': true
|
'advanced': true
|
||||||
}).inject(self.content)
|
}).inject(self.content)
|
||||||
|
|
||||||
new Element('div.item.header').adopt(
|
|
||||||
|
new Element('div.item.head').adopt(
|
||||||
new Element('span.label', {'text': 'Quality'}),
|
new Element('span.label', {'text': 'Quality'}),
|
||||||
new Element('span.min', {'text': 'Min'}),
|
new Element('span.min', {'text': 'Min'}),
|
||||||
new Element('span.max', {'text': 'Max'})
|
new Element('span.max', {'text': 'Max'})
|
||||||
).inject(group)
|
).inject(group)
|
||||||
|
|
||||||
Object.each(self.qualities, function(quality){
|
Object.each(self.qualities, function(quality){
|
||||||
new Element('div.item').adopt(
|
new Element('div.ctrlHolder.item').adopt(
|
||||||
new Element('span.label', {'text': quality.label}),
|
new Element('span.label', {'text': quality.label}),
|
||||||
new Element('input.min', {'value': quality.size_min}),
|
new Element('input.min', {'value': quality.size_min}),
|
||||||
new Element('input.max', {'value': quality.size_max})
|
new Element('input.max', {'value': quality.size_max})
|
||||||
).inject(group)
|
).inject(group)
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
|
from couchpotato.core.helpers.request import getParam, jsonified
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import File, Release, Movie
|
from couchpotato.core.settings.model import File, Release as Relea, Movie
|
||||||
from sqlalchemy.sql.expression import and_, or_
|
from sqlalchemy.sql.expression import and_, or_
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -13,6 +15,9 @@ class Release(Plugin):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('release.add', self.add)
|
addEvent('release.add', self.add)
|
||||||
|
|
||||||
|
addApiView('release.download', self.download)
|
||||||
|
addApiView('release.delete', self.delete)
|
||||||
|
|
||||||
def add(self, group):
|
def add(self, group):
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
@@ -30,22 +35,22 @@ class Release(Plugin):
|
|||||||
db.add(movie)
|
db.add(movie)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Add release
|
# Add Release
|
||||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||||
release = db.query(Release).filter(
|
rel = db.query(Relea).filter(
|
||||||
or_(
|
or_(
|
||||||
Release.identifier == identifier,
|
Relea.identifier == identifier,
|
||||||
and_(Release.identifier.startswith(group['library']['identifier'], Release.status_id == snatched_status.get('id')))
|
and_(Relea.identifier.startswith(group['library']['identifier'], Relea.status_id == snatched_status.get('id')))
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
if not release:
|
if not rel:
|
||||||
release = Release(
|
rel = Relea(
|
||||||
identifier = identifier,
|
identifier = identifier,
|
||||||
movie = movie,
|
movie = movie,
|
||||||
quality_id = group['meta_data']['quality'].get('id'),
|
quality_id = group['meta_data']['quality'].get('id'),
|
||||||
status_id = done_status.get('id')
|
status_id = done_status.get('id')
|
||||||
)
|
)
|
||||||
db.add(release)
|
db.add(rel)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Add each file type
|
# Add each file type
|
||||||
@@ -54,10 +59,10 @@ class Release(Plugin):
|
|||||||
added_file = self.saveFile(file, type = type, include_media_info = type is 'movie')
|
added_file = self.saveFile(file, type = type, include_media_info = type is 'movie')
|
||||||
try:
|
try:
|
||||||
added_file = db.query(File).filter_by(id = added_file.get('id')).one()
|
added_file = db.query(File).filter_by(id = added_file.get('id')).one()
|
||||||
release.files.append(added_file)
|
Relea.files.append(added_file)
|
||||||
db.commit()
|
db.commit()
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
log.debug('Failed to attach "%s" to release: %s' % (file, e))
|
log.debug('Failed to attach "%s" to Relea: %s' % (file, e))
|
||||||
|
|
||||||
db.remove()
|
db.remove()
|
||||||
|
|
||||||
@@ -73,3 +78,48 @@ class Release(Plugin):
|
|||||||
# Check database and update/insert if necessary
|
# Check database and update/insert if necessary
|
||||||
return fireEvent('file.add', path = file, part = self.getPartNumber(file), type = self.file_types[type], properties = properties, single = True)
|
return fireEvent('file.add', path = file, part = self.getPartNumber(file), type = self.file_types[type], properties = properties, single = True)
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
id = getParam('id')
|
||||||
|
|
||||||
|
rel = db.query(Relea).filter_by(id = id).first()
|
||||||
|
if rel:
|
||||||
|
rel.delete()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return jsonified({
|
||||||
|
'success': True
|
||||||
|
})
|
||||||
|
|
||||||
|
def download(self):
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
id = getParam('id')
|
||||||
|
|
||||||
|
rel = db.query(Relea).filter_by(id = id).first()
|
||||||
|
if rel:
|
||||||
|
item = {}
|
||||||
|
for info in rel.info:
|
||||||
|
item[info.identifier] = info.value
|
||||||
|
|
||||||
|
# Get matching provider
|
||||||
|
provider = fireEvent('provider.belongs_to', item['url'], single = True)
|
||||||
|
item['download'] = provider.download
|
||||||
|
|
||||||
|
fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
|
||||||
|
'profile': {'types': {'quality': {}}},
|
||||||
|
'releases': {'status': {}, 'quality': {}},
|
||||||
|
'library': {'titles': {}, 'files':{}},
|
||||||
|
'files': {}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return jsonified({
|
||||||
|
'success': True
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
log.error('Couldn\'t find release with id: %s' % id)
|
||||||
|
|
||||||
|
return jsonified({
|
||||||
|
'success': False
|
||||||
|
})
|
||||||
|
|||||||
@@ -61,20 +61,22 @@ config = [{
|
|||||||
'advanced': True,
|
'advanced': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'trailer_name',
|
'name': 'rename_nfo',
|
||||||
'label': 'Trailer naming',
|
'label': 'Rename .NFO',
|
||||||
'default': '<filename>-trailer.<ext>',
|
'description': 'Rename original .nfo file',
|
||||||
|
'type': 'bool',
|
||||||
|
'default': True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'nfo_name',
|
'name': 'nfo_name',
|
||||||
'label': 'NFO naming',
|
'label': 'NFO naming',
|
||||||
'default': '<filename>.<ext>',
|
'default': '<filename>.<ext>-orig',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'backdrop_name',
|
'name': 'trailer_name',
|
||||||
'label': 'Backdrop naming',
|
'label': 'Trailer naming',
|
||||||
'default': '<filename>-backdrop.<ext>',
|
'default': '<filename>-trailer.<ext>',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.variable import getExt
|
from couchpotato.core.helpers.variable import getExt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Library
|
from couchpotato.core.settings.model import Library, Movie
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
@@ -40,7 +40,7 @@ class Renamer(Plugin):
|
|||||||
group = groups[group_identifier]
|
group = groups[group_identifier]
|
||||||
rename_files = {}
|
rename_files = {}
|
||||||
|
|
||||||
# Add _UNKNOWN_ if no library is connected
|
# Add _UNKNOWN_ if no library item is connected
|
||||||
if not group['library']:
|
if not group['library']:
|
||||||
if group['dirname']:
|
if group['dirname']:
|
||||||
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname'])
|
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname'])
|
||||||
@@ -53,6 +53,10 @@ class Renamer(Plugin):
|
|||||||
# Rename the files using the library data
|
# Rename the files using the library data
|
||||||
else:
|
else:
|
||||||
group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True)
|
group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True)
|
||||||
|
if not group['library']:
|
||||||
|
log.error('Could not rename, no library item to work with: %s' % group_identifier)
|
||||||
|
continue
|
||||||
|
|
||||||
library = group['library']
|
library = group['library']
|
||||||
|
|
||||||
# Find subtitle for renaming
|
# Find subtitle for renaming
|
||||||
@@ -85,12 +89,9 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
for file_type in group['files']:
|
for file_type in group['files']:
|
||||||
|
|
||||||
# Move DVD files (no renaming)
|
|
||||||
if group['is_dvd'] and file_type is 'movie':
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Move nfo depending on settings
|
# Move nfo depending on settings
|
||||||
if file_type is 'nfo' and not self.conf('rename_nfo'):
|
if file_type is 'nfo' and not self.conf('rename_nfo'):
|
||||||
|
log.debug('Skipping, renaming of %s disabled' % file_type)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Subtitle extra
|
# Subtitle extra
|
||||||
@@ -98,7 +99,7 @@ class Renamer(Plugin):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Move other files
|
# Move other files
|
||||||
multiple = len(group['files']['movie']) > 1
|
multiple = len(group['files']['movie']) > 1 and not group['is_dvd']
|
||||||
cd = 1 if multiple else 0
|
cd = 1 if multiple else 0
|
||||||
|
|
||||||
for file in sorted(list(group['files'][file_type])):
|
for file in sorted(list(group['files'][file_type])):
|
||||||
@@ -118,21 +119,35 @@ class Renamer(Plugin):
|
|||||||
final_folder_name = self.doReplace(folder_name, replacements)
|
final_folder_name = self.doReplace(folder_name, replacements)
|
||||||
final_file_name = self.doReplace(file_name, replacements)
|
final_file_name = self.doReplace(file_name, replacements)
|
||||||
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
|
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
|
||||||
|
group['filename'] = replacements['filename']
|
||||||
|
|
||||||
# Meta naming
|
# Meta naming
|
||||||
if file_type is 'trailer':
|
if file_type is 'trailer':
|
||||||
final_file_name = self.doReplace(trailer_name, replacements)
|
final_file_name = self.doReplace(trailer_name, replacements)
|
||||||
elif file_type is 'nfo':
|
elif file_type is 'nfo':
|
||||||
final_file_name = self.doReplace(nfo_name, replacements) + '-orig'
|
final_file_name = self.doReplace(nfo_name, replacements)
|
||||||
elif file_type is 'backdrop':
|
|
||||||
final_file_name = self.doReplace(backdrop_name, replacements)
|
|
||||||
|
|
||||||
# Seperator replace
|
# Seperator replace
|
||||||
if separator:
|
if separator:
|
||||||
final_file_name = final_file_name.replace(' ', separator)
|
final_file_name = final_file_name.replace(' ', separator)
|
||||||
|
|
||||||
# Main file
|
# Move DVD files (no structure renaming)
|
||||||
rename_files[file] = os.path.join(destination, final_folder_name, final_file_name)
|
if group['is_dvd'] and file_type is 'movie':
|
||||||
|
found = False
|
||||||
|
for top_dir in ['video_ts', 'audio_ts', 'bdmv', 'certificate']:
|
||||||
|
has_string = file.lower().find(os.path.sep + top_dir + os.path.sep)
|
||||||
|
if has_string >= 0:
|
||||||
|
structure_dir = file[has_string:].lstrip(os.path.sep)
|
||||||
|
rename_files[file] = os.path.join(destination, final_folder_name, structure_dir)
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
log.error('Could not determin dvd structure for: %s' % file)
|
||||||
|
|
||||||
|
# Do rename others
|
||||||
|
else:
|
||||||
|
rename_files[file] = os.path.join(destination, final_folder_name, final_file_name)
|
||||||
|
|
||||||
# Check for extra subtitle files
|
# Check for extra subtitle files
|
||||||
if file_type is 'subtitle':
|
if file_type is 'subtitle':
|
||||||
@@ -154,21 +169,43 @@ class Renamer(Plugin):
|
|||||||
if multiple:
|
if multiple:
|
||||||
cd += 1
|
cd += 1
|
||||||
|
|
||||||
# Notify on download
|
|
||||||
download_message = 'Download of %s (%s) successful.' % (group['library']['titles'][0]['title'], replacements['quality'])
|
|
||||||
fireEvent('movie.downloaded', message = download_message, data = group)
|
|
||||||
|
|
||||||
# Before renaming, remove the lower quality files
|
# Before renaming, remove the lower quality files
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
|
library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
|
||||||
done_status = fireEvent('status.get', 'done', single = True)
|
done_status = fireEvent('status.get', 'done', single = True)
|
||||||
|
active_status = fireEvent('status.get', 'active', single = True)
|
||||||
|
|
||||||
for movie in library.movies:
|
for movie in library.movies:
|
||||||
|
|
||||||
|
# Mark movie "done" onces it found the quality with the finish check
|
||||||
|
try:
|
||||||
|
if movie.status_id == active_status.get('id'):
|
||||||
|
for type in movie.profile.types:
|
||||||
|
if type.quality_id == group['meta_data']['quality']['id'] and type.finish:
|
||||||
|
movie.status_id = done_status.get('id')
|
||||||
|
db.commit()
|
||||||
|
except Exception, e:
|
||||||
|
log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc()))
|
||||||
|
|
||||||
|
# Go over current movie releases
|
||||||
for release in movie.releases:
|
for release in movie.releases:
|
||||||
if release.quality.order < group['meta_data']['quality']['order']:
|
|
||||||
|
# This is where CP removes older, lesser quality releases
|
||||||
|
if release.quality.order > group['meta_data']['quality']['order']:
|
||||||
log.info('Removing older release for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
|
log.info('Removing older release for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
|
||||||
|
|
||||||
|
for file in release.files:
|
||||||
|
log.info('Removing (not really) "%s"' % file.path)
|
||||||
|
|
||||||
|
# When a release already exists
|
||||||
elif release.status_id is done_status.get('id'):
|
elif release.status_id is done_status.get('id'):
|
||||||
|
|
||||||
|
# Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc
|
||||||
if release.quality.order is group['meta_data']['quality']['order']:
|
if release.quality.order is group['meta_data']['quality']['order']:
|
||||||
log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label))
|
log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label))
|
||||||
|
|
||||||
|
# Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan
|
||||||
else:
|
else:
|
||||||
log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
|
log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
|
||||||
|
|
||||||
@@ -188,10 +225,7 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
for file in release.files:
|
# Rename all files marked
|
||||||
log.info('Removing (not really) "%s"' % file.path)
|
|
||||||
|
|
||||||
# Rename
|
|
||||||
for src in rename_files:
|
for src in rename_files:
|
||||||
if rename_files[src]:
|
if rename_files[src]:
|
||||||
|
|
||||||
@@ -200,21 +234,24 @@ class Renamer(Plugin):
|
|||||||
log.info('Renaming "%s" to "%s"' % (src, dst))
|
log.info('Renaming "%s" to "%s"' % (src, dst))
|
||||||
|
|
||||||
path = os.path.dirname(dst)
|
path = os.path.dirname(dst)
|
||||||
try:
|
|
||||||
if not os.path.isdir(path): os.makedirs(path)
|
# Create dir
|
||||||
except:
|
self.makeDir(path)
|
||||||
log.error('Failed creating dir %s: %s' % (path, traceback.format_exc()))
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
shutil.move(src, dst)
|
pass
|
||||||
|
#shutil.move(src, dst)
|
||||||
except:
|
except:
|
||||||
log.error('Failed moving the file "%s" : %s' % (os.path.basename(src), traceback.format_exc()))
|
log.error('Failed moving the file "%s" : %s' % (os.path.basename(src), traceback.format_exc()))
|
||||||
|
|
||||||
#print rename_me, rename_files[rename_me]
|
#print rename_me, rename_files[rename_me]
|
||||||
|
|
||||||
# Search for trailers etc
|
# Search for trailers etc
|
||||||
fireEvent('renamer.after', group)
|
fireEventAsync('renamer.after', group)
|
||||||
|
|
||||||
|
# Notify on download
|
||||||
|
download_message = 'Download of %s (%s) successful.' % (group['library']['titles'][0]['title'], replacements['quality'])
|
||||||
|
fireEventAsync('movie.downloaded', message = download_message, data = group)
|
||||||
|
|
||||||
# Break if CP wants to shut down
|
# Break if CP wants to shut down
|
||||||
if self.shuttingDown():
|
if self.shuttingDown():
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
|||||||
from couchpotato.core.helpers.variable import getExt
|
from couchpotato.core.helpers.variable import getExt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import File, Release, Movie
|
from couchpotato.core.settings.model import File
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from flask.helpers import json
|
from flask.helpers import json
|
||||||
from sqlalchemy.sql.expression import and_, or_
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -23,11 +23,11 @@ class Scanner(Plugin):
|
|||||||
'trailer': 1048576, # 1MB
|
'trailer': 1048576, # 1MB
|
||||||
}
|
}
|
||||||
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
|
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
|
||||||
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads']
|
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
|
||||||
extensions = {
|
extensions = {
|
||||||
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'],
|
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'],
|
||||||
'dvd': ['vts_*', 'vob'],
|
'dvd': ['vts_*', 'vob'],
|
||||||
'nfo': ['nfo', 'txt', 'tag'],
|
'nfo': ['nfo', 'nfo-orig', 'txt', 'tag'],
|
||||||
'subtitle': ['sub', 'srt', 'ssa', 'ass'],
|
'subtitle': ['sub', 'srt', 'ssa', 'ass'],
|
||||||
'subtitle_extra': ['idx'],
|
'subtitle_extra': ['idx'],
|
||||||
'trailer': ['mov', 'mp4', 'flv']
|
'trailer': ['mov', 'mp4', 'flv']
|
||||||
@@ -172,9 +172,21 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
|
|
||||||
# Determine file types
|
# Determine file types
|
||||||
|
delete_identifier = []
|
||||||
for identifier in movie_files:
|
for identifier in movie_files:
|
||||||
group = movie_files[identifier]
|
group = movie_files[identifier]
|
||||||
|
|
||||||
|
# Check if movie is fresh and maybe still unpacking, ignore files new then 1 minute
|
||||||
|
file_too_new = False
|
||||||
|
for file in group['unsorted_files']:
|
||||||
|
if os.path.getmtime(file) > time.time() - 60:
|
||||||
|
file_too_new = True
|
||||||
|
|
||||||
|
if file_too_new:
|
||||||
|
log.info('Files seem to be still unpacking or just unpacked, ignoring for now: %s' % identifier)
|
||||||
|
delete_identifier.append(identifier)
|
||||||
|
continue
|
||||||
|
|
||||||
# Group extra (and easy) files first
|
# Group extra (and easy) files first
|
||||||
images = self.getImages(group['unsorted_files'])
|
images = self.getImages(group['unsorted_files'])
|
||||||
group['files'] = {
|
group['files'] = {
|
||||||
@@ -182,7 +194,7 @@ class Scanner(Plugin):
|
|||||||
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
|
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
|
||||||
'nfo': self.getNfo(group['unsorted_files']),
|
'nfo': self.getNfo(group['unsorted_files']),
|
||||||
'trailer': self.getTrailers(group['unsorted_files']),
|
'trailer': self.getTrailers(group['unsorted_files']),
|
||||||
'backdrop': images['backdrop'],
|
#'backdrop': images['backdrop'],
|
||||||
'leftover': set(group['unsorted_files']),
|
'leftover': set(group['unsorted_files']),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,12 +210,13 @@ class Scanner(Plugin):
|
|||||||
group['parentdir'] = os.path.dirname(movie_file)
|
group['parentdir'] = os.path.dirname(movie_file)
|
||||||
group['dirname'] = None
|
group['dirname'] = None
|
||||||
|
|
||||||
folders = group['parentdir'].replace(folder, '').split(os.path.sep)
|
folder_names = group['parentdir'].replace(folder, '').split(os.path.sep)
|
||||||
|
folder_names.reverse()
|
||||||
|
|
||||||
# Try and get a proper dirname, so no "A", "Movie", "Download"
|
# Try and get a proper dirname, so no "A", "Movie", "Download" etc
|
||||||
for folder in folders:
|
for folder_name in folder_names:
|
||||||
if folder.lower() in self.ignore_names or len(folder) < 2:
|
if folder_name.lower() not in self.ignore_names and len(folder_name) > 2:
|
||||||
group['dirname'] = folder
|
group['dirname'] = folder_name
|
||||||
break
|
break
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -220,12 +233,16 @@ class Scanner(Plugin):
|
|||||||
if not group['library']:
|
if not group['library']:
|
||||||
log.error('Unable to determin movie: %s' % group['identifiers'])
|
log.error('Unable to determin movie: %s' % group['identifiers'])
|
||||||
|
|
||||||
|
# Delete still (asuming) unpacking files
|
||||||
|
for identifier in delete_identifier:
|
||||||
|
del movie_files[identifier]
|
||||||
|
|
||||||
return movie_files
|
return movie_files
|
||||||
|
|
||||||
def getMetaData(self, group):
|
def getMetaData(self, group):
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
files = group['files']['movie']
|
files = list(group['files']['movie'])
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
if os.path.getsize(file) < self.minimal_filesize['media']: continue # Ignore smaller files
|
if os.path.getsize(file) < self.minimal_filesize['media']: continue # Ignore smaller files
|
||||||
@@ -246,10 +263,11 @@ class Scanner(Plugin):
|
|||||||
if not data['quality']:
|
if not data['quality']:
|
||||||
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
|
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
|
||||||
|
|
||||||
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 720 else 'SD'
|
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 else 'SD'
|
||||||
|
|
||||||
data['group'] = self.getGroup(file[0])
|
file = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
|
||||||
data['source'] = self.getSourceMedia(file[0])
|
data['group'] = self.getGroup(file)
|
||||||
|
data['source'] = self.getSourceMedia(file)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -365,7 +383,6 @@ class Scanner(Plugin):
|
|||||||
return set(filter(test, files))
|
return set(filter(test, files))
|
||||||
|
|
||||||
def getDVDFiles(self, files):
|
def getDVDFiles(self, files):
|
||||||
|
|
||||||
def test(s):
|
def test(s):
|
||||||
return self.isDVDFile(s)
|
return self.isDVDFile(s)
|
||||||
|
|
||||||
@@ -409,7 +426,7 @@ class Scanner(Plugin):
|
|||||||
if list(set(file.lower().split(os.path.sep)) & set(['video_ts', 'audio_ts'])):
|
if list(set(file.lower().split(os.path.sep)) & set(['video_ts', 'audio_ts'])):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
for needle in ['vts_', 'video_ts', 'audio_ts']:
|
for needle in ['vts_', 'video_ts', 'audio_ts', 'bdmv', 'certificate']:
|
||||||
if needle in file.lower():
|
if needle in file.lower():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -510,8 +527,8 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
def getGroup(self, file):
|
def getGroup(self, file):
|
||||||
try:
|
try:
|
||||||
group = re.search('-(?P<group>[A-Z0-9]+)$', file, re.I)
|
match = re.search('-(?P<group>[A-Z0-9]+).', file, re.I)
|
||||||
return group.group('group') or ''
|
return match.group('group') or ''
|
||||||
except:
|
except:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ config = [{
|
|||||||
'name': 'required_words',
|
'name': 'required_words',
|
||||||
'label': 'Required words',
|
'label': 'Required words',
|
||||||
'default': '',
|
'default': '',
|
||||||
'description': 'Ignore releases that doesn\'t contain one of these words.'
|
'description': 'Ignore releases that don\'t contain at least one of these words.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'ignored_words',
|
'name': 'ignored_words',
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ from couchpotato.core.logger import CPLog
|
|||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
|
from sqlalchemy.exc import InterfaceError
|
||||||
import re
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@ class Searcher(Plugin):
|
|||||||
addEvent('searcher.all', self.all)
|
addEvent('searcher.all', self.all)
|
||||||
addEvent('searcher.single', self.single)
|
addEvent('searcher.single', self.single)
|
||||||
addEvent('searcher.correct_movie', self.correctMovie)
|
addEvent('searcher.correct_movie', self.correctMovie)
|
||||||
|
addEvent('searcher.download', self.download)
|
||||||
|
|
||||||
# Schedule cronjob
|
# Schedule cronjob
|
||||||
fireEvent('schedule.cron', 'searcher.all', self.all, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
fireEvent('schedule.cron', 'searcher.all', self.all, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
||||||
@@ -34,7 +37,7 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
for movie in movies:
|
for movie in movies:
|
||||||
|
|
||||||
self.single(movie.to_dict(deep = {
|
self.single(movie.to_dict({
|
||||||
'profile': {'types': {'quality': {}}},
|
'profile': {'types': {'quality': {}}},
|
||||||
'releases': {'status': {}, 'quality': {}},
|
'releases': {'status': {}, 'quality': {}},
|
||||||
'library': {'titles': {}, 'files':{}},
|
'library': {'titles': {}, 'files':{}},
|
||||||
@@ -47,11 +50,8 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
def single(self, movie):
|
def single(self, movie):
|
||||||
|
|
||||||
downloaded_status = fireEvent('status.get', 'downloaded', single = True)
|
|
||||||
available_status = fireEvent('status.get', 'available', single = True)
|
available_status = fireEvent('status.get', 'available', single = True)
|
||||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
|
||||||
|
|
||||||
successful = False
|
|
||||||
for type in movie['profile']['types']:
|
for type in movie['profile']['types']:
|
||||||
|
|
||||||
has_better_quality = 0
|
has_better_quality = 0
|
||||||
@@ -85,37 +85,22 @@ class Searcher(Plugin):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
for info in nzb:
|
for info in nzb:
|
||||||
rls_info = ReleaseInfo(
|
try:
|
||||||
identifier = info,
|
if not isinstance(nzb[info], (str, unicode, int, long)):
|
||||||
value = nzb[info]
|
continue
|
||||||
)
|
|
||||||
rls.info.append(rls_info)
|
rls_info = ReleaseInfo(
|
||||||
db.commit()
|
identifier = info,
|
||||||
|
value = nzb[info]
|
||||||
|
)
|
||||||
|
rls.info.append(rls_info)
|
||||||
|
db.commit()
|
||||||
|
except InterfaceError:
|
||||||
|
log.debug('Couldn\'t add %s to ReleaseInfo: %s' % (info, traceback.format_exc()))
|
||||||
|
|
||||||
|
|
||||||
for nzb in sorted_results:
|
for nzb in sorted_results:
|
||||||
successful = fireEvent('download', data = nzb, movie = movie, single = True)
|
return self.download(data = nzb, movie = movie)
|
||||||
|
|
||||||
if successful:
|
|
||||||
|
|
||||||
# Mark release as snatched
|
|
||||||
db = get_session()
|
|
||||||
rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first()
|
|
||||||
rls.status_id = snatched_status.get('id')
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# Mark movie snatched if quality is finish-checked
|
|
||||||
if type['finish']:
|
|
||||||
mvie = db.query(Movie).filter_by(id = movie['id']).first()
|
|
||||||
mvie.status_id = snatched_status.get('id')
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
log.info('Downloading of %s successful.' % nzb.get('name'))
|
|
||||||
fireEvent('movie.snatched', message = 'Downloading of %s successful.' % nzb.get('name'), data = rls.to_dict())
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
log.info('Better quality (%s) already available or snatched for %s' % (type['quality']['label'], default_title))
|
log.info('Better quality (%s) already available or snatched for %s' % (type['quality']['label'], default_title))
|
||||||
break
|
break
|
||||||
@@ -126,6 +111,26 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def download(self, data, movie):
|
||||||
|
|
||||||
|
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||||
|
|
||||||
|
successful = fireEvent('download', data = data, movie = movie, single = True)
|
||||||
|
|
||||||
|
if successful:
|
||||||
|
|
||||||
|
# Mark release as snatched
|
||||||
|
db = get_session()
|
||||||
|
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
||||||
|
rls.status_id = snatched_status.get('id')
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
log.info('Downloading of %s successful.' % data.get('name'))
|
||||||
|
fireEvent('movie.snatched', message = 'Downloading of %s successful.' % data.get('name'), data = rls.to_dict())
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
|
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
|
||||||
|
|
||||||
@@ -192,6 +197,7 @@ class Searcher(Plugin):
|
|||||||
if len(movie_words) == 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
|
if len(movie_words) == 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie['library']['titles'][0]['title'], movie['library']['year']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def containsOtherQuality(self, name, preferred_quality = {}, single_category = False):
|
def containsOtherQuality(self, name, preferred_quality = {}, single_category = False):
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
|
from couchpotato.core.helpers.request import jsonified
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
@@ -10,14 +12,13 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class Updater(Plugin):
|
class Updater(Plugin):
|
||||||
|
|
||||||
git = 'git://github.com/CouchPotato/CouchPotato.git'
|
repo_name = 'RuudBurger/CouchPotatoServer'
|
||||||
|
|
||||||
running = False
|
running = False
|
||||||
version = None
|
version = None
|
||||||
updateFailed = False
|
update_failed = False
|
||||||
updateAvailable = False
|
update_version = None
|
||||||
updateVersion = None
|
last_check = 0
|
||||||
lastCheck = 0
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
@@ -27,6 +28,18 @@ class Updater(Plugin):
|
|||||||
|
|
||||||
addEvent('app.load', self.check)
|
addEvent('app.load', self.check)
|
||||||
|
|
||||||
|
addApiView('updater.info', self.getInfo)
|
||||||
|
addApiView('updater.update', self.doUpdateView)
|
||||||
|
|
||||||
|
def getInfo(self):
|
||||||
|
|
||||||
|
return jsonified({
|
||||||
|
'repo_name': self.repo_name,
|
||||||
|
'last_check': self.last_check,
|
||||||
|
'update_version': self.update_version,
|
||||||
|
'version': self.getVersion(),
|
||||||
|
})
|
||||||
|
|
||||||
def getVersion(self):
|
def getVersion(self):
|
||||||
|
|
||||||
if not self.version:
|
if not self.version:
|
||||||
@@ -42,7 +55,7 @@ class Updater(Plugin):
|
|||||||
|
|
||||||
def check(self):
|
def check(self):
|
||||||
|
|
||||||
if self.updateAvailable or self.isDisabled():
|
if self.update_version or self.isDisabled():
|
||||||
return
|
return
|
||||||
|
|
||||||
current_branch = self.repo.getCurrentBranch().name
|
current_branch = self.repo.getCurrentBranch().name
|
||||||
@@ -54,13 +67,17 @@ class Updater(Plugin):
|
|||||||
remote = branch.getHead()
|
remote = branch.getHead()
|
||||||
|
|
||||||
if local.getDate() < remote.getDate():
|
if local.getDate() < remote.getDate():
|
||||||
if self.conf('automatic') and not self.updateFailed:
|
if self.conf('automatic') and not self.update_failed:
|
||||||
self.doUpdate()
|
self.doUpdate()
|
||||||
else:
|
else:
|
||||||
self.updateAvailable = True
|
self.update_version = remote.hash
|
||||||
self.updateVersion = remote.hash
|
|
||||||
|
|
||||||
self.lastCheck = time.time()
|
self.last_check = time.time()
|
||||||
|
|
||||||
|
def doUpdateView(self):
|
||||||
|
return jsonified({
|
||||||
|
'success': self.doUpdate()
|
||||||
|
})
|
||||||
|
|
||||||
def doUpdate(self):
|
def doUpdate(self):
|
||||||
try:
|
try:
|
||||||
@@ -70,7 +87,7 @@ class Updater(Plugin):
|
|||||||
except Exception, e:
|
except Exception, e:
|
||||||
log.error('Failed updating via GIT: %s' % e)
|
log.error('Failed updating via GIT: %s' % e)
|
||||||
|
|
||||||
self.updateFailed = True
|
self.update_failed = True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
var UpdaterBase = new Class({
|
||||||
|
|
||||||
|
initialize: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
App.addEvent('load', self.info.bind(self, 1000))
|
||||||
|
},
|
||||||
|
|
||||||
|
info: function(timeout){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(self.timer) clearTimeout(self.timer);
|
||||||
|
|
||||||
|
self.timer = setTimeout(function(){
|
||||||
|
Api.request('updater.info', {
|
||||||
|
'onComplete': function(json){
|
||||||
|
if(json.update_version){
|
||||||
|
self.createMessage(json);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(self.message)
|
||||||
|
self.message.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, (timeout || 0))
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
createMessage: function(data){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.message = new Element('div.message.update').adopt(
|
||||||
|
new Element('span', {
|
||||||
|
'text': 'A new version is available'
|
||||||
|
}),
|
||||||
|
new Element('a', {
|
||||||
|
'href': 'https://github.com/'+data.repo_name+'/compare/'+data.version.substr(0, 7)+'...'+data.update_version.substr(0, 7),
|
||||||
|
'text': 'see what has changed',
|
||||||
|
'target': '_blank'
|
||||||
|
}),
|
||||||
|
new Element('span[text=or]'),
|
||||||
|
new Element('a', {
|
||||||
|
'text': 'just update, gogogo!',
|
||||||
|
'events': {
|
||||||
|
'click': self.doUpdate.bind(self)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).inject($(document.body).getElement('.header'))
|
||||||
|
},
|
||||||
|
|
||||||
|
doUpdate: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
Api.request('updater.update', {
|
||||||
|
'onComplete': function(json){
|
||||||
|
|
||||||
|
if(json.success){
|
||||||
|
App.restart();
|
||||||
|
|
||||||
|
$(document.body).set('spin', {
|
||||||
|
'message': 'Updating'
|
||||||
|
});
|
||||||
|
$(document.body).spin();
|
||||||
|
|
||||||
|
var checks = 0;
|
||||||
|
var interval = 0;
|
||||||
|
interval = setInterval(function(){
|
||||||
|
Api.request('', {
|
||||||
|
'onSuccess': function(){
|
||||||
|
if(checks > 2){
|
||||||
|
clearInterval(interval);
|
||||||
|
$(document.body).unspin();
|
||||||
|
self.info();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
checks++;
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var Updater = new UpdaterBase();
|
||||||
@@ -4,7 +4,7 @@ def start():
|
|||||||
return Wizard()
|
return Wizard()
|
||||||
|
|
||||||
config = [{
|
config = [{
|
||||||
'name': 'global',
|
'name': 'core',
|
||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'general',
|
'tab': 'general',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from couchpotato.core.event import addEvent
|
|||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from urllib2 import URLError
|
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -60,22 +59,36 @@ class YarrProvider(Provider):
|
|||||||
sizeMb = ['mb', 'mib']
|
sizeMb = ['mb', 'mib']
|
||||||
sizeKb = ['kb', 'kib']
|
sizeKb = ['kb', 'kib']
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
addEvent('provider.belongs_to', self.belongsTo)
|
||||||
|
|
||||||
|
def belongsTo(self, url, host = None):
|
||||||
|
try:
|
||||||
|
hostname = urlparse(url).hostname
|
||||||
|
download_url = host if host else self.urls['download']
|
||||||
|
if hostname in download_url:
|
||||||
|
return self
|
||||||
|
except:
|
||||||
|
log.debug('Url % s doesn\'t belong to %s' % (url, self.getName()))
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
def parseSize(self, size):
|
def parseSize(self, size):
|
||||||
|
|
||||||
sizeRaw = size.lower()
|
sizeRaw = size.lower()
|
||||||
size = re.sub(r'[^0-9.]', '', size).strip()
|
size = float(re.sub(r'[^0-9.]', '', size).strip())
|
||||||
|
|
||||||
for s in self.sizeGb:
|
for s in self.sizeGb:
|
||||||
if s in sizeRaw:
|
if s in sizeRaw:
|
||||||
return float(size) * 1024
|
return int(size) * 1024
|
||||||
|
|
||||||
for s in self.sizeMb:
|
for s in self.sizeMb:
|
||||||
if s in sizeRaw:
|
if s in sizeRaw:
|
||||||
return float(size)
|
return int(size)
|
||||||
|
|
||||||
for s in self.sizeKb:
|
for s in self.sizeKb:
|
||||||
if s in sizeRaw:
|
if s in sizeRaw:
|
||||||
return float(size) / 1024
|
return int(size) / 1024
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -96,11 +109,16 @@ class NZBProvider(YarrProvider):
|
|||||||
type = 'nzb'
|
type = 'nzb'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super(NZBProvider, self).__init__()
|
||||||
|
|
||||||
addEvent('provider.nzb.search', self.search)
|
addEvent('provider.nzb.search', self.search)
|
||||||
addEvent('provider.yarr.search', self.search)
|
addEvent('provider.yarr.search', self.search)
|
||||||
|
|
||||||
addEvent('provider.nzb.feed', self.feed)
|
addEvent('provider.nzb.feed', self.feed)
|
||||||
|
|
||||||
|
def download(self, url = '', nzb_id = ''):
|
||||||
|
return self.urlopen(url)
|
||||||
|
|
||||||
def feed(self):
|
def feed(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ class MetaDataBase(Plugin):
|
|||||||
|
|
||||||
log.info('Creating %s metadata.' % self.getName())
|
log.info('Creating %s metadata.' % self.getName())
|
||||||
|
|
||||||
root = self.getRootName()
|
root = self.getRootName(release)
|
||||||
|
|
||||||
for type in ['nfo', 'thumbnail', 'fanart']:
|
for type in ['nfo', 'thumbnail', 'fanart']:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'renamer',
|
'tab': 'renamer',
|
||||||
'name': 'metadata',
|
'name': 'mediabrowser_metadata',
|
||||||
'label': 'MediaBrowser',
|
'label': 'MediaBrowser',
|
||||||
'description': 'Enable metadata MediaBrowser can understand',
|
'description': 'Enable metadata MediaBrowser can understand',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'renamer',
|
'tab': 'renamer',
|
||||||
'name': 'metadata',
|
'name': 'sonyps3_metadata',
|
||||||
'label': 'Sony PS3',
|
'label': 'Sony PS3',
|
||||||
'description': 'Enable metadata your Playstation 3 can understand',
|
'description': 'Enable metadata your Playstation 3 can understand',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'renamer',
|
'tab': 'renamer',
|
||||||
'name': 'metadata',
|
'name': 'wdtv_metadata',
|
||||||
'label': 'WDTV',
|
'label': 'WDTV',
|
||||||
'description': 'Enable metadata WDTV can understand',
|
'description': 'Enable metadata WDTV can understand',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'renamer',
|
'tab': 'renamer',
|
||||||
'name': 'metadata',
|
'name': 'xbmc_metadata',
|
||||||
'label': 'XBMC',
|
'label': 'XBMC',
|
||||||
'description': 'Enable metadata XBMC can understand',
|
'description': 'Enable metadata XBMC can understand',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.providers.metadata.base import MetaDataBase
|
from couchpotato.core.providers.metadata.base import MetaDataBase
|
||||||
from xml.etree.ElementTree import Element, SubElement, tostring
|
from xml.etree.ElementTree import Element, SubElement, tostring
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
|
|
||||||
class XBMC(MetaDataBase):
|
class XBMC(MetaDataBase):
|
||||||
|
|
||||||
def getRootName(self, data = {}):
|
def getRootName(self, data = {}):
|
||||||
|
return os.path.join(data['destination_dir'], data['filename'])
|
||||||
|
|
||||||
|
|
||||||
return '/Users/ruud/Downloads/Test/Transformers'
|
|
||||||
|
|
||||||
def getFanartName(self, root):
|
def getFanartName(self, root):
|
||||||
return '%s-fanart.jpg' % root
|
return '%s-fanart.jpg' % root
|
||||||
@@ -23,8 +22,30 @@ class XBMC(MetaDataBase):
|
|||||||
def getNfo(self, data):
|
def getNfo(self, data):
|
||||||
nfoxml = Element('movie')
|
nfoxml = Element('movie')
|
||||||
|
|
||||||
types = ['title', 'rating', 'year', 'votes', 'rating', 'mpaa', 'originaltitle:original_title', 'outline:overview', 'premiered:released', 'id:imdb_id']
|
types = ['rating', 'year', 'votes', 'rating', 'mpaa', 'originaltitle:original_title', 'outline:plot', 'premiered:released']
|
||||||
|
|
||||||
|
# Title
|
||||||
|
try:
|
||||||
|
el = SubElement(nfoxml, 'title')
|
||||||
|
el.text = toUnicode(data['library']['titles'][0]['title'])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# IMDB id
|
||||||
|
try:
|
||||||
|
el = SubElement(nfoxml, 'id')
|
||||||
|
el.text = toUnicode(data['library']['identifier'])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Runtime
|
||||||
|
try:
|
||||||
|
runtime = SubElement(nfoxml, 'runtime')
|
||||||
|
runtime.text = '%s min' % data['library']['runtime']
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Other values
|
||||||
for type in types:
|
for type in types:
|
||||||
|
|
||||||
if ':' in type:
|
if ':' in type:
|
||||||
@@ -33,20 +54,17 @@ class XBMC(MetaDataBase):
|
|||||||
name = type
|
name = type
|
||||||
|
|
||||||
try:
|
try:
|
||||||
el = SubElement(nfoxml, name)
|
if data['library'].get(type):
|
||||||
el.text = data.get(type, '')
|
el = SubElement(nfoxml, name)
|
||||||
|
el.text = toUnicode(data['library'].get(type, ''))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
#for genre in self.get('genres'):
|
# Genre
|
||||||
# genres = SubElement(nfoxml, 'genre')
|
for genre in data['library'].get('genres', []):
|
||||||
# genres.text = genre
|
genres = SubElement(nfoxml, 'genre')
|
||||||
|
genres.text = genre.get('name')
|
||||||
|
|
||||||
try:
|
|
||||||
runtime = SubElement(nfoxml, 'runtime')
|
|
||||||
runtime.text = data.get('runtime') + " min"
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Clean up the xml and return it
|
# Clean up the xml and return it
|
||||||
nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
|
nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
|
||||||
@@ -55,130 +73,3 @@ class XBMC(MetaDataBase):
|
|||||||
xml_string = text_re.sub('>\g<1></', xml_string)
|
xml_string = text_re.sub('>\g<1></', xml_string)
|
||||||
|
|
||||||
return xml_string.encode('utf-8')
|
return xml_string.encode('utf-8')
|
||||||
"""
|
|
||||||
def _get_fanart(self, min_height, min_width):
|
|
||||||
''' Fetches the fanart for the specified imdb_id and saves it to dir.
|
|
||||||
Arguments
|
|
||||||
|
|
||||||
min_height/width: Sets lowest acceptable resolution fanart. 0 means
|
|
||||||
disregard. If no fanart available at specified resolution or greater, then
|
|
||||||
we disregard.
|
|
||||||
'''
|
|
||||||
images = [image['image'] for image in self.tmdb_data['backdrops'] if image['image'].get('size') == 'original']
|
|
||||||
if len(images) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
return self._get_image(images, min_height, min_width)
|
|
||||||
|
|
||||||
def get_fanart_url(self, min_height, min_width):
|
|
||||||
return self._get_fanart(min_height, min_width)['url']
|
|
||||||
|
|
||||||
def write_fanart(self, filename_root, path, min_height, min_width):
|
|
||||||
fanart_url = self.get_fanart_url(min_height, min_width)
|
|
||||||
#fetch and write to disk
|
|
||||||
dest = os.path.join(path, filename_root)
|
|
||||||
try:
|
|
||||||
f = open(dest, 'wb')
|
|
||||||
except:
|
|
||||||
raise IOError("Can't open for writing: %s" % dest)
|
|
||||||
|
|
||||||
response = urllib2.urlopen(fanart_url)
|
|
||||||
f.write(response.read())
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_poster(self, min_height, min_width):
|
|
||||||
''' Fetches the poster for the specified imdb_id and saves it to dir.
|
|
||||||
Arguments
|
|
||||||
|
|
||||||
min_height/width: Sets lowest acceptable resolution poster. 0 means
|
|
||||||
disregard. If no poster available at specified resolution or greater, then
|
|
||||||
we disregard.
|
|
||||||
'''
|
|
||||||
images = [image['image'] for image in self.tmdb_data['posters'] if image['image'].get('size') == 'original']
|
|
||||||
if len(images) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
return self._get_image(images, min_height, min_width)
|
|
||||||
|
|
||||||
def get_poster_url(self, min_height, min_width):
|
|
||||||
return self._get_poster(min_height, min_width)['url']
|
|
||||||
|
|
||||||
def write_poster(self, filename_root, path, min_height, min_width):
|
|
||||||
poster_url = self.get_poster_url(min_height, min_width)
|
|
||||||
dest = os.path.join(path, filename_root)
|
|
||||||
|
|
||||||
try:
|
|
||||||
f = open(dest, 'wb')
|
|
||||||
except:
|
|
||||||
raise IOError("Can't open for writing: %s" % dest)
|
|
||||||
|
|
||||||
response = urllib2.urlopen(poster_url)
|
|
||||||
f.write(response.read())
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_tmdb_imdb(self):
|
|
||||||
url = "http://api.themoviedb.org/2.1/Movie.imdbLookup/en/json/%s/%s" % (__tmdb_apikey__, self.imdbid)
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
while 1:
|
|
||||||
count += 1
|
|
||||||
response = urllib2.urlopen(url)
|
|
||||||
json_string = response.read()
|
|
||||||
try:
|
|
||||||
tmdb_data = json.loads(json_string)[0]
|
|
||||||
return tmdb_data
|
|
||||||
except ValueError, e:
|
|
||||||
if count < 3:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
raise ApiError("Invalid JSON: %s: %s" % (e, json_string))
|
|
||||||
except:
|
|
||||||
ApiError("JSON error with: %s" % json_string)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_image(self, image_list, min_height, min_width):
|
|
||||||
#Select image
|
|
||||||
images = []
|
|
||||||
for image in image_list:
|
|
||||||
if not min_height or min_width:
|
|
||||||
images.append(image)
|
|
||||||
break
|
|
||||||
elif min_height and not min_width:
|
|
||||||
if image['height'] >= min_height:
|
|
||||||
images.append(image)
|
|
||||||
break
|
|
||||||
elif min_width and not min_height:
|
|
||||||
if image['width'] >= min_width:
|
|
||||||
images.append(image)
|
|
||||||
break
|
|
||||||
elif min_width and min_height:
|
|
||||||
if image['width'] >= min_width and image['height'] >= min_height:
|
|
||||||
images.append(image)
|
|
||||||
break
|
|
||||||
|
|
||||||
#No image meets our resolution requirements, so disregard those requirements
|
|
||||||
if len(images) == 0 and min_height or min_width:
|
|
||||||
images.append(image_list[0])
|
|
||||||
|
|
||||||
return images[0]
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
try:
|
|
||||||
id = sys.argv[1]
|
|
||||||
except:
|
|
||||||
id = 'tt0111161'
|
|
||||||
|
|
||||||
x = MetaGen(id)
|
|
||||||
x.write_nfo("movie.nfo")
|
|
||||||
try:
|
|
||||||
x.write_fanart("fanart.jpg", ".", 0, 0)
|
|
||||||
except: pass
|
|
||||||
try:
|
|
||||||
x.write_poster("movie.tbn", ".", 0, 0)
|
|
||||||
except: pass
|
|
||||||
"""
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class IMDB(MovieProvider):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
addEvent('provider.movie.search', self.search)
|
#addEvent('provider.movie.search', self.search)
|
||||||
|
|
||||||
self.p = IMDb('http')
|
self.p = IMDb('http')
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class TheMovieDb(MovieProvider):
|
|||||||
def getInfo(self, identifier = None):
|
def getInfo(self, identifier = None):
|
||||||
|
|
||||||
cache_key = 'tmdb.cache.%s' % identifier
|
cache_key = 'tmdb.cache.%s' % identifier
|
||||||
result = self.getCache(cache_key)
|
result = None #self.getCache(cache_key)
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
result = {}
|
result = {}
|
||||||
@@ -112,27 +112,33 @@ class TheMovieDb(MovieProvider):
|
|||||||
|
|
||||||
def parseMovie(self, movie):
|
def parseMovie(self, movie):
|
||||||
|
|
||||||
year = str(movie.get('released', 'none'))[:4]
|
|
||||||
|
|
||||||
# Poster url
|
# Poster url
|
||||||
poster = self.getImage(movie, type = 'poster')
|
poster = self.getImage(movie, type = 'poster')
|
||||||
backdrop = self.getImage(movie, type = 'backdrop')
|
backdrop = self.getImage(movie, type = 'backdrop')
|
||||||
|
|
||||||
|
# Genres
|
||||||
|
genres = self.getCategory(movie, 'genre')
|
||||||
|
|
||||||
# 1900 is the same as None
|
# 1900 is the same as None
|
||||||
|
year = str(movie.get('released', 'none'))[:4]
|
||||||
if year == '1900' or year.lower() == 'none':
|
if year == '1900' or year.lower() == 'none':
|
||||||
year = None
|
year = None
|
||||||
|
|
||||||
movie_data = {
|
movie_data = {
|
||||||
'id': int(movie.get('id', 0)),
|
'id': int(movie.get('id', 0)),
|
||||||
'titles': [toUnicode(movie.get('name'))],
|
'titles': [toUnicode(movie.get('name'))],
|
||||||
|
'original_title': movie.get('original_name'),
|
||||||
'images': {
|
'images': {
|
||||||
'posters': [poster],
|
'posters': [poster],
|
||||||
'backdrops': [backdrop],
|
'backdrops': [backdrop],
|
||||||
},
|
},
|
||||||
'imdb': movie.get('imdb_id'),
|
'imdb': movie.get('imdb_id'),
|
||||||
|
'runtime': movie.get('runtime'),
|
||||||
|
'released': movie.get('released'),
|
||||||
'year': year,
|
'year': year,
|
||||||
'plot': movie.get('overview', ''),
|
'plot': movie.get('overview', ''),
|
||||||
'tagline': '',
|
'tagline': '',
|
||||||
|
'genres': genres,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add alternative names
|
# Add alternative names
|
||||||
@@ -153,6 +159,19 @@ class TheMovieDb(MovieProvider):
|
|||||||
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
def getCategory(self, movie, type = 'genre'):
|
||||||
|
|
||||||
|
cats = movie.get('categories', {}).get(type)
|
||||||
|
|
||||||
|
categories = []
|
||||||
|
for category in cats:
|
||||||
|
try:
|
||||||
|
categories.append(category)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return categories
|
||||||
|
|
||||||
def isDisabled(self):
|
def isDisabled(self):
|
||||||
if self.conf('api_key') == '':
|
if self.conf('api_key') == '':
|
||||||
log.error('No API key provided.')
|
log.error('No API key provided.')
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ log = CPLog(__name__)
|
|||||||
class Newzbin(NZBProvider, RSS):
|
class Newzbin(NZBProvider, RSS):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'search': 'https://www.newzbin.com/search/',
|
|
||||||
'download': 'http://www.newzbin.com/api/dnzb/',
|
'download': 'http://www.newzbin.com/api/dnzb/',
|
||||||
|
'search': 'https://www.newzbin.com/search/',
|
||||||
}
|
}
|
||||||
searchUrl = 'https://www.newzbin.com/search/'
|
|
||||||
|
|
||||||
format_ids = {
|
format_ids = {
|
||||||
2: ['scr'],
|
2: ['scr'],
|
||||||
@@ -36,7 +35,7 @@ class Newzbin(NZBProvider, RSS):
|
|||||||
def search(self, movie, quality):
|
def search(self, movie, quality):
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
if self.isDisabled() or not self.isAvailable(self.searchUrl):
|
if self.isDisabled() or not self.isAvailable(self.urls['search']):
|
||||||
return results
|
return results
|
||||||
|
|
||||||
format_id = self.getFormatId(type)
|
format_id = self.getFormatId(type)
|
||||||
@@ -97,11 +96,12 @@ class Newzbin(NZBProvider, RSS):
|
|||||||
new = {
|
new = {
|
||||||
'id': id,
|
'id': id,
|
||||||
'type': 'nzb',
|
'type': 'nzb',
|
||||||
|
'provider': self.getName(),
|
||||||
'name': title,
|
'name': title,
|
||||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||||
'size': self.parseSize(size),
|
'size': self.parseSize(size),
|
||||||
'url': str(self.getTextElement(nzb, '{%s}nzb' % REPORT_NS)),
|
'url': str(self.getTextElement(nzb, '{%s}nzb' % REPORT_NS)),
|
||||||
'download': lambda: self.download(id),
|
'download': self.download,
|
||||||
'detail_url': str(self.getTextElement(nzb, 'link')),
|
'detail_url': str(self.getTextElement(nzb, 'link')),
|
||||||
'description': self.getTextElement(nzb, "description"),
|
'description': self.getTextElement(nzb, "description"),
|
||||||
'check_nzb': False,
|
'check_nzb': False,
|
||||||
@@ -121,7 +121,7 @@ class Newzbin(NZBProvider, RSS):
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def download(self, nzb_id):
|
def download(self, url = '', nzb_id = ''):
|
||||||
try:
|
try:
|
||||||
log.info('Download nzb from newzbin, report id: %s ' % nzb_id)
|
log.info('Download nzb from newzbin, report id: %s ' % nzb_id)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ config = [{
|
|||||||
{
|
{
|
||||||
'tab': 'providers',
|
'tab': 'providers',
|
||||||
'name': 'newznab',
|
'name': 'newznab',
|
||||||
'description': 'Enable multiple NewzNab providers',
|
'description': 'Enable multiple NewzNab providers such as <a href="http://nzb.su" target="_blank">NZB.su</a>',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
@@ -21,8 +21,8 @@ config = [{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'host',
|
'name': 'host',
|
||||||
'default': 'http://nzb.su',
|
'default': 'nzb.su',
|
||||||
'description': 'The hostname of your newznab provider, like http://nzb.su'
|
'description': 'The hostname of your newznab provider'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'api_key',
|
'name': 'api_key',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from couchpotato.core.logger import CPLog
|
|||||||
from couchpotato.core.providers.base import NZBProvider
|
from couchpotato.core.providers.base import NZBProvider
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
|
from urlparse import urlparse
|
||||||
import time
|
import time
|
||||||
import xml.etree.ElementTree as XMLTree
|
import xml.etree.ElementTree as XMLTree
|
||||||
|
|
||||||
@@ -130,11 +131,13 @@ class Newznab(NZBProvider, RSS):
|
|||||||
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
|
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
|
||||||
new = {
|
new = {
|
||||||
'id': id,
|
'id': id,
|
||||||
|
'provider': self.getName(),
|
||||||
'type': 'nzb',
|
'type': 'nzb',
|
||||||
'name': self.getTextElement(nzb, "title"),
|
'name': self.getTextElement(nzb, "title"),
|
||||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||||
'size': int(size) / 1024 / 1024,
|
'size': int(size) / 1024 / 1024,
|
||||||
'url': (self.getUrl(host['host'], self.urls['download']) % id) + self.getApiExt(host),
|
'url': (self.getUrl(host['host'], self.urls['download']) % id) + self.getApiExt(host),
|
||||||
|
'download': self.download,
|
||||||
'detail_url': (self.getUrl(host['host'], self.urls['detail']) % id) + self.getApiExt(host),
|
'detail_url': (self.getUrl(host['host'], self.urls['detail']) % id) + self.getApiExt(host),
|
||||||
'content': self.getTextElement(nzb, "description"),
|
'content': self.getTextElement(nzb, "description"),
|
||||||
}
|
}
|
||||||
@@ -173,6 +176,17 @@ class Newznab(NZBProvider, RSS):
|
|||||||
|
|
||||||
return list
|
return list
|
||||||
|
|
||||||
|
def belongsTo(self, url):
|
||||||
|
|
||||||
|
hosts = self.getHosts()
|
||||||
|
|
||||||
|
for host in hosts:
|
||||||
|
result = super(Newznab, self).belongsTo(url, host = host['host'])
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
def getUrl(self, host, type):
|
def getUrl(self, host, type):
|
||||||
return cleanHost(host) + 'api?t=' + type
|
return cleanHost(host) + 'api?t=' + type
|
||||||
|
|
||||||
|
|||||||
@@ -31,28 +31,51 @@ var MultipleNewznab = new Class({
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.inputs[name].getParent().hide()
|
self.inputs[name].getParent().hide()
|
||||||
|
self.inputs[name].addEvent('change', self.addEmpty.bind(self))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
self.values.each(function(item, nr){
|
self.values.each(function(item, nr){
|
||||||
self.createItem(item.use, item.host, item.api_key);
|
self.createItem(item.use, item.host, item.api_key);
|
||||||
});
|
});
|
||||||
|
|
||||||
new Element('a.nice_button', {
|
new Element('div.head').adopt(
|
||||||
'text': 'Add new NewzNab provider',
|
new Element('abbr.host', {
|
||||||
'events': {
|
'text': 'Host',
|
||||||
'click': function(e){
|
'title': self.inputs['host'].getNext().get('text')
|
||||||
(e).stop();
|
}),
|
||||||
|
new Element('abbr.api_key', {
|
||||||
self.createItem(1, '', '');
|
'text': 'Api Key',
|
||||||
}
|
'title': self.inputs['api_key'].getNext().get('text')
|
||||||
}
|
})
|
||||||
}).inject(self.fieldset.getElement('h2'), 'after');
|
).inject(self.fieldset.getElement('h2'), 'after');
|
||||||
|
|
||||||
|
self.addEmpty();
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
add_empty_timeout: 0,
|
||||||
|
addEmpty: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(self.add_empty_timeout) clearTimeout(self.add_empty_timeout);
|
||||||
|
|
||||||
|
var has_empty = false;
|
||||||
|
self.items.each(function(ctrl_holder){
|
||||||
|
if(ctrl_holder.getElement('.host').get('value') == '' && ctrl_holder.getElement('.api_key').get('value') == ''){
|
||||||
|
has_empty = true;
|
||||||
|
}
|
||||||
|
ctrl_holder[has_empty ? 'addClass' : 'removeClass']('is_empty');
|
||||||
|
});
|
||||||
|
if(has_empty) return;
|
||||||
|
|
||||||
|
self.add_empty_timeout = setTimeout(function(){
|
||||||
|
self.createItem(false, null, null);
|
||||||
|
}, 10);
|
||||||
|
},
|
||||||
|
|
||||||
createItem: function(use, host, api){
|
createItem: function(use, host, api){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@@ -83,12 +106,12 @@ var MultipleNewznab = new Class({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new Element('a.icon.delete', {
|
new Element('a.icon.delete', {
|
||||||
'text': 'delete',
|
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.deleteItem.bind(self)
|
'click': self.deleteItem.bind(self)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
).inject(self.fieldset);
|
).inject(self.fieldset);
|
||||||
|
item[!host ? 'addClass' : 'removeClass']('is_empty');
|
||||||
|
|
||||||
new Form.Check(checkbox, {
|
new Form.Check(checkbox, {
|
||||||
'onChange': checkbox.fireEvent.bind(checkbox, 'change')
|
'onChange': checkbox.fireEvent.bind(checkbox, 'change')
|
||||||
@@ -105,6 +128,7 @@ var MultipleNewznab = new Class({
|
|||||||
self.items.each(function(item, nr){
|
self.items.each(function(item, nr){
|
||||||
self.input_types.each(function(type){
|
self.input_types.each(function(type){
|
||||||
var input = item.getElement('input.'+type);
|
var input = item.getElement('input.'+type);
|
||||||
|
if(input.getParent('.ctrlHolder').hasClass('is_empty')) return;
|
||||||
|
|
||||||
if(!temp[type]) temp[type] = [];
|
if(!temp[type]) temp[type] = [];
|
||||||
temp[type][nr] = input.get('type') == 'checkbox' ? +input.get('checked') : input.get('value').trim();
|
temp[type][nr] = input.get('type') == 'checkbox' ? +input.get('checked') : input.get('value').trim();
|
||||||
@@ -125,7 +149,7 @@ var MultipleNewznab = new Class({
|
|||||||
(e).stop();
|
(e).stop();
|
||||||
|
|
||||||
var item = e.target.getParent();
|
var item = e.target.getParent();
|
||||||
|
|
||||||
self.items.erase(item);
|
self.items.erase(item);
|
||||||
item.destroy();
|
item.destroy();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ log = CPLog(__name__)
|
|||||||
class NzbIndex(NZBProvider, RSS):
|
class NzbIndex(NZBProvider, RSS):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'download': 'http://www.nzbindex.nl/download/%s/%s',
|
'download': 'http://www.nzbindex.nl/download/',
|
||||||
'api': 'http://www.nzbindex.nl/rss/', #http://www.nzbindex.nl/rss/?q=due+date+720p&age=1000&sort=agedesc&minsize=3500&maxsize=10000
|
'api': 'http://www.nzbindex.nl/rss/', #http://www.nzbindex.nl/rss/?q=due+date+720p&age=1000&sort=agedesc&minsize=3500&maxsize=10000
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,10 +63,12 @@ class NzbIndex(NZBProvider, RSS):
|
|||||||
new = {
|
new = {
|
||||||
'id': id,
|
'id': id,
|
||||||
'type': 'nzb',
|
'type': 'nzb',
|
||||||
|
'provider': self.getName(),
|
||||||
'name': self.getTextElement(nzb, "title"),
|
'name': self.getTextElement(nzb, "title"),
|
||||||
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
|
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
|
||||||
'size': enclosure['length'],
|
'size': enclosure['length'],
|
||||||
'url': enclosure['url'],
|
'url': enclosure['url'],
|
||||||
|
'download': self.download,
|
||||||
'detail_url': enclosure['url'].replace('/download/', '/release/'),
|
'detail_url': enclosure['url'].replace('/download/', '/release/'),
|
||||||
'description': self.getTextElement(nzb, "description"),
|
'description': self.getTextElement(nzb, "description"),
|
||||||
'check_nzb': True,
|
'check_nzb': True,
|
||||||
|
|||||||
@@ -81,10 +81,12 @@ class NZBMatrix(NZBProvider, RSS):
|
|||||||
new = {
|
new = {
|
||||||
'id': id,
|
'id': id,
|
||||||
'type': 'nzb',
|
'type': 'nzb',
|
||||||
|
'provider': self.getName(),
|
||||||
'name': title,
|
'name': title,
|
||||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||||
'size': self.parseSize(size),
|
'size': self.parseSize(size),
|
||||||
'url': self.urls['download'] % id + self.getApiExt(),
|
'url': self.urls['download'] % id + self.getApiExt(),
|
||||||
|
'download': self.download,
|
||||||
'detail_url': self.urls['detail'] % id,
|
'detail_url': self.urls['detail'] % id,
|
||||||
'description': self.getTextElement(nzb, "description"),
|
'description': self.getTextElement(nzb, "description"),
|
||||||
'check_nzb': True,
|
'check_nzb': True,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ config = [{
|
|||||||
{
|
{
|
||||||
'tab': 'providers',
|
'tab': 'providers',
|
||||||
'name': 'nzbs',
|
'name': 'nzbs',
|
||||||
|
'description': 'Id and Key can be found <a href="http://nzbs.org/index.php?action=rss" target="_blank">on your nzbs.org RSS page</a>.',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
@@ -17,12 +18,12 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'id',
|
'name': 'id',
|
||||||
'label': 'Id',
|
'label': 'Id',
|
||||||
'description': 'Can be found <a href="http://nzbs.org/index.php?action=rss" target="_blank">here</a>, the number after "&i="',
|
'description': 'The number after "&i="',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'api_key',
|
'name': 'api_key',
|
||||||
'label': 'Api Key',
|
'label': 'Api Key',
|
||||||
'description': 'Can be found <a href="http://nzbs.org/index.php?action=rss" target="_blank">here</a>, the string after "&h="'
|
'description': 'The string after "&h="'
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,10 +71,12 @@ class Nzbs(NZBProvider, RSS):
|
|||||||
new = {
|
new = {
|
||||||
'id': id,
|
'id': id,
|
||||||
'type': 'nzb',
|
'type': 'nzb',
|
||||||
|
'provider': self.getName(),
|
||||||
'name': self.getTextElement(nzb, "title"),
|
'name': self.getTextElement(nzb, "title"),
|
||||||
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
|
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
|
||||||
'size': self.parseSize(self.getTextElement(nzb, "description").split('</a><br />')[1].split('">')[1]),
|
'size': self.parseSize(self.getTextElement(nzb, "description").split('</a><br />')[1].split('">')[1]),
|
||||||
'url': self.urls['download'] % (id, self.getApiExt()),
|
'url': self.urls['download'] % (id, self.getApiExt()),
|
||||||
|
'download': self.download,
|
||||||
'detail_url': self.urls['detail'] % id,
|
'detail_url': self.urls['detail'] % id,
|
||||||
'description': self.getTextElement(nzb, "description"),
|
'description': self.getTextElement(nzb, "description"),
|
||||||
'check_nzb': True,
|
'check_nzb': True,
|
||||||
|
|||||||
@@ -3,22 +3,4 @@ from .main import ThePirateBay
|
|||||||
def start():
|
def start():
|
||||||
return ThePirateBay()
|
return ThePirateBay()
|
||||||
|
|
||||||
config = [{
|
config = []
|
||||||
'name': 'themoviedb',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'providers',
|
|
||||||
'name': 'tmdb',
|
|
||||||
'label': 'TheMovieDB',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Used for all calls to TheMovieDB.',
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'api_key',
|
|
||||||
'default': '9b939aee0aaafc12a65bf448e4af9543',
|
|
||||||
'label': 'Api Key',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from couchpotato.api import addApiView
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import isInt
|
from couchpotato.core.helpers.encoding import isInt
|
||||||
from couchpotato.core.helpers.request import getParams, jsonified
|
from couchpotato.core.helpers.request import getParams, jsonified
|
||||||
|
from couchpotato.core.helpers.variable import mergeDicts
|
||||||
import ConfigParser
|
import ConfigParser
|
||||||
import os.path
|
import os.path
|
||||||
import time
|
import time
|
||||||
@@ -93,7 +94,12 @@ class Settings():
|
|||||||
self.p.set(section, option, value)
|
self.p.set(section, option, value)
|
||||||
|
|
||||||
def addOptions(self, section_name, options):
|
def addOptions(self, section_name, options):
|
||||||
self.options[section_name] = options
|
|
||||||
|
if not self.options.get(section_name):
|
||||||
|
self.options[section_name] = options
|
||||||
|
else:
|
||||||
|
options['groups'] = self.options[section_name].get('groups') + options.get('groups')
|
||||||
|
self.options[section_name] = mergeDicts(self.options[section_name], options)
|
||||||
|
|
||||||
def getOptions(self):
|
def getOptions(self):
|
||||||
return self.options
|
return self.options
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class Library(Entity):
|
|||||||
status = ManyToOne('Status')
|
status = ManyToOne('Status')
|
||||||
movies = OneToMany('Movie')
|
movies = OneToMany('Movie')
|
||||||
titles = OneToMany('LibraryTitle')
|
titles = OneToMany('LibraryTitle')
|
||||||
|
genres = ManyToMany('LibraryGenre')
|
||||||
files = ManyToMany('File')
|
files = ManyToMany('File')
|
||||||
info = OneToMany('LibraryInfo')
|
info = OneToMany('LibraryInfo')
|
||||||
|
|
||||||
@@ -68,6 +69,14 @@ class LibraryTitle(Entity):
|
|||||||
libraries = ManyToOne('Library')
|
libraries = ManyToOne('Library')
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryGenre(Entity):
|
||||||
|
""""""
|
||||||
|
|
||||||
|
name = Field(Unicode)
|
||||||
|
|
||||||
|
libraries = ManyToMany('Library')
|
||||||
|
|
||||||
|
|
||||||
class Language(Entity):
|
class Language(Entity):
|
||||||
""""""
|
""""""
|
||||||
|
|
||||||
|
|||||||
@@ -115,8 +115,10 @@ def runCouchPotato(options, base_path, args):
|
|||||||
|
|
||||||
latest_db_version = version(repo)
|
latest_db_version = version(repo)
|
||||||
|
|
||||||
|
initialize = True
|
||||||
try:
|
try:
|
||||||
current_db_version = db_version(db, repo)
|
current_db_version = db_version(db, repo)
|
||||||
|
initialize = False
|
||||||
except:
|
except:
|
||||||
version_control(db, repo, version = latest_db_version)
|
version_control(db, repo, version = latest_db_version)
|
||||||
current_db_version = db_version(db, repo)
|
current_db_version = db_version(db, repo)
|
||||||
@@ -131,6 +133,9 @@ def runCouchPotato(options, base_path, args):
|
|||||||
|
|
||||||
fireEventAsync('app.load')
|
fireEventAsync('app.load')
|
||||||
|
|
||||||
|
if initialize:
|
||||||
|
fireEventAsync('app.initialize')
|
||||||
|
|
||||||
# Create app
|
# Create app
|
||||||
from couchpotato import app
|
from couchpotato import app
|
||||||
api_key = Env.setting('api_key')
|
api_key = Env.setting('api_key')
|
||||||
@@ -138,10 +143,12 @@ def runCouchPotato(options, base_path, args):
|
|||||||
reloader = debug and not options.daemonize
|
reloader = debug and not options.daemonize
|
||||||
|
|
||||||
# Basic config
|
# Basic config
|
||||||
app.host = Env.setting('host', default = '0.0.0.0')
|
|
||||||
app.port = Env.setting('port', default = 5000)
|
|
||||||
app.debug = debug
|
|
||||||
app.secret_key = api_key
|
app.secret_key = api_key
|
||||||
|
config = {
|
||||||
|
'use_reloader': reloader,
|
||||||
|
'host': Env.setting('host', default = '0.0.0.0'),
|
||||||
|
'port': Env.setting('port', default = 5000)
|
||||||
|
}
|
||||||
|
|
||||||
# Static path
|
# Static path
|
||||||
web.add_url_rule(url_base + '/static/<path:filename>',
|
web.add_url_rule(url_base + '/static/<path:filename>',
|
||||||
@@ -153,4 +160,4 @@ def runCouchPotato(options, base_path, args):
|
|||||||
app.register_blueprint(api, url_prefix = '%s/%s/' % (url_base, api_key))
|
app.register_blueprint(api, url_prefix = '%s/%s/' % (url_base, api_key))
|
||||||
|
|
||||||
# Go go go!
|
# Go go go!
|
||||||
app.run(use_reloader = reloader)
|
app.run(**config)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1015 B |
|
Before Width: | Height: | Size: 206 B |
|
After Width: | Height: | Size: 451 B |
|
Before Width: | Height: | Size: 228 B After Width: | Height: | Size: 228 B |
|
After Width: | Height: | Size: 502 B |
|
After Width: | Height: | Size: 624 B |
|
After Width: | Height: | Size: 757 B |
|
Before Width: | Height: | Size: 159 B After Width: | Height: | Size: 159 B |
|
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 726 B |
|
After Width: | Height: | Size: 223 B |
@@ -29,7 +29,7 @@ var CouchPotato = new Class({
|
|||||||
else
|
else
|
||||||
self.openPage(window.location.pathname);
|
self.openPage(window.location.pathname);
|
||||||
|
|
||||||
self.c.addEvent('click:relay(a)', self.pushState.bind(self));
|
self.c.addEvent('click:relay(a:not([target=_blank]))', self.pushState.bind(self));
|
||||||
},
|
},
|
||||||
|
|
||||||
pushState: function(e){
|
pushState: function(e){
|
||||||
@@ -100,6 +100,14 @@ var CouchPotato = new Class({
|
|||||||
|
|
||||||
getPage: function(name){
|
getPage: function(name){
|
||||||
return this.pages[name]
|
return this.pages[name]
|
||||||
|
},
|
||||||
|
|
||||||
|
shutdown: function(){
|
||||||
|
Api.request('app.shutdown');
|
||||||
|
},
|
||||||
|
|
||||||
|
restart: function(){
|
||||||
|
Api.request('app.restart');
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -96,14 +96,14 @@ Form.Check = new Class({
|
|||||||
this.fireEvent('removeHighlight', this);
|
this.fireEvent('removeHighlight', this);
|
||||||
},
|
},
|
||||||
keyToggle: function(e) {
|
keyToggle: function(e) {
|
||||||
var evt = new Event(e);
|
var evt = (e);
|
||||||
if (evt.key === 'space') { this.toggle(e); }
|
if (evt.key === 'space') { this.toggle(e); }
|
||||||
},
|
},
|
||||||
toggle: function(e) {
|
toggle: function(e) {
|
||||||
var evt;
|
var evt;
|
||||||
if (this.disabled) { return this; }
|
if (this.disabled) { return this; }
|
||||||
if (e) {
|
if (e) {
|
||||||
evt = new Event(e).stopPropagation();
|
evt = (e).stopPropagation();
|
||||||
if (evt.target.tagName.toLowerCase() !== 'a') {
|
if (evt.target.tagName.toLowerCase() !== 'a') {
|
||||||
evt.stop();
|
evt.stop();
|
||||||
}
|
}
|
||||||
@@ -114,6 +114,7 @@ Form.Check = new Class({
|
|||||||
this.check();
|
this.check();
|
||||||
}
|
}
|
||||||
this.fireEvent('change', this);
|
this.fireEvent('change', this);
|
||||||
|
this.input.fireEvent('change', this);
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
uncheck: function() {
|
uncheck: function() {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ Form.Dropdown = new Class({
|
|||||||
},
|
},
|
||||||
expand: function(e) {
|
expand: function(e) {
|
||||||
clearTimeout(this.collapseInterval);
|
clearTimeout(this.collapseInterval);
|
||||||
var evt = e ? new Event(e).stop() : null;
|
var evt = e ? (e).stop() : null;
|
||||||
this.open = true;
|
this.open = true;
|
||||||
this.input.focus();
|
this.input.focus();
|
||||||
this.element.addClass('active').addClass('dropdown-active');
|
this.element.addClass('active').addClass('dropdown-active');
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Form.Radio = new Class({
|
|||||||
toggle: function(e) {
|
toggle: function(e) {
|
||||||
if (this.element.hasClass('checked') || this.disabled) { return; }
|
if (this.element.hasClass('checked') || this.disabled) { return; }
|
||||||
var evt;
|
var evt;
|
||||||
if (e) { evt = new Event(e).stop(); }
|
if (e) { evt = (e).stop(); }
|
||||||
if (this.checked) {
|
if (this.checked) {
|
||||||
this.uncheck();
|
this.uncheck();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// MooTools: the javascript framework.
|
// MooTools: the javascript framework.
|
||||||
// Load this file's selection again by visiting: http://mootools.net/more/1e3edb90c5e02d9b9013b54e6ab001ea
|
// Load this file's selection again by visiting: http://mootools.net/more/13115b95c0560a5c35a61ccf237f3ed9
|
||||||
// Or build this file again with packager using: packager build More/Element.Forms More/Element.Delegation More/Element.Shortcuts More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical More/Spinner
|
// Or build this file again with packager using: packager build More/Element.Forms More/Element.Shortcuts More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical More/Spinner
|
||||||
/*
|
/*
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ authors:
|
|||||||
- Tim Wienk
|
- Tim Wienk
|
||||||
- Christoph Pojer
|
- Christoph Pojer
|
||||||
- Aaron Newton
|
- Aaron Newton
|
||||||
|
- Jacob Thornton
|
||||||
|
|
||||||
requires:
|
requires:
|
||||||
- Core/MooTools
|
- Core/MooTools
|
||||||
@@ -30,8 +31,8 @@ provides: [MooTools.More]
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
MooTools.More = {
|
MooTools.More = {
|
||||||
'version': '1.3.1.1',
|
'version': '1.4.0.1',
|
||||||
'build': '0292a3af1eea242b817fecf9daa127417d10d4ce'
|
'build': 'a4244edf2aa97ac8a196fc96082dd35af1abab87'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -182,7 +183,7 @@ String.implement({
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}).call(this);
|
})();
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -327,368 +328,6 @@ Element.implement({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
---
|
|
||||||
|
|
||||||
name: Events.Pseudos
|
|
||||||
|
|
||||||
description: Adds the functionality to add pseudo events
|
|
||||||
|
|
||||||
license: MIT-style license
|
|
||||||
|
|
||||||
authors:
|
|
||||||
- Arian Stolwijk
|
|
||||||
|
|
||||||
requires: [Core/Class.Extras, Core/Slick.Parser, More/MooTools.More]
|
|
||||||
|
|
||||||
provides: [Events.Pseudos]
|
|
||||||
|
|
||||||
...
|
|
||||||
*/
|
|
||||||
|
|
||||||
Events.Pseudos = function(pseudos, addEvent, removeEvent){
|
|
||||||
|
|
||||||
var storeKey = 'monitorEvents:';
|
|
||||||
|
|
||||||
var storageOf = function(object){
|
|
||||||
return {
|
|
||||||
store: object.store ? function(key, value){
|
|
||||||
object.store(storeKey + key, value);
|
|
||||||
} : function(key, value){
|
|
||||||
(object.$monitorEvents || (object.$monitorEvents = {}))[key] = value;
|
|
||||||
},
|
|
||||||
retrieve: object.retrieve ? function(key, dflt){
|
|
||||||
return object.retrieve(storeKey + key, dflt);
|
|
||||||
} : function(key, dflt){
|
|
||||||
if (!object.$monitorEvents) return dflt;
|
|
||||||
return object.$monitorEvents[key] || dflt;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var splitType = function(type){
|
|
||||||
if (type.indexOf(':') == -1 || !pseudos) return null;
|
|
||||||
|
|
||||||
var parsed = Slick.parse(type).expressions[0][0],
|
|
||||||
parsedPseudos = parsed.pseudos,
|
|
||||||
l = parsedPseudos.length,
|
|
||||||
splits = [];
|
|
||||||
|
|
||||||
while (l--) if (pseudos[parsedPseudos[l].key]){
|
|
||||||
splits.push({
|
|
||||||
event: parsed.tag,
|
|
||||||
value: parsedPseudos[l].value,
|
|
||||||
pseudo: parsedPseudos[l].key,
|
|
||||||
original: type
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return splits.length ? splits : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
var mergePseudoOptions = function(split){
|
|
||||||
return Object.merge.apply(this, split.map(function(item){
|
|
||||||
return pseudos[item.pseudo].options || {};
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
addEvent: function(type, fn, internal){
|
|
||||||
var split = splitType(type);
|
|
||||||
if (!split) return addEvent.call(this, type, fn, internal);
|
|
||||||
|
|
||||||
var storage = storageOf(this),
|
|
||||||
events = storage.retrieve(type, []),
|
|
||||||
eventType = split[0].event,
|
|
||||||
options = mergePseudoOptions(split),
|
|
||||||
stack = fn,
|
|
||||||
eventOptions = options[eventType] || {},
|
|
||||||
args = Array.slice(arguments, 2),
|
|
||||||
self = this,
|
|
||||||
monitor;
|
|
||||||
|
|
||||||
if (eventOptions.args) args.append(Array.from(eventOptions.args));
|
|
||||||
if (eventOptions.base) eventType = eventOptions.base;
|
|
||||||
if (eventOptions.onAdd) eventOptions.onAdd(this);
|
|
||||||
|
|
||||||
split.each(function(item){
|
|
||||||
var stackFn = stack;
|
|
||||||
stack = function(){
|
|
||||||
(eventOptions.listener || pseudos[item.pseudo].listener).call(self, item, stackFn, arguments, monitor, options);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
monitor = stack.bind(this);
|
|
||||||
|
|
||||||
events.include({event: fn, monitor: monitor});
|
|
||||||
storage.store(type, events);
|
|
||||||
|
|
||||||
addEvent.apply(this, [type, fn].concat(args));
|
|
||||||
return addEvent.apply(this, [eventType, monitor].concat(args));
|
|
||||||
},
|
|
||||||
|
|
||||||
removeEvent: function(type, fn){
|
|
||||||
var split = splitType(type);
|
|
||||||
if (!split) return removeEvent.call(this, type, fn);
|
|
||||||
|
|
||||||
var storage = storageOf(this),
|
|
||||||
events = storage.retrieve(type);
|
|
||||||
if (!events) return this;
|
|
||||||
|
|
||||||
var eventType = split[0].event,
|
|
||||||
options = mergePseudoOptions(split),
|
|
||||||
eventOptions = options[eventType] || {},
|
|
||||||
args = Array.slice(arguments, 2);
|
|
||||||
|
|
||||||
if (eventOptions.args) args.append(Array.from(eventOptions.args));
|
|
||||||
if (eventOptions.base) eventType = eventOptions.base;
|
|
||||||
if (eventOptions.onRemove) eventOptions.onRemove(this);
|
|
||||||
|
|
||||||
removeEvent.apply(this, [type, fn].concat(args));
|
|
||||||
events.each(function(monitor, i){
|
|
||||||
if (!fn || monitor.event == fn) removeEvent.apply(this, [eventType, monitor.monitor].concat(args));
|
|
||||||
delete events[i];
|
|
||||||
}, this);
|
|
||||||
|
|
||||||
storage.store(type, events);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
(function(){
|
|
||||||
|
|
||||||
var pseudos = {
|
|
||||||
|
|
||||||
once: {
|
|
||||||
listener: function(split, fn, args, monitor){
|
|
||||||
fn.apply(this, args);
|
|
||||||
this.removeEvent(split.event, monitor)
|
|
||||||
.removeEvent(split.original, fn);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
throttle: {
|
|
||||||
listener: function(split, fn, args){
|
|
||||||
if (!fn._throttled){
|
|
||||||
fn.apply(this, args);
|
|
||||||
fn._throttled = setTimeout(function(){
|
|
||||||
fn._throttled = false;
|
|
||||||
}, split.value || 250);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
pause: {
|
|
||||||
listener: function(split, fn, args){
|
|
||||||
clearTimeout(fn._pause);
|
|
||||||
fn._pause = fn.delay(split.value || 250, this, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
Events.definePseudo = function(key, listener){
|
|
||||||
pseudos[key] = Type.isFunction(listener) ? {listener: listener} : listener;
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
Events.lookupPseudo = function(key){
|
|
||||||
return pseudos[key];
|
|
||||||
};
|
|
||||||
|
|
||||||
var proto = Events.prototype;
|
|
||||||
Events.implement(Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent));
|
|
||||||
|
|
||||||
['Request', 'Fx'].each(function(klass){
|
|
||||||
if (this[klass]) this[klass].implement(Events.prototype);
|
|
||||||
});
|
|
||||||
|
|
||||||
}).call(this);
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
---
|
|
||||||
|
|
||||||
name: Element.Event.Pseudos
|
|
||||||
|
|
||||||
description: Adds the functionality to add pseudo events for Elements
|
|
||||||
|
|
||||||
license: MIT-style license
|
|
||||||
|
|
||||||
authors:
|
|
||||||
- Arian Stolwijk
|
|
||||||
|
|
||||||
requires: [Core/Element.Event, Events.Pseudos]
|
|
||||||
|
|
||||||
provides: [Element.Event.Pseudos]
|
|
||||||
|
|
||||||
...
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function(){
|
|
||||||
|
|
||||||
var pseudos = {},
|
|
||||||
copyFromEvents = ['once', 'throttle', 'pause'],
|
|
||||||
count = copyFromEvents.length;
|
|
||||||
|
|
||||||
while (count--) pseudos[copyFromEvents[count]] = Events.lookupPseudo(copyFromEvents[count]);
|
|
||||||
|
|
||||||
Event.definePseudo = function(key, listener){
|
|
||||||
pseudos[key] = Type.isFunction(listener) ? {listener: listener} : listener;
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
var proto = Element.prototype;
|
|
||||||
[Element, Window, Document].invoke('implement', Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent));
|
|
||||||
|
|
||||||
}).call(this);
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
---
|
|
||||||
|
|
||||||
script: Element.Delegation.js
|
|
||||||
|
|
||||||
name: Element.Delegation
|
|
||||||
|
|
||||||
description: Extends the Element native object to include the delegate method for more efficient event management.
|
|
||||||
|
|
||||||
credits:
|
|
||||||
- "Event checking based on the work of Daniel Steigerwald. License: MIT-style license. Copyright: Copyright (c) 2008 Daniel Steigerwald, daniel.steigerwald.cz"
|
|
||||||
|
|
||||||
license: MIT-style license
|
|
||||||
|
|
||||||
authors:
|
|
||||||
- Aaron Newton
|
|
||||||
- Daniel Steigerwald
|
|
||||||
|
|
||||||
requires: [/MooTools.More, Element.Event.Pseudos]
|
|
||||||
|
|
||||||
provides: [Element.Delegation]
|
|
||||||
|
|
||||||
...
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function(){
|
|
||||||
|
|
||||||
var eventListenerSupport = !(window.attachEvent && !window.addEventListener),
|
|
||||||
nativeEvents = Element.NativeEvents;
|
|
||||||
|
|
||||||
nativeEvents.focusin = 2;
|
|
||||||
nativeEvents.focusout = 2;
|
|
||||||
|
|
||||||
var check = function(split, target, event){
|
|
||||||
var elementEvent = Element.Events[split.event], condition;
|
|
||||||
if (elementEvent) condition = elementEvent.condition;
|
|
||||||
return Slick.match(target, split.value) && (!condition || condition.call(target, event));
|
|
||||||
};
|
|
||||||
|
|
||||||
var formObserver = function(eventName){
|
|
||||||
|
|
||||||
var $delegationKey = '$delegation:';
|
|
||||||
|
|
||||||
return {
|
|
||||||
base: 'focusin',
|
|
||||||
|
|
||||||
onRemove: function(element){
|
|
||||||
element.retrieve($delegationKey + 'forms', []).each(function(el){
|
|
||||||
el.retrieve($delegationKey + 'listeners', []).each(function(listener){
|
|
||||||
el.removeEvent(eventName, listener);
|
|
||||||
});
|
|
||||||
el.eliminate($delegationKey + eventName + 'listeners')
|
|
||||||
.eliminate($delegationKey + eventName + 'originalFn');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
listener: function(split, fn, args, monitor, options){
|
|
||||||
var event = args[0],
|
|
||||||
forms = this.retrieve($delegationKey + 'forms', []),
|
|
||||||
target = event.target,
|
|
||||||
form = (target.get('tag') == 'form') ? target : event.target.getParent('form'),
|
|
||||||
formEvents = form.retrieve($delegationKey + 'originalFn', []),
|
|
||||||
formListeners = form.retrieve($delegationKey + 'listeners', []);
|
|
||||||
|
|
||||||
forms.include(form);
|
|
||||||
this.store($delegationKey + 'forms', forms);
|
|
||||||
|
|
||||||
if (!formEvents.contains(fn)){
|
|
||||||
var formListener = function(event){
|
|
||||||
if (check(split, this, event)) fn.call(this, event);
|
|
||||||
};
|
|
||||||
form.addEvent(eventName, formListener);
|
|
||||||
|
|
||||||
formEvents.push(fn);
|
|
||||||
formListeners.push(formListener);
|
|
||||||
|
|
||||||
form.store($delegationKey + eventName + 'originalFn', formEvents)
|
|
||||||
.store($delegationKey + eventName + 'listeners', formListeners);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var inputObserver = function(eventName){
|
|
||||||
return {
|
|
||||||
base: 'focusin',
|
|
||||||
listener: function(split, fn, args){
|
|
||||||
var events = {blur: function(){
|
|
||||||
this.removeEvents(events);
|
|
||||||
}};
|
|
||||||
events[eventName] = function(event){
|
|
||||||
if (check(split, this, event)) fn.call(this, event);
|
|
||||||
};
|
|
||||||
args[0].target.addEvents(events);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
var eventOptions = {
|
|
||||||
mouseenter: {
|
|
||||||
base: 'mouseover'
|
|
||||||
},
|
|
||||||
mouseleave: {
|
|
||||||
base: 'mouseout'
|
|
||||||
},
|
|
||||||
focus: {
|
|
||||||
base: 'focus' + (eventListenerSupport ? '' : 'in'),
|
|
||||||
args: [true]
|
|
||||||
},
|
|
||||||
blur: {
|
|
||||||
base: eventListenerSupport ? 'blur' : 'focusout',
|
|
||||||
args: [true]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!eventListenerSupport) Object.append(eventOptions, {
|
|
||||||
submit: formObserver('submit'),
|
|
||||||
reset: formObserver('reset'),
|
|
||||||
change: inputObserver('change'),
|
|
||||||
select: inputObserver('select')
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
Event.definePseudo('relay', {
|
|
||||||
listener: function(split, fn, args, monitor, options){
|
|
||||||
var event = args[0];
|
|
||||||
|
|
||||||
for (var target = event.target; target && target != this; target = target.parentNode){
|
|
||||||
var finalTarget = document.id(target);
|
|
||||||
if (check(split, finalTarget, event)){
|
|
||||||
if (finalTarget) fn.call(finalTarget, event, finalTarget);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
options: eventOptions
|
|
||||||
});
|
|
||||||
|
|
||||||
}).call(this);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -826,7 +465,7 @@ Fx.Slide = new Class({
|
|||||||
|
|
||||||
this.addEvent('complete', function(){
|
this.addEvent('complete', function(){
|
||||||
this.open = (wrapper['offset' + this.layout.capitalize()] != 0);
|
this.open = (wrapper['offset' + this.layout.capitalize()] != 0);
|
||||||
if (this.open && options.resetHeight) wrapper.setStyle('height', '');
|
if (this.open && this.options.resetHeight) wrapper.setStyle('height', '');
|
||||||
}, true);
|
}, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1050,12 +689,6 @@ var Drag = new Class({
|
|||||||
var limit = options.limit;
|
var limit = options.limit;
|
||||||
this.limit = {x: [], y: []};
|
this.limit = {x: [], y: []};
|
||||||
|
|
||||||
var styles = this.element.getStyles('left', 'right', 'top', 'bottom');
|
|
||||||
this._invert = {
|
|
||||||
x: options.modifiers.x == 'left' && styles.left == 'auto' && !isNaN(styles.right.toInt()) && (options.modifiers.x = 'right'),
|
|
||||||
y: options.modifiers.y == 'top' && styles.top == 'auto' && !isNaN(styles.bottom.toInt()) && (options.modifiers.y = 'bottom')
|
|
||||||
};
|
|
||||||
|
|
||||||
var z, coordinates;
|
var z, coordinates;
|
||||||
for (z in options.modifiers){
|
for (z in options.modifiers){
|
||||||
if (!options.modifiers[z]) continue;
|
if (!options.modifiers[z]) continue;
|
||||||
@@ -1072,7 +705,6 @@ var Drag = new Class({
|
|||||||
else this.value.now[z] = this.element[options.modifiers[z]];
|
else this.value.now[z] = this.element[options.modifiers[z]];
|
||||||
|
|
||||||
if (options.invert) this.value.now[z] *= -1;
|
if (options.invert) this.value.now[z] *= -1;
|
||||||
if (this._invert[z]) this.value.now[z] *= -1;
|
|
||||||
|
|
||||||
this.mouse.pos[z] = event.page[z] - this.value.now[z];
|
this.mouse.pos[z] = event.page[z] - this.value.now[z];
|
||||||
|
|
||||||
@@ -1122,7 +754,6 @@ var Drag = new Class({
|
|||||||
this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z];
|
this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z];
|
||||||
|
|
||||||
if (options.invert) this.value.now[z] *= -1;
|
if (options.invert) this.value.now[z] *= -1;
|
||||||
if (this._invert[z]) this.value.now[z] *= -1;
|
|
||||||
|
|
||||||
if (options.limit && this.limit[z]){
|
if (options.limit && this.limit[z]){
|
||||||
if ((this.limit[z][1] || this.limit[z][1] === 0) && (this.value.now[z] > this.limit[z][1])){
|
if ((this.limit[z][1] || this.limit[z][1] === 0) && (this.value.now[z] > this.limit[z][1])){
|
||||||
@@ -1236,10 +867,9 @@ Drag.Move = new Class({
|
|||||||
this.container = document.id(this.container.getDocument().body);
|
this.container = document.id(this.container.getDocument().body);
|
||||||
|
|
||||||
if (this.options.style){
|
if (this.options.style){
|
||||||
if (this.options.modifiers.x == "left" && this.options.modifiers.y == "top"){
|
if (this.options.modifiers.x == 'left' && this.options.modifiers.y == 'top'){
|
||||||
var parentStyles,
|
var parent = element.getOffsetParent(),
|
||||||
parent = element.getOffsetParent();
|
styles = element.getStyles('left', 'top');
|
||||||
var styles = element.getStyles('left', 'top');
|
|
||||||
if (parent && (styles.left == 'auto' || styles.top == 'auto')){
|
if (parent && (styles.left == 'auto' || styles.top == 'auto')){
|
||||||
element.setPosition(element.getPosition(parent));
|
element.setPosition(element.getPosition(parent));
|
||||||
}
|
}
|
||||||
@@ -1529,7 +1159,7 @@ var Sortables = new Class({
|
|||||||
if (
|
if (
|
||||||
!this.idle ||
|
!this.idle ||
|
||||||
event.rightClick ||
|
event.rightClick ||
|
||||||
['button', 'input', 'a'].contains(event.target.get('tag'))
|
['button', 'input', 'a', 'textarea'].contains(event.target.get('tag'))
|
||||||
) return;
|
) return;
|
||||||
|
|
||||||
this.idle = false;
|
this.idle = false;
|
||||||
@@ -1640,8 +1270,7 @@ Request.JSONP = new Class({
|
|||||||
|
|
||||||
Implements: [Chain, Events, Options],
|
Implements: [Chain, Events, Options],
|
||||||
|
|
||||||
options: {
|
options: {/*
|
||||||
/*
|
|
||||||
onRequest: function(src, scriptElement){},
|
onRequest: function(src, scriptElement){},
|
||||||
onComplete: function(data){},
|
onComplete: function(data){},
|
||||||
onSuccess: function(data){},
|
onSuccess: function(data){},
|
||||||
@@ -1708,7 +1337,8 @@ Request.JSONP = new Class({
|
|||||||
},
|
},
|
||||||
|
|
||||||
getScript: function(src){
|
getScript: function(src){
|
||||||
if (!this.script) this.script = new Element('script[type=text/javascript]', {
|
if (!this.script) this.script = new Element('script', {
|
||||||
|
type: 'text/javascript',
|
||||||
async: true,
|
async: true,
|
||||||
src: src
|
src: src
|
||||||
});
|
});
|
||||||
@@ -1716,7 +1346,7 @@ Request.JSONP = new Class({
|
|||||||
},
|
},
|
||||||
|
|
||||||
success: function(args, index){
|
success: function(args, index){
|
||||||
if (!this.running) return false;
|
if (!this.running) return;
|
||||||
this.clear()
|
this.clear()
|
||||||
.fireEvent('complete', args).fireEvent('success', args)
|
.fireEvent('complete', args).fireEvent('success', args)
|
||||||
.callChain();
|
.callChain();
|
||||||
@@ -1835,10 +1465,10 @@ Class.refactor = function(original, refactors){
|
|||||||
|
|
||||||
Object.each(refactors, function(item, name){
|
Object.each(refactors, function(item, name){
|
||||||
var origin = original.prototype[name];
|
var origin = original.prototype[name];
|
||||||
if (origin && origin.$origin) origin = origin.$origin;
|
origin = (origin && origin.$origin) || origin || function(){};
|
||||||
original.implement(name, (typeof item == 'function') ? function(){
|
original.implement(name, (typeof item == 'function') ? function(){
|
||||||
var old = this.previous;
|
var old = this.previous;
|
||||||
this.previous = origin || function(){};
|
this.previous = origin;
|
||||||
var value = item.apply(this, arguments);
|
var value = item.apply(this, arguments);
|
||||||
this.previous = old;
|
this.previous = old;
|
||||||
return value;
|
return value;
|
||||||
@@ -1875,7 +1505,7 @@ provides: [Class.Binds]
|
|||||||
|
|
||||||
Class.Mutators.Binds = function(binds){
|
Class.Mutators.Binds = function(binds){
|
||||||
if (!this.prototype.initialize) this.implement('initialize', function(){});
|
if (!this.prototype.initialize) this.implement('initialize', function(){});
|
||||||
return binds;
|
return Array.from(binds).concat(this.prototype.Binds || []);
|
||||||
};
|
};
|
||||||
|
|
||||||
Class.Mutators.initialize = function(initialize){
|
Class.Mutators.initialize = function(initialize){
|
||||||
@@ -2055,7 +1685,7 @@ Element.implement({
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}).call(this);
|
})();
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -2071,219 +1701,228 @@ license: MIT-style license
|
|||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Aaron Newton
|
- Aaron Newton
|
||||||
|
- Jacob Thornton
|
||||||
|
|
||||||
requires:
|
requires:
|
||||||
|
- Core/Options
|
||||||
- Core/Element.Dimensions
|
- Core/Element.Dimensions
|
||||||
- /Element.Measure
|
- Element.Measure
|
||||||
|
|
||||||
provides: [Element.Position]
|
provides: [Element.Position]
|
||||||
|
|
||||||
...
|
...
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function(){
|
(function(original){
|
||||||
|
|
||||||
var original = Element.prototype.position;
|
var local = Element.Position = {
|
||||||
|
|
||||||
|
options: {/*
|
||||||
|
edge: false,
|
||||||
|
returnPos: false,
|
||||||
|
minimum: {x: 0, y: 0},
|
||||||
|
maximum: {x: 0, y: 0},
|
||||||
|
relFixedPosition: false,
|
||||||
|
ignoreMargins: false,
|
||||||
|
ignoreScroll: false,
|
||||||
|
allowNegative: false,*/
|
||||||
|
relativeTo: document.body,
|
||||||
|
position: {
|
||||||
|
x: 'center', //left, center, right
|
||||||
|
y: 'center' //top, center, bottom
|
||||||
|
},
|
||||||
|
offset: {x: 0, y: 0}
|
||||||
|
},
|
||||||
|
|
||||||
|
getOptions: function(element, options){
|
||||||
|
options = Object.merge({}, local.options, options);
|
||||||
|
local.setPositionOption(options);
|
||||||
|
local.setEdgeOption(options);
|
||||||
|
local.setOffsetOption(element, options);
|
||||||
|
local.setDimensionsOption(element, options);
|
||||||
|
return options;
|
||||||
|
},
|
||||||
|
|
||||||
|
setPositionOption: function(options){
|
||||||
|
options.position = local.getCoordinateFromValue(options.position);
|
||||||
|
},
|
||||||
|
|
||||||
|
setEdgeOption: function(options){
|
||||||
|
var edgeOption = local.getCoordinateFromValue(options.edge);
|
||||||
|
options.edge = edgeOption ? edgeOption :
|
||||||
|
(options.position.x == 'center' && options.position.y == 'center') ? {x: 'center', y: 'center'} :
|
||||||
|
{x: 'left', y: 'top'};
|
||||||
|
},
|
||||||
|
|
||||||
|
setOffsetOption: function(element, options){
|
||||||
|
var parentOffset = {x: 0, y: 0},
|
||||||
|
offsetParent = element.measure(function(){
|
||||||
|
return document.id(this.getOffsetParent());
|
||||||
|
}),
|
||||||
|
parentScroll = offsetParent.getScroll();
|
||||||
|
|
||||||
|
if (!offsetParent || offsetParent == element.getDocument().body) return;
|
||||||
|
parentOffset = offsetParent.measure(function(){
|
||||||
|
var position = this.getPosition();
|
||||||
|
if (this.getStyle('position') == 'fixed'){
|
||||||
|
var scroll = window.getScroll();
|
||||||
|
position.x += scroll.x;
|
||||||
|
position.y += scroll.y;
|
||||||
|
}
|
||||||
|
return position;
|
||||||
|
});
|
||||||
|
|
||||||
|
options.offset = {
|
||||||
|
parentPositioned: offsetParent != document.id(options.relativeTo),
|
||||||
|
x: options.offset.x - parentOffset.x + parentScroll.x,
|
||||||
|
y: options.offset.y - parentOffset.y + parentScroll.y
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
setDimensionsOption: function(element, options){
|
||||||
|
options.dimensions = element.getDimensions({
|
||||||
|
computeSize: true,
|
||||||
|
styles: ['padding', 'border', 'margin']
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getPosition: function(element, options){
|
||||||
|
var position = {};
|
||||||
|
options = local.getOptions(element, options);
|
||||||
|
var relativeTo = document.id(options.relativeTo) || document.body;
|
||||||
|
|
||||||
|
local.setPositionCoordinates(options, position, relativeTo);
|
||||||
|
if (options.edge) local.toEdge(position, options);
|
||||||
|
|
||||||
|
var offset = options.offset;
|
||||||
|
position.left = ((position.x >= 0 || offset.parentPositioned || options.allowNegative) ? position.x : 0).toInt();
|
||||||
|
position.top = ((position.y >= 0 || offset.parentPositioned || options.allowNegative) ? position.y : 0).toInt();
|
||||||
|
|
||||||
|
local.toMinMax(position, options);
|
||||||
|
|
||||||
|
if (options.relFixedPosition || relativeTo.getStyle('position') == 'fixed') local.toRelFixedPosition(relativeTo, position);
|
||||||
|
if (options.ignoreScroll) local.toIgnoreScroll(relativeTo, position);
|
||||||
|
if (options.ignoreMargins) local.toIgnoreMargins(position, options);
|
||||||
|
|
||||||
|
position.left = Math.ceil(position.left);
|
||||||
|
position.top = Math.ceil(position.top);
|
||||||
|
delete position.x;
|
||||||
|
delete position.y;
|
||||||
|
|
||||||
|
return position;
|
||||||
|
},
|
||||||
|
|
||||||
|
setPositionCoordinates: function(options, position, relativeTo){
|
||||||
|
var offsetY = options.offset.y,
|
||||||
|
offsetX = options.offset.x,
|
||||||
|
calc = (relativeTo == document.body) ? window.getScroll() : relativeTo.getPosition(),
|
||||||
|
top = calc.y,
|
||||||
|
left = calc.x,
|
||||||
|
winSize = window.getSize();
|
||||||
|
|
||||||
|
switch(options.position.x){
|
||||||
|
case 'left': position.x = left + offsetX; break;
|
||||||
|
case 'right': position.x = left + offsetX + relativeTo.offsetWidth; break;
|
||||||
|
default: position.x = left + ((relativeTo == document.body ? winSize.x : relativeTo.offsetWidth) / 2) + offsetX; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(options.position.y){
|
||||||
|
case 'top': position.y = top + offsetY; break;
|
||||||
|
case 'bottom': position.y = top + offsetY + relativeTo.offsetHeight; break;
|
||||||
|
default: position.y = top + ((relativeTo == document.body ? winSize.y : relativeTo.offsetHeight) / 2) + offsetY; break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toMinMax: function(position, options){
|
||||||
|
var xy = {left: 'x', top: 'y'}, value;
|
||||||
|
['minimum', 'maximum'].each(function(minmax){
|
||||||
|
['left', 'top'].each(function(lr){
|
||||||
|
value = options[minmax] ? options[minmax][xy[lr]] : null;
|
||||||
|
if (value != null && ((minmax == 'minimum') ? position[lr] < value : position[lr] > value)) position[lr] = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toRelFixedPosition: function(relativeTo, position){
|
||||||
|
var winScroll = window.getScroll();
|
||||||
|
position.top += winScroll.y;
|
||||||
|
position.left += winScroll.x;
|
||||||
|
},
|
||||||
|
|
||||||
|
toIgnoreScroll: function(relativeTo, position){
|
||||||
|
var relScroll = relativeTo.getScroll();
|
||||||
|
position.top -= relScroll.y;
|
||||||
|
position.left -= relScroll.x;
|
||||||
|
},
|
||||||
|
|
||||||
|
toIgnoreMargins: function(position, options){
|
||||||
|
position.left += options.edge.x == 'right'
|
||||||
|
? options.dimensions['margin-right']
|
||||||
|
: (options.edge.x != 'center'
|
||||||
|
? -options.dimensions['margin-left']
|
||||||
|
: -options.dimensions['margin-left'] + ((options.dimensions['margin-right'] + options.dimensions['margin-left']) / 2));
|
||||||
|
|
||||||
|
position.top += options.edge.y == 'bottom'
|
||||||
|
? options.dimensions['margin-bottom']
|
||||||
|
: (options.edge.y != 'center'
|
||||||
|
? -options.dimensions['margin-top']
|
||||||
|
: -options.dimensions['margin-top'] + ((options.dimensions['margin-bottom'] + options.dimensions['margin-top']) / 2));
|
||||||
|
},
|
||||||
|
|
||||||
|
toEdge: function(position, options){
|
||||||
|
var edgeOffset = {},
|
||||||
|
dimensions = options.dimensions,
|
||||||
|
edge = options.edge;
|
||||||
|
|
||||||
|
switch(edge.x){
|
||||||
|
case 'left': edgeOffset.x = 0; break;
|
||||||
|
case 'right': edgeOffset.x = -dimensions.x - dimensions.computedRight - dimensions.computedLeft; break;
|
||||||
|
// center
|
||||||
|
default: edgeOffset.x = -(Math.round(dimensions.totalWidth / 2)); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(edge.y){
|
||||||
|
case 'top': edgeOffset.y = 0; break;
|
||||||
|
case 'bottom': edgeOffset.y = -dimensions.y - dimensions.computedTop - dimensions.computedBottom; break;
|
||||||
|
// center
|
||||||
|
default: edgeOffset.y = -(Math.round(dimensions.totalHeight / 2)); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
position.x += edgeOffset.x;
|
||||||
|
position.y += edgeOffset.y;
|
||||||
|
},
|
||||||
|
|
||||||
|
getCoordinateFromValue: function(option){
|
||||||
|
if (typeOf(option) != 'string') return option;
|
||||||
|
option = option.toLowerCase();
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: option.test('left') ? 'left'
|
||||||
|
: (option.test('right') ? 'right' : 'center'),
|
||||||
|
y: option.test(/upper|top/) ? 'top'
|
||||||
|
: (option.test('bottom') ? 'bottom' : 'center')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
Element.implement({
|
Element.implement({
|
||||||
|
|
||||||
position: function(options){
|
position: function(options){
|
||||||
//call original position if the options are x/y values
|
|
||||||
if (options && (options.x != null || options.y != null)){
|
if (options && (options.x != null || options.y != null)){
|
||||||
return original ? original.apply(this, arguments) : this;
|
return (original ? original.apply(this, arguments) : this);
|
||||||
}
|
}
|
||||||
|
var position = this.setStyle('position', 'absolute').calculatePosition(options);
|
||||||
|
return (options && options.returnPos) ? position : this.setStyles(position);
|
||||||
|
},
|
||||||
|
|
||||||
Object.each(options || {}, function(v, k){
|
calculatePosition: function(options){
|
||||||
if (v == null) delete options[k];
|
return local.getPosition(this, options);
|
||||||
});
|
|
||||||
|
|
||||||
options = Object.merge({
|
|
||||||
// minimum: { x: 0, y: 0 },
|
|
||||||
// maximum: { x: 0, y: 0},
|
|
||||||
relativeTo: document.body,
|
|
||||||
position: {
|
|
||||||
x: 'center', //left, center, right
|
|
||||||
y: 'center' //top, center, bottom
|
|
||||||
},
|
|
||||||
offset: {x: 0, y: 0}/*,
|
|
||||||
edge: false,
|
|
||||||
returnPos: false,
|
|
||||||
relFixedPosition: false,
|
|
||||||
ignoreMargins: false,
|
|
||||||
ignoreScroll: false,
|
|
||||||
allowNegative: false*/
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
//compute the offset of the parent positioned element if this element is in one
|
|
||||||
var parentOffset = {x: 0, y: 0},
|
|
||||||
parentPositioned = false;
|
|
||||||
|
|
||||||
/* dollar around getOffsetParent should not be necessary, but as it does not return
|
|
||||||
* a mootools extended element in IE, an error occurs on the call to expose. See:
|
|
||||||
* http://mootools.lighthouseapp.com/projects/2706/tickets/333-element-getoffsetparent-inconsistency-between-ie-and-other-browsers */
|
|
||||||
var offsetParent = this.measure(function(){
|
|
||||||
return document.id(this.getOffsetParent());
|
|
||||||
});
|
|
||||||
if (offsetParent && offsetParent != this.getDocument().body){
|
|
||||||
parentOffset = offsetParent.measure(function(){
|
|
||||||
return this.getPosition();
|
|
||||||
});
|
|
||||||
parentPositioned = offsetParent != document.id(options.relativeTo);
|
|
||||||
options.offset.x = options.offset.x - parentOffset.x;
|
|
||||||
options.offset.y = options.offset.y - parentOffset.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
//upperRight, bottomRight, centerRight, upperLeft, bottomLeft, centerLeft
|
|
||||||
//topRight, topLeft, centerTop, centerBottom, center
|
|
||||||
var fixValue = function(option){
|
|
||||||
if (typeOf(option) != 'string') return option;
|
|
||||||
option = option.toLowerCase();
|
|
||||||
var val = {};
|
|
||||||
|
|
||||||
if (option.test('left')){
|
|
||||||
val.x = 'left';
|
|
||||||
} else if (option.test('right')){
|
|
||||||
val.x = 'right';
|
|
||||||
} else {
|
|
||||||
val.x = 'center';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (option.test('upper') || option.test('top')){
|
|
||||||
val.y = 'top';
|
|
||||||
} else if (option.test('bottom')){
|
|
||||||
val.y = 'bottom';
|
|
||||||
} else {
|
|
||||||
val.y = 'center';
|
|
||||||
}
|
|
||||||
|
|
||||||
return val;
|
|
||||||
};
|
|
||||||
|
|
||||||
options.edge = fixValue(options.edge);
|
|
||||||
options.position = fixValue(options.position);
|
|
||||||
if (!options.edge){
|
|
||||||
if (options.position.x == 'center' && options.position.y == 'center') options.edge = {x:'center', y:'center'};
|
|
||||||
else options.edge = {x:'left', y:'top'};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setStyle('position', 'absolute');
|
|
||||||
var rel = document.id(options.relativeTo) || document.body,
|
|
||||||
calc = rel == document.body ? window.getScroll() : rel.getPosition(),
|
|
||||||
top = calc.y, left = calc.x;
|
|
||||||
|
|
||||||
var dim = this.getDimensions({
|
|
||||||
computeSize: true,
|
|
||||||
styles:['padding', 'border','margin']
|
|
||||||
});
|
|
||||||
|
|
||||||
var pos = {},
|
|
||||||
prefY = options.offset.y,
|
|
||||||
prefX = options.offset.x,
|
|
||||||
winSize = window.getSize();
|
|
||||||
|
|
||||||
switch (options.position.x){
|
|
||||||
case 'left':
|
|
||||||
pos.x = left + prefX;
|
|
||||||
break;
|
|
||||||
case 'right':
|
|
||||||
pos.x = left + prefX + rel.offsetWidth;
|
|
||||||
break;
|
|
||||||
default: //center
|
|
||||||
pos.x = left + ((rel == document.body ? winSize.x : rel.offsetWidth)/2) + prefX;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (options.position.y){
|
|
||||||
case 'top':
|
|
||||||
pos.y = top + prefY;
|
|
||||||
break;
|
|
||||||
case 'bottom':
|
|
||||||
pos.y = top + prefY + rel.offsetHeight;
|
|
||||||
break;
|
|
||||||
default: //center
|
|
||||||
pos.y = top + ((rel == document.body ? winSize.y : rel.offsetHeight)/2) + prefY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.edge){
|
|
||||||
var edgeOffset = {};
|
|
||||||
|
|
||||||
switch (options.edge.x){
|
|
||||||
case 'left':
|
|
||||||
edgeOffset.x = 0;
|
|
||||||
break;
|
|
||||||
case 'right':
|
|
||||||
edgeOffset.x = -dim.x-dim.computedRight-dim.computedLeft;
|
|
||||||
break;
|
|
||||||
default: //center
|
|
||||||
edgeOffset.x = -(dim.totalWidth/2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (options.edge.y){
|
|
||||||
case 'top':
|
|
||||||
edgeOffset.y = 0;
|
|
||||||
break;
|
|
||||||
case 'bottom':
|
|
||||||
edgeOffset.y = -dim.y-dim.computedTop-dim.computedBottom;
|
|
||||||
break;
|
|
||||||
default: //center
|
|
||||||
edgeOffset.y = -(dim.totalHeight/2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
pos.x += edgeOffset.x;
|
|
||||||
pos.y += edgeOffset.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
pos = {
|
|
||||||
left: ((pos.x >= 0 || parentPositioned || options.allowNegative) ? pos.x : 0).toInt(),
|
|
||||||
top: ((pos.y >= 0 || parentPositioned || options.allowNegative) ? pos.y : 0).toInt()
|
|
||||||
};
|
|
||||||
|
|
||||||
var xy = {left: 'x', top: 'y'};
|
|
||||||
|
|
||||||
['minimum', 'maximum'].each(function(minmax){
|
|
||||||
['left', 'top'].each(function(lr){
|
|
||||||
var val = options[minmax] ? options[minmax][xy[lr]] : null;
|
|
||||||
if (val != null && ((minmax == 'minimum') ? pos[lr] < val : pos[lr] > val)) pos[lr] = val;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (rel.getStyle('position') == 'fixed' || options.relFixedPosition){
|
|
||||||
var winScroll = window.getScroll();
|
|
||||||
pos.top+= winScroll.y;
|
|
||||||
pos.left+= winScroll.x;
|
|
||||||
}
|
|
||||||
if (options.ignoreScroll){
|
|
||||||
var relScroll = rel.getScroll();
|
|
||||||
pos.top -= relScroll.y;
|
|
||||||
pos.left -= relScroll.x;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.ignoreMargins){
|
|
||||||
pos.left += (
|
|
||||||
options.edge.x == 'right' ? dim['margin-right'] :
|
|
||||||
options.edge.x == 'center' ? -dim['margin-left'] + ((dim['margin-right'] + dim['margin-left'])/2) :
|
|
||||||
- dim['margin-left']
|
|
||||||
);
|
|
||||||
pos.top += (
|
|
||||||
options.edge.y == 'bottom' ? dim['margin-bottom'] :
|
|
||||||
options.edge.y == 'center' ? -dim['margin-top'] + ((dim['margin-bottom'] + dim['margin-top'])/2) :
|
|
||||||
- dim['margin-top']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pos.left = Math.ceil(pos.left);
|
|
||||||
pos.top = Math.ceil(pos.top);
|
|
||||||
if (options.returnPos) return pos;
|
|
||||||
else this.setStyles(pos);
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}).call(this);
|
})(Element.prototype.position);
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
var Question = new Class( {
|
||||||
|
|
||||||
|
initialize : function(question, hint, answers) {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
self.question = question
|
||||||
|
self.hint = hint
|
||||||
|
self.answers = answers
|
||||||
|
|
||||||
|
self.createQuestion()
|
||||||
|
self.answers.each(function(answer) {
|
||||||
|
self.createAnswer(answer)
|
||||||
|
})
|
||||||
|
self.createMask()
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
createMask : function() {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
$(document.body).mask( {
|
||||||
|
'hideOnClick' : true,
|
||||||
|
'destroyOnHide' : true,
|
||||||
|
'onHide' : function() {
|
||||||
|
self.container.destroy();
|
||||||
|
}
|
||||||
|
}).show();
|
||||||
|
},
|
||||||
|
|
||||||
|
createQuestion : function() {
|
||||||
|
|
||||||
|
this.container = new Element('div', {
|
||||||
|
'class' : 'question'
|
||||||
|
}).adopt(
|
||||||
|
new Element('h3', {
|
||||||
|
'html': this.question
|
||||||
|
}),
|
||||||
|
new Element('div.hint', {
|
||||||
|
'html': this.hint
|
||||||
|
})
|
||||||
|
).inject(document.body)
|
||||||
|
|
||||||
|
this.container.position( {
|
||||||
|
'position' : 'center'
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
createAnswer : function(options) {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
var answer = new Element('a', Object.merge(options, {
|
||||||
|
'class' : 'answer button '+(options['class'] || '')+(options['cancel'] ? ' cancel' : '')
|
||||||
|
})).inject(this.container)
|
||||||
|
|
||||||
|
if (options.cancel) {
|
||||||
|
answer.addEvent('click', self.close.bind(self))
|
||||||
|
}
|
||||||
|
else if (options.request) {
|
||||||
|
answer.addEvent('click', function(e){
|
||||||
|
e.stop();
|
||||||
|
new Request(Object.merge(options, {
|
||||||
|
'url': options.href,
|
||||||
|
'onComplete': function() {
|
||||||
|
(options.onComplete || function(){})()
|
||||||
|
self.close();
|
||||||
|
}
|
||||||
|
})).send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
close : function() {
|
||||||
|
$(document.body).get('mask').destroy();
|
||||||
|
},
|
||||||
|
|
||||||
|
toElement : function() {
|
||||||
|
return this.container
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
@@ -82,7 +82,9 @@ Page.Settings = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var c = self.advanced_toggle.checked ? 'addClass' : 'removeClass';
|
var c = self.advanced_toggle.checked ? 'addClass' : 'removeClass';
|
||||||
self.el[c]('show_advanced')
|
self.el[c]('show_advanced');
|
||||||
|
|
||||||
|
Cookie.write('advanced_toggle_checked', +self.advanced_toggle.checked, {'duration': 365});
|
||||||
},
|
},
|
||||||
|
|
||||||
create: function(json){
|
create: function(json){
|
||||||
@@ -96,6 +98,7 @@ Page.Settings = new Class({
|
|||||||
'text': 'Show advanced settings'
|
'text': 'Show advanced settings'
|
||||||
}),
|
}),
|
||||||
self.advanced_toggle = new Element('input[type=checkbox].inlay', {
|
self.advanced_toggle = new Element('input[type=checkbox].inlay', {
|
||||||
|
'checked': +Cookie.read('advanced_toggle_checked'),
|
||||||
'events': {
|
'events': {
|
||||||
'change': self.showAdvanced.bind(self)
|
'change': self.showAdvanced.bind(self)
|
||||||
}
|
}
|
||||||
@@ -103,10 +106,9 @@ Page.Settings = new Class({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
self.showAdvanced();
|
||||||
new Form.Check(self.advanced_toggle, {
|
|
||||||
'onChange': self.showAdvanced.bind(self)
|
new Form.Check(self.advanced_toggle)
|
||||||
})
|
|
||||||
|
|
||||||
// Create tabs
|
// Create tabs
|
||||||
Object.each(self.tabs, function(tab, tab_name){
|
Object.each(self.tabs, function(tab, tab_name){
|
||||||
@@ -132,7 +134,7 @@ Page.Settings = new Class({
|
|||||||
var class_name = (option.type || 'string').capitalize();
|
var class_name = (option.type || 'string').capitalize();
|
||||||
var input = new Option[class_name](self, section_name, option.name, option);
|
var input = new Option[class_name](self, section_name, option.name, option);
|
||||||
input.inject(self.tabs[group.tab].groups[group.name]);
|
input.inject(self.tabs[group.tab].groups[group.name]);
|
||||||
input.fireEvent('injected')
|
input.fireEvent('injected');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -182,7 +184,7 @@ Page.Settings = new Class({
|
|||||||
'text': (group.label || group.name).capitalize()
|
'text': (group.label || group.name).capitalize()
|
||||||
}).adopt(
|
}).adopt(
|
||||||
new Element('span.hint', {
|
new Element('span.hint', {
|
||||||
'html': group.description
|
'html': group.description || ''
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -397,9 +399,7 @@ Option.Checkbox = new Class({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Form.Check(self.input, {
|
new Form.Check(self.input);
|
||||||
'onChange': self.changed.bind(self)
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -435,16 +435,11 @@ Option.Enabler = new Class({
|
|||||||
self.input = new Element('input.inlay', {
|
self.input = new Element('input.inlay', {
|
||||||
'type': 'checkbox',
|
'type': 'checkbox',
|
||||||
'checked': self.getSettingValue(),
|
'checked': self.getSettingValue(),
|
||||||
'id': 'r-'+randomString(),
|
'id': 'r-'+randomString()
|
||||||
'events': {
|
|
||||||
'change': self.checkState.bind(self)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Form.Check(self.input, {
|
new Form.Check(self.input);
|
||||||
'onChange': self.changed.bind(self)
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
changed: function(){
|
changed: function(){
|
||||||
@@ -479,63 +474,86 @@ Option.Directory = new Class({
|
|||||||
type: 'span',
|
type: 'span',
|
||||||
browser: '',
|
browser: '',
|
||||||
save_on_change: false,
|
save_on_change: false,
|
||||||
|
use_cache: false,
|
||||||
|
|
||||||
create: function(){
|
create: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.el.adopt(
|
self.el.adopt(
|
||||||
self.createLabel(),
|
self.createLabel(),
|
||||||
self.input = new Element('span.directory', {
|
new Element('span.directory.inlay', {
|
||||||
'text': self.getSettingValue(),
|
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.showBrowser.bind(self)
|
'click': self.showBrowser.bind(self)
|
||||||
}
|
}
|
||||||
})
|
}).adopt(
|
||||||
|
self.input = new Element('span', {
|
||||||
|
'text': self.getSettingValue()
|
||||||
|
})
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
self.cached = {};
|
self.cached = {};
|
||||||
},
|
},
|
||||||
|
|
||||||
selectDirectory: function(e, el){
|
selectDirectory: function(dir){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.input.set('text', el.get('data-value'));
|
self.input.set('text', dir);
|
||||||
|
|
||||||
self.getDirs()
|
self.getDirs()
|
||||||
self.fireEvent('change')
|
|
||||||
},
|
},
|
||||||
|
|
||||||
previousDirectory: function(e){
|
previousDirectory: function(e){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.selectDirectory(null, self.back_button)
|
self.selectDirectory(self.getParentDir())
|
||||||
},
|
},
|
||||||
|
|
||||||
showBrowser: function(){
|
showBrowser: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if(!self.browser)
|
if(!self.browser){
|
||||||
self.browser = new Element('div.directory_list').adopt(
|
self.browser = new Element('div.directory_list').adopt(
|
||||||
|
new Element('div.pointer'),
|
||||||
new Element('div.actions').adopt(
|
new Element('div.actions').adopt(
|
||||||
self.back_button = new Element('a.button.back', {
|
self.back_button = new Element('a.back', {
|
||||||
'text': '',
|
'html': '',
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.previousDirectory.bind(self)
|
'click': self.previousDirectory.bind(self)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new Element('label', {
|
new Element('label', {
|
||||||
'text': 'Show hidden files'
|
'text': 'Hidden folders'
|
||||||
}).adopt(
|
}).adopt(
|
||||||
self.show_hidden = new Element('input[type=checkbox].inlay')
|
self.show_hidden = new Element('input[type=checkbox].inlay', {
|
||||||
|
'events': {
|
||||||
|
'change': self.getDirs.bind(self)
|
||||||
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
self.dir_list = new Element('ul', {
|
self.dir_list = new Element('ul', {
|
||||||
'events': {
|
'events': {
|
||||||
'click:relay(li)': self.selectDirectory.bind(self)
|
'click:relay(li)': function(e, el){
|
||||||
|
(e).stop();
|
||||||
|
self.selectDirectory(el.get('data-value'))
|
||||||
|
},
|
||||||
|
'mousewheel': function(e){
|
||||||
|
(e).stopPropagation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new Element('div.actions').adopt(
|
new Element('div.actions').adopt(
|
||||||
new Element('a.button.cancel', {
|
new Element('a.clear.button', {
|
||||||
|
'text': 'Clear',
|
||||||
|
'events': {
|
||||||
|
'click': function(e){
|
||||||
|
self.input.set('text', '');
|
||||||
|
self.hideBrowser(e, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Element('a.cancel', {
|
||||||
'text': 'Cancel',
|
'text': 'Cancel',
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.hideBrowser.bind(self)
|
'click': self.hideBrowser.bind(self)
|
||||||
@@ -547,21 +565,32 @@ Option.Directory = new Class({
|
|||||||
self.save_button = new Element('a.button.save', {
|
self.save_button = new Element('a.button.save', {
|
||||||
'text': 'Save',
|
'text': 'Save',
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.hideBrowser.bind(self, true)
|
'click': function(e){
|
||||||
|
self.hideBrowser(e, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
).inject(self.input, 'after')
|
).inject(self.el)
|
||||||
|
|
||||||
|
new Form.Check(self.show_hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.initial_directory = self.input.get('text');
|
||||||
|
|
||||||
self.getDirs()
|
self.getDirs()
|
||||||
self.browser.show()
|
self.browser.show()
|
||||||
self.el.addEvent('outerClick', self.hideBrowser.bind(self))
|
self.el.addEvent('outerClick', self.hideBrowser.bind(self))
|
||||||
},
|
},
|
||||||
|
|
||||||
hideBrowser: function(save){
|
hideBrowser: function(e, save){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
(e).stop();
|
||||||
|
|
||||||
if(save) self.save()
|
if(save)
|
||||||
|
self.save()
|
||||||
|
else
|
||||||
|
self.input.set('text', self.initial_directory);
|
||||||
|
|
||||||
self.browser.hide()
|
self.browser.hide()
|
||||||
self.el.removeEvent('outerClick', self.hideBrowser.bind(self))
|
self.el.removeEvent('outerClick', self.hideBrowser.bind(self))
|
||||||
@@ -571,41 +600,43 @@ Option.Directory = new Class({
|
|||||||
fillBrowser: function(json){
|
fillBrowser: function(json){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var c = self.getParentDir();
|
|
||||||
var v = self.input.get('text');
|
var v = self.input.get('text');
|
||||||
var previous_dir = self.getParentDir(c.substring(0, c.length-1));
|
var previous_dir = self.getParentDir();
|
||||||
|
|
||||||
if(previous_dir){
|
if(previous_dir != v){
|
||||||
self.back_button.set('data-value', previous_dir)
|
self.back_button.set('data-value', previous_dir)
|
||||||
self.back_button.set('text', self.getCurrentDirname(previous_dir))
|
self.back_button.set('html', '« '+self.getCurrentDirname(previous_dir))
|
||||||
self.back_button.show()
|
self.back_button.show()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
self.back_button.hide()
|
self.back_button.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!json)
|
if(self.use_cache)
|
||||||
json = self.cached[c];
|
if(!json)
|
||||||
else
|
json = self.cached[v];
|
||||||
self.cached[c] = json;
|
else
|
||||||
|
self.cached[v] = json;
|
||||||
|
|
||||||
self.dir_list.empty();
|
setTimeout(function(){
|
||||||
json.dirs.each(function(dir){
|
self.dir_list.empty();
|
||||||
if(dir.indexOf(v) != -1){
|
json.dirs.each(function(dir){
|
||||||
new Element('li', {
|
if(dir.indexOf(v) != -1){
|
||||||
'data-value': dir,
|
new Element('li', {
|
||||||
'text': self.getCurrentDirname(dir)
|
'data-value': dir,
|
||||||
}).inject(self.dir_list)
|
'text': self.getCurrentDirname(dir)
|
||||||
}
|
}).inject(self.dir_list)
|
||||||
})
|
}
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
},
|
},
|
||||||
|
|
||||||
getDirs: function(){
|
getDirs: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var c = self.getParentDir();
|
var c = self.input.get('text');
|
||||||
|
|
||||||
if(self.cached[c]){
|
if(self.cached[c] && self.use_cache){
|
||||||
self.fillBrowser()
|
self.fillBrowser()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -625,7 +656,8 @@ Option.Directory = new Class({
|
|||||||
var v = dir || self.input.get('text');
|
var v = dir || self.input.get('text');
|
||||||
var sep = Api.getOption('path_sep');
|
var sep = Api.getOption('path_sep');
|
||||||
var dirs = v.split(sep);
|
var dirs = v.split(sep);
|
||||||
dirs.pop();
|
if(dirs.pop() == '')
|
||||||
|
dirs.pop();
|
||||||
|
|
||||||
return dirs.join(sep) + sep
|
return dirs.join(sep) + sep
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ Page.Wanted = new Class({
|
|||||||
indexAction: function(param){
|
indexAction: function(param){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if(!self.list){
|
if(!self.wanted){
|
||||||
|
|
||||||
// Wanted movies
|
// Wanted movies
|
||||||
self.wanted = new MovieList({
|
self.wanted = new MovieList({
|
||||||
'status': 'active',
|
'status': 'active',
|
||||||
'actions': WantedActions
|
'actions': MovieActions
|
||||||
});
|
});
|
||||||
$(self.wanted).inject(self.el);
|
$(self.wanted).inject(self.el);
|
||||||
App.addEvent('library.update', self.wanted.update.bind(self.wanted));
|
App.addEvent('library.update', self.wanted.update.bind(self.wanted));
|
||||||
@@ -23,30 +23,32 @@ Page.Wanted = new Class({
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var WantedActions = {
|
var MovieActions = {};
|
||||||
|
|
||||||
|
MovieActions.Wanted = {
|
||||||
'IMBD': IMDBAction
|
'IMBD': IMDBAction
|
||||||
//,'releases': ReleaseAction
|
,'releases': ReleaseAction
|
||||||
|
|
||||||
,'Edit': new Class({
|
,'Edit': new Class({
|
||||||
|
|
||||||
Extends: MovieAction,
|
Extends: MovieAction,
|
||||||
|
|
||||||
create: function(){
|
create: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.el = new Element('a.edit', {
|
self.el = new Element('a.edit', {
|
||||||
'title': 'Refresh the movie info and do a forced search',
|
'title': 'Refresh the movie info and do a forced search',
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.editMovie.bind(self)
|
'click': self.editMovie.bind(self)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
editMovie: function(e){
|
editMovie: function(e){
|
||||||
var self = this;
|
var self = this;
|
||||||
(e).stop();
|
(e).stop();
|
||||||
|
|
||||||
if(!self.options_container){
|
if(!self.options_container){
|
||||||
self.options_container = new Element('div.options').adopt(
|
self.options_container = new Element('div.options').adopt(
|
||||||
$(self.movie.thumbnail).clone(),
|
$(self.movie.thumbnail).clone(),
|
||||||
@@ -69,29 +71,29 @@ var WantedActions = {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
).inject(self.movie, 'top');
|
).inject(self.movie, 'top');
|
||||||
|
|
||||||
Array.each(self.movie.data.library.titles, function(alt){
|
Array.each(self.movie.data.library.titles, function(alt){
|
||||||
new Element('option', {
|
new Element('option', {
|
||||||
'text': alt.title
|
'text': alt.title
|
||||||
}).inject(self.title_select);
|
}).inject(self.title_select);
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.each(Quality.profiles, function(profile){
|
Object.each(Quality.getActiveProfiles(), function(profile){
|
||||||
new Element('option', {
|
new Element('option', {
|
||||||
'value': profile.id ? profile.id : profile.data.id,
|
'value': profile.id ? profile.id : profile.data.id,
|
||||||
'text': profile.label ? profile.label : profile.data.label
|
'text': profile.label ? profile.label : profile.data.label
|
||||||
}).inject(self.profile_select);
|
}).inject(self.profile_select);
|
||||||
self.profile_select.set('value', self.movie.profile.get('id'));
|
self.profile_select.set('value', self.movie.profile.get('id'));
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
self.movie.slide('in');
|
self.movie.slide('in');
|
||||||
},
|
},
|
||||||
|
|
||||||
save: function(e){
|
save: function(e){
|
||||||
(e).stop();
|
(e).stop();
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
Api.request('movie.edit', {
|
Api.request('movie.edit', {
|
||||||
'data': {
|
'data': {
|
||||||
'id': self.movie.get('id'),
|
'id': self.movie.get('id'),
|
||||||
@@ -105,63 +107,63 @@ var WantedActions = {
|
|||||||
self.movie.title.set('text', self.title_select.getSelected()[0].get('text'));
|
self.movie.title.set('text', self.title_select.getSelected()[0].get('text'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.movie.slide('out');
|
self.movie.slide('out');
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
,'Refresh': new Class({
|
,'Refresh': new Class({
|
||||||
|
|
||||||
Extends: MovieAction,
|
Extends: MovieAction,
|
||||||
|
|
||||||
create: function(){
|
create: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.el = new Element('a.refresh', {
|
self.el = new Element('a.refresh', {
|
||||||
'title': 'Refresh the movie info and do a forced search',
|
'title': 'Refresh the movie info and do a forced search',
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.doSearch.bind(self)
|
'click': self.doSearch.bind(self)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
doSearch: function(e){
|
doSearch: function(e){
|
||||||
var self = this;
|
var self = this;
|
||||||
(e).stop();
|
(e).stop();
|
||||||
|
|
||||||
Api.request('movie.refresh', {
|
Api.request('movie.refresh', {
|
||||||
'data': {
|
'data': {
|
||||||
'id': self.movie.get('id')
|
'id': self.movie.get('id')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
,'Delete': new Class({
|
,'Delete': new Class({
|
||||||
|
|
||||||
Extends: MovieAction,
|
Extends: MovieAction,
|
||||||
|
|
||||||
Implements: [Chain],
|
Implements: [Chain],
|
||||||
|
|
||||||
create: function(){
|
create: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.el = new Element('a.delete', {
|
self.el = new Element('a.delete', {
|
||||||
'title': 'Remove the movie from your wanted list',
|
'title': 'Remove the movie from your wanted list',
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.showConfirm.bind(self)
|
'click': self.showConfirm.bind(self)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showConfirm: function(e){
|
showConfirm: function(e){
|
||||||
var self = this;
|
var self = this;
|
||||||
(e).stop();
|
(e).stop();
|
||||||
|
|
||||||
if(!self.delete_container){
|
if(!self.delete_container){
|
||||||
self.delete_container = new Element('div.delete_container', {
|
self.delete_container = new Element('div.delete_container', {
|
||||||
'styles': {
|
'styles': {
|
||||||
@@ -185,27 +187,26 @@ var WantedActions = {
|
|||||||
})
|
})
|
||||||
).inject(self.movie, 'top');
|
).inject(self.movie, 'top');
|
||||||
}
|
}
|
||||||
|
|
||||||
self.movie.slide('in');
|
self.movie.slide('in');
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
hideConfirm: function(e){
|
hideConfirm: function(e){
|
||||||
var self = this;
|
var self = this;
|
||||||
(e).stop();
|
(e).stop();
|
||||||
|
|
||||||
self.movie.slide('out');
|
self.movie.slide('out');
|
||||||
},
|
},
|
||||||
|
|
||||||
del: function(e){
|
del: function(e){
|
||||||
(e).stop();
|
(e).stop();
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var movie = $(self.movie);
|
var movie = $(self.movie);
|
||||||
|
|
||||||
self.chain(
|
self.chain(
|
||||||
function(){
|
function(){
|
||||||
$(movie).mask().addClass('loading');
|
|
||||||
self.callChain();
|
self.callChain();
|
||||||
},
|
},
|
||||||
function(){
|
function(){
|
||||||
@@ -224,16 +225,15 @@ var WantedActions = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
self.callChain();
|
self.callChain();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
var SnatchedActions = {
|
MovieActions.Snatched = {
|
||||||
'IMBD': IMDBAction
|
'IMBD': IMDBAction
|
||||||
,'Releases': ReleaseAction
|
,'Delete': MovieActions.Wanted.Delete
|
||||||
,'Delete': WantedActions.Delete
|
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
/* @override http://localhost:5000/static/style/main.css */
|
/* @override
|
||||||
|
http://localhost:5000/static/style/main.css
|
||||||
|
http://192.168.1.20:5000/static/style/main.css
|
||||||
|
http://127.0.0.1:5000/static/style/main.css
|
||||||
|
*/
|
||||||
|
|
||||||
html {
|
html {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -35,15 +39,24 @@ input, textarea {
|
|||||||
font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
|
font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:-moz-placeholder, textarea:-moz-placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-input-placeholder, ::-webkit-textarea-placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
a img {
|
a img {
|
||||||
border:none;
|
border:none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration:none;
|
text-decoration:none;
|
||||||
color: #fff;
|
color: #ebfcbc;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
a:hover { color: #f3f3f3; }
|
a:hover { color: #f3f3f3; }
|
||||||
|
|
||||||
@@ -125,10 +138,18 @@ form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*** Icons ***/
|
/*** Icons ***/
|
||||||
.icon.delete {
|
.icon {
|
||||||
background: url('../images/delete.png') no-repeat;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
background: center no-repeat;
|
||||||
}
|
}
|
||||||
|
.icon.delete { background-image: url('../images/icon.delete.png'); }
|
||||||
|
.icon.download { background-image: url('../images/icon.download.png'); }
|
||||||
|
.icon.edit { background-image: url('../images/icon.edit.png'); }
|
||||||
|
.icon.check { background-image: url('../images/icon.check.png'); }
|
||||||
|
.icon.folder { background-image: url('../images/icon.folder.png'); }
|
||||||
|
.icon.imdb { background-image: url('../images/icon.imdb.png'); }
|
||||||
|
.icon.refresh { background-image: url('../images/icon.refresh.png'); }
|
||||||
|
.icon.rating { background-image: url('../images/icon.rating.png'); }
|
||||||
|
|
||||||
/*** Navigation ***/
|
/*** Navigation ***/
|
||||||
.header {
|
.header {
|
||||||
@@ -191,6 +212,27 @@ form {
|
|||||||
.header .navigation li a:hover, .header .navigation li a:active {
|
.header .navigation li a:hover, .header .navigation li a:active {
|
||||||
color: #b1d8dc;
|
color: #b1d8dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header .message.update {
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
top: -70px;
|
||||||
|
padding: 15px 0 20px;
|
||||||
|
background: #ff6134;
|
||||||
|
font-size: 26px;
|
||||||
|
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
-moz-border-radius: 0 0 5px 5px;
|
||||||
|
-webkit-border-radius: 0 0 5px 5px;
|
||||||
|
|
||||||
|
box-shadow: 0 2px 1px rgba(0,0,0, 0.3);
|
||||||
|
-moz-box-shadow: 0 2px 1px rgba(0,0,0, 0.3);
|
||||||
|
-webkit-box-shadow: 0 2px 1px rgba(0,0,0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .message a {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/*** Global Styles ***/
|
/*** Global Styles ***/
|
||||||
.check {
|
.check {
|
||||||
@@ -333,3 +375,59 @@ form {
|
|||||||
rgb(73,83,98) 100%
|
rgb(73,83,98) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
background: rgba(0,0,0, 0.7);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question {
|
||||||
|
display: block;
|
||||||
|
width: 600px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
position:fixed;
|
||||||
|
z-index:101;
|
||||||
|
text-align: center;
|
||||||
|
background: #5c697b;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
|
||||||
|
box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
||||||
|
-moz-box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
||||||
|
-webkit-box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.question h3 {
|
||||||
|
font-size: 25px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question .hint {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #ccc;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question .answer {
|
||||||
|
font-size: 17px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 1%;
|
||||||
|
cursor: pointer;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.question .answer:hover {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question .answer.delete {
|
||||||
|
background-color: #a82f12;
|
||||||
|
}
|
||||||
|
.question .answer.cancel {
|
||||||
|
margin-top: 20px;
|
||||||
|
background-color: #4c5766;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
/* @override http://localhost:5000/static/style/page/settings.css */
|
/* @override
|
||||||
|
http://localhost:5000/static/style/page/settings.css
|
||||||
|
http://192.168.1.20:5000/static/style/page/settings.css
|
||||||
|
*/
|
||||||
|
|
||||||
.page.settings {
|
.page.settings:after {
|
||||||
overflow: hidden;
|
content: ".";
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
visibility: hidden;
|
||||||
|
line-height: 0;
|
||||||
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.settings .tabs {
|
.page.settings .tabs {
|
||||||
@@ -30,6 +38,7 @@
|
|||||||
.page.settings .tabs a {
|
.page.settings .tabs a {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 11px 15px;
|
padding: 11px 15px;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
.page.settings .tabs .active a {
|
.page.settings .tabs .active a {
|
||||||
background: #4e5969;
|
background: #4e5969;
|
||||||
@@ -84,6 +93,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: -25px;
|
margin-bottom: -25px;
|
||||||
border: none;
|
border: none;
|
||||||
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.settings .ctrlHolder {
|
.page.settings .ctrlHolder {
|
||||||
@@ -92,9 +102,13 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
.page.settings .ctrlHolder.save_success:not(:first-child) {
|
||||||
|
background: url('../../images/icon.check.png') no-repeat 7px center;
|
||||||
|
}
|
||||||
.page.settings .ctrlHolder:last-child { border: none; }
|
.page.settings .ctrlHolder:last-child { border: none; }
|
||||||
.page.settings .ctrlHolder:hover { background: rgba(255,255,255,0.05); }
|
.page.settings .ctrlHolder:hover { background-color: rgba(255,255,255,0.05); }
|
||||||
.page.settings .ctrlHolder.focused { background: rgba(255,255,255,0.2); }
|
.page.settings .ctrlHolder.focused { background-color: rgba(255,255,255,0.2); }
|
||||||
|
.page.settings .ctrlHolder.focused:first-child, .page.settings .ctrlHolder:first-child{ background-color: transparent; }
|
||||||
|
|
||||||
.page.settings .ctrlHolder .formHint {
|
.page.settings .ctrlHolder .formHint {
|
||||||
float: right;
|
float: right;
|
||||||
@@ -135,6 +149,8 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 6px 0 0;
|
padding: 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page.settings .xsmall { width: 20px !important; text-align: center; }
|
||||||
|
|
||||||
.page.settings input[type=text], .page.settings input[type=password] {
|
.page.settings input[type=text], .page.settings input[type=password] {
|
||||||
padding: 5px 3px;
|
padding: 5px 3px;
|
||||||
@@ -165,39 +181,154 @@
|
|||||||
|
|
||||||
.page.settings .directory {
|
.page.settings .directory {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0 4px;
|
padding: 0 4% 0 4px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
width: 29.7%;
|
width: 26.3%;
|
||||||
}
|
background-image: url('../../images/icon.folder.gif');
|
||||||
.page.settings .directory_list {
|
background-repeat: no-repeat;
|
||||||
position: absolute;
|
background-position: 97% center;
|
||||||
width: 300px;
|
overflow: hidden;
|
||||||
margin: 0 0 0 16.5%;
|
vertical-align: top;
|
||||||
background: #282d34;
|
|
||||||
border: 1px solid #1f242b;
|
|
||||||
position: absolute;
|
|
||||||
box-shadow: 0 1px 2px rgba(0,0,0,0.4);
|
|
||||||
-moz-box-shadow: 0 1px 2px rgba(0,0,0,0.4);
|
|
||||||
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.4);
|
|
||||||
border-radius:3px;
|
|
||||||
-moz-border-radius: 3px;
|
|
||||||
-webkit-border-radius: 3px;
|
|
||||||
}
|
}
|
||||||
|
.page.settings .directory > span {
|
||||||
|
height: 25px;
|
||||||
|
display: inline-block;
|
||||||
|
float: right;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .directory_list {
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
width: 360px;
|
||||||
|
margin: -2px 0 20px 60px;
|
||||||
|
background: #5c697b;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
||||||
|
-moz-box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
||||||
|
-webkit-box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .directory_list .pointer {
|
||||||
|
border-right: 10px solid transparent;
|
||||||
|
border-left: 10px solid transparent;
|
||||||
|
border-bottom: 10px solid #5c697b;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 0px;
|
||||||
|
margin: -9px 0 0 48.5%;
|
||||||
|
}
|
||||||
|
|
||||||
.page.settings .directory_list ul {
|
.page.settings .directory_list ul {
|
||||||
width: 100%;
|
width: 92%;
|
||||||
max-height: 200px;
|
height: 300px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
margin: 0 4%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.settings .directory_list li {
|
.page.settings .directory_list li {
|
||||||
padding: 2px 10px;
|
padding: 0 10px;
|
||||||
}
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.1);
|
||||||
|
background: url('../../images/right.arrow.png') no-repeat 98% center;
|
||||||
|
}
|
||||||
|
.page.settings .directory_list li:last-child {
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .directory_list li:hover {
|
||||||
|
background-color: #515c68;
|
||||||
|
}
|
||||||
|
|
||||||
.page.settings .directory_list .actions {
|
.page.settings .directory_list .actions {
|
||||||
clear: both;
|
clear: both;
|
||||||
padding: 10px;
|
padding: 4% 4% 2%;
|
||||||
background: #414953;
|
min-height: 25px;
|
||||||
}
|
}
|
||||||
.page.settings .directory_list .actions:first-child { border-bottom: 1px solid #1f242b; }
|
|
||||||
.page.settings .directory_list .actions:last-child { border-top: 1px solid #1f242b; }
|
.page.settings .directory_list .actions label {
|
||||||
|
float: right;
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.page.settings .directory_list .actions .inlay {
|
||||||
|
margin: -2px 0 0 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .directory_list .actions .back {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 160px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 120%;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .directory_list .actions:last-child {
|
||||||
|
float: right;
|
||||||
|
padding: 4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .directory_list .actions:last-child > span {
|
||||||
|
padding: 0 5px;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .directory_list .actions:last-child > .clear {
|
||||||
|
left: -90%;
|
||||||
|
position: relative;
|
||||||
|
background-color: #af3128;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .directory_list .actions:last-child > .cancel {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .directory_list .actions:last-child > .save {
|
||||||
|
background: #9dc156;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .section_newznab {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .section_newznab .head {
|
||||||
|
margin: 0 0 0 60px;
|
||||||
|
}
|
||||||
|
.page.settings .section_newznab .head abbr {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 1px dotted #fff;
|
||||||
|
line-height: 140%;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
.page.settings .section_newznab .head abbr.host {
|
||||||
|
margin-right: 197px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .section_newznab .ctrlHolder {
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .section_newznab .ctrlHolder > * {
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .section_newznab .ctrlHolder .delete {
|
||||||
|
display: inline-block;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-position: left center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.settings .section_newznab .ctrlHolder.is_empty .delete, .page.settings .section_newznab .ctrlHolder.is_empty .use {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_radio.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_radio.js') }}"></script>
|
||||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_dropdown.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_dropdown.js') }}"></script>
|
||||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_selectoption.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_selectoption.js') }}"></script>
|
||||||
|
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/question.js') }}"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/couchpotato.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/couchpotato.js') }}"></script>
|
||||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/history.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/history.js') }}"></script>
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
Dependencies
|
|
||||||
===========
|
|
||||||
|
|
||||||
Holds all dependencies that are required by CouchPotato.
|
|
||||||
@@ -11,7 +11,10 @@
|
|||||||
# Source: http://pypi.python.org/pypi/axel
|
# Source: http://pypi.python.org/pypi/axel
|
||||||
# Docs: http://packages.python.org/axel
|
# Docs: http://packages.python.org/axel
|
||||||
|
|
||||||
import sys, threading, Queue
|
from couchpotato.core.helpers.variable import natcmp
|
||||||
|
import Queue
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
class Event(object):
|
class Event(object):
|
||||||
"""
|
"""
|
||||||
@@ -100,7 +103,7 @@ class Event(object):
|
|||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
self.memoize = {}
|
self.memoize = {}
|
||||||
|
|
||||||
def handle(self, handler):
|
def handle(self, handler, priority = 0):
|
||||||
""" Registers a handler. The handler can be transmitted together
|
""" Registers a handler. The handler can be transmitted together
|
||||||
with two arguments as a list or dictionary. The arguments are:
|
with two arguments as a list or dictionary. The arguments are:
|
||||||
|
|
||||||
@@ -118,7 +121,7 @@ class Event(object):
|
|||||||
event += {'handler':handler, 'memoize':True, 'timeout':1.5}
|
event += {'handler':handler, 'memoize':True, 'timeout':1.5}
|
||||||
"""
|
"""
|
||||||
handler_, memoize, timeout = self._extract(handler)
|
handler_, memoize, timeout = self._extract(handler)
|
||||||
self.handlers[hash(handler_)] = (handler_, memoize, timeout)
|
self.handlers['%s.%s' % (priority, hash(handler_))] = (handler_, memoize, timeout)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def unhandle(self, handler):
|
def unhandle(self, handler):
|
||||||
@@ -144,7 +147,7 @@ class Event(object):
|
|||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
for handler in self.handlers:
|
for handler in sorted(self.handlers.iterkeys(), cmp = natcmp):
|
||||||
self.queue.put(handler)
|
self.queue.put(handler)
|
||||||
|
|
||||||
if self.asynchronous:
|
if self.asynchronous:
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
####
|
||||||
|
# 06/2010 Nic Wolfe <nic@wolfeden.ca>
|
||||||
|
# 02/2006 Will Holcomb <wholcomb@gmail.com>
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
import mimetools, mimetypes
|
||||||
|
import os, sys
|
||||||
|
|
||||||
|
# Controls how sequences are uncoded. If true, elements may be given multiple values by
|
||||||
|
# assigning a sequence.
|
||||||
|
doseq = 1
|
||||||
|
|
||||||
|
class MultipartPostHandler(urllib2.BaseHandler):
|
||||||
|
handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first
|
||||||
|
|
||||||
|
def http_request(self, request):
|
||||||
|
data = request.get_data()
|
||||||
|
if data is not None and type(data) != str:
|
||||||
|
v_files = []
|
||||||
|
v_vars = []
|
||||||
|
try:
|
||||||
|
for(key, value) in data.items():
|
||||||
|
if type(value) in (file, list, tuple):
|
||||||
|
v_files.append((key, value))
|
||||||
|
else:
|
||||||
|
v_vars.append((key, value))
|
||||||
|
except TypeError:
|
||||||
|
systype, value, traceback = sys.exc_info()
|
||||||
|
raise TypeError, "not a valid non-string sequence or mapping object", traceback
|
||||||
|
|
||||||
|
if len(v_files) == 0:
|
||||||
|
data = urllib.urlencode(v_vars, doseq)
|
||||||
|
else:
|
||||||
|
boundary, data = MultipartPostHandler.multipart_encode(v_vars, v_files)
|
||||||
|
contenttype = 'multipart/form-data; boundary=%s' % boundary
|
||||||
|
if(request.has_header('Content-Type')
|
||||||
|
and request.get_header('Content-Type').find('multipart/form-data') != 0):
|
||||||
|
print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data')
|
||||||
|
request.add_unredirected_header('Content-Type', contenttype)
|
||||||
|
|
||||||
|
request.add_data(data)
|
||||||
|
return request
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def multipart_encode(vars, files, boundary = None, buffer = None):
|
||||||
|
if boundary is None:
|
||||||
|
boundary = mimetools.choose_boundary()
|
||||||
|
if buffer is None:
|
||||||
|
buffer = ''
|
||||||
|
for(key, value) in vars:
|
||||||
|
buffer += '--%s\r\n' % boundary
|
||||||
|
buffer += 'Content-Disposition: form-data; name="%s"' % key
|
||||||
|
buffer += '\r\n\r\n' + value + '\r\n'
|
||||||
|
for(key, fd) in files:
|
||||||
|
|
||||||
|
# allow them to pass in a file or a tuple with name & data
|
||||||
|
if type(fd) == file:
|
||||||
|
name_in = fd.name
|
||||||
|
fd.seek(0)
|
||||||
|
data_in = fd.read()
|
||||||
|
elif type(fd) in (tuple, list):
|
||||||
|
name_in, data_in = fd
|
||||||
|
|
||||||
|
filename = os.path.basename(name_in)
|
||||||
|
contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||||
|
buffer += '--%s\r\n' % boundary
|
||||||
|
buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename)
|
||||||
|
buffer += 'Content-Type: %s\r\n' % contenttype
|
||||||
|
# buffer += 'Content-Length: %s\r\n' % file_size
|
||||||
|
buffer += '\r\n' + data_in + '\r\n'
|
||||||
|
buffer += '--%s--\r\n\r\n' % boundary
|
||||||
|
return boundary, buffer
|
||||||
|
|
||||||
|
https_request = http_request
|
||||||
@@ -350,7 +350,7 @@ class MovieDb:
|
|||||||
etree = XmlHandler(url).getEt()
|
etree = XmlHandler(url).getEt()
|
||||||
lookup_results = SearchResults()
|
lookup_results = SearchResults()
|
||||||
for cur_lookup in etree.find("movies").findall("movie"):
|
for cur_lookup in etree.find("movies").findall("movie"):
|
||||||
cur_movie = self._parseSearchResults(cur_lookup)
|
cur_movie = self._parseMovie(cur_lookup)
|
||||||
lookup_results.append(cur_movie)
|
lookup_results.append(cur_movie)
|
||||||
return lookup_results
|
return lookup_results
|
||||||
|
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import urllib2
|
|
||||||
|
|
||||||
__author__ = 'Therms'
|
|
||||||
__tmdb_apikey__ = '6d96a9efb4752ed0d126d94e12e52036'
|
|
||||||
|
|
||||||
class XmgException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ApiError(XmgException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class IdError(XmgException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class NfoError(XmgException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class MetaGen():
|
|
||||||
def __init__(self, imdbid, imdbpy = None):
|
|
||||||
''' metagen is used to download metadata for a movie or tv show and then create
|
|
||||||
the necessary files for the media to be imported into XBMC.
|
|
||||||
|
|
||||||
Arguments
|
|
||||||
===========
|
|
||||||
fanart/poster_height/width_min: Sets lowest acceptable image resolution. 0 means
|
|
||||||
disregard. If no fanart available at specified resolution or greater, then
|
|
||||||
we disregard this setting, and download highest resolution that is available.
|
|
||||||
|
|
||||||
name*: In the case of a movie, ideally this should be the full movie name
|
|
||||||
followed by the year of the movie in parentheses. e.g. "The Matrix (1999)".
|
|
||||||
If this is specific enough to generate only one search result then we'll
|
|
||||||
continue. Otherwise, we'll raise IdError.
|
|
||||||
|
|
||||||
Because of the imprecise nature of this method of id, only use it if you
|
|
||||||
don't have the imdb_id or tmdb_id
|
|
||||||
|
|
||||||
imdb_id: Use this argument if you know the imdb id of the show/movie. If
|
|
||||||
this is used, the tmdb_id argument is ignored.
|
|
||||||
|
|
||||||
tmdb_id*: Use this argument if you know the tmdb id of the movie. If this
|
|
||||||
is used, the imdb_id argument is ignored.
|
|
||||||
|
|
||||||
imdbpy: When xmg is used as a library, imdbpy may not be installed
|
|
||||||
system-wide, but included with your application. If this is the case, pass
|
|
||||||
your instance of imdb.IMDb() to metagen, so we can use it.
|
|
||||||
|
|
||||||
* These arguments are not yet supported.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
if imdbid[:2].lower() == 'tt':
|
|
||||||
self.imdbid = imdbid[2:]
|
|
||||||
else:
|
|
||||||
self.imdbid = imdbid
|
|
||||||
|
|
||||||
self.nfo_string = 'http://www.imdb.com/title/' + imdbid + '/'
|
|
||||||
self.tmdb_data = self._get_tmdb_imdb()
|
|
||||||
self._validate_tmdb_json()
|
|
||||||
|
|
||||||
#TODO: Search by movie name
|
|
||||||
#TODO: Search by tmdb_id
|
|
||||||
#TODO: Search by movie hash
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_tmdb_json(self):
|
|
||||||
try:
|
|
||||||
_ = self._get_fanart(0,0)
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
_ = self._get_poster(0,0)
|
|
||||||
except:
|
|
||||||
raise ApiError("Unknown TMDB data format: %s" % self.tmdb_data)
|
|
||||||
|
|
||||||
def write_nfo(self, path):
|
|
||||||
try:
|
|
||||||
f = open(path, 'w')
|
|
||||||
f.write(self.nfo_string)
|
|
||||||
f.close()
|
|
||||||
except:
|
|
||||||
raise NfoError("Couldn't write nfo")
|
|
||||||
|
|
||||||
def _get_fanart(self, min_height, min_width):
|
|
||||||
''' Fetches the fanart for the specified imdb_id and saves it to dir.
|
|
||||||
Arguments
|
|
||||||
|
|
||||||
min_height/width: Sets lowest acceptable resolution fanart. 0 means
|
|
||||||
disregard. If no fanart available at specified resolution or greater, then
|
|
||||||
we disregard.
|
|
||||||
'''
|
|
||||||
images = [image['image'] for image in self.tmdb_data['backdrops'] if image['image'].get('size') == 'original']
|
|
||||||
if len(images) == 0:
|
|
||||||
raise ApiError("No fanart")
|
|
||||||
|
|
||||||
return self._get_image(images, min_height, min_width)
|
|
||||||
|
|
||||||
def get_fanart_url(self, min_height, min_width):
|
|
||||||
return self._get_fanart(min_height, min_width)['url']
|
|
||||||
|
|
||||||
def write_fanart(self, filename_root, path, min_height, min_width):
|
|
||||||
fanart_url = self.get_fanart_url(min_height, min_width)
|
|
||||||
#fetch and write to disk
|
|
||||||
dest = os.path.join(path, filename_root)
|
|
||||||
try:
|
|
||||||
f = open(dest, 'wb')
|
|
||||||
except:
|
|
||||||
raise IOError("Can't open for writing: %s" % dest)
|
|
||||||
|
|
||||||
response = urllib2.urlopen(fanart_url)
|
|
||||||
f.write(response.read())
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_poster(self, min_height, min_width):
|
|
||||||
''' Fetches the poster for the specified imdb_id and saves it to dir.
|
|
||||||
Arguments
|
|
||||||
|
|
||||||
min_height/width: Sets lowest acceptable resolution poster. 0 means
|
|
||||||
disregard. If no poster available at specified resolution or greater, then
|
|
||||||
we disregard.
|
|
||||||
'''
|
|
||||||
images = [image['image'] for image in self.tmdb_data['posters'] if image['image'].get('size') == 'original']
|
|
||||||
if len(images) == 0:
|
|
||||||
raise ApiError("No posters")
|
|
||||||
|
|
||||||
return self._get_image(images, min_height, min_width)
|
|
||||||
|
|
||||||
def get_poster_url(self, min_height, min_width):
|
|
||||||
return self._get_poster(min_height, min_width)['url']
|
|
||||||
|
|
||||||
def write_poster(self, filename_root, path, min_height, min_width):
|
|
||||||
poster_url = self.get_poster_url(min_height, min_width)
|
|
||||||
dest = os.path.join(path, filename_root)
|
|
||||||
|
|
||||||
try:
|
|
||||||
f = open(dest, 'wb')
|
|
||||||
except:
|
|
||||||
raise IOError("Can't open for writing: %s" % dest)
|
|
||||||
|
|
||||||
response = urllib2.urlopen(poster_url)
|
|
||||||
f.write(response.read())
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_tmdb_imdb(self):
|
|
||||||
url = "http://api.themoviedb.org/2.1/Movie.imdbLookup/en/json/%s/%s" % (__tmdb_apikey__, "tt" + self.imdbid)
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
while 1:
|
|
||||||
count += 1
|
|
||||||
response = urllib2.urlopen(url)
|
|
||||||
json_string = response.read()
|
|
||||||
try:
|
|
||||||
tmdb_data = json.loads(json_string)[0]
|
|
||||||
return tmdb_data
|
|
||||||
except ValueError, e:
|
|
||||||
if count < 3:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
raise ApiError("Invalid JSON: %s: %s" % (e, json_string))
|
|
||||||
except:
|
|
||||||
ApiError("JSON error with: %s" % json_string)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_image(self, image_list, min_height, min_width):
|
|
||||||
#Select image
|
|
||||||
images = []
|
|
||||||
for image in image_list:
|
|
||||||
if not min_height or min_width:
|
|
||||||
images.append(image)
|
|
||||||
break
|
|
||||||
elif min_height and not min_width:
|
|
||||||
if image['height'] >= min_height:
|
|
||||||
images.append(image)
|
|
||||||
break
|
|
||||||
elif min_width and not min_height:
|
|
||||||
if image['width'] >= min_width:
|
|
||||||
images.append(image)
|
|
||||||
break
|
|
||||||
elif min_width and min_height:
|
|
||||||
if image['width'] >= min_width and image['height'] >= min_height:
|
|
||||||
images.append(image)
|
|
||||||
break
|
|
||||||
|
|
||||||
#No image meets our resolution requirements, so disregard those requirements
|
|
||||||
if len(images) == 0 and min_height or min_width:
|
|
||||||
images.append(image_list[0])
|
|
||||||
|
|
||||||
return images[0]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
try:
|
|
||||||
id = sys.argv[1]
|
|
||||||
except:
|
|
||||||
id = 'tt0111161'
|
|
||||||
|
|
||||||
x = MetaGen(id)
|
|
||||||
x.write_nfo(".\movie.nfo")
|
|
||||||
x.write_fanart("fanart", ".", 0, 0)
|
|
||||||
x.write_poster("movie", ".", 0, 0)
|
|
||||||