diff --git a/CouchPotato.py b/CouchPotato.py
index c5fefe5b..bf5b1f72 100755
--- a/CouchPotato.py
+++ b/CouchPotato.py
@@ -33,7 +33,7 @@ def start():
new_environ[key] = value.encode('iso-8859-1')
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:
log.critical(e)
return 0
diff --git a/couchpotato/api/__init__.py b/couchpotato/api/__init__.py
index 8063d0a6..1f2f1a36 100644
--- a/couchpotato/api/__init__.py
+++ b/couchpotato/api/__init__.py
@@ -18,3 +18,4 @@ def index():
return jsonified({'routes': routes})
addApiView('', index)
+addApiView('default', index)
diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py
index 77b4eb0d..4e14d69e 100644
--- a/couchpotato/core/_base/_core/__init__.py
+++ b/couchpotato/core/_base/_core/__init__.py
@@ -46,7 +46,7 @@ config = [{
{
'tab': 'general',
'name': 'advanced',
- 'description': "For those who know what the're doing",
+ 'description': "For those who know what they're doing",
'advanced': True,
'options': [
{
@@ -67,7 +67,7 @@ config = [{
'name': 'data_dir',
'label': 'Data dir',
'type': 'directory',
- 'description': 'Where cache/logs/etc are stored.',
+ 'description': 'Where cache/logs/etc are stored. Keep empty for ./_data.',
},
{
'name': 'url_base',
@@ -79,13 +79,13 @@ config = [{
'name': 'permission_folder',
'default': 0755,
'label': 'Folder CHMOD',
- 'description': 'Permission for creating/copying folders',
+ 'description': 'Permission (octal) for creating/copying folders.',
},
{
'name': 'permission_file',
'default': 0755,
'label': 'File CHMOD',
- 'description': 'Permission for creating/copying files',
+ 'description': 'Permission (octal) for creating/copying files',
},
],
},
diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py
index fe6b067a..2c54f435 100644
--- a/couchpotato/core/_base/_core/main.py
+++ b/couchpotato/core/_base/_core/main.py
@@ -1,5 +1,5 @@
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.plugins.base import Plugin
from couchpotato.environment import Env
@@ -43,14 +43,13 @@ class Core(Plugin):
time.sleep(1)
-
if restart:
self.createFile(self.restartFilePath(), 'This is the most suckiest way to register if CP is restarted. Ever...')
- func = request.environ.get('werkzeug.server.shutdown')
- if func is None:
+ try:
+ request.environ.get('werkzeug.server.shutdown')()
+ except:
log.error('Failed shutting down the server')
- func()
def removeRestartFile(self):
try:
@@ -59,4 +58,4 @@ class Core(Plugin):
pass
def restartFilePath(self):
- return os.path.join(Env.get('data_dir'), 'restart')
+ return os.path.join(Env.get('app_dir'), 'restart')
diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py
index ceb04391..48ea3aa6 100644
--- a/couchpotato/core/downloaders/base.py
+++ b/couchpotato/core/downloaders/base.py
@@ -1,6 +1,7 @@
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
+from couchpotato.environment import Env
log = CPLog(__name__)
@@ -16,7 +17,10 @@ class Downloader(Plugin):
pass
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):
return not self.isEnabled()
diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py
index a0d142bd..f59a70b6 100644
--- a/couchpotato/core/downloaders/blackhole/main.py
+++ b/couchpotato/core/downloaders/blackhole/main.py
@@ -27,18 +27,16 @@ class Blackhole(Downloader):
try:
if not os.path.isfile(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'))
return False
- with open(fullPath, 'wb') as f:
- f.write(file)
-
return True
else:
log.info('File %s already exists.' % fullPath)
diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py
index e49522fb..8d56a8fa 100644
--- a/couchpotato/core/downloaders/sabnzbd/main.py
+++ b/couchpotato/core/downloaders/sabnzbd/main.py
@@ -6,6 +6,7 @@ from urllib import urlencode
import base64
import os
import re
+import traceback
log = CPLog(__name__)
@@ -37,15 +38,11 @@ class Sabnzbd(Downloader):
params = {
'apikey': self.conf('api_key'),
'cat': self.conf('category'),
- 'mode': 'addurl',
- 'name': data.get('url'),
+ 'mode': 'addfile',
'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)),
}
- # sabNzbd complains about "invalid archive file" for newzbin urls
- # added using addurl, works fine with addid
- if data.get('addbyid'):
- params['mode'] = 'addid'
+ nzb_file = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
if pp:
params['script'] = pp_script_fn
@@ -53,9 +50,9 @@ class Sabnzbd(Downloader):
url = cleanHost(self.conf('host')) + "api?" + urlencode(params)
try:
- data = self.urlopen(url)
- except Exception, e:
- log.error("Unable to connect to SAB: %s" % e)
+ data = self.urlopen(url, params = {"nzbfile": (params['nzbname'] + ".nzb", nzb_file)}, multipart = True)
+ except Exception:
+ log.error("Unable to connect to SAB: %s" % traceback.format_exc())
return False
result = data.strip()
@@ -63,7 +60,7 @@ class Sabnzbd(Downloader):
log.error("SABnzbd didn't return anything.")
return False
- log.debug("Result text from SAB: " + result)
+ log.debug("Result text from SAB: " + result[:40])
if result == "ok":
log.info("NZB sent to SAB successfully.")
return True
@@ -71,7 +68,7 @@ class Sabnzbd(Downloader):
log.error("Incorrect username/password.")
return False
else:
- log.error("Unknown error: " + result)
+ log.error("Unknown error: " + result[:40])
return False
def buildPp(self, imdb_id):
diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py
index f9044610..ecbf7db3 100644
--- a/couchpotato/core/event.py
+++ b/couchpotato/core/event.py
@@ -8,7 +8,7 @@ log = CPLog(__name__)
events = {}
-def addEvent(name, handler):
+def addEvent(name, handler, priority = 0):
if events.get(name):
e = events[name]
@@ -27,7 +27,7 @@ def addEvent(name, handler):
return h
- e += createHandle
+ e.handle(createHandle, priority = priority)
def removeEvent(name, handler):
e = events[name]
diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py
index a26ab814..6ffb7d55 100644
--- a/couchpotato/core/helpers/request.py
+++ b/couchpotato/core/helpers/request.py
@@ -1,3 +1,4 @@
+from couchpotato.core.helpers.variable import natcmp
from flask.globals import current_app
from flask.helpers import json
from libs.werkzeug.urls import url_decode
@@ -42,7 +43,7 @@ def dictToList(params):
new = {}
for x, value in params.iteritems():
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:
new_value = value
@@ -70,4 +71,3 @@ def jsonified(*args, **kwargs):
return padded_jsonify(callback, *args, **kwargs)
else:
return jsonify('text/javascript' if Env.doDebug() else 'application/json', *args, **kwargs)
-
diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py
index 208709d1..5dc7319f 100644
--- a/couchpotato/core/helpers/variable.py
+++ b/couchpotato/core/helpers/variable.py
@@ -1,10 +1,10 @@
import hashlib
import os.path
+import re
def isDict(object):
return isinstance(object, dict)
-
def mergeDicts(a, b):
assert isDict(a), isDict(b)
dst = a.copy()
@@ -16,7 +16,7 @@ def mergeDicts(a, b):
if key not in current_dst:
current_dst[key] = current_src[key]
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]))
else:
current_dst[key] = current_src[key]
@@ -42,3 +42,13 @@ def cleanHost(host):
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))
diff --git a/couchpotato/core/logger.py b/couchpotato/core/logger.py
index f50bc066..a0494fb0 100644
--- a/couchpotato/core/logger.py
+++ b/couchpotato/core/logger.py
@@ -4,7 +4,7 @@ import re
class CPLog():
context = ''
- replace_private = ['api', 'apikey', 'api_key', 'password', 'username']
+ replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h']
def __init__(self, context = ''):
self.context = context
diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py
index bc2e6a39..8feea71e 100644
--- a/couchpotato/core/notifications/base.py
+++ b/couchpotato/core/notifications/base.py
@@ -39,7 +39,7 @@ class Notification(Plugin):
data = {}
)
- #return jsonified({'success': success})
+ return jsonified({'success': success})
def testNotifyName(self):
return 'notify.%s.test' % self.getName().lower()
diff --git a/couchpotato/core/notifications/history/main.py b/couchpotato/core/notifications/history/main.py
index d770498b..aa55cf0d 100644
--- a/couchpotato/core/notifications/history/main.py
+++ b/couchpotato/core/notifications/history/main.py
@@ -1,9 +1,7 @@
from couchpotato import get_session
-from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import History as Hist
-from couchpotato.environment import Env
import time
log = CPLog(__name__)
@@ -13,12 +11,6 @@ class History(Notification):
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 = {}):
db = get_session()
diff --git a/couchpotato/core/notifications/synoindex/main.py b/couchpotato/core/notifications/synoindex/main.py
index d866a246..9541d9d8 100644
--- a/couchpotato/core/notifications/synoindex/main.py
+++ b/couchpotato/core/notifications/synoindex/main.py
@@ -12,6 +12,7 @@ class Synoindex(Notification):
addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, group = {}):
+ if self.isDisabled(): return
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
log.info(u'Executing synoindex command: %s ' % command)
diff --git a/couchpotato/core/plugins/__init__.py b/couchpotato/core/plugins/__init__.py
index 84714c75..8b137891 100644
--- a/couchpotato/core/plugins/__init__.py
+++ b/couchpotato/core/plugins/__init__.py
@@ -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.',
- },
- ],
- },
- ],
-}]
diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py
index 0fcc0b3b..6e342154 100644
--- a/couchpotato/core/plugins/base.py
+++ b/couchpotato/core/plugins/base.py
@@ -4,7 +4,9 @@ from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from flask.helpers import send_from_directory
+from libs.multipartpost import MultipartPostHandler
from urlparse import urlparse
+import cookielib
import glob
import math
import os.path
@@ -73,11 +75,14 @@ class Plugin(object):
try:
if not os.path.isdir(path):
os.makedirs(path, Env.getPermission('folder'))
+ return True
except Exception, e:
log.error('Unable to create folder "%s": %s' % (path, e))
+ return False
+
# http request
- def urlopen(self, url, timeout = 10, params = {}, headers = {}):
+ def urlopen(self, url, timeout = 10, params = {}, headers = {}, multipart = False):
socket.setdefaulttimeout(timeout)
@@ -85,15 +90,24 @@ class Plugin(object):
self.wait(host)
try:
- 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)
+ if multipart:
+ 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:
log.error('Failed opening url, %s: %s' % (url, e))
- data = ''
+ raise
self.http_last_use[host] = time.time()
@@ -111,7 +125,7 @@ class Plugin(object):
time.sleep(last_use - now + self.http_time_between_calls)
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__))
def afterCall(self, handler):
diff --git a/couchpotato/core/plugins/browser/main.py b/couchpotato/core/plugins/browser/main.py
index d4829b40..d1d23730 100644
--- a/couchpotato/core/plugins/browser/main.py
+++ b/couchpotato/core/plugins/browser/main.py
@@ -1,6 +1,7 @@
from couchpotato.api import addApiView
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.plugins.base import Plugin
+import ctypes
import os
import string
@@ -23,7 +24,7 @@ class FileBrowser(Plugin):
dirs = []
for f in os.listdir(path):
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 + '/')
return dirs
@@ -48,6 +49,21 @@ class FileBrowser(Plugin):
dirs = []
return jsonified({
+ 'is_root': getParam('path', '/') == '/',
'empty': len(dirs) == 0,
'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
diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py
index 3cbc3694..b3b9918f 100644
--- a/couchpotato/core/plugins/file/main.py
+++ b/couchpotato/core/plugins/file/main.py
@@ -42,17 +42,11 @@ class FileManager(Plugin):
if not dest: # to Cache
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
- if overwrite or not os.path.exists(dest):
- log.debug('Writing file to: %s' % dest)
- output = open(dest, 'wb')
- output.write(file)
- output.close()
- else:
- log.debug('File already exists: %s' % dest)
+ if overwrite or not os.path.isfile(dest):
+ self.createFile(dest, file)
return dest
-
def add(self, path = '', part = 1, type = (), available = 1, properties = {}):
db = get_session()
diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py
index 77179b0a..986ba3de 100644
--- a/couchpotato/core/plugins/library/main.py
+++ b/couchpotato/core/plugins/library/main.py
@@ -2,14 +2,15 @@ from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.logger import CPLog
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
log = CPLog(__name__)
class LibraryPlugin(Plugin):
- default_dict = {'titles': {}, 'files':{}, 'info':{}}
+ default_dict = {'titles': {}, 'files':{}, 'info':{}, 'genres':{}}
def __init__(self):
addEvent('library.add', self.add)
@@ -51,7 +52,9 @@ class LibraryPlugin(Plugin):
library = db.query(Library).filter_by(identifier = identifier).first()
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
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)
if not info or len(info) == 0:
log.error('Could not update, no movie info to work with: %s' % identifier)
- do_update = False
+ return False
# Main info
if do_update:
@@ -75,7 +78,6 @@ class LibraryPlugin(Plugin):
db.commit()
titles = info.get('titles', [])
-
log.debug('Adding titles: %s' % titles)
for title in titles:
t = LibraryTitle(
@@ -86,6 +88,20 @@ class LibraryPlugin(Plugin):
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
images = info.get('images', [])
for type in images:
diff --git a/couchpotato/core/plugins/metadata/main.py b/couchpotato/core/plugins/metadata/main.py
index 1a6e6e57..232ef4e2 100644
--- a/couchpotato/core/plugins/metadata/main.py
+++ b/couchpotato/core/plugins/metadata/main.py
@@ -8,9 +8,7 @@ log = CPLog(__name__)
class MetaData(Plugin):
def __init__(self):
- addEvent('renaming.after', self.add)
-
- addEvent('app.load', self.add)
+ addEvent('renamer.after', self.add)
def add(self, data = {}):
log.info('Getting meta data')
diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
index c20048ad..dcf12311 100644
--- a/couchpotato/core/plugins/movie/main.py
+++ b/couchpotato/core/plugins/movie/main.py
@@ -5,11 +5,20 @@ from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie
from couchpotato.environment import Env
+from sqlalchemy.sql.expression import or_
from urllib import urlencode
class MoviePlugin(Plugin):
+ default_dict = {
+ 'profile': {'types': {'quality': {}}},
+ 'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
+ 'library': {'titles': {}, 'files':{}},
+ 'files': {},
+ 'status': {}
+ }
+
def __init__(self):
addApiView('movie.search', self.search)
addApiView('movie.list', self.list)
@@ -24,18 +33,16 @@ class MoviePlugin(Plugin):
params = getParams()
db = get_session()
- results = db.query(Movie).filter(
- Movie.status.has(identifier = params.get('status', 'active'))
- ).all()
+ # Make a list from string
+ status = params.get('status', ['active'])
+ 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 = []
for movie in results:
- temp = movie.to_dict(deep = {
- 'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
- 'library': {'titles': {}, 'files':{}},
- 'files': {}
- })
-
+ temp = movie.to_dict(self.default_dict)
movies.append(temp)
return jsonified({
@@ -59,12 +66,7 @@ class MoviePlugin(Plugin):
if movie:
#addEvent('library.update.after', )
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
- fireEventAsync('searcher.single', movie.to_dict(deep = {
- 'profile': {'types': {'quality': {}}},
- 'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}},
- 'library': {'titles': {}, 'files':{}},
- 'files': {}
- }))
+ fireEventAsync('searcher.single', movie.to_dict(self.default_dict))
return jsonified({
'success': True,
@@ -116,13 +118,14 @@ class MoviePlugin(Plugin):
if release.status_id == status_snatched.get('id'):
release.delete()
+ m.profile_id = params.get('profile_id')
+
m.status_id = status_active.get('id')
db.commit()
- movie_dict = m.to_dict(deep = {
- 'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}},
- 'library': {'titles': {}}
- })
+ movie_dict = m.to_dict(self.default_dict)
+
+ fireEventAsync('searcher.single', movie_dict)
return jsonified({
'success': True,
diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
index 94ced765..7faf408a 100644
--- a/couchpotato/core/plugins/movie/static/list.js
+++ b/couchpotato/core/plugins/movie/static/list.js
@@ -27,12 +27,17 @@ var MovieList = new Class({
self.createNavigation();
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, {
- 'actions': self.options.actions
+ 'actions': actions
}, info);
$(m).inject(self.el);
m.fireEvent('injected');
-
+
if(self.options.navigation){
var first_char = m.getTitle().substr(0, 1);
self.activateLetter(first_char);
@@ -71,7 +76,7 @@ var MovieList = new Class({
});
},
-
+
activateLetter: function(letter){
this.letters[letter].addClass('active');
},
diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css
index d7088690..067ebfd7 100644
--- a/couchpotato/core/plugins/movie/static/movie.css
+++ b/couchpotato/core/plugins/movie/static/movie.css
@@ -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 {
padding: 20px 0;
@@ -79,13 +83,21 @@
float: left;
width: 5%;
padding: 0 0 0 3%;
- background: url('../images/rating.png') no-repeat left center;
}
.movies .info .description {
clear: both;
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 {
position: absolute;
right: 15px;
@@ -96,17 +108,14 @@
.movies .data:hover .action:hover { opacity: 1; }
.movies .data .action {
- background: no-repeat center;
+ background-repeat: no-repeat;
+ background-position: center;
display: inline-block;
width: 20px;
height: 20px;
padding: 3px;
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 {
clear: both;
@@ -142,6 +151,65 @@
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 {
list-style: none;
padding: 0;
diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js
index f8abf403..98d7c51e 100644
--- a/couchpotato/core/plugins/movie/static/movie.js
+++ b/couchpotato/core/plugins/movie/static/movie.js
@@ -1,7 +1,7 @@
var Movie = new Class({
Extends: BlockBase,
-
+
action: {},
initialize: function(self, options, data){
@@ -32,7 +32,7 @@ var Movie = new Class({
self.year = new Element('div.year', {
'text': self.data.library.year || 'Unknown'
}),
- self.rating = new Element('div.rating', {
+ self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', {
@@ -45,13 +45,20 @@ var Movie = new Class({
self.actions = new Element('div.actions')
)
);
-
+
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);
new Element('span', {
- 'text': ' '+q.label
+ 'text': q.label,
+ 'class': is_snatched ? 'snatched' : ''
}).inject(self.quality);
- })
+ });
Object.each(self.options.actions, function(action, key){
self.actions.adopt(
@@ -127,7 +134,7 @@ var Movie = new Class({
var MovieAction = new Class({
- class_name: 'action',
+ class_name: 'action icon',
initialize: function(movie){
var self = this;
@@ -193,7 +200,7 @@ var ReleaseAction = new Class({
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(),
'events': {
'click': self.show.bind(self)
@@ -211,16 +218,78 @@ var ReleaseAction = new Class({
$(self.movie.thumbnail).clone(),
self.release_container = new Element('div.releases')
).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){
- p(release);
new Element('div', {
- 'text': release.title
- }).inject(self.release_container)
+ 'class': 'item ' + release.status.identifier
+ }).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');
},
+ 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
+ }
+ })
+
+ }
+
});
\ No newline at end of file
diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css
index e94a4499..f65235f9 100644
--- a/couchpotato/core/plugins/movie/static/search.css
+++ b/couchpotato/core/plugins/movie/static/search.css
@@ -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 {
display: inline-block;
@@ -86,7 +89,7 @@
margin-right: 10px;
}
.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 {
vertical-align: middle;
diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js
index c3cfc467..67483115 100644
--- a/couchpotato/core/plugins/movie/static/search.js
+++ b/couchpotato/core/plugins/movie/static/search.js
@@ -10,6 +10,7 @@ Block.Search = new Class({
self.el = new Element('div.search_form').adopt(
new Element('div.input').adopt(
self.input = new Element('input.inlay', {
+ 'placeholder': 'Search for new movies',
'events': {
'keyup': self.keyup.bind(self),
'focus': self.hideResults.bind(self, false)
@@ -28,7 +29,7 @@ Block.Search = new Class({
}).adopt(
new Element('div.pointer'),
self.results = new Element('div.results')
- ).fade('hide')
+ ).hide()
);
self.spinner = new Spinner(self.result_container);
@@ -51,7 +52,7 @@ Block.Search = new Class({
if(self.hidden == bool) return;
- self.result_container.fade(bool ? 0 : 1)
+ self.result_container[bool ? 'hide' : 'show']();
if(bool){
History.removeEvent('change', self.hideResults.bind(self, !bool));
@@ -302,7 +303,7 @@ Block.Search.Item = new Class({
}).inject(self.title_select)
})
- Object.each(Quality.profiles, function(profile){
+ Object.each(Quality.getActiveProfiles(), function(profile){
new Element('option', {
'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label
diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py
index b43a6e6a..579b0cde 100644
--- a/couchpotato/core/plugins/profile/main.py
+++ b/couchpotato/core/plugins/profile/main.py
@@ -1,6 +1,7 @@
from couchpotato import get_session
from couchpotato.api import addApiView
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.logger import CPLog
from couchpotato.core.plugins.base import Plugin
@@ -15,8 +16,11 @@ class ProfilePlugin(Plugin):
addEvent('profile.all', self.all)
addApiView('profile.save', self.save)
+ addApiView('profile.save_order', self.saveOrder)
addApiView('profile.delete', self.delete)
+ addEvent('app.initialize', self.fill, priority = 90)
+
def all(self):
db = get_session()
@@ -50,7 +54,7 @@ class ProfilePlugin(Plugin):
for type in params.get('types', []):
t = ProfileType(
order = order,
- finish = type.get('finish'),
+ finish = type.get('finish') if order > 0 else 1,
wait_for = params.get('wait_for'),
quality_id = type.get('quality_id')
)
@@ -67,6 +71,25 @@ class ProfilePlugin(Plugin):
'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):
id = getParam('id')
@@ -90,3 +113,44 @@ class ProfilePlugin(Plugin):
'success': success,
'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
diff --git a/couchpotato/core/plugins/profile/static/handle.png b/couchpotato/core/plugins/profile/static/handle.png
new file mode 100644
index 00000000..adff5b29
Binary files /dev/null and b/couchpotato/core/plugins/profile/static/handle.png differ
diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css
index ab6ef985..204bc4ce 100644
--- a/couchpotato/core/plugins/profile/static/profile.css
+++ b/couchpotato/core/plugins/profile/static/profile.css
@@ -1,18 +1,134 @@
-.profile > .delete {
- background-position: center;
- height: 20px;
- width: 20px;
+/* @override http://192.168.1.20:5000/static/profile_plugin/profile.css */
+
+.add_new_profile {
+ padding: 20px;
+ display: block;
+ text-align: center;
+ font-size: 20px;
+ border-bottom: 1px solid rgba(255,255,255,0.2);
}
-.profile .types .type .handle {
- background: url('../../images/handle.png') center;
- display: inline-block;
- height: 20px;
- width: 20px;
+.profile { border-bottom: 1px solid rgba(255,255,255,0.2) }
+
+ .profile > .delete {
+ height: 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 {
- background-position: center;
- height: 20px;
- width: 20px;
-}
\ No newline at end of file
+ #profile_ordering ul {
+ float: left;
+ margin: 0;
+ 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;
+ }
\ No newline at end of file
diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js
index 080f971b..a945f7e5 100644
--- a/couchpotato/core/plugins/profile/static/profile.js
+++ b/couchpotato/core/plugins/profile/static/profile.js
@@ -24,47 +24,32 @@ var Profile = new Class({
var data = self.data;
self.el = new Element('div.profile').adopt(
- self.header = new Element('h4', {'text': data.label}),
new Element('span.delete.icon', {
'events': {
'click': self.del.bind(self)
}
}),
- new Element('div', {
- 'class': 'ctrlHolder'
- }).adopt(
+ new Element('.quality_label.ctrlHolder').adopt(
new Element('label', {'text':'Name'}),
- new Element('input.label.textInput.large', {
+ new Element('input.inlay', {
'type':'text',
'value': data.label,
- 'events': {
- 'keyup': function(){
- self.header.set('text', this.get('value'))
- }
- }
+ 'placeholder': 'Profile name'
})
),
- new Element('div.ctrlHolder').adopt(
- new Element('label', {'text':'Wait'}),
- new Element('input.wait_for.textInput.xsmall', {
+ new Element('div.wait_for.ctrlHolder').adopt(
+ new Element('span', {'text':'Wait'}),
+ new Element('input.inlay.xsmall', {
'type':'text',
'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('label', {'text': 'Qualities'}),
- new Element('div.head').adopt(
- new Element('span.quality_type', {'text': 'Search for'}),
- new Element('span.finish', {'html': 'Finish'})
- ),
+ new Element('div.qualities.ctrlHolder').adopt(
+ new Element('label', {'text': 'Search for'}),
self.type_container = new Element('ol.types'),
- new Element('a.addType', {
- 'text': 'Add another quality to search for.',
- 'href': '#',
- 'events': {
- 'click': self.addType.bind(self)
- }
+ new Element('div.formHint', {
+ 'html': "Search these qualities (2 minimum), from top to bottom. Use the checkbox, to stop searching after it found this quality."
})
)
);
@@ -73,6 +58,8 @@ var Profile = new Class({
if(data.types)
Object.each(data.types, self.addType.bind(self))
+
+ self.addType();
},
save: function(delay){
@@ -81,6 +68,8 @@ var Profile = new Class({
if(self.save_timer) clearTimeout(self.save_timer);
self.save_timer = (function(){
+ self.addType();
+
var data = self.getData();
if(data.types.length < 2) return;
@@ -96,6 +85,7 @@ var Profile = new Class({
}
}
});
+
}).delay(delay, self)
},
@@ -105,8 +95,8 @@ var Profile = new Class({
var data = {
'id' : self.data.id,
- 'label' : self.el.getElement('.label').get('value'),
- 'wait_for' : self.el.getElement('.wait_for').get('value'),
+ 'label' : self.el.getElement('.quality_label input').get('value'),
+ 'wait_for' : self.el.getElement('.wait_for input').get('value'),
'types': []
}
@@ -124,8 +114,19 @@ var Profile = new Class({
addType: function(data){
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);
+
self.sortable.addItems($(t));
self.types.include(t);
@@ -135,23 +136,35 @@ var Profile = new Class({
del: function(){
var self = this;
- if(!confirm('Are you sure you want to delete this profile?')) return
-
- Api.request('profile.delete', {
- 'data': {
- 'id': self.data.id
- },
- 'useSpinner': true,
- 'spinnerOptions': {
- 'target': self.el
- },
- 'onComplete': function(json){
- if(json.success)
- self.el.destroy();
- else
- alert(json.message)
+ var label = self.el.getElement('.quality_label input').get('value');
+ new Question('Are you sure you want to delete "'+label+'"?', 'Items using this profile, will be set to the default quality.', [{
+ 'text': 'Delete "'+label+'"',
+ 'class': 'delete',
+ 'events': {
+ 'click': function(e){
+ (e).stop();
+ Api.request('profile.delete', {
+ 'data': {
+ 'id': self.data.id
+ },
+ 'useSpinner': true,
+ 'spinnerOptions': {
+ 'target': self.el
+ },
+ 'onComplete': function(json){
+ if(json.success)
+ self.el.destroy();
+ else
+ alert(json.message)
+ }
+ });
+ }
}
- });
+ }, {
+ 'text': 'Cancel',
+ 'cancel': true
+ }]);
+
},
makeSortable: function(){
@@ -180,16 +193,24 @@ var Profile = new Class({
});
-Profile.Type = Class({
+Profile.Type = new Class({
+
+ Implements: [Events, Options],
deleted: false,
- initialize: function(data){
+ initialize: function(data, options){
var self = this;
+ self.setOptions(options);
- self.data = data;
+ self.data = data || {};
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(){
@@ -201,10 +222,11 @@ Profile.Type = Class({
self.fillQualities()
),
new Element('span.finish').adopt(
- self.finish = new Element('input', {
- 'type':'checkbox',
- 'class':'finish',
- 'checked': data.finish
+ self.finish = new Element('input.inlay.finish[type=checkbox]', {
+ 'checked': data.finish,
+ 'events': {
+ 'change': self.fireEvent.bind(self, 'change')
+ }
})
),
new Element('span.delete.icon', {
@@ -213,14 +235,27 @@ Profile.Type = Class({
}
}),
new Element('span.handle')
- )
+ );
+
+ self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
+
+ new Form.Check(self.finish);
},
fillQualities: function(){
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){
new Element('option', {
@@ -250,6 +285,8 @@ Profile.Type = Class({
self.el.addClass('deleted');
self.el.hide();
self.deleted = true;
+
+ self.fireEvent('change');
},
toElement: function(){
diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py
index 406697ca..c2089d48 100644
--- a/couchpotato/core/plugins/quality/main.py
+++ b/couchpotato/core/plugins/quality/main.py
@@ -13,13 +13,13 @@ log = CPLog(__name__)
class QualityPlugin(Plugin):
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': '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': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc']},
- {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
- {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['dvdscr'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
+ {'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': ['dvdrip'], 'allow': [], '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': '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']},
@@ -31,7 +31,8 @@ class QualityPlugin(Plugin):
addEvent('quality.all', self.all)
addEvent('quality.single', self.single)
addEvent('quality.guess', self.guess)
- addEvent('app.load', self.fill)
+
+ addEvent('app.initialize', self.fill, priority = 10)
def all(self):
@@ -112,40 +113,44 @@ class QualityPlugin(Plugin):
return True
- def guess(self, files, extra = {}):
- found = False
+ def guess(self, files, extra = {}, loose = False):
for file in files:
size = (os.path.getsize(file) / 1024 / 1024)
words = re.split('\W+', file.lower())
- for quality in self.all():
- correctSize = False
- if size >= quality['size_min'] and size <= quality['size_max']:
- correctSize = True
+ for quality in self.all():
# Check tags
- if type in words:
- found = True
-
- 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:
+ if quality['identifier'] in words:
+ log.debug('Found via identifier "%s" in %s' % (quality['identifier'], file))
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 {}
diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js
index f11334cd..e783f8e8 100644
--- a/couchpotato/core/plugins/quality/static/quality.js
+++ b/couchpotato/core/plugins/quality/static/quality.js
@@ -19,6 +19,13 @@ var QualityBase = new Class({
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){
return this.qualities.filter(function(q){
return q.id == id;
@@ -31,7 +38,7 @@ var QualityBase = new Class({
self.settings = App.getPage('Settings')
self.settings.addEvent('create', function(){
var tab = self.settings.createTab('profile', {
- 'label': 'Profile',
+ 'label': 'Quality',
'name': 'profile'
});
@@ -39,6 +46,7 @@ var QualityBase = new Class({
self.content = tab.content;
self.createProfiles();
+ self.createProfileOrdering();
self.createSizes();
})
@@ -50,42 +58,104 @@ var QualityBase = new Class({
*/
createProfiles: function(){
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({
- 'label': 'Custom',
- 'description': 'Discriptions'
+ 'label': 'Quality Profiles',
+ 'description': 'Create your own profiles with multiple qualities.'
}).inject(self.content).adopt(
- new Element('a.add_new', {
- 'text': 'Create a new quality profile',
+ self.profile_container = new Element('div.container'),
+ new Element('a.add_new_profile', {
+ 'text': count > 0 ? 'Create another quality profile' : 'Click here to create a quality profile.',
'events': {
'click': function(){
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){
- if(!profile.isCore())
- $(profile).inject(self.profile_container, 'top')
- })
+ // Add profiles, that aren't part of the core (for editing)
+ Object.each(non_core_profiles, function(profile){
+ $(profile).inject(self.profile_container)
+ });
},
createProfilesClass: function(data){
var self = this;
- if(data){
- return self.profiles[data.id] = new Profile(data);
- }
- else {
- var data = {
- 'id': randomString()
+ var data = data || {'id': randomString()}
+
+ return self.profiles[data.id] = new Profile(data);
+ },
+
+ 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.
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({
'label': 'Sizes',
- 'description': 'Discriptions',
+ 'description': 'Edit the minimal and maximum sizes (in MB) for each quality.',
'advanced': true
}).inject(self.content)
-
- new Element('div.item.header').adopt(
+
+
+ new Element('div.item.head').adopt(
new Element('span.label', {'text': 'Quality'}),
new Element('span.min', {'text': 'Min'}),
new Element('span.max', {'text': 'Max'})
).inject(group)
-
+
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('input.min', {'value': quality.size_min}),
new Element('input.max', {'value': quality.size_max})
).inject(group)
});
-
+
}
});
diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py
index 96f7a3d2..332aefea 100644
--- a/couchpotato/core/plugins/release/main.py
+++ b/couchpotato/core/plugins/release/main.py
@@ -1,8 +1,10 @@
from couchpotato import get_session
+from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent
+from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.logger import CPLog
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_
log = CPLog(__name__)
@@ -13,6 +15,9 @@ class Release(Plugin):
def __init__(self):
addEvent('release.add', self.add)
+ addApiView('release.download', self.download)
+ addApiView('release.delete', self.delete)
+
def add(self, group):
db = get_session()
@@ -30,22 +35,22 @@ class Release(Plugin):
db.add(movie)
db.commit()
- # Add release
+ # Add Release
snatched_status = fireEvent('status.get', 'snatched', single = True)
- release = db.query(Release).filter(
+ rel = db.query(Relea).filter(
or_(
- Release.identifier == identifier,
- and_(Release.identifier.startswith(group['library']['identifier'], Release.status_id == snatched_status.get('id')))
+ Relea.identifier == identifier,
+ and_(Relea.identifier.startswith(group['library']['identifier'], Relea.status_id == snatched_status.get('id')))
)
).first()
- if not release:
- release = Release(
+ if not rel:
+ rel = Relea(
identifier = identifier,
movie = movie,
quality_id = group['meta_data']['quality'].get('id'),
status_id = done_status.get('id')
)
- db.add(release)
+ db.add(rel)
db.commit()
# Add each file type
@@ -54,10 +59,10 @@ class Release(Plugin):
added_file = self.saveFile(file, type = type, include_media_info = type is 'movie')
try:
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()
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()
@@ -73,3 +78,48 @@ class Release(Plugin):
# 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)
+ 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
+ })
diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py
index 07ad2349..c5e2bc23 100644
--- a/couchpotato/core/plugins/renamer/__init__.py
+++ b/couchpotato/core/plugins/renamer/__init__.py
@@ -61,20 +61,22 @@ config = [{
'advanced': True,
'options': [
{
- 'name': 'trailer_name',
- 'label': 'Trailer naming',
- 'default': '-trailer.',
+ 'name': 'rename_nfo',
+ 'label': 'Rename .NFO',
+ 'description': 'Rename original .nfo file',
+ 'type': 'bool',
+ 'default': True,
},
{
'name': 'nfo_name',
'label': 'NFO naming',
- 'default': '.',
+ 'default': '.-orig',
},
{
- 'name': 'backdrop_name',
- 'label': 'Backdrop naming',
- 'default': '-backdrop.',
- }
+ 'name': 'trailer_name',
+ 'label': 'Trailer naming',
+ 'default': '-trailer.',
+ },
],
},
],
diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
index 9ec86e3f..10f1240c 100644
--- a/couchpotato/core/plugins/renamer/main.py
+++ b/couchpotato/core/plugins/renamer/main.py
@@ -1,10 +1,10 @@
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.variable import getExt
from couchpotato.core.logger import CPLog
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 re
import shutil
@@ -40,7 +40,7 @@ class Renamer(Plugin):
group = groups[group_identifier]
rename_files = {}
- # Add _UNKNOWN_ if no library is connected
+ # Add _UNKNOWN_ if no library item is connected
if not group['library']:
if 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
else:
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']
# Find subtitle for renaming
@@ -85,12 +89,9 @@ class Renamer(Plugin):
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
if file_type is 'nfo' and not self.conf('rename_nfo'):
+ log.debug('Skipping, renaming of %s disabled' % file_type)
continue
# Subtitle extra
@@ -98,7 +99,7 @@ class Renamer(Plugin):
continue
# 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
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_file_name = self.doReplace(file_name, replacements)
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
+ group['filename'] = replacements['filename']
# Meta naming
if file_type is 'trailer':
final_file_name = self.doReplace(trailer_name, replacements)
elif file_type is 'nfo':
- final_file_name = self.doReplace(nfo_name, replacements) + '-orig'
- elif file_type is 'backdrop':
- final_file_name = self.doReplace(backdrop_name, replacements)
+ final_file_name = self.doReplace(nfo_name, replacements)
# Seperator replace
if separator:
final_file_name = final_file_name.replace(' ', separator)
- # Main file
- rename_files[file] = os.path.join(destination, final_folder_name, final_file_name)
+ # Move DVD files (no structure renaming)
+ 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
if file_type is 'subtitle':
@@ -154,21 +169,43 @@ class Renamer(Plugin):
if multiple:
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
db = get_session()
+
library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
done_status = fireEvent('status.get', 'done', single = True)
+ active_status = fireEvent('status.get', 'active', single = True)
+
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:
- 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))
+
+ 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'):
+
+ # Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc
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))
+
+ # Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan
else:
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
- for file in release.files:
- log.info('Removing (not really) "%s"' % file.path)
-
- # Rename
+ # Rename all files marked
for src in rename_files:
if rename_files[src]:
@@ -200,21 +234,24 @@ class Renamer(Plugin):
log.info('Renaming "%s" to "%s"' % (src, dst))
path = os.path.dirname(dst)
- try:
- if not os.path.isdir(path): os.makedirs(path)
- except:
- log.error('Failed creating dir %s: %s' % (path, traceback.format_exc()))
- continue
+
+ # Create dir
+ self.makeDir(path)
try:
- shutil.move(src, dst)
+ pass
+ #shutil.move(src, dst)
except:
log.error('Failed moving the file "%s" : %s' % (os.path.basename(src), traceback.format_exc()))
#print rename_me, rename_files[rename_me]
# 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
if self.shuttingDown():
diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
index ec1fc67e..fe9ce2b7 100644
--- a/couchpotato/core/plugins/scanner/main.py
+++ b/couchpotato/core/plugins/scanner/main.py
@@ -4,13 +4,13 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog
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 flask.helpers import json
-from sqlalchemy.sql.expression import and_, or_
import os
import re
import subprocess
+import time
import traceback
log = CPLog(__name__)
@@ -23,11 +23,11 @@ class Scanner(Plugin):
'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
- 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 = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'],
'dvd': ['vts_*', 'vob'],
- 'nfo': ['nfo', 'txt', 'tag'],
+ 'nfo': ['nfo', 'nfo-orig', 'txt', 'tag'],
'subtitle': ['sub', 'srt', 'ssa', 'ass'],
'subtitle_extra': ['idx'],
'trailer': ['mov', 'mp4', 'flv']
@@ -172,9 +172,21 @@ class Scanner(Plugin):
# Determine file types
+ delete_identifier = []
for identifier in movie_files:
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
images = self.getImages(group['unsorted_files'])
group['files'] = {
@@ -182,7 +194,7 @@ class Scanner(Plugin):
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
'nfo': self.getNfo(group['unsorted_files']),
'trailer': self.getTrailers(group['unsorted_files']),
- 'backdrop': images['backdrop'],
+ #'backdrop': images['backdrop'],
'leftover': set(group['unsorted_files']),
}
@@ -198,12 +210,13 @@ class Scanner(Plugin):
group['parentdir'] = os.path.dirname(movie_file)
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"
- for folder in folders:
- if folder.lower() in self.ignore_names or len(folder) < 2:
- group['dirname'] = folder
+ # Try and get a proper dirname, so no "A", "Movie", "Download" etc
+ for folder_name in folder_names:
+ if folder_name.lower() not in self.ignore_names and len(folder_name) > 2:
+ group['dirname'] = folder_name
break
break
@@ -220,12 +233,16 @@ class Scanner(Plugin):
if not group['library']:
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
def getMetaData(self, group):
data = {}
- files = group['files']['movie']
+ files = list(group['files']['movie'])
for file in 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']:
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])
- data['source'] = self.getSourceMedia(file[0])
+ file = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
+ data['group'] = self.getGroup(file)
+ data['source'] = self.getSourceMedia(file)
return data
@@ -365,7 +383,6 @@ class Scanner(Plugin):
return set(filter(test, files))
def getDVDFiles(self, files):
-
def test(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'])):
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():
return True
@@ -510,8 +527,8 @@ class Scanner(Plugin):
def getGroup(self, file):
try:
- group = re.search('-(?P[A-Z0-9]+)$', file, re.I)
- return group.group('group') or ''
+ match = re.search('-(?P[A-Z0-9]+).', file, re.I)
+ return match.group('group') or ''
except:
return ''
diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py
index 673c0a4b..568875f3 100644
--- a/couchpotato/core/plugins/searcher/__init__.py
+++ b/couchpotato/core/plugins/searcher/__init__.py
@@ -23,7 +23,7 @@ config = [{
'name': 'required_words',
'label': 'Required words',
'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',
diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py
index 445f8af3..8cc8e509 100644
--- a/couchpotato/core/plugins/searcher/main.py
+++ b/couchpotato/core/plugins/searcher/main.py
@@ -6,7 +6,9 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env
+from sqlalchemy.exc import InterfaceError
import re
+import traceback
log = CPLog(__name__)
@@ -17,6 +19,7 @@ class Searcher(Plugin):
addEvent('searcher.all', self.all)
addEvent('searcher.single', self.single)
addEvent('searcher.correct_movie', self.correctMovie)
+ addEvent('searcher.download', self.download)
# Schedule cronjob
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:
- self.single(movie.to_dict(deep = {
+ self.single(movie.to_dict({
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
@@ -47,11 +50,8 @@ class Searcher(Plugin):
def single(self, movie):
- downloaded_status = fireEvent('status.get', 'downloaded', 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']:
has_better_quality = 0
@@ -85,37 +85,22 @@ class Searcher(Plugin):
db.commit()
for info in nzb:
- rls_info = ReleaseInfo(
- identifier = info,
- value = nzb[info]
- )
- rls.info.append(rls_info)
- db.commit()
+ try:
+ if not isinstance(nzb[info], (str, unicode, int, long)):
+ continue
+
+ rls_info = ReleaseInfo(
+ 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:
- successful = fireEvent('download', data = nzb, movie = movie, single = True)
-
- 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
+ return self.download(data = nzb, movie = movie)
else:
log.info('Better quality (%s) already available or snatched for %s' % (type['quality']['label'], default_title))
break
@@ -126,6 +111,26 @@ class Searcher(Plugin):
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):
@@ -192,6 +197,7 @@ class Searcher(Plugin):
if len(movie_words) == 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
return True
+ log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie['library']['titles'][0]['title'], movie['library']['year']))
return False
def containsOtherQuality(self, name, preferred_quality = {}, single_category = False):
diff --git a/couchpotato/core/plugins/updater/main.py b/couchpotato/core/plugins/updater/main.py
index 5b2dea68..edd5e3f0 100644
--- a/couchpotato/core/plugins/updater/main.py
+++ b/couchpotato/core/plugins/updater/main.py
@@ -1,4 +1,6 @@
+from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
@@ -10,14 +12,13 @@ log = CPLog(__name__)
class Updater(Plugin):
- git = 'git://github.com/CouchPotato/CouchPotato.git'
+ repo_name = 'RuudBurger/CouchPotatoServer'
running = False
version = None
- updateFailed = False
- updateAvailable = False
- updateVersion = None
- lastCheck = 0
+ update_failed = False
+ update_version = None
+ last_check = 0
def __init__(self):
@@ -27,6 +28,18 @@ class Updater(Plugin):
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):
if not self.version:
@@ -42,7 +55,7 @@ class Updater(Plugin):
def check(self):
- if self.updateAvailable or self.isDisabled():
+ if self.update_version or self.isDisabled():
return
current_branch = self.repo.getCurrentBranch().name
@@ -54,13 +67,17 @@ class Updater(Plugin):
remote = branch.getHead()
if local.getDate() < remote.getDate():
- if self.conf('automatic') and not self.updateFailed:
+ if self.conf('automatic') and not self.update_failed:
self.doUpdate()
else:
- self.updateAvailable = True
- self.updateVersion = remote.hash
+ self.update_version = remote.hash
- self.lastCheck = time.time()
+ self.last_check = time.time()
+
+ def doUpdateView(self):
+ return jsonified({
+ 'success': self.doUpdate()
+ })
def doUpdate(self):
try:
@@ -70,7 +87,7 @@ class Updater(Plugin):
except Exception, e:
log.error('Failed updating via GIT: %s' % e)
- self.updateFailed = True
+ self.update_failed = True
return False
diff --git a/couchpotato/core/plugins/updater/static/updater.js b/couchpotato/core/plugins/updater/static/updater.js
new file mode 100644
index 00000000..f7268b2f
--- /dev/null
+++ b/couchpotato/core/plugins/updater/static/updater.js
@@ -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();
diff --git a/couchpotato/core/plugins/wizard/__init__.py b/couchpotato/core/plugins/wizard/__init__.py
index 7ee4a45c..78876470 100644
--- a/couchpotato/core/plugins/wizard/__init__.py
+++ b/couchpotato/core/plugins/wizard/__init__.py
@@ -4,7 +4,7 @@ def start():
return Wizard()
config = [{
- 'name': 'global',
+ 'name': 'core',
'groups': [
{
'tab': 'general',
diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py
index df27865e..332a9896 100644
--- a/couchpotato/core/providers/base.py
+++ b/couchpotato/core/providers/base.py
@@ -2,7 +2,6 @@ from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
-from urllib2 import URLError
from urlparse import urlparse
import re
import time
@@ -60,22 +59,36 @@ class YarrProvider(Provider):
sizeMb = ['mb', 'mib']
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):
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:
if s in sizeRaw:
- return float(size) * 1024
+ return int(size) * 1024
for s in self.sizeMb:
if s in sizeRaw:
- return float(size)
+ return int(size)
for s in self.sizeKb:
if s in sizeRaw:
- return float(size) / 1024
+ return int(size) / 1024
return 0
@@ -96,11 +109,16 @@ class NZBProvider(YarrProvider):
type = 'nzb'
def __init__(self):
+ super(NZBProvider, self).__init__()
+
addEvent('provider.nzb.search', self.search)
addEvent('provider.yarr.search', self.search)
addEvent('provider.nzb.feed', self.feed)
+ def download(self, url = '', nzb_id = ''):
+ return self.urlopen(url)
+
def feed(self):
return []
diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py
index 0403f630..92ce9470 100644
--- a/couchpotato/core/providers/metadata/base.py
+++ b/couchpotato/core/providers/metadata/base.py
@@ -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.plugins.base import Plugin
@@ -17,7 +17,7 @@ class MetaDataBase(Plugin):
log.info('Creating %s metadata.' % self.getName())
- root = self.getRootName()
+ root = self.getRootName(release)
for type in ['nfo', 'thumbnail', 'fanart']:
try:
diff --git a/couchpotato/core/providers/metadata/mediabrowser/__init__.py b/couchpotato/core/providers/metadata/mediabrowser/__init__.py
index 3ead271b..873fe76a 100644
--- a/couchpotato/core/providers/metadata/mediabrowser/__init__.py
+++ b/couchpotato/core/providers/metadata/mediabrowser/__init__.py
@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'renamer',
- 'name': 'metadata',
+ 'name': 'mediabrowser_metadata',
'label': 'MediaBrowser',
'description': 'Enable metadata MediaBrowser can understand',
'options': [
diff --git a/couchpotato/core/providers/metadata/sonyps3/__init__.py b/couchpotato/core/providers/metadata/sonyps3/__init__.py
index 246c06c1..ceefc847 100644
--- a/couchpotato/core/providers/metadata/sonyps3/__init__.py
+++ b/couchpotato/core/providers/metadata/sonyps3/__init__.py
@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'renamer',
- 'name': 'metadata',
+ 'name': 'sonyps3_metadata',
'label': 'Sony PS3',
'description': 'Enable metadata your Playstation 3 can understand',
'options': [
diff --git a/couchpotato/core/providers/metadata/wdtv/__init__.py b/couchpotato/core/providers/metadata/wdtv/__init__.py
index 26c49ab9..b75c8658 100644
--- a/couchpotato/core/providers/metadata/wdtv/__init__.py
+++ b/couchpotato/core/providers/metadata/wdtv/__init__.py
@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'renamer',
- 'name': 'metadata',
+ 'name': 'wdtv_metadata',
'label': 'WDTV',
'description': 'Enable metadata WDTV can understand',
'options': [
diff --git a/couchpotato/core/providers/metadata/xbmc/__init__.py b/couchpotato/core/providers/metadata/xbmc/__init__.py
index 3c197a3d..e3b25926 100644
--- a/couchpotato/core/providers/metadata/xbmc/__init__.py
+++ b/couchpotato/core/providers/metadata/xbmc/__init__.py
@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'renamer',
- 'name': 'metadata',
+ 'name': 'xbmc_metadata',
'label': 'XBMC',
'description': 'Enable metadata XBMC can understand',
'options': [
diff --git a/couchpotato/core/providers/metadata/xbmc/main.py b/couchpotato/core/providers/metadata/xbmc/main.py
index 2a7e90b3..0f83e7b3 100644
--- a/couchpotato/core/providers/metadata/xbmc/main.py
+++ b/couchpotato/core/providers/metadata/xbmc/main.py
@@ -1,15 +1,14 @@
+from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.providers.metadata.base import MetaDataBase
from xml.etree.ElementTree import Element, SubElement, tostring
+import os
import re
import xml.dom.minidom
class XBMC(MetaDataBase):
def getRootName(self, data = {}):
-
-
-
- return '/Users/ruud/Downloads/Test/Transformers'
+ return os.path.join(data['destination_dir'], data['filename'])
def getFanartName(self, root):
return '%s-fanart.jpg' % root
@@ -23,8 +22,30 @@ class XBMC(MetaDataBase):
def getNfo(self, data):
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:
if ':' in type:
@@ -33,20 +54,17 @@ class XBMC(MetaDataBase):
name = type
try:
- el = SubElement(nfoxml, name)
- el.text = data.get(type, '')
+ if data['library'].get(type):
+ el = SubElement(nfoxml, name)
+ el.text = toUnicode(data['library'].get(type, ''))
except:
pass
- #for genre in self.get('genres'):
- # genres = SubElement(nfoxml, 'genre')
- # genres.text = genre
+ # Genre
+ for genre in data['library'].get('genres', []):
+ 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
nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
@@ -55,130 +73,3 @@ class XBMC(MetaDataBase):
xml_string = text_re.sub('>\g<1>', xml_string)
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
-"""
diff --git a/couchpotato/core/providers/movie/imdb/main.py b/couchpotato/core/providers/movie/imdb/main.py
index 3eb7cac2..735ea5a7 100644
--- a/couchpotato/core/providers/movie/imdb/main.py
+++ b/couchpotato/core/providers/movie/imdb/main.py
@@ -10,7 +10,7 @@ class IMDB(MovieProvider):
def __init__(self):
- addEvent('provider.movie.search', self.search)
+ #addEvent('provider.movie.search', self.search)
self.p = IMDb('http')
diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/movie/themoviedb/main.py
index 8bf098e3..a5823f8d 100644
--- a/couchpotato/core/providers/movie/themoviedb/main.py
+++ b/couchpotato/core/providers/movie/themoviedb/main.py
@@ -92,7 +92,7 @@ class TheMovieDb(MovieProvider):
def getInfo(self, identifier = None):
cache_key = 'tmdb.cache.%s' % identifier
- result = self.getCache(cache_key)
+ result = None #self.getCache(cache_key)
if not result:
result = {}
@@ -112,27 +112,33 @@ class TheMovieDb(MovieProvider):
def parseMovie(self, movie):
- year = str(movie.get('released', 'none'))[:4]
-
# Poster url
poster = self.getImage(movie, type = 'poster')
backdrop = self.getImage(movie, type = 'backdrop')
+ # Genres
+ genres = self.getCategory(movie, 'genre')
+
# 1900 is the same as None
+ year = str(movie.get('released', 'none'))[:4]
if year == '1900' or year.lower() == 'none':
year = None
movie_data = {
'id': int(movie.get('id', 0)),
'titles': [toUnicode(movie.get('name'))],
+ 'original_title': movie.get('original_name'),
'images': {
'posters': [poster],
'backdrops': [backdrop],
},
'imdb': movie.get('imdb_id'),
+ 'runtime': movie.get('runtime'),
+ 'released': movie.get('released'),
'year': year,
'plot': movie.get('overview', ''),
'tagline': '',
+ 'genres': genres,
}
# Add alternative names
@@ -153,6 +159,19 @@ class TheMovieDb(MovieProvider):
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):
if self.conf('api_key') == '':
log.error('No API key provided.')
diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py
index 8933be9c..e7e157a4 100644
--- a/couchpotato/core/providers/nzb/newzbin/main.py
+++ b/couchpotato/core/providers/nzb/newzbin/main.py
@@ -13,10 +13,9 @@ log = CPLog(__name__)
class Newzbin(NZBProvider, RSS):
urls = {
- 'search': 'https://www.newzbin.com/search/',
'download': 'http://www.newzbin.com/api/dnzb/',
+ 'search': 'https://www.newzbin.com/search/',
}
- searchUrl = 'https://www.newzbin.com/search/'
format_ids = {
2: ['scr'],
@@ -36,7 +35,7 @@ class Newzbin(NZBProvider, RSS):
def search(self, movie, quality):
results = []
- if self.isDisabled() or not self.isAvailable(self.searchUrl):
+ if self.isDisabled() or not self.isAvailable(self.urls['search']):
return results
format_id = self.getFormatId(type)
@@ -97,11 +96,12 @@ class Newzbin(NZBProvider, RSS):
new = {
'id': id,
'type': 'nzb',
+ 'provider': self.getName(),
'name': title,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': self.parseSize(size),
'url': str(self.getTextElement(nzb, '{%s}nzb' % REPORT_NS)),
- 'download': lambda: self.download(id),
+ 'download': self.download,
'detail_url': str(self.getTextElement(nzb, 'link')),
'description': self.getTextElement(nzb, "description"),
'check_nzb': False,
@@ -121,7 +121,7 @@ class Newzbin(NZBProvider, RSS):
return results
- def download(self, nzb_id):
+ def download(self, url = '', nzb_id = ''):
try:
log.info('Download nzb from newzbin, report id: %s ' % nzb_id)
diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py
index 8af1643a..341e6fd3 100644
--- a/couchpotato/core/providers/nzb/newznab/__init__.py
+++ b/couchpotato/core/providers/nzb/newznab/__init__.py
@@ -9,7 +9,7 @@ config = [{
{
'tab': 'providers',
'name': 'newznab',
- 'description': 'Enable multiple NewzNab providers',
+ 'description': 'Enable multiple NewzNab providers such as NZB.su',
'options': [
{
'name': 'enabled',
@@ -21,8 +21,8 @@ config = [{
},
{
'name': 'host',
- 'default': 'http://nzb.su',
- 'description': 'The hostname of your newznab provider, like http://nzb.su'
+ 'default': 'nzb.su',
+ 'description': 'The hostname of your newznab provider'
},
{
'name': 'api_key',
diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py
index 17ac3832..028162eb 100644
--- a/couchpotato/core/providers/nzb/newznab/main.py
+++ b/couchpotato/core/providers/nzb/newznab/main.py
@@ -5,6 +5,7 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import NZBProvider
from dateutil.parser import parse
from urllib import urlencode
+from urlparse import urlparse
import time
import xml.etree.ElementTree as XMLTree
@@ -130,11 +131,13 @@ class Newznab(NZBProvider, RSS):
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
new = {
'id': id,
+ 'provider': self.getName(),
'type': 'nzb',
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(size) / 1024 / 1024,
'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),
'content': self.getTextElement(nzb, "description"),
}
@@ -173,6 +176,17 @@ class Newznab(NZBProvider, RSS):
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):
return cleanHost(host) + 'api?t=' + type
diff --git a/couchpotato/core/providers/nzb/newznab/static/newznab.js b/couchpotato/core/providers/nzb/newznab/static/newznab.js
index 61d9dc12..e82ec35c 100644
--- a/couchpotato/core/providers/nzb/newznab/static/newznab.js
+++ b/couchpotato/core/providers/nzb/newznab/static/newznab.js
@@ -31,28 +31,51 @@ var MultipleNewznab = new Class({
});
self.inputs[name].getParent().hide()
+ self.inputs[name].addEvent('change', self.addEmpty.bind(self))
});
self.values.each(function(item, nr){
self.createItem(item.use, item.host, item.api_key);
});
-
- new Element('a.nice_button', {
- 'text': 'Add new NewzNab provider',
- 'events': {
- 'click': function(e){
- (e).stop();
-
- self.createItem(1, '', '');
- }
- }
- }).inject(self.fieldset.getElement('h2'), 'after');
+
+ new Element('div.head').adopt(
+ new Element('abbr.host', {
+ 'text': 'Host',
+ 'title': self.inputs['host'].getNext().get('text')
+ }),
+ new Element('abbr.api_key', {
+ 'text': 'Api Key',
+ 'title': self.inputs['api_key'].getNext().get('text')
+ })
+ ).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){
var self = this;
@@ -83,12 +106,12 @@ var MultipleNewznab = new Class({
}
}),
new Element('a.icon.delete', {
- 'text': 'delete',
'events': {
'click': self.deleteItem.bind(self)
}
})
).inject(self.fieldset);
+ item[!host ? 'addClass' : 'removeClass']('is_empty');
new Form.Check(checkbox, {
'onChange': checkbox.fireEvent.bind(checkbox, 'change')
@@ -105,6 +128,7 @@ var MultipleNewznab = new Class({
self.items.each(function(item, nr){
self.input_types.each(function(type){
var input = item.getElement('input.'+type);
+ if(input.getParent('.ctrlHolder').hasClass('is_empty')) return;
if(!temp[type]) temp[type] = [];
temp[type][nr] = input.get('type') == 'checkbox' ? +input.get('checked') : input.get('value').trim();
@@ -125,7 +149,7 @@ var MultipleNewznab = new Class({
(e).stop();
var item = e.target.getParent();
-
+
self.items.erase(item);
item.destroy();
diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py
index a1b377c7..f44e586a 100644
--- a/couchpotato/core/providers/nzb/nzbindex/main.py
+++ b/couchpotato/core/providers/nzb/nzbindex/main.py
@@ -14,7 +14,7 @@ log = CPLog(__name__)
class NzbIndex(NZBProvider, RSS):
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
}
@@ -63,10 +63,12 @@ class NzbIndex(NZBProvider, RSS):
new = {
'id': id,
'type': 'nzb',
+ 'provider': self.getName(),
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': enclosure['length'],
'url': enclosure['url'],
+ 'download': self.download,
'detail_url': enclosure['url'].replace('/download/', '/release/'),
'description': self.getTextElement(nzb, "description"),
'check_nzb': True,
diff --git a/couchpotato/core/providers/nzb/nzbmatrix/main.py b/couchpotato/core/providers/nzb/nzbmatrix/main.py
index 11049159..da602adc 100644
--- a/couchpotato/core/providers/nzb/nzbmatrix/main.py
+++ b/couchpotato/core/providers/nzb/nzbmatrix/main.py
@@ -81,10 +81,12 @@ class NZBMatrix(NZBProvider, RSS):
new = {
'id': id,
'type': 'nzb',
+ 'provider': self.getName(),
'name': title,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': self.parseSize(size),
'url': self.urls['download'] % id + self.getApiExt(),
+ 'download': self.download,
'detail_url': self.urls['detail'] % id,
'description': self.getTextElement(nzb, "description"),
'check_nzb': True,
diff --git a/couchpotato/core/providers/nzb/nzbs/__init__.py b/couchpotato/core/providers/nzb/nzbs/__init__.py
index 1fe5a642..c4c60002 100644
--- a/couchpotato/core/providers/nzb/nzbs/__init__.py
+++ b/couchpotato/core/providers/nzb/nzbs/__init__.py
@@ -9,6 +9,7 @@ config = [{
{
'tab': 'providers',
'name': 'nzbs',
+ 'description': 'Id and Key can be found on your nzbs.org RSS page.',
'options': [
{
'name': 'enabled',
@@ -17,12 +18,12 @@ config = [{
{
'name': 'id',
'label': 'Id',
- 'description': 'Can be found here, the number after "&i="',
+ 'description': 'The number after "&i="',
},
{
'name': 'api_key',
'label': 'Api Key',
- 'description': 'Can be found here, the string after "&h="'
+ 'description': 'The string after "&h="'
},
],
},
diff --git a/couchpotato/core/providers/nzb/nzbs/main.py b/couchpotato/core/providers/nzb/nzbs/main.py
index 86489329..a86f9d76 100644
--- a/couchpotato/core/providers/nzb/nzbs/main.py
+++ b/couchpotato/core/providers/nzb/nzbs/main.py
@@ -71,10 +71,12 @@ class Nzbs(NZBProvider, RSS):
new = {
'id': id,
'type': 'nzb',
+ 'provider': self.getName(),
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': self.parseSize(self.getTextElement(nzb, "description").split('
')[1].split('">')[1]),
'url': self.urls['download'] % (id, self.getApiExt()),
+ 'download': self.download,
'detail_url': self.urls['detail'] % id,
'description': self.getTextElement(nzb, "description"),
'check_nzb': True,
diff --git a/couchpotato/core/providers/torrent/thepiratebay/__init__.py b/couchpotato/core/providers/torrent/thepiratebay/__init__.py
index 1e154ce0..810e713a 100644
--- a/couchpotato/core/providers/torrent/thepiratebay/__init__.py
+++ b/couchpotato/core/providers/torrent/thepiratebay/__init__.py
@@ -3,22 +3,4 @@ from .main import ThePirateBay
def start():
return ThePirateBay()
-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',
- },
- ],
- },
- ],
-}]
+config = []
diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py
index 1c462932..77813586 100644
--- a/couchpotato/core/settings/__init__.py
+++ b/couchpotato/core/settings/__init__.py
@@ -3,6 +3,7 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.helpers.request import getParams, jsonified
+from couchpotato.core.helpers.variable import mergeDicts
import ConfigParser
import os.path
import time
@@ -93,7 +94,12 @@ class Settings():
self.p.set(section, option, value)
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):
return self.options
diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py
index 4b96bfe6..7c4510c4 100644
--- a/couchpotato/core/settings/model.py
+++ b/couchpotato/core/settings/model.py
@@ -44,6 +44,7 @@ class Library(Entity):
status = ManyToOne('Status')
movies = OneToMany('Movie')
titles = OneToMany('LibraryTitle')
+ genres = ManyToMany('LibraryGenre')
files = ManyToMany('File')
info = OneToMany('LibraryInfo')
@@ -68,6 +69,14 @@ class LibraryTitle(Entity):
libraries = ManyToOne('Library')
+class LibraryGenre(Entity):
+ """"""
+
+ name = Field(Unicode)
+
+ libraries = ManyToMany('Library')
+
+
class Language(Entity):
""""""
diff --git a/couchpotato/runner.py b/couchpotato/runner.py
index a422f32c..f12bd78a 100644
--- a/couchpotato/runner.py
+++ b/couchpotato/runner.py
@@ -115,8 +115,10 @@ def runCouchPotato(options, base_path, args):
latest_db_version = version(repo)
+ initialize = True
try:
current_db_version = db_version(db, repo)
+ initialize = False
except:
version_control(db, repo, version = latest_db_version)
current_db_version = db_version(db, repo)
@@ -131,6 +133,9 @@ def runCouchPotato(options, base_path, args):
fireEventAsync('app.load')
+ if initialize:
+ fireEventAsync('app.initialize')
+
# Create app
from couchpotato import app
api_key = Env.setting('api_key')
@@ -138,10 +143,12 @@ def runCouchPotato(options, base_path, args):
reloader = debug and not options.daemonize
# 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
+ config = {
+ 'use_reloader': reloader,
+ 'host': Env.setting('host', default = '0.0.0.0'),
+ 'port': Env.setting('port', default = 5000)
+ }
# Static path
web.add_url_rule(url_base + '/static/',
@@ -153,4 +160,4 @@ def runCouchPotato(options, base_path, args):
app.register_blueprint(api, url_prefix = '%s/%s/' % (url_base, api_key))
# Go go go!
- app.run(use_reloader = reloader)
+ app.run(**config)
diff --git a/couchpotato/static/images/edit.png b/couchpotato/static/images/edit.png
deleted file mode 100644
index 53192528..00000000
Binary files a/couchpotato/static/images/edit.png and /dev/null differ
diff --git a/couchpotato/static/images/handle.png b/couchpotato/static/images/handle.png
deleted file mode 100644
index f78e248d..00000000
Binary files a/couchpotato/static/images/handle.png and /dev/null differ
diff --git a/couchpotato/static/images/icon.check.png b/couchpotato/static/images/icon.check.png
new file mode 100644
index 00000000..c277e6b4
Binary files /dev/null and b/couchpotato/static/images/icon.check.png differ
diff --git a/couchpotato/static/images/delete.png b/couchpotato/static/images/icon.delete.png
similarity index 100%
rename from couchpotato/static/images/delete.png
rename to couchpotato/static/images/icon.delete.png
diff --git a/couchpotato/static/images/icon.download.png b/couchpotato/static/images/icon.download.png
new file mode 100644
index 00000000..ca3d0434
Binary files /dev/null and b/couchpotato/static/images/icon.download.png differ
diff --git a/couchpotato/static/images/icon.edit.png b/couchpotato/static/images/icon.edit.png
new file mode 100644
index 00000000..19ff8bd2
Binary files /dev/null and b/couchpotato/static/images/icon.edit.png differ
diff --git a/couchpotato/static/images/icon.folder.gif b/couchpotato/static/images/icon.folder.gif
new file mode 100644
index 00000000..e19ce53a
Binary files /dev/null and b/couchpotato/static/images/icon.folder.gif differ
diff --git a/couchpotato/static/images/imdb.png b/couchpotato/static/images/icon.imdb.png
similarity index 100%
rename from couchpotato/static/images/imdb.png
rename to couchpotato/static/images/icon.imdb.png
diff --git a/couchpotato/static/images/rating.png b/couchpotato/static/images/icon.rating.png
similarity index 100%
rename from couchpotato/static/images/rating.png
rename to couchpotato/static/images/icon.rating.png
diff --git a/couchpotato/static/images/icon.refresh.png b/couchpotato/static/images/icon.refresh.png
new file mode 100644
index 00000000..257cfee3
Binary files /dev/null and b/couchpotato/static/images/icon.refresh.png differ
diff --git a/couchpotato/static/images/reload.png b/couchpotato/static/images/reload.png
deleted file mode 100644
index 031f2fd2..00000000
Binary files a/couchpotato/static/images/reload.png and /dev/null differ
diff --git a/couchpotato/static/images/right.arrow.png b/couchpotato/static/images/right.arrow.png
new file mode 100644
index 00000000..39677d05
Binary files /dev/null and b/couchpotato/static/images/right.arrow.png differ
diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js
index 65128294..95758b16 100644
--- a/couchpotato/static/scripts/couchpotato.js
+++ b/couchpotato/static/scripts/couchpotato.js
@@ -29,7 +29,7 @@ var CouchPotato = new Class({
else
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){
@@ -100,6 +100,14 @@ var CouchPotato = new Class({
getPage: function(name){
return this.pages[name]
+ },
+
+ shutdown: function(){
+ Api.request('app.shutdown');
+ },
+
+ restart: function(){
+ Api.request('app.restart');
}
});
diff --git a/couchpotato/static/scripts/library/form_replacement/form_check.js b/couchpotato/static/scripts/library/form_replacement/form_check.js
index 3b29819b..4c240f6a 100644
--- a/couchpotato/static/scripts/library/form_replacement/form_check.js
+++ b/couchpotato/static/scripts/library/form_replacement/form_check.js
@@ -96,14 +96,14 @@ Form.Check = new Class({
this.fireEvent('removeHighlight', this);
},
keyToggle: function(e) {
- var evt = new Event(e);
+ var evt = (e);
if (evt.key === 'space') { this.toggle(e); }
},
toggle: function(e) {
var evt;
if (this.disabled) { return this; }
if (e) {
- evt = new Event(e).stopPropagation();
+ evt = (e).stopPropagation();
if (evt.target.tagName.toLowerCase() !== 'a') {
evt.stop();
}
@@ -114,6 +114,7 @@ Form.Check = new Class({
this.check();
}
this.fireEvent('change', this);
+ this.input.fireEvent('change', this);
return this;
},
uncheck: function() {
diff --git a/couchpotato/static/scripts/library/form_replacement/form_dropdown.js b/couchpotato/static/scripts/library/form_replacement/form_dropdown.js
index 0b01adf9..86c2c3c8 100644
--- a/couchpotato/static/scripts/library/form_replacement/form_dropdown.js
+++ b/couchpotato/static/scripts/library/form_replacement/form_dropdown.js
@@ -117,7 +117,7 @@ Form.Dropdown = new Class({
},
expand: function(e) {
clearTimeout(this.collapseInterval);
- var evt = e ? new Event(e).stop() : null;
+ var evt = e ? (e).stop() : null;
this.open = true;
this.input.focus();
this.element.addClass('active').addClass('dropdown-active');
diff --git a/couchpotato/static/scripts/library/form_replacement/form_radio.js b/couchpotato/static/scripts/library/form_replacement/form_radio.js
index 2fa15f7d..245aa4d3 100644
--- a/couchpotato/static/scripts/library/form_replacement/form_radio.js
+++ b/couchpotato/static/scripts/library/form_replacement/form_radio.js
@@ -22,7 +22,7 @@ Form.Radio = new Class({
toggle: function(e) {
if (this.element.hasClass('checked') || this.disabled) { return; }
var evt;
- if (e) { evt = new Event(e).stop(); }
+ if (e) { evt = (e).stop(); }
if (this.checked) {
this.uncheck();
} else {
diff --git a/couchpotato/static/scripts/library/mootools.js b/couchpotato/static/scripts/library/mootools.js
index 6dc82f2d..9f25f676 100644
--- a/couchpotato/static/scripts/library/mootools.js
+++ b/couchpotato/static/scripts/library/mootools.js
@@ -3,10 +3,10 @@
MooTools: the javascript framework
web build:
- - http://mootools.net/core/c1215700e7dedaa9d48503126daf2111
+ - http://mootools.net/core/f42fb6d73ea1a13146c5ad9502b442f0
packager build:
- - packager build Core/Class Core/Class.Extras Core/Element Core/Element.Style Core/Element.Dimensions Core/Fx.Tween Core/Fx.Morph Core/Fx.Transitions Core/Request.JSON Core/DOMReady
+ - packager build Core/Class Core/Class.Extras Core/Element Core/Element.Style Core/Element.Delegation Core/Element.Dimensions Core/Fx.Tween Core/Fx.Morph Core/Fx.Transitions Core/Request.JSON Core/Cookie Core/DOMReady
/*
---
@@ -33,8 +33,8 @@ provides: [Core, MooTools, Type, typeOf, instanceOf, Native]
(function(){
this.MooTools = {
- version: '1.3.1',
- build: 'af48c8d589f43f32212f9bb8ff68a127e6a3ba6c'
+ version: '1.4.0',
+ build: 'a15e35b4dbd12e8d86d9b50aa67a27e8e0071ea3'
};
// typeOf, instanceOf
@@ -203,7 +203,7 @@ var implement = function(name, method){
if (typeOf(hook) == 'type') implement.call(hook, name, method);
else hook.call(this, name, method);
}
-
+
var previous = this.prototype[name];
if (previous == null || !previous.$protected) this.prototype[name] = method;
@@ -265,7 +265,7 @@ var force = function(name, object, methods){
force('String', String, [
'charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf', 'match', 'quote', 'replace', 'search',
- 'slice', 'split', 'substr', 'substring', 'toLowerCase', 'toUpperCase'
+ 'slice', 'split', 'substr', 'substring', 'trim', 'toLowerCase', 'toUpperCase'
])('Array', Array, [
'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift', 'concat', 'join', 'slice',
'indexOf', 'lastIndexOf', 'filter', 'forEach', 'every', 'map', 'some', 'reduce', 'reduceRight'
@@ -398,7 +398,7 @@ String.extend('uniqueID', function(){
-}).call(this);
+})();
/*
@@ -419,15 +419,9 @@ provides: Array
Array.implement({
- invoke: function(methodName){
- var args = Array.slice(arguments, 1);
- return this.map(function(item){
- return item[methodName].apply(item, args);
- });
- },
-
+ /**/
every: function(fn, bind){
- for (var i = 0, l = this.length; i < l; i++){
+ for (var i = 0, l = this.length >>> 0; i < l; i++){
if ((i in this) && !fn.call(bind, this[i], i, this)) return false;
}
return true;
@@ -435,39 +429,47 @@ Array.implement({
filter: function(fn, bind){
var results = [];
- for (var i = 0, l = this.length; i < l; i++){
+ for (var i = 0, l = this.length >>> 0; i < l; i++){
if ((i in this) && fn.call(bind, this[i], i, this)) results.push(this[i]);
}
return results;
},
+ indexOf: function(item, from){
+ var length = this.length >>> 0;
+ for (var i = (from < 0) ? Math.max(0, length + from) : from || 0; i < length; i++){
+ if (this[i] === item) return i;
+ }
+ return -1;
+ },
+
+ map: function(fn, bind){
+ var length = this.length >>> 0, results = Array(length);
+ for (var i = 0; i < length; i++){
+ if (i in this) results[i] = fn.call(bind, this[i], i, this);
+ }
+ return results;
+ },
+
+ some: function(fn, bind){
+ for (var i = 0, l = this.length >>> 0; i < l; i++){
+ if ((i in this) && fn.call(bind, this[i], i, this)) return true;
+ }
+ return false;
+ },
+ /*!ES5>*/
+
clean: function(){
return this.filter(function(item){
return item != null;
});
},
- indexOf: function(item, from){
- var len = this.length;
- for (var i = (from < 0) ? Math.max(0, len + from) : from || 0; i < len; i++){
- if (this[i] === item) return i;
- }
- return -1;
- },
-
- map: function(fn, bind){
- var results = [];
- for (var i = 0, l = this.length; i < l; i++){
- if (i in this) results[i] = fn.call(bind, this[i], i, this);
- }
- return results;
- },
-
- some: function(fn, bind){
- for (var i = 0, l = this.length; i < l; i++){
- if ((i in this) && fn.call(bind, this[i], i, this)) return true;
- }
- return false;
+ invoke: function(methodName){
+ var args = Array.slice(arguments, 1);
+ return this.map(function(item){
+ return item[methodName].apply(item, args);
+ });
},
associate: function(keys){
@@ -594,37 +596,37 @@ String.implement({
},
contains: function(string, separator){
- return (separator) ? (separator + this + separator).indexOf(separator + string + separator) > -1 : this.indexOf(string) > -1;
+ return (separator) ? (separator + this + separator).indexOf(separator + string + separator) > -1 : String(this).indexOf(string) > -1;
},
trim: function(){
- return this.replace(/^\s+|\s+$/g, '');
+ return String(this).replace(/^\s+|\s+$/g, '');
},
clean: function(){
- return this.replace(/\s+/g, ' ').trim();
+ return String(this).replace(/\s+/g, ' ').trim();
},
camelCase: function(){
- return this.replace(/-\D/g, function(match){
+ return String(this).replace(/-\D/g, function(match){
return match.charAt(1).toUpperCase();
});
},
hyphenate: function(){
- return this.replace(/[A-Z]/g, function(match){
+ return String(this).replace(/[A-Z]/g, function(match){
return ('-' + match.charAt(0).toLowerCase());
});
},
capitalize: function(){
- return this.replace(/\b[a-z]/g, function(match){
+ return String(this).replace(/\b[a-z]/g, function(match){
return match.toUpperCase();
});
},
escapeRegExp: function(){
- return this.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
+ return String(this).replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
},
toInt: function(base){
@@ -636,17 +638,17 @@ String.implement({
},
hexToRgb: function(array){
- var hex = this.match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);
+ var hex = String(this).match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);
return (hex) ? hex.slice(1).hexToRgb(array) : null;
},
rgbToHex: function(array){
- var rgb = this.match(/\d{1,3}/g);
+ var rgb = String(this).match(/\d{1,3}/g);
return (rgb) ? rgb.rgbToHex(array) : null;
},
substitute: function(object, regexp){
- return this.replace(regexp || (/\\?\{([^{}]+)\}/g), function(match, name){
+ return String(this).replace(regexp || (/\\?\{([^{}]+)\}/g), function(match, name){
if (match.charAt(0) == '\\') return match.slice(1);
return (object[name] != null) ? object[name] : '';
});
@@ -690,20 +692,30 @@ Function.implement({
try {
return this.apply(bind, Array.from(args));
} catch (e){}
-
+
return null;
},
- bind: function(bind){
+ /**/
+ bind: function(that){
var self = this,
- args = (arguments.length > 1) ? Array.slice(arguments, 1) : null;
-
- return function(){
- if (!args && !arguments.length) return self.call(bind);
- if (args && arguments.length) return self.apply(bind, args.concat(Array.from(arguments)));
- return self.apply(bind, args || arguments);
+ args = arguments.length > 1 ? Array.slice(arguments, 1) : null,
+ F = function(){};
+
+ var bound = function(){
+ var context = that, length = arguments.length;
+ if (this instanceof bound){
+ F.prototype = self.prototype;
+ context = new F;
+ }
+ var result = (!args && !length)
+ ? self.call(context)
+ : self.apply(context, args && length ? args.concat(Array.slice(arguments)) : args || arguments);
+ return context == that ? result : context;
};
+ return bound;
},
+ /*!ES5-bind>*/
pass: function(args, bind){
var self = this;
@@ -894,7 +906,7 @@ Class.Mutators = {
}
};
-}).call(this);
+})();
/*
@@ -971,7 +983,7 @@ this.Events = new Class({
}, this);
return this;
},
-
+
removeEvent: function(type, fn){
type = removeOn(type);
var events = this.$events[type];
@@ -1015,7 +1027,7 @@ this.Options = new Class({
});
-}).call(this);
+})();
/*
@@ -1179,12 +1191,13 @@ Document.mirror(function(name, method){
});
document.html = document.documentElement;
-document.head = document.getElementsByTagName('head')[0];
+if (!document.head) document.head = document.getElementsByTagName('head')[0];
if (document.execCommand) try {
document.execCommand("BackgroundImageCache", false, true);
} catch (e){}
+/**/
if (this.attachEvent && !this.addEventListener){
var unloadEvent = function(){
this.detachEvent('onunload', unloadEvent);
@@ -1216,10 +1229,134 @@ try {
};
});
}
+/**/
-}).call(this);
+})();
+
+
+/*
+---
+
+name: Object
+
+description: Object generic methods
+
+license: MIT-style license.
+
+requires: Type
+
+provides: [Object, Hash]
+
+...
+*/
+
+(function(){
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+Object.extend({
+
+ subset: function(object, keys){
+ var results = {};
+ for (var i = 0, l = keys.length; i < l; i++){
+ var k = keys[i];
+ if (k in object) results[k] = object[k];
+ }
+ return results;
+ },
+
+ map: function(object, fn, bind){
+ var results = {};
+ for (var key in object){
+ if (hasOwnProperty.call(object, key)) results[key] = fn.call(bind, object[key], key, object);
+ }
+ return results;
+ },
+
+ filter: function(object, fn, bind){
+ var results = {};
+ for (var key in object){
+ var value = object[key];
+ if (hasOwnProperty.call(object, key) && fn.call(bind, value, key, object)) results[key] = value;
+ }
+ return results;
+ },
+
+ every: function(object, fn, bind){
+ for (var key in object){
+ if (hasOwnProperty.call(object, key) && !fn.call(bind, object[key], key)) return false;
+ }
+ return true;
+ },
+
+ some: function(object, fn, bind){
+ for (var key in object){
+ if (hasOwnProperty.call(object, key) && fn.call(bind, object[key], key)) return true;
+ }
+ return false;
+ },
+
+ keys: function(object){
+ var keys = [];
+ for (var key in object){
+ if (hasOwnProperty.call(object, key)) keys.push(key);
+ }
+ return keys;
+ },
+
+ values: function(object){
+ var values = [];
+ for (var key in object){
+ if (hasOwnProperty.call(object, key)) values.push(object[key]);
+ }
+ return values;
+ },
+
+ getLength: function(object){
+ return Object.keys(object).length;
+ },
+
+ keyOf: function(object, value){
+ for (var key in object){
+ if (hasOwnProperty.call(object, key) && object[key] === value) return key;
+ }
+ return null;
+ },
+
+ contains: function(object, value){
+ return Object.keyOf(object, value) != null;
+ },
+
+ toQueryString: function(object, base){
+ var queryString = [];
+
+ Object.each(object, function(value, key){
+ if (base) key = base + '[' + key + ']';
+ var result;
+ switch (typeOf(value)){
+ case 'object': result = Object.toQueryString(value, key); break;
+ case 'array':
+ var qs = {};
+ value.each(function(val, i){
+ qs[i] = val;
+ });
+ result = Object.toQueryString(qs, key);
+ break;
+ default: result = key + '=' + encodeURIComponent(value);
+ }
+ if (value != null) queryString.push(result);
+ });
+
+ return queryString.join('&');
+ }
+
+});
+
+})();
+
+
/*
@@ -1530,7 +1667,7 @@ local.setDocument = function(document){
var selected, id = 'slick_uniqueid';
var testNode = document.createElement('div');
-
+
var testRoot = document.body || document.getElementsByTagName('body')[0] || root;
testRoot.appendChild(testNode);
@@ -1581,7 +1718,7 @@ local.setDocument = function(document){
features.brokenGEBCN = cachedGetElementsByClassName || brokenSecondClassNameGEBCN;
}
-
+
if (testNode.querySelectorAll){
// IE 8 returns closed nodes (EG:"") for querySelectorAll('*') for some documents
try {
@@ -1707,7 +1844,7 @@ var reSimpleSelector = /^([#.]?)((?:[\w-]+|\*))$/,
local.search = function(context, expression, append, first){
var found = this.found = (first) ? null : (append || []);
-
+
if (!context) return found;
else if (context.navigator) context = context.document; // Convert the node from a window to a document
else if (!context.nodeType) return found;
@@ -1785,17 +1922,28 @@ local.search = function(context, expression, append, first){
/**/
querySelector: if (context.querySelectorAll) {
- if (!this.isHTMLDocument || this.brokenMixedCaseQSA || qsaFailExpCache[expression] ||
- (this.brokenCheckedQSA && expression.indexOf(':checked') > -1) ||
- (this.brokenEmptyAttributeQSA && reEmptyAttribute.test(expression)) || Slick.disableQSA) break querySelector;
+ if (!this.isHTMLDocument
+ || qsaFailExpCache[expression]
+ //TODO: only skip when expression is actually mixed case
+ || this.brokenMixedCaseQSA
+ || (this.brokenCheckedQSA && expression.indexOf(':checked') > -1)
+ || (this.brokenEmptyAttributeQSA && reEmptyAttribute.test(expression))
+ || (!contextIsDocument //Abort when !contextIsDocument and...
+ // there are multiple expressions in the selector
+ // since we currently only fix non-document rooted QSA for single expression selectors
+ && expression.indexOf(',') > -1
+ )
+ || Slick.disableQSA
+ ) break querySelector;
- var _expression = expression;
+ var _expression = expression, _context = context;
if (!contextIsDocument){
// non-document rooted QSA
// credits to Andrew Dupont
- var currentId = context.getAttribute('id'), slickid = 'slickid__';
- context.setAttribute('id', slickid);
+ var currentId = _context.getAttribute('id'), slickid = 'slickid__';
+ _context.setAttribute('id', slickid);
_expression = '#' + slickid + ' ' + _expression;
+ context = _context.parentNode;
}
try {
@@ -1806,8 +1954,9 @@ local.search = function(context, expression, append, first){
break querySelector;
} finally {
if (!contextIsDocument){
- if (currentId) context.setAttribute('id', currentId);
- else context.removeAttribute('id');
+ if (currentId) _context.setAttribute('id', currentId);
+ else _context.removeAttribute('id');
+ context = _context;
}
}
@@ -2001,12 +2150,12 @@ local.matchNode = function(node, selector){
return this.nativeMatchesSelector.call(node, selector.replace(/\[([^=]+)=\s*([^'"\]]+?)\s*\]/g, '[$1="$2"]'));
} catch(matchError) {}
}
-
+
var parsed = this.Slick.parse(selector);
if (!parsed) return true;
// simple (single) selectors
- var expressions = parsed.expressions, reversedExpressions, simpleExpCounter = 0, i;
+ var expressions = parsed.expressions, simpleExpCounter = 0, i;
for (i = 0; (currentExpression = expressions[i]); i++){
if (currentExpression.length == 1){
var exp = currentExpression[0];
@@ -2080,7 +2229,7 @@ var combinators = {
this.push(item, tag, null, classes, attributes, pseudos);
break;
}
- }
+ }
return;
}
if (!item){
@@ -2289,7 +2438,7 @@ var pseudos = {
'root': function(node){
return (node === this.root);
},
-
+
'selected': function(node){
return node.selected;
}
@@ -2301,7 +2450,7 @@ for (var p in pseudos) local['pseudo:' + p] = pseudos[p];
// attributes methods
-local.attributeGetters = {
+var attributeGetters = local.attributeGetters = {
'class': function(){
return this.getAttribute('class') || this.className;
@@ -2318,7 +2467,7 @@ local.attributeGetters = {
'style': function(){
return (this.style) ? this.style.cssText : this.getAttribute('style');
},
-
+
'tabindex': function(){
var attributeNode = this.getAttributeNode('tabindex');
return (attributeNode && attributeNode.specified) ? attributeNode.nodeValue : null;
@@ -2326,15 +2475,22 @@ local.attributeGetters = {
'type': function(){
return this.getAttribute('type');
+ },
+
+ 'maxlength': function(){
+ var attributeNode = this.getAttributeNode('maxLength');
+ return (attributeNode && attributeNode.specified) ? attributeNode.nodeValue : null;
}
};
+attributeGetters.MAXLENGTH = attributeGetters.maxLength = attributeGetters.maxlength;
+
// Slick
var Slick = local.Slick = (this.Slick || {});
-Slick.version = '1.1.5';
+Slick.version = '1.1.6';
// Slick finder
@@ -2356,9 +2512,15 @@ Slick.contains = function(container, node){
// Slick attribute getter
Slick.getAttribute = function(node, name){
+ local.setDocument(node);
return local.getAttribute(node, name);
};
+Slick.hasAttribute = function(node, name){
+ local.setDocument(node);
+ return local.hasAttribute(node, name);
+};
+
// Slick matcher
Slick.match = function(node, selector){
@@ -2423,7 +2585,7 @@ description: One of the most important items in MooTools. Contains the dollar fu
license: MIT-style license.
-requires: [Window, Document, Array, String, Function, Number, Slick.Parser, Slick.Finder]
+requires: [Window, Document, Array, String, Function, Object, Number, Slick.Parser, Slick.Finder]
provides: [Element, Elements, $, $$, Iframe, Selectors]
@@ -2443,10 +2605,12 @@ var Element = function(tag, props){
if (parsed.id && props.id == null) props.id = parsed.id;
var attributes = parsed.attributes;
- if (attributes) for (var i = 0, l = attributes.length; i < l; i++){
- var attr = attributes[i];
- if (attr.value != null && attr.operator == '=' && props[attr.key] == null)
- props[attr.key] = attr.value;
+ if (attributes) for (var attr, i = 0, l = attributes.length; i < l; i++){
+ attr = attributes[i];
+ if (props[attr.key] != null) continue;
+
+ if (attr.value != null && attr.operator == '=') props[attr.key] = attr.value;
+ else if (!attr.value && !attr.operator) props[attr.key] = true;
}
if (parsed.classList && props['class'] == null) props['class'] = parsed.classList.join(' ');
@@ -2586,9 +2750,9 @@ var splice = Array.prototype.splice, object = {'0': 0, '1': 1, length: 2};
splice.call(object, 1, 1);
if (object[1] == 1) Elements.implement('splice', function(){
var length = this.length;
- splice.apply(this, arguments);
+ var result = splice.apply(this, arguments);
while (length >= this.length) delete this[length--];
- return this;
+ return result;
}.protect());
Elements.implement(Array.prototype);
@@ -2708,6 +2872,79 @@ Window.implement({
});
+var contains = {contains: function(element){
+ return Slick.contains(this, element);
+}};
+
+if (!document.contains) Document.implement(contains);
+if (!document.createElement('div').contains) Element.implement(contains);
+
+
+
+// tree walking
+
+var injectCombinator = function(expression, combinator){
+ if (!expression) return combinator;
+
+ expression = Object.clone(Slick.parse(expression));
+
+ var expressions = expression.expressions;
+ for (var i = expressions.length; i--;)
+ expressions[i][0].combinator = combinator;
+
+ return expression;
+};
+
+Object.forEach({
+ getNext: '~',
+ getPrevious: '!~',
+ getParent: '!'
+}, function(combinator, method){
+ Element.implement(method, function(expression){
+ return this.getElement(injectCombinator(expression, combinator));
+ });
+});
+
+Object.forEach({
+ getAllNext: '~',
+ getAllPrevious: '!~',
+ getSiblings: '~~',
+ getChildren: '>',
+ getParents: '!'
+}, function(combinator, method){
+ Element.implement(method, function(expression){
+ return this.getElements(injectCombinator(expression, combinator));
+ });
+});
+
+Element.implement({
+
+ getFirst: function(expression){
+ return document.id(Slick.search(this, injectCombinator(expression, '>'))[0]);
+ },
+
+ getLast: function(expression){
+ return document.id(Slick.search(this, injectCombinator(expression, '>')).getLast());
+ },
+
+ getWindow: function(){
+ return this.ownerDocument.window;
+ },
+
+ getDocument: function(){
+ return this.ownerDocument;
+ },
+
+ getElementById: function(id){
+ return document.id(Slick.find(this, '#' + ('' + id).replace(/(\W)/g, '\\$1')));
+ },
+
+ match: function(expression){
+ return !expression || Slick.match(this, expression);
+ }
+
+});
+
if (window.$$ == null) Window.implement('$$', function(selector){
@@ -2720,48 +2957,7 @@ if (window.$$ == null) Window.implement('$$', function(selector){
(function(){
-var collected = {}, storage = {};
-var formProps = {input: 'checked', option: 'selected', textarea: 'value'};
-
-var get = function(uid){
- return (storage[uid] || (storage[uid] = {}));
-};
-
-var clean = function(item){
- var uid = item.uid;
- if (item.removeEvents) item.removeEvents();
- if (item.clearAttributes) item.clearAttributes();
- if (uid != null){
- delete collected[uid];
- delete storage[uid];
- }
- return item;
-};
-
-var camels = ['defaultValue', 'accessKey', 'cellPadding', 'cellSpacing', 'colSpan', 'frameBorder', 'maxLength', 'readOnly',
- 'rowSpan', 'tabIndex', 'useMap'
-];
-var bools = ['compact', 'nowrap', 'ismap', 'declare', 'noshade', 'checked', 'disabled', 'readOnly', 'multiple', 'selected',
- 'noresize', 'defer', 'defaultChecked'
-];
- var attributes = {
- 'html': 'innerHTML',
- 'class': 'className',
- 'for': 'htmlFor',
- 'text': (function(){
- var temp = document.createElement('div');
- return (temp.textContent == null) ? 'innerText' : 'textContent';
- })()
-};
-var readOnly = ['type'];
-var expandos = ['value', 'defaultValue'];
-var uriAttrs = /^(?:href|src|usemap)$/i;
-
-bools = bools.associate(bools);
-camels = camels.associate(camels.map(String.toLowerCase));
-readOnly = readOnly.associate(readOnly);
-
-Object.append(attributes, expandos.associate(expandos));
+// Inserters
var inserters = {
@@ -2789,20 +2985,116 @@ inserters.inside = inserters.bottom;
-var injectCombinator = function(expression, combinator){
- if (!expression) return combinator;
+// getProperty / setProperty
- expression = Object.clone(Slick.parse(expression));
+var propertyGetters = {}, propertySetters = {};
- var expressions = expression.expressions;
- for (var i = expressions.length; i--;)
- expressions[i][0].combinator = combinator;
+// properties
- return expression;
-};
+var properties = {};
+Array.forEach([
+ 'type', 'value', 'defaultValue', 'accessKey', 'cellPadding', 'cellSpacing', 'colSpan',
+ 'frameBorder', 'readOnly', 'rowSpan', 'tabIndex', 'useMap'
+], function(property){
+ properties[property.toLowerCase()] = property;
+});
+
+Object.append(properties, {
+ 'html': 'innerHTML',
+ 'text': (function(){
+ var temp = document.createElement('div');
+ return (temp.innerText == null) ? 'textContent' : 'innerText';
+ })()
+});
+
+Object.forEach(properties, function(real, key){
+ propertySetters[key] = function(node, value){
+ node[real] = value;
+ };
+ propertyGetters[key] = function(node){
+ return node[real];
+ };
+});
+
+// Booleans
+
+var bools = [
+ 'compact', 'nowrap', 'ismap', 'declare', 'noshade', 'checked',
+ 'disabled', 'readOnly', 'multiple', 'selected', 'noresize',
+ 'defer', 'defaultChecked', 'autofocus', 'controls', 'autoplay',
+ 'loop'
+];
+
+var booleans = {};
+Array.forEach(bools, function(bool){
+ var lower = bool.toLowerCase();
+ booleans[lower] = bool;
+ propertySetters[lower] = function(node, value){
+ node[bool] = !!value;
+ };
+ propertyGetters[lower] = function(node){
+ return !!node[bool];
+ };
+});
+
+// Special cases
+
+Object.append(propertySetters, {
+
+ 'class': function(node, value){
+ ('className' in node) ? node.className = value : node.setAttribute('class', value);
+ },
+
+ 'for': function(node, value){
+ ('htmlFor' in node) ? node.htmlFor = value : node.setAttribute('for', value);
+ },
+
+ 'style': function(node, value){
+ (node.style) ? node.style.cssText = value : node.setAttribute('style', value);
+ }
+
+});
+
+/* getProperty, setProperty */
Element.implement({
+ setProperty: function(name, value){
+ var setter = propertySetters[name.toLowerCase()];
+ if (setter) setter(this, value);
+ else this.setAttribute(name, value);
+ return this;
+ },
+
+ setProperties: function(attributes){
+ for (var attribute in attributes) this.setProperty(attribute, attributes[attribute]);
+ return this;
+ },
+
+ getProperty: function(name){
+ var getter = propertyGetters[name.toLowerCase()];
+ if (getter) return getter(this);
+ var result = Slick.getAttribute(this, name);
+ return (!result && !Slick.hasAttribute(this, name)) ? null : result;
+ },
+
+ getProperties: function(){
+ var args = Array.from(arguments);
+ return args.map(this.getProperty, this).associate(args);
+ },
+
+ removeProperty: function(name){
+ name = name.toLowerCase();
+ if (booleans[name]) this.setProperty(name, false);
+ this.removeAttribute(name);
+ return this;
+ },
+
+ removeProperties: function(){
+ Array.each(arguments, this.removeProperty, this);
+ return this;
+ },
+
set: function(prop, value){
var property = Element.Properties[prop];
(property && property.set) ? property.set.call(this, value) : this.setProperty(prop, value);
@@ -2819,47 +3111,6 @@ Element.implement({
return this;
},
- setProperty: function(attribute, value){
- attribute = camels[attribute] || attribute;
- if (value == null) return this.removeProperty(attribute);
- var key = attributes[attribute];
- (key) ? this[key] = value :
- (bools[attribute]) ? this[attribute] = !!value : this.setAttribute(attribute, '' + value);
- return this;
- },
-
- setProperties: function(attributes){
- for (var attribute in attributes) this.setProperty(attribute, attributes[attribute]);
- return this;
- },
-
- getProperty: function(attribute){
- attribute = camels[attribute] || attribute;
- var key = attributes[attribute] || readOnly[attribute];
- return (key) ? this[key] :
- (bools[attribute]) ? !!this[attribute] :
- (uriAttrs.test(attribute) ? this.getAttribute(attribute, 2) :
- (key = this.getAttributeNode(attribute)) ? key.nodeValue : null) || null;
- },
-
- getProperties: function(){
- var args = Array.from(arguments);
- return args.map(this.getProperty, this).associate(args);
- },
-
- removeProperty: function(attribute){
- attribute = camels[attribute] || attribute;
- var key = attributes[attribute];
- (key) ? this[key] = '' :
- (bools[attribute]) ? this[attribute] = false : this.removeAttribute(attribute);
- return this;
- },
-
- removeProperties: function(){
- Array.each(arguments, this.removeProperty, this);
- return this;
- },
-
hasClass: function(className){
return this.className.clean().contains(className, ' ');
},
@@ -2918,58 +3169,6 @@ Element.implement({
return this.replaces(el).grab(el, where);
},
- getPrevious: function(expression){
- return document.id(Slick.find(this, injectCombinator(expression, '!~')));
- },
-
- getAllPrevious: function(expression){
- return Slick.search(this, injectCombinator(expression, '!~'), new Elements);
- },
-
- getNext: function(expression){
- return document.id(Slick.find(this, injectCombinator(expression, '~')));
- },
-
- getAllNext: function(expression){
- return Slick.search(this, injectCombinator(expression, '~'), new Elements);
- },
-
- getFirst: function(expression){
- return document.id(Slick.search(this, injectCombinator(expression, '>'))[0]);
- },
-
- getLast: function(expression){
- return document.id(Slick.search(this, injectCombinator(expression, '>')).getLast());
- },
-
- getParent: function(expression){
- return document.id(Slick.find(this, injectCombinator(expression, '!')));
- },
-
- getParents: function(expression){
- return Slick.search(this, injectCombinator(expression, '!'), new Elements);
- },
-
- getSiblings: function(expression){
- return Slick.search(this, injectCombinator(expression, '~~'), new Elements);
- },
-
- getChildren: function(expression){
- return Slick.search(this, injectCombinator(expression, '>'), new Elements);
- },
-
- getWindow: function(){
- return this.ownerDocument.window;
- },
-
- getDocument: function(){
- return this.ownerDocument;
- },
-
- getElementById: function(id){
- return document.id(Slick.find(this, '#' + ('' + id).replace(/(\W)/g, '\\$1')));
- },
-
getSelected: function(){
this.selectedIndex; // Safari 3.2.1
return new Elements(Array.from(this.options).filter(function(option){
@@ -2993,7 +3192,30 @@ Element.implement({
});
});
return queryString.join('&');
- },
+ }
+
+});
+
+var collected = {}, storage = {};
+
+var get = function(uid){
+ return (storage[uid] || (storage[uid] = {}));
+};
+
+var clean = function(item){
+ var uid = item.uid;
+ if (item.removeEvents) item.removeEvents();
+ if (item.clearAttributes) item.clearAttributes();
+ if (uid != null){
+ delete collected[uid];
+ delete storage[uid];
+ }
+ return item;
+};
+
+var formProps = {input: 'checked', option: 'selected', textarea: 'value'};
+
+Element.implement({
destroy: function(){
var children = clean(this).getElementsByTagName('*');
@@ -3011,55 +3233,44 @@ Element.implement({
return (this.parentNode) ? this.parentNode.removeChild(this) : this;
},
- match: function(expression){
- return !expression || Slick.match(this, expression);
- }
+ clone: function(contents, keepid){
+ contents = contents !== false;
+ var clone = this.cloneNode(contents), ce = [clone], te = [this], i;
-});
-
-var cleanClone = function(node, element, keepid){
- if (!keepid) node.setAttributeNode(document.createAttribute('id'));
- if (node.clearAttributes){
- node.clearAttributes();
- node.mergeAttributes(element);
- node.removeAttribute('uid');
- if (node.options){
- var no = node.options, eo = element.options;
- for (var i = no.length; i--;) no[i].selected = eo[i].selected;
+ if (contents){
+ ce.append(Array.from(clone.getElementsByTagName('*')));
+ te.append(Array.from(this.getElementsByTagName('*')));
}
+
+ for (i = ce.length; i--;){
+ var node = ce[i], element = te[i];
+ if (!keepid) node.removeAttribute('id');
+ /**/
+ if (node.clearAttributes){
+ node.clearAttributes();
+ node.mergeAttributes(element);
+ node.removeAttribute('uid');
+ if (node.options){
+ var no = node.options, eo = element.options;
+ for (var j = no.length; j--;) no[j].selected = eo[j].selected;
+ }
+ }
+ /**/
+ var prop = formProps[element.tagName.toLowerCase()];
+ if (prop && element[prop]) node[prop] = element[prop];
+ }
+
+ /**/
+ if (Browser.ie){
+ var co = clone.getElementsByTagName('object'), to = this.getElementsByTagName('object');
+ for (i = co.length; i--;) co[i].outerHTML = to[i].outerHTML;
+ }
+ /**/
+ return document.id(clone);
}
- var prop = formProps[element.tagName.toLowerCase()];
- if (prop && element[prop]) node[prop] = element[prop];
-};
-
-Element.implement('clone', function(contents, keepid){
- contents = contents !== false;
- var clone = this.cloneNode(contents), i;
-
- if (contents){
- var ce = clone.getElementsByTagName('*'), te = this.getElementsByTagName('*');
- for (i = ce.length; i--;) cleanClone(ce[i], te[i], keepid);
- }
-
- cleanClone(clone, this, keepid);
-
- if (Browser.ie){
- var co = clone.getElementsByTagName('object'), to = this.getElementsByTagName('object');
- for (i = co.length; i--;) co[i].outerHTML = to[i].outerHTML;
- }
- return document.id(clone);
});
-var contains = {contains: function(element){
- return Slick.contains(this, element);
-}};
-
-if (!document.contains) Document.implement(contains);
-if (!document.createElement('div').contains) Element.implement(contains);
-
-
-
[Element, Window, Document].invoke('implement', {
addListener: function(type, fn){
@@ -3103,13 +3314,12 @@ if (!document.createElement('div').contains) Element.implement(contains);
});
-// IE purge
+/**/
if (window.attachEvent && !window.addEventListener) window.addListener('unload', function(){
Object.each(collected, clean);
if (window.CollectGarbage) CollectGarbage();
});
-
-})();
+/**/
Element.Properties = {};
@@ -3139,15 +3349,7 @@ Element.Properties.tag = {
};
-(function(maxLength){
- if (maxLength != null) Element.Properties.maxlength = Element.Properties.maxLength = {
- get: function(){
- var maxlength = this.getAttribute('maxLength');
- return maxlength == maxLength ? null : maxlength;
- }
- };
-})(document.createElement('input').getAttribute('maxLength'));
-
+/**/
Element.Properties.html = (function(){
var tableTest = Function.attempt(function(){
@@ -3165,10 +3367,26 @@ Element.Properties.html = (function(){
};
translations.thead = translations.tfoot = translations.tbody;
+ /**/
+ // technique by jdbarlett - http://jdbartlett.com/innershiv/
+ wrapper.innerHTML = '';
+ var HTML5Test = wrapper.childNodes.length == 1;
+ if (!HTML5Test){
+ var tags = 'abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video'.split(' '),
+ fragment = document.createDocumentFragment(), l = tags.length;
+ while (l--) fragment.createElement(tags[l]);
+ fragment.appendChild(wrapper);
+ }
+ /**/
+
var html = {
- set: function(){
- var html = Array.flatten(arguments).join('');
+ set: function(html){
+ if (typeOf(html) == 'array') html = html.join('');
+
var wrap = (!tableTest && translations[this.get('tag')]);
+ /**/
+ if (!wrap && !HTML5Test) wrap = [0, '', ''];
+ /**/
if (wrap){
var first = wrapper;
first.innerHTML = wrap[1] + html + wrap[2];
@@ -3184,6 +3402,41 @@ Element.Properties.html = (function(){
return html;
})();
+/*!webkit>*/
+
+/**/
+var testForm = document.createElement('form');
+testForm.innerHTML = '';
+
+if (testForm.firstChild.value != 's') Element.Properties.value = {
+
+ set: function(value){
+ var tag = this.get('tag');
+ if (tag != 'select') return this.setProperty('value', value);
+ var options = this.getElements('option');
+ for (var i = 0; i < options.length; i++){
+ var option = options[i],
+ attr = option.getAttributeNode('value'),
+ optionValue = (attr && attr.specified) ? option.value : option.get('text');
+ if (optionValue == value) return option.selected = true;
+ }
+ },
+
+ get: function(){
+ var option = this, tag = option.get('tag');
+
+ if (tag != 'select' && tag != 'option') return this.getProperty('value');
+
+ if (tag == 'select' && !(option = option.getSelected()[0])) return '';
+
+ var attr = option.getAttributeNode('value');
+ return (attr && attr.specified) ? option.value : option.get('text');
+ }
+
+};
+/**/
+
+})();
/*
@@ -3210,40 +3463,38 @@ Element.Properties.styles = {set: function(styles){
this.setStyles(styles);
}};
-var hasOpacity = (html.style.opacity != null);
-var reAlpha = /alpha\(opacity=([\d.]+)\)/i;
+var hasOpacity = (html.style.opacity != null),
+ hasFilter = (html.style.filter != null),
+ reAlpha = /alpha\(opacity=([\d.]+)\)/i;
-var setOpacity = function(element, opacity){
+var setVisibility = function(element, opacity){
+ element.store('$opacity', opacity);
+ element.style.visibility = opacity > 0 ? 'visible' : 'hidden';
+};
+
+var setOpacity = (hasOpacity ? function(element, opacity){
+ element.style.opacity = opacity;
+} : (hasFilter ? function(element, opacity){
if (!element.currentStyle || !element.currentStyle.hasLayout) element.style.zoom = 1;
- if (hasOpacity){
- element.style.opacity = opacity;
- } else {
- opacity = (opacity == 1) ? '' : 'alpha(opacity=' + opacity * 100 + ')';
- var filter = element.style.filter || element.getComputedStyle('filter') || '';
- element.style.filter = reAlpha.test(filter) ? filter.replace(reAlpha, opacity) : filter + opacity;
- }
-};
+ opacity = (opacity * 100).limit(0, 100).round();
+ opacity = (opacity == 100) ? '' : 'alpha(opacity=' + opacity + ')';
+ var filter = element.style.filter || element.getComputedStyle('filter') || '';
+ element.style.filter = reAlpha.test(filter) ? filter.replace(reAlpha, opacity) : filter + opacity;
+} : setVisibility));
-Element.Properties.opacity = {
-
- set: function(opacity){
- var visibility = this.style.visibility;
- if (opacity == 0 && visibility != 'hidden') this.style.visibility = 'hidden';
- else if (opacity != 0 && visibility != 'visible') this.style.visibility = 'visible';
-
- setOpacity(this, opacity);
- },
-
- get: (hasOpacity) ? function(){
- var opacity = this.style.opacity || this.getComputedStyle('opacity');
- return (opacity == '') ? 1 : opacity;
- } : function(){
- var opacity, filter = (this.style.filter || this.getComputedStyle('filter'));
- if (filter) opacity = filter.match(reAlpha);
- return (opacity == null || filter == null) ? 1 : (opacity[1] / 100);
- }
-
-};
+var getOpacity = (hasOpacity ? function(element){
+ var opacity = element.style.opacity || element.getComputedStyle('opacity');
+ return (opacity == '') ? 1 : opacity.toFloat();
+} : (hasFilter ? function(element){
+ var filter = (element.style.filter || element.getComputedStyle('filter')),
+ opacity;
+ if (filter) opacity = filter.match(reAlpha);
+ return (opacity == null || filter == null) ? 1 : (opacity[1] / 100);
+} : function(element){
+ var opacity = element.retrieve('$opacity');
+ if (opacity == null) opacity = (element.style.visibility == 'hidden' ? 0 : 1);
+ return opacity;
+}));
var floatName = (html.style.cssFloat == null) ? 'styleFloat' : 'cssFloat';
@@ -3256,21 +3507,12 @@ Element.implement({
return (computed) ? computed.getPropertyValue((property == floatName) ? 'float' : property.hyphenate()) : null;
},
- setOpacity: function(value){
- setOpacity(this, value);
- return this;
- },
-
- getOpacity: function(){
- return this.get('opacity');
- },
-
setStyle: function(property, value){
- switch (property){
- case 'opacity': return this.set('opacity', parseFloat(value));
- case 'float': property = floatName;
+ if (property == 'opacity'){
+ setOpacity(this, parseFloat(value));
+ return this;
}
- property = property.camelCase();
+ property = (property == 'float' ? floatName : property).camelCase();
if (typeOf(value) != 'string'){
var map = (Element.Styles[property] || '@').split(' ');
value = Array.from(value).map(function(val, i){
@@ -3285,11 +3527,8 @@ Element.implement({
},
getStyle: function(property){
- switch (property){
- case 'opacity': return this.get('opacity');
- case 'float': property = floatName;
- }
- property = property.camelCase();
+ if (property == 'opacity') return getOpacity(this);
+ property = (property == 'float' ? floatName : property).camelCase();
var result = this.style[property];
if (!result || property == 'zIndex'){
result = [];
@@ -3346,6 +3585,8 @@ Element.Styles = {
+
+
Element.ShortStyles = {margin: {}, padding: {}, border: {}, borderWidth: {}, borderStyle: {}, borderColor: {}};
['Top', 'Right', 'Bottom', 'Left'].each(function(direction){
@@ -3364,7 +3605,515 @@ Element.ShortStyles = {margin: {}, padding: {}, border: {}, borderWidth: {}, bor
Short.borderColor[bdc] = Short[bd][bdc] = All[bdc] = 'rgb(@, @, @)';
});
-}).call(this);
+})();
+
+
+/*
+---
+
+name: Event
+
+description: Contains the Event Type, to make the event object cross-browser.
+
+license: MIT-style license.
+
+requires: [Window, Document, Array, Function, String, Object]
+
+provides: Event
+
+...
+*/
+
+(function() {
+
+var _keys = {};
+
+var DOMEvent = this.DOMEvent = new Type('DOMEvent', function(event, win){
+ if (!win) win = window;
+ event = event || win.event;
+ if (event.$extended) return event;
+ this.event = event;
+ this.$extended = true;
+ this.shift = event.shiftKey;
+ this.control = event.ctrlKey;
+ this.alt = event.altKey;
+ this.meta = event.metaKey;
+ var type = this.type = event.type;
+ var target = event.target || event.srcElement;
+ while (target && target.nodeType == 3) target = target.parentNode;
+ this.target = document.id(target);
+
+ if (type.indexOf('key') == 0){
+ var code = this.code = (event.which || event.keyCode);
+ this.key = _keys[code];
+ if (type == 'keydown'){
+ if (code > 111 && code < 124) this.key = 'f' + (code - 111);
+ else if (code > 95 && code < 106) this.key = code - 96;
+ }
+ if (this.key == null) this.key = String.fromCharCode(code).toLowerCase();
+ } else if (type == 'click' || type == 'dblclick' || type == 'contextmenu' || type.indexOf('mouse') == 0){
+ var doc = win.document;
+ doc = (!doc.compatMode || doc.compatMode == 'CSS1Compat') ? doc.html : doc.body;
+ this.page = {
+ x: (event.pageX != null) ? event.pageX : event.clientX + doc.scrollLeft,
+ y: (event.pageY != null) ? event.pageY : event.clientY + doc.scrollTop
+ };
+ this.client = {
+ x: (event.pageX != null) ? event.pageX - win.pageXOffset : event.clientX,
+ y: (event.pageY != null) ? event.pageY - win.pageYOffset : event.clientY
+ };
+ if (type == 'DOMMouseScroll' || type == 'mousewheel')
+ this.wheel = (event.wheelDelta) ? event.wheelDelta / 120 : -(event.detail || 0) / 3;
+
+ this.rightClick = (event.which == 3 || event.button == 2);
+ if (type == 'mouseover' || type == 'mouseout'){
+ var related = event.relatedTarget || event[(type == 'mouseover' ? 'from' : 'to') + 'Element'];
+ while (related && related.nodeType == 3) related = related.parentNode;
+ this.relatedTarget = document.id(related);
+ }
+ } else if (type.indexOf('touch') == 0 ||Â type.indexOf('gesture') == 0){
+ this.rotation = event.rotation;
+ this.scale = event.scale;
+ this.targetTouches = event.targetTouches;
+ this.changedTouches = event.changedTouches;
+ var touches = this.touches = event.touches;
+ if (touches && touches[0]){
+ var touch = touches[0];
+ this.page = {x: touch.pageX, y: touch.pageY};
+ this.client = {x: touch.clientX, y: touch.clientY};
+ }
+ }
+
+ if (!this.client) this.client = {};
+ if (!this.page) this.page = {};
+});
+
+DOMEvent.implement({
+
+ stop: function(){
+ return this.preventDefault().stopPropagation();
+ },
+
+ stopPropagation: function(){
+ if (this.event.stopPropagation) this.event.stopPropagation();
+ else this.event.cancelBubble = true;
+ return this;
+ },
+
+ preventDefault: function(){
+ if (this.event.preventDefault) this.event.preventDefault();
+ else this.event.returnValue = false;
+ return this;
+ }
+
+});
+
+DOMEvent.defineKey = function(code, key){
+ _keys[code] = key;
+ return this;
+};
+
+DOMEvent.defineKeys = DOMEvent.defineKey.overloadSetter(true);
+
+DOMEvent.defineKeys({
+ '38': 'up', '40': 'down', '37': 'left', '39': 'right',
+ '27': 'esc', '32': 'space', '8': 'backspace', '9': 'tab',
+ '46': 'delete', '13': 'enter'
+});
+
+})();
+
+
+
+
+
+
+/*
+---
+
+name: Element.Event
+
+description: Contains Element methods for dealing with events. This file also includes mouseenter and mouseleave custom Element Events.
+
+license: MIT-style license.
+
+requires: [Element, Event]
+
+provides: Element.Event
+
+...
+*/
+
+(function(){
+
+Element.Properties.events = {set: function(events){
+ this.addEvents(events);
+}};
+
+[Element, Window, Document].invoke('implement', {
+
+ addEvent: function(type, fn){
+ var events = this.retrieve('events', {});
+ if (!events[type]) events[type] = {keys: [], values: []};
+ if (events[type].keys.contains(fn)) return this;
+ events[type].keys.push(fn);
+ var realType = type,
+ custom = Element.Events[type],
+ condition = fn,
+ self = this;
+ if (custom){
+ if (custom.onAdd) custom.onAdd.call(this, fn, type);
+ if (custom.condition){
+ condition = function(event){
+ if (custom.condition.call(this, event, type)) return fn.call(this, event);
+ return true;
+ };
+ }
+ if (custom.base) realType = Function.from(custom.base).call(this, type);
+ }
+ var defn = function(){
+ return fn.call(self);
+ };
+ var nativeEvent = Element.NativeEvents[realType];
+ if (nativeEvent){
+ if (nativeEvent == 2){
+ defn = function(event){
+ event = new DOMEvent(event, self.getWindow());
+ if (condition.call(self, event) === false) event.stop();
+ };
+ }
+ this.addListener(realType, defn, arguments[2]);
+ }
+ events[type].values.push(defn);
+ return this;
+ },
+
+ removeEvent: function(type, fn){
+ var events = this.retrieve('events');
+ if (!events || !events[type]) return this;
+ var list = events[type];
+ var index = list.keys.indexOf(fn);
+ if (index == -1) return this;
+ var value = list.values[index];
+ delete list.keys[index];
+ delete list.values[index];
+ var custom = Element.Events[type];
+ if (custom){
+ if (custom.onRemove) custom.onRemove.call(this, fn, type);
+ if (custom.base) type = Function.from(custom.base).call(this, type);
+ }
+ return (Element.NativeEvents[type]) ? this.removeListener(type, value, arguments[2]) : this;
+ },
+
+ addEvents: function(events){
+ for (var event in events) this.addEvent(event, events[event]);
+ return this;
+ },
+
+ removeEvents: function(events){
+ var type;
+ if (typeOf(events) == 'object'){
+ for (type in events) this.removeEvent(type, events[type]);
+ return this;
+ }
+ var attached = this.retrieve('events');
+ if (!attached) return this;
+ if (!events){
+ for (type in attached) this.removeEvents(type);
+ this.eliminate('events');
+ } else if (attached[events]){
+ attached[events].keys.each(function(fn){
+ this.removeEvent(events, fn);
+ }, this);
+ delete attached[events];
+ }
+ return this;
+ },
+
+ fireEvent: function(type, args, delay){
+ var events = this.retrieve('events');
+ if (!events || !events[type]) return this;
+ args = Array.from(args);
+
+ events[type].keys.each(function(fn){
+ if (delay) fn.delay(delay, this, args);
+ else fn.apply(this, args);
+ }, this);
+ return this;
+ },
+
+ cloneEvents: function(from, type){
+ from = document.id(from);
+ var events = from.retrieve('events');
+ if (!events) return this;
+ if (!type){
+ for (var eventType in events) this.cloneEvents(from, eventType);
+ } else if (events[type]){
+ events[type].keys.each(function(fn){
+ this.addEvent(type, fn);
+ }, this);
+ }
+ return this;
+ }
+
+});
+
+Element.NativeEvents = {
+ click: 2, dblclick: 2, mouseup: 2, mousedown: 2, contextmenu: 2, //mouse buttons
+ mousewheel: 2, DOMMouseScroll: 2, //mouse wheel
+ mouseover: 2, mouseout: 2, mousemove: 2, selectstart: 2, selectend: 2, //mouse movement
+ keydown: 2, keypress: 2, keyup: 2, //keyboard
+ orientationchange: 2, // mobile
+ touchstart: 2, touchmove: 2, touchend: 2, touchcancel: 2, // touch
+ gesturestart: 2, gesturechange: 2, gestureend: 2, // gesture
+ focus: 2, blur: 2, change: 2, reset: 2, select: 2, submit: 2, paste: 2, oninput: 2, //form elements
+ load: 2, unload: 1, beforeunload: 2, resize: 1, move: 1, DOMContentLoaded: 1, readystatechange: 1, //window
+ error: 1, abort: 1, scroll: 1 //misc
+};
+
+var check = function(event){
+ var related = event.relatedTarget;
+ if (related == null) return true;
+ if (!related) return false;
+ return (related != this && related.prefix != 'xul' && typeOf(this) != 'document' && !this.contains(related));
+};
+
+Element.Events = {
+
+ mouseenter: {
+ base: 'mouseover',
+ condition: check
+ },
+
+ mouseleave: {
+ base: 'mouseout',
+ condition: check
+ },
+
+ mousewheel: {
+ base: (Browser.firefox) ? 'DOMMouseScroll' : 'mousewheel'
+ }
+
+};
+
+/**/
+if (!window.addEventListener){
+ Element.NativeEvents.propertychange = 2;
+ Element.Events.change = {
+ base: function(){
+ var type = this.type;
+ return (this.get('tag') == 'input' && (type == 'radio' || type == 'checkbox')) ? 'propertychange' : 'change'
+ },
+ condition: function(event){
+ return !!(this.type != 'radio' || this.checked);
+ }
+ }
+}
+/**/
+
+
+
+})();
+
+
+/*
+---
+
+name: Element.Delegation
+
+description: Extends the Element native object to include the delegate method for more efficient event management.
+
+license: MIT-style license.
+
+requires: [Element.Event]
+
+provides: [Element.Delegation]
+
+...
+*/
+
+(function(){
+
+var eventListenerSupport = !!window.addEventListener;
+
+Element.NativeEvents.focusin = Element.NativeEvents.focusout = 2;
+
+var bubbleUp = function(self, match, fn, event){
+ var target = event.target;
+ while (target && target != self){
+ if (match(target, event)) return fn.call(target, event, target);
+ target = document.id(target.parentNode);
+ }
+};
+
+var map = {
+ mouseenter: {
+ base: 'mouseover'
+ },
+ mouseleave: {
+ base: 'mouseout'
+ },
+ focus: {
+ base: 'focus' + (eventListenerSupport ? '' : 'in'),
+ capture: true
+ },
+ blur: {
+ base: eventListenerSupport ? 'blur' : 'focusout',
+ capture: true
+ }
+};
+
+/**/
+var _key = '$delegation:';
+var formObserver = function(type){
+
+ return {
+
+ base: 'focusin',
+
+ remove: function(self, uid){
+ var list = self.retrieve(_key + type + 'listeners', {})[uid];
+ if (list && list.forms) for (var i = list.forms.length; i--;){
+ list.forms[i].removeEvent(type, list.fns[i]);
+ }
+ },
+
+ listen: function(self, match, fn, event, uid){
+ var target = event.target,
+ form = (target.get('tag') == 'form') ? target : event.target.getParent('form');
+ if (!form) return;
+
+ var listeners = self.retrieve(_key + type + 'listeners', {}),
+ listener = listeners[uid] || {forms: [], fns: []},
+ forms = listener.forms, fns = listener.fns;
+
+ if (forms.indexOf(form) != -1) return;
+ forms.push(form);
+
+ var _fn = function(event){
+ bubbleUp(self, match, fn, event);
+ };
+ form.addEvent(type, _fn);
+ fns.push(_fn);
+
+ listeners[uid] = listener;
+ self.store(_key + type + 'listeners', listeners);
+ }
+ };
+};
+
+var inputObserver = function(type){
+ return {
+ base: 'focusin',
+ listen: function(self, match, fn, event){
+ var events = {blur: function(){
+ this.removeEvents(events);
+ }};
+ events[type] = function(event){
+ bubbleUp(self, match, fn, event);
+ };
+ event.target.addEvents(events);
+ }
+ };
+};
+
+if (!eventListenerSupport) Object.append(map, {
+ submit: formObserver('submit'),
+ reset: formObserver('reset'),
+ change: inputObserver('change'),
+ select: inputObserver('select')
+});
+/**/
+
+var proto = Element.prototype,
+ addEvent = proto.addEvent,
+ removeEvent = proto.removeEvent;
+
+var relay = function(old, method){
+ return function(type, fn, useCapture){
+ if (type.indexOf(':relay') == -1) return old.call(this, type, fn, useCapture);
+ var parsed = Slick.parse(type).expressions[0][0];
+ if (parsed.pseudos[0].key != 'relay') return old.call(this, type, fn, useCapture);
+ var newType = parsed.tag;
+ parsed.pseudos.slice(1).each(function(pseudo){
+ newType += ':' + pseudo.key + (pseudo.value ? '(' + pseudo.value + ')' : '');
+ });
+ return method.call(this, newType, parsed.pseudos[0].value, fn);
+ };
+};
+
+var delegation = {
+
+ addEvent: function(type, match, fn){
+ var storage = this.retrieve('$delegates', {}), stored = storage[type];
+ if (stored) for (var _uid in stored){
+ if (stored[_uid].fn == fn && stored[_uid].match == match) return this;
+ }
+
+ var _type = type, _match = match, _fn = fn, _map = map[type] || {};
+ type = _map.base || _type;
+
+ match = function(target){
+ return Slick.match(target, _match);
+ };
+
+ var elementEvent = Element.Events[_type];
+ if (elementEvent && elementEvent.condition){
+ var __match = match, condition = elementEvent.condition;
+ match = function(target, event){
+ return __match(target, event) && condition.call(target, event, type);
+ };
+ }
+
+ var self = this, uid = String.uniqueID();
+ var delegator = _map.listen ? function(event){
+ _map.listen(self, match, fn, event, uid);
+ } : function(event){
+ bubbleUp(self, match, fn, event);
+ };
+
+ if (!stored) stored = {};
+ stored[uid] = {
+ match: _match,
+ fn: _fn,
+ delegator: delegator
+ };
+ storage[_type] = stored;
+ return addEvent.call(this, type, delegator, _map.capture);
+ },
+
+ removeEvent: function(type, match, fn, _uid){
+ var storage = this.retrieve('$delegates', {}), stored = storage[type];
+ if (!stored) return this;
+
+ if (_uid){
+ var _type = type, delegator = stored[_uid].delegator, _map = map[type] || {};
+ type = _map.base || _type;
+ if (_map.remove) _map.remove(this, _uid);
+ delete stored[_uid];
+ storage[_type] = stored;
+ return removeEvent.call(this, type, delegator);
+ }
+
+ var __uid, s;
+ if (fn) for (__uid in stored){
+ s = stored[__uid];
+ if (s.match == match && s.fn == fn) return delegation.removeEvent.call(this, type, match, fn, __uid);
+ } else for (__uid in stored){
+ s = stored[__uid];
+ if (s.match == match) delegation.removeEvent.call(this, type, match, s.fn, __uid);
+ }
+ return this;
+ }
+
+};
+
+[Element, Window, Document].invoke('implement', {
+ addEvent: relay(addEvent, delegation.addEvent),
+ removeEvent: relay(removeEvent, delegation.removeEvent)
+});
+
+})();
/*
@@ -3506,14 +4255,13 @@ Element.implement({
},
getPosition: function(relative){
- if (isBody(this)) return {x: 0, y: 0};
var offset = this.getOffsets(),
scroll = this.getScrolls();
var position = {
x: offset.x - scroll.x,
y: offset.y - scroll.y
};
-
+
if (relative && (relative = document.id(relative))){
var relativePosition = relative.getPosition();
return {x: position.x - relativePosition.x - leftBorder(relative), y: position.y - relativePosition.y - topBorder(relative)};
@@ -3610,7 +4358,7 @@ function getCompatElement(element){
return (!doc.compatMode || doc.compatMode == 'CSS1Compat') ? doc.html : doc.body;
}
-}).call(this);
+})();
//aliases
Element.alias({position: 'setPosition'}); //compatability
@@ -3707,7 +4455,7 @@ var Fx = this.Fx = new Class({
} else {
this.frame++;
}
-
+
if (this.frame < this.frames){
var delta = this.transition(this.frame / this.frames);
this.set(this.compute(this.from, this.to, delta));
@@ -3750,7 +4498,7 @@ var Fx = this.Fx = new Class({
pushInstance.call(this, fps);
return this;
},
-
+
stop: function(){
if (this.isRunning()){
this.time = null;
@@ -3764,7 +4512,7 @@ var Fx = this.Fx = new Class({
}
return this;
},
-
+
cancel: function(){
if (this.isRunning()){
this.time = null;
@@ -3774,7 +4522,7 @@ var Fx = this.Fx = new Class({
}
return this;
},
-
+
pause: function(){
if (this.isRunning()){
this.time = null;
@@ -3782,12 +4530,12 @@ var Fx = this.Fx = new Class({
}
return this;
},
-
+
resume: function(){
if ((this.frame < this.frames) && !this.isRunning()) pushInstance.call(this, this.options.fps);
return this;
},
-
+
isRunning: function(){
var list = instances[this.options.fps];
return list && list.contains(this);
@@ -3830,7 +4578,7 @@ var pullInstance = function(fps){
}
};
-}).call(this);
+})();
/*
@@ -4058,7 +4806,7 @@ Element.implement({
case 'show': fade.set(o, 1); break;
case 'hide': fade.set(o, 0); break;
case 'toggle':
- var flag = this.retrieve('fade:flag', this.get('opacity') == 1);
+ var flag = this.retrieve('fade:flag', this.getStyle('opacity') == 1);
fade.start(o, (flag) ? 0 : 1);
this.store('fade:flag', !flag);
toggle = true;
@@ -4275,128 +5023,6 @@ Fx.Transitions.extend({
});
-/*
----
-
-name: Object
-
-description: Object generic methods
-
-license: MIT-style license.
-
-requires: Type
-
-provides: [Object, Hash]
-
-...
-*/
-
-(function(){
-
-var hasOwnProperty = Object.prototype.hasOwnProperty;
-
-Object.extend({
-
- subset: function(object, keys){
- var results = {};
- for (var i = 0, l = keys.length; i < l; i++){
- var k = keys[i];
- results[k] = object[k];
- }
- return results;
- },
-
- map: function(object, fn, bind){
- var results = {};
- for (var key in object){
- if (hasOwnProperty.call(object, key)) results[key] = fn.call(bind, object[key], key, object);
- }
- return results;
- },
-
- filter: function(object, fn, bind){
- var results = {};
- Object.each(object, function(value, key){
- if (fn.call(bind, value, key, object)) results[key] = value;
- });
- return results;
- },
-
- every: function(object, fn, bind){
- for (var key in object){
- if (hasOwnProperty.call(object, key) && !fn.call(bind, object[key], key)) return false;
- }
- return true;
- },
-
- some: function(object, fn, bind){
- for (var key in object){
- if (hasOwnProperty.call(object, key) && fn.call(bind, object[key], key)) return true;
- }
- return false;
- },
-
- keys: function(object){
- var keys = [];
- for (var key in object){
- if (hasOwnProperty.call(object, key)) keys.push(key);
- }
- return keys;
- },
-
- values: function(object){
- var values = [];
- for (var key in object){
- if (hasOwnProperty.call(object, key)) values.push(object[key]);
- }
- return values;
- },
-
- getLength: function(object){
- return Object.keys(object).length;
- },
-
- keyOf: function(object, value){
- for (var key in object){
- if (hasOwnProperty.call(object, key) && object[key] === value) return key;
- }
- return null;
- },
-
- contains: function(object, value){
- return Object.keyOf(object, value) != null;
- },
-
- toQueryString: function(object, base){
- var queryString = [];
-
- Object.each(object, function(value, key){
- if (base) key = base + '[' + key + ']';
- var result;
- switch (typeOf(value)){
- case 'object': result = Object.toQueryString(value, key); break;
- case 'array':
- var qs = {};
- value.each(function(val, i){
- qs[i] = val;
- });
- result = Object.toQueryString(qs, key);
- break;
- default: result = key + '=' + encodeURIComponent(value);
- }
- if (value != null) queryString.push(result);
- });
-
- return queryString.join('&');
- }
-
-});
-
-})();
-
-
-
-
/*
---
@@ -4472,7 +5098,7 @@ var Request = this.Request = new Class({
xhr.onreadystatechange = empty;
if (progressSupport) xhr.onprogress = xhr.onloadstart = empty;
clearTimeout(this.timer);
-
+
this.response = {text: this.xhr.responseText || '', xml: this.xhr.responseXML};
if (this.options.isSuccess.call(this, this.status))
this.success(this.response.text, this.response.xml);
@@ -4509,15 +5135,15 @@ var Request = this.Request = new Class({
onFailure: function(){
this.fireEvent('complete').fireEvent('failure', this.xhr);
},
-
+
loadstart: function(event){
this.fireEvent('loadstart', [event, this.xhr]);
},
-
+
progress: function(event){
this.fireEvent('progress', [event, this.xhr]);
},
-
+
timeout: function(){
this.fireEvent('timeout', this.xhr);
},
@@ -4541,7 +5167,7 @@ var Request = this.Request = new Class({
}
return false;
},
-
+
send: function(options){
if (!this.check(options)) return this;
@@ -4577,7 +5203,7 @@ var Request = this.Request = new Class({
}
if (!url) url = document.location.pathname;
-
+
var trimPosition = url.lastIndexOf('/');
if (trimPosition > -1 && (trimPosition = url.indexOf('#')) > -1) url = url.substr(0, trimPosition);
@@ -4597,7 +5223,7 @@ var Request = this.Request = new Class({
xhr.open(method.toUpperCase(), url, this.options.async, this.options.user, this.options.password);
if (this.options.user && 'withCredentials' in xhr) xhr.withCredentials = true;
-
+
xhr.onreadystatechange = this.onStateChange.bind(this);
Object.each(this.headers, function(value, key){
@@ -4685,7 +5311,7 @@ description: JSON encoder and decoder.
license: MIT-style license.
-See Also:
+SeeAlso:
requires: [Array, String, Number, Function]
@@ -4749,7 +5375,7 @@ JSON.decode = function(string, secure){
return eval('(' + string + ')');
};
-}).call(this);
+})();
/*
@@ -4803,309 +5429,79 @@ Request.JSON = new Class({
/*
---
-name: Event
+name: Cookie
-description: Contains the Event Class, to make the event object cross-browser.
+description: Class for creating, reading, and deleting browser Cookies.
license: MIT-style license.
-requires: [Window, Document, Array, Function, String, Object]
+credits:
+ - Based on the functions by Peter-Paul Koch (http://quirksmode.org).
-provides: Event
+requires: [Options, Browser]
+
+provides: Cookie
...
*/
-var Event = new Type('Event', function(event, win){
- if (!win) win = window;
- var doc = win.document;
- event = event || win.event;
- if (event.$extended) return event;
- this.$extended = true;
- var type = event.type,
- target = event.target || event.srcElement,
- page = {},
- client = {},
- related = null,
- rightClick, wheel, code, key;
- while (target && target.nodeType == 3) target = target.parentNode;
+var Cookie = new Class({
- if (type.indexOf('key') != -1){
- code = event.which || event.keyCode;
- key = Object.keyOf(Event.Keys, code);
- if (type == 'keydown'){
- var fKey = code - 111;
- if (fKey > 0 && fKey < 13) key = 'f' + fKey;
- }
- if (!key) key = String.fromCharCode(code).toLowerCase();
- } else if ((/click|mouse|menu/i).test(type)){
- doc = (!doc.compatMode || doc.compatMode == 'CSS1Compat') ? doc.html : doc.body;
- page = {
- x: (event.pageX != null) ? event.pageX : event.clientX + doc.scrollLeft,
- y: (event.pageY != null) ? event.pageY : event.clientY + doc.scrollTop
- };
- client = {
- x: (event.pageX != null) ? event.pageX - win.pageXOffset : event.clientX,
- y: (event.pageY != null) ? event.pageY - win.pageYOffset : event.clientY
- };
- if ((/DOMMouseScroll|mousewheel/).test(type)){
- wheel = (event.wheelDelta) ? event.wheelDelta / 120 : -(event.detail || 0) / 3;
- }
- rightClick = (event.which == 3) || (event.button == 2);
- if ((/over|out/).test(type)){
- related = event.relatedTarget || event[(type == 'mouseover' ? 'from' : 'to') + 'Element'];
- var testRelated = function(){
- while (related && related.nodeType == 3) related = related.parentNode;
- return true;
- };
- var hasRelated = (Browser.firefox2) ? testRelated.attempt() : testRelated();
- related = (hasRelated) ? related : null;
- }
- } else if ((/gesture|touch/i).test(type)){
- this.rotation = event.rotation;
- this.scale = event.scale;
- this.targetTouches = event.targetTouches;
- this.changedTouches = event.changedTouches;
- var touches = this.touches = event.touches;
- if (touches && touches[0]){
- var touch = touches[0];
- page = {x: touch.pageX, y: touch.pageY};
- client = {x: touch.clientX, y: touch.clientY};
- }
- }
+ Implements: Options,
- return Object.append(this, {
- event: event,
- type: type,
-
- page: page,
- client: client,
- rightClick: rightClick,
-
- wheel: wheel,
-
- relatedTarget: document.id(related),
- target: document.id(target),
-
- code: code,
- key: key,
-
- shift: event.shiftKey,
- control: event.ctrlKey,
- alt: event.altKey,
- meta: event.metaKey
- });
-});
-
-Event.Keys = {
- 'enter': 13,
- 'up': 38,
- 'down': 40,
- 'left': 37,
- 'right': 39,
- 'esc': 27,
- 'space': 32,
- 'backspace': 8,
- 'tab': 9,
- 'delete': 46
-};
-
-
-
-Event.implement({
-
- stop: function(){
- return this.stopPropagation().preventDefault();
+ options: {
+ path: '/',
+ domain: false,
+ duration: false,
+ secure: false,
+ document: document,
+ encode: true
},
- stopPropagation: function(){
- if (this.event.stopPropagation) this.event.stopPropagation();
- else this.event.cancelBubble = true;
+ initialize: function(key, options){
+ this.key = key;
+ this.setOptions(options);
+ },
+
+ write: function(value){
+ if (this.options.encode) value = encodeURIComponent(value);
+ if (this.options.domain) value += '; domain=' + this.options.domain;
+ if (this.options.path) value += '; path=' + this.options.path;
+ if (this.options.duration){
+ var date = new Date();
+ date.setTime(date.getTime() + this.options.duration * 24 * 60 * 60 * 1000);
+ value += '; expires=' + date.toGMTString();
+ }
+ if (this.options.secure) value += '; secure';
+ this.options.document.cookie = this.key + '=' + value;
return this;
},
- preventDefault: function(){
- if (this.event.preventDefault) this.event.preventDefault();
- else this.event.returnValue = false;
+ read: function(){
+ var value = this.options.document.cookie.match('(?:^|;)\\s*' + this.key.escapeRegExp() + '=([^;]*)');
+ return (value) ? decodeURIComponent(value[1]) : null;
+ },
+
+ dispose: function(){
+ new Cookie(this.key, Object.merge({}, this.options, {duration: -1})).write('');
return this;
}
});
-
-/*
----
-
-name: Element.Event
-
-description: Contains Element methods for dealing with events. This file also includes mouseenter and mouseleave custom Element Events.
-
-license: MIT-style license.
-
-requires: [Element, Event]
-
-provides: Element.Event
-
-...
-*/
-
-(function(){
-
-Element.Properties.events = {set: function(events){
- this.addEvents(events);
-}};
-
-[Element, Window, Document].invoke('implement', {
-
- addEvent: function(type, fn){
- var events = this.retrieve('events', {});
- if (!events[type]) events[type] = {keys: [], values: []};
- if (events[type].keys.contains(fn)) return this;
- events[type].keys.push(fn);
- var realType = type,
- custom = Element.Events[type],
- condition = fn,
- self = this;
- if (custom){
- if (custom.onAdd) custom.onAdd.call(this, fn);
- if (custom.condition){
- condition = function(event){
- if (custom.condition.call(this, event)) return fn.call(this, event);
- return true;
- };
- }
- realType = custom.base || realType;
- }
- var defn = function(){
- return fn.call(self);
- };
- var nativeEvent = Element.NativeEvents[realType];
- if (nativeEvent){
- if (nativeEvent == 2){
- defn = function(event){
- event = new Event(event, self.getWindow());
- if (condition.call(self, event) === false) event.stop();
- };
- }
- this.addListener(realType, defn, arguments[2]);
- }
- events[type].values.push(defn);
- return this;
- },
-
- removeEvent: function(type, fn){
- var events = this.retrieve('events');
- if (!events || !events[type]) return this;
- var list = events[type];
- var index = list.keys.indexOf(fn);
- if (index == -1) return this;
- var value = list.values[index];
- delete list.keys[index];
- delete list.values[index];
- var custom = Element.Events[type];
- if (custom){
- if (custom.onRemove) custom.onRemove.call(this, fn);
- type = custom.base || type;
- }
- return (Element.NativeEvents[type]) ? this.removeListener(type, value, arguments[2]) : this;
- },
-
- addEvents: function(events){
- for (var event in events) this.addEvent(event, events[event]);
- return this;
- },
-
- removeEvents: function(events){
- var type;
- if (typeOf(events) == 'object'){
- for (type in events) this.removeEvent(type, events[type]);
- return this;
- }
- var attached = this.retrieve('events');
- if (!attached) return this;
- if (!events){
- for (type in attached) this.removeEvents(type);
- this.eliminate('events');
- } else if (attached[events]){
- attached[events].keys.each(function(fn){
- this.removeEvent(events, fn);
- }, this);
- delete attached[events];
- }
- return this;
- },
-
- fireEvent: function(type, args, delay){
- var events = this.retrieve('events');
- if (!events || !events[type]) return this;
- args = Array.from(args);
-
- events[type].keys.each(function(fn){
- if (delay) fn.delay(delay, this, args);
- else fn.apply(this, args);
- }, this);
- return this;
- },
-
- cloneEvents: function(from, type){
- from = document.id(from);
- var events = from.retrieve('events');
- if (!events) return this;
- if (!type){
- for (var eventType in events) this.cloneEvents(from, eventType);
- } else if (events[type]){
- events[type].keys.each(function(fn){
- this.addEvent(type, fn);
- }, this);
- }
- return this;
- }
-
-});
-
-Element.NativeEvents = {
- click: 2, dblclick: 2, mouseup: 2, mousedown: 2, contextmenu: 2, //mouse buttons
- mousewheel: 2, DOMMouseScroll: 2, //mouse wheel
- mouseover: 2, mouseout: 2, mousemove: 2, selectstart: 2, selectend: 2, //mouse movement
- keydown: 2, keypress: 2, keyup: 2, //keyboard
- orientationchange: 2, // mobile
- touchstart: 2, touchmove: 2, touchend: 2, touchcancel: 2, // touch
- gesturestart: 2, gesturechange: 2, gestureend: 2, // gesture
- focus: 2, blur: 2, change: 2, reset: 2, select: 2, submit: 2, //form elements
- load: 2, unload: 1, beforeunload: 2, resize: 1, move: 1, DOMContentLoaded: 1, readystatechange: 1, //window
- error: 1, abort: 1, scroll: 1 //misc
+Cookie.write = function(key, value, options){
+ return new Cookie(key, options).write(value);
};
-var check = function(event){
- var related = event.relatedTarget;
- if (related == null) return true;
- if (!related) return false;
- return (related != this && related.prefix != 'xul' && typeOf(this) != 'document' && !this.contains(related));
+Cookie.read = function(key){
+ return new Cookie(key).read();
};
-Element.Events = {
-
- mouseenter: {
- base: 'mouseover',
- condition: check
- },
-
- mouseleave: {
- base: 'mouseout',
- condition: check
- },
-
- mousewheel: {
- base: (Browser.firefox) ? 'DOMMouseScroll' : 'mousewheel'
- }
-
+Cookie.dispose = function(key, options){
+ return new Cookie(key, options).dispose();
};
-
-}).call(this);
-
-
/*
---
@@ -5129,19 +5525,14 @@ var ready,
checks = [],
shouldPoll,
timer,
- isFramed = true;
-
-// Thanks to Rich Dougherty
-try {
- isFramed = window.frameElement != null;
-} catch(e){}
+ testElement = document.createElement('div');
var domready = function(){
clearTimeout(timer);
if (ready) return;
Browser.loaded = ready = true;
document.removeListener('DOMContentLoaded', domready).removeListener('readystatechange', check);
-
+
document.fireEvent('domready');
window.fireEvent('domready');
};
@@ -5151,7 +5542,6 @@ var check = function(){
domready();
return true;
}
-
return false;
};
@@ -5162,19 +5552,23 @@ var poll = function(){
document.addListener('DOMContentLoaded', domready);
+/**/
// doScroll technique by Diego Perini http://javascript.nwbox.com/IEContentLoaded/
-var testElement = document.createElement('div');
-if (testElement.doScroll && !isFramed){
- checks.push(function(){
- try {
- testElement.doScroll();
- return true;
- } catch (e){}
-
- return false;
- });
+// testElement.doScroll() throws when the DOM is not ready, only in the top window
+var doScrollWorks = function(){
+ try {
+ testElement.doScroll();
+ return true;
+ } catch (e){}
+ return false;
+};
+// If doScroll works already, it can't be used to determine domready
+// e.g. in an iframe
+if (testElement.doScroll && !doScrollWorks()){
+ checks.push(doScrollWorks);
shouldPoll = true;
}
+/**/
if (document.readyState) checks.push(function(){
var state = document.readyState;
@@ -5203,7 +5597,6 @@ Element.Events.load = {
domready();
delete Element.Events.load;
}
-
return true;
}
};
diff --git a/couchpotato/static/scripts/library/mootools_more.js b/couchpotato/static/scripts/library/mootools_more.js
index 6a49319a..18f00451 100644
--- a/couchpotato/static/scripts/library/mootools_more.js
+++ b/couchpotato/static/scripts/library/mootools_more.js
@@ -1,6 +1,6 @@
// MooTools: the javascript framework.
-// Load this file's selection again by visiting: http://mootools.net/more/1e3edb90c5e02d9b9013b54e6ab001ea
-// 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
+// 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.Shortcuts More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical More/Spinner
/*
---
@@ -20,6 +20,7 @@ authors:
- Tim Wienk
- Christoph Pojer
- Aaron Newton
+ - Jacob Thornton
requires:
- Core/MooTools
@@ -30,8 +31,8 @@ provides: [MooTools.More]
*/
MooTools.More = {
- 'version': '1.3.1.1',
- 'build': '0292a3af1eea242b817fecf9daa127417d10d4ce'
+ 'version': '1.4.0.1',
+ '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.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);
},
@@ -1050,12 +689,6 @@ var Drag = new Class({
var limit = options.limit;
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;
for (z in options.modifiers){
if (!options.modifiers[z]) continue;
@@ -1072,7 +705,6 @@ var Drag = new Class({
else this.value.now[z] = this.element[options.modifiers[z]];
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];
@@ -1122,7 +754,6 @@ var Drag = new Class({
this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z];
if (options.invert) this.value.now[z] *= -1;
- if (this._invert[z]) this.value.now[z] *= -1;
if (options.limit && this.limit[z]){
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);
if (this.options.style){
- if (this.options.modifiers.x == "left" && this.options.modifiers.y == "top"){
- var parentStyles,
- parent = element.getOffsetParent();
- var styles = element.getStyles('left', 'top');
+ if (this.options.modifiers.x == 'left' && this.options.modifiers.y == 'top'){
+ var parent = element.getOffsetParent(),
+ styles = element.getStyles('left', 'top');
if (parent && (styles.left == 'auto' || styles.top == 'auto')){
element.setPosition(element.getPosition(parent));
}
@@ -1529,7 +1159,7 @@ var Sortables = new Class({
if (
!this.idle ||
event.rightClick ||
- ['button', 'input', 'a'].contains(event.target.get('tag'))
+ ['button', 'input', 'a', 'textarea'].contains(event.target.get('tag'))
) return;
this.idle = false;
@@ -1640,8 +1270,7 @@ Request.JSONP = new Class({
Implements: [Chain, Events, Options],
- options: {
- /*
+ options: {/*
onRequest: function(src, scriptElement){},
onComplete: function(data){},
onSuccess: function(data){},
@@ -1708,7 +1337,8 @@ Request.JSONP = new Class({
},
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,
src: src
});
@@ -1716,7 +1346,7 @@ Request.JSONP = new Class({
},
success: function(args, index){
- if (!this.running) return false;
+ if (!this.running) return;
this.clear()
.fireEvent('complete', args).fireEvent('success', args)
.callChain();
@@ -1835,10 +1465,10 @@ Class.refactor = function(original, refactors){
Object.each(refactors, function(item, 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(){
var old = this.previous;
- this.previous = origin || function(){};
+ this.previous = origin;
var value = item.apply(this, arguments);
this.previous = old;
return value;
@@ -1875,7 +1505,7 @@ provides: [Class.Binds]
Class.Mutators.Binds = function(binds){
if (!this.prototype.initialize) this.implement('initialize', function(){});
- return binds;
+ return Array.from(binds).concat(this.prototype.Binds || []);
};
Class.Mutators.initialize = function(initialize){
@@ -2055,7 +1685,7 @@ Element.implement({
});
-}).call(this);
+})();
/*
@@ -2071,219 +1701,228 @@ license: MIT-style license
authors:
- Aaron Newton
+ - Jacob Thornton
requires:
+ - Core/Options
- Core/Element.Dimensions
- - /Element.Measure
+ - Element.Measure
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({
position: function(options){
- //call original position if the options are x/y values
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){
- if (v == null) delete options[k];
- });
-
- 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;
+ calculatePosition: function(options){
+ return local.getPosition(this, options);
}
});
-}).call(this);
+})(Element.prototype.position);
/*
diff --git a/couchpotato/static/scripts/library/question.js b/couchpotato/static/scripts/library/question.js
new file mode 100644
index 00000000..b80005e3
--- /dev/null
+++ b/couchpotato/static/scripts/library/question.js
@@ -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
+ }
+
+})
diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js
index 8a43d525..2c5ca29f 100644
--- a/couchpotato/static/scripts/page/settings.js
+++ b/couchpotato/static/scripts/page/settings.js
@@ -82,7 +82,9 @@ Page.Settings = new Class({
var self = this;
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){
@@ -96,6 +98,7 @@ Page.Settings = new Class({
'text': 'Show advanced settings'
}),
self.advanced_toggle = new Element('input[type=checkbox].inlay', {
+ 'checked': +Cookie.read('advanced_toggle_checked'),
'events': {
'change': self.showAdvanced.bind(self)
}
@@ -103,10 +106,9 @@ Page.Settings = new Class({
)
)
);
-
- new Form.Check(self.advanced_toggle, {
- 'onChange': self.showAdvanced.bind(self)
- })
+ self.showAdvanced();
+
+ new Form.Check(self.advanced_toggle)
// Create tabs
Object.each(self.tabs, function(tab, tab_name){
@@ -132,7 +134,7 @@ Page.Settings = new Class({
var class_name = (option.type || 'string').capitalize();
var input = new Option[class_name](self, section_name, option.name, option);
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()
}).adopt(
new Element('span.hint', {
- 'html': group.description
+ 'html': group.description || ''
})
)
)
@@ -397,9 +399,7 @@ Option.Checkbox = new Class({
})
);
- new Form.Check(self.input, {
- 'onChange': self.changed.bind(self)
- });
+ new Form.Check(self.input);
},
@@ -435,16 +435,11 @@ Option.Enabler = new Class({
self.input = new Element('input.inlay', {
'type': 'checkbox',
'checked': self.getSettingValue(),
- 'id': 'r-'+randomString(),
- 'events': {
- 'change': self.checkState.bind(self)
- }
+ 'id': 'r-'+randomString()
})
);
- new Form.Check(self.input, {
- 'onChange': self.changed.bind(self)
- });
+ new Form.Check(self.input);
},
changed: function(){
@@ -479,63 +474,86 @@ Option.Directory = new Class({
type: 'span',
browser: '',
save_on_change: false,
+ use_cache: false,
create: function(){
var self = this;
self.el.adopt(
self.createLabel(),
- self.input = new Element('span.directory', {
- 'text': self.getSettingValue(),
+ new Element('span.directory.inlay', {
'events': {
'click': self.showBrowser.bind(self)
}
- })
+ }).adopt(
+ self.input = new Element('span', {
+ 'text': self.getSettingValue()
+ })
+ )
);
self.cached = {};
},
- selectDirectory: function(e, el){
+ selectDirectory: function(dir){
var self = this;
- self.input.set('text', el.get('data-value'));
+ self.input.set('text', dir);
self.getDirs()
- self.fireEvent('change')
},
previousDirectory: function(e){
var self = this;
- self.selectDirectory(null, self.back_button)
+ self.selectDirectory(self.getParentDir())
},
showBrowser: function(){
var self = this;
- if(!self.browser)
+ if(!self.browser){
self.browser = new Element('div.directory_list').adopt(
+ new Element('div.pointer'),
new Element('div.actions').adopt(
- self.back_button = new Element('a.button.back', {
- 'text': '',
+ self.back_button = new Element('a.back', {
+ 'html': '',
'events': {
'click': self.previousDirectory.bind(self)
}
}),
new Element('label', {
- 'text': 'Show hidden files'
+ 'text': 'Hidden folders'
}).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', {
'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('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',
'events': {
'click': self.hideBrowser.bind(self)
@@ -547,21 +565,32 @@ Option.Directory = new Class({
self.save_button = new Element('a.button.save', {
'text': 'Save',
'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.browser.show()
self.el.addEvent('outerClick', self.hideBrowser.bind(self))
},
- hideBrowser: function(save){
+ hideBrowser: function(e, save){
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.el.removeEvent('outerClick', self.hideBrowser.bind(self))
@@ -571,41 +600,43 @@ Option.Directory = new Class({
fillBrowser: function(json){
var self = this;
- var c = self.getParentDir();
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('text', self.getCurrentDirname(previous_dir))
+ self.back_button.set('html', '« '+self.getCurrentDirname(previous_dir))
self.back_button.show()
}
else {
self.back_button.hide()
}
- if(!json)
- json = self.cached[c];
- else
- self.cached[c] = json;
+ if(self.use_cache)
+ if(!json)
+ json = self.cached[v];
+ else
+ self.cached[v] = json;
- self.dir_list.empty();
- json.dirs.each(function(dir){
- if(dir.indexOf(v) != -1){
- new Element('li', {
- 'data-value': dir,
- 'text': self.getCurrentDirname(dir)
- }).inject(self.dir_list)
- }
- })
+ setTimeout(function(){
+ self.dir_list.empty();
+ json.dirs.each(function(dir){
+ if(dir.indexOf(v) != -1){
+ new Element('li', {
+ 'data-value': dir,
+ 'text': self.getCurrentDirname(dir)
+ }).inject(self.dir_list)
+ }
+ });
+ }, 50);
},
getDirs: function(){
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()
}
else {
@@ -625,7 +656,8 @@ Option.Directory = new Class({
var v = dir || self.input.get('text');
var sep = Api.getOption('path_sep');
var dirs = v.split(sep);
- dirs.pop();
+ if(dirs.pop() == '')
+ dirs.pop();
return dirs.join(sep) + sep
},
diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js
index 48d860fe..c2814d65 100644
--- a/couchpotato/static/scripts/page/wanted.js
+++ b/couchpotato/static/scripts/page/wanted.js
@@ -8,12 +8,12 @@ Page.Wanted = new Class({
indexAction: function(param){
var self = this;
- if(!self.list){
-
+ if(!self.wanted){
+
// Wanted movies
self.wanted = new MovieList({
'status': 'active',
- 'actions': WantedActions
+ 'actions': MovieActions
});
$(self.wanted).inject(self.el);
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
- //,'releases': ReleaseAction
+ ,'releases': ReleaseAction
,'Edit': new Class({
Extends: MovieAction,
-
+
create: function(){
var self = this;
-
+
self.el = new Element('a.edit', {
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.editMovie.bind(self)
}
});
-
+
},
-
+
editMovie: function(e){
var self = this;
(e).stop();
-
+
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
$(self.movie.thumbnail).clone(),
@@ -69,29 +71,29 @@ var WantedActions = {
})
)
).inject(self.movie, 'top');
-
+
Array.each(self.movie.data.library.titles, function(alt){
new Element('option', {
'text': alt.title
}).inject(self.title_select);
});
-
- Object.each(Quality.profiles, function(profile){
+
+ Object.each(Quality.getActiveProfiles(), function(profile){
new Element('option', {
'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select);
self.profile_select.set('value', self.movie.profile.get('id'));
});
-
+
}
self.movie.slide('in');
},
-
+
save: function(e){
(e).stop();
var self = this;
-
+
Api.request('movie.edit', {
'data': {
'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.slide('out');
}
-
+
})
,'Refresh': new Class({
Extends: MovieAction,
-
+
create: function(){
var self = this;
-
+
self.el = new Element('a.refresh', {
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.doSearch.bind(self)
}
});
-
+
},
-
+
doSearch: function(e){
var self = this;
(e).stop();
-
+
Api.request('movie.refresh', {
'data': {
'id': self.movie.get('id')
}
});
}
-
+
})
,'Delete': new Class({
Extends: MovieAction,
-
+
Implements: [Chain],
-
+
create: function(){
var self = this;
-
+
self.el = new Element('a.delete', {
'title': 'Remove the movie from your wanted list',
'events': {
'click': self.showConfirm.bind(self)
}
});
-
+
},
-
+
showConfirm: function(e){
var self = this;
(e).stop();
-
+
if(!self.delete_container){
self.delete_container = new Element('div.delete_container', {
'styles': {
@@ -185,27 +187,26 @@ var WantedActions = {
})
).inject(self.movie, 'top');
}
-
+
self.movie.slide('in');
-
+
},
-
+
hideConfirm: function(e){
var self = this;
(e).stop();
-
+
self.movie.slide('out');
},
-
+
del: function(e){
(e).stop();
var self = this;
-
+
var movie = $(self.movie);
-
+
self.chain(
function(){
- $(movie).mask().addClass('loading');
self.callChain();
},
function(){
@@ -224,16 +225,15 @@ var WantedActions = {
});
}
);
-
+
self.callChain();
-
+
}
})
};
-var SnatchedActions = {
+MovieActions.Snatched = {
'IMBD': IMDBAction
- ,'Releases': ReleaseAction
- ,'Delete': WantedActions.Delete
+ ,'Delete': MovieActions.Wanted.Delete
};
\ No newline at end of file
diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css
index 88289c6b..93e97876 100644
--- a/couchpotato/static/style/main.css
+++ b/couchpotato/static/style/main.css
@@ -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 {
color: #fff;
@@ -35,15 +39,24 @@ input, textarea {
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 {
border:none;
}
a {
text-decoration:none;
- color: #fff;
+ color: #ebfcbc;
outline: 0;
cursor: pointer;
+ font-weight: bold;
}
a:hover { color: #f3f3f3; }
@@ -125,10 +138,18 @@ form {
}
/*** Icons ***/
-.icon.delete {
- background: url('../images/delete.png') no-repeat;
+.icon {
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 ***/
.header {
@@ -191,6 +212,27 @@ form {
.header .navigation li a:hover, .header .navigation li a:active {
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 ***/
.check {
@@ -333,3 +375,59 @@ form {
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;
+ }
diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css
index 7d7ec344..ae93a2f6 100644
--- a/couchpotato/static/style/page/settings.css
+++ b/couchpotato/static/style/page/settings.css
@@ -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 {
- overflow: hidden;
+.page.settings:after {
+ content: ".";
+ display: block;
+ clear: both;
+ visibility: hidden;
+ line-height: 0;
+ height: 0;
}
.page.settings .tabs {
@@ -30,6 +38,7 @@
.page.settings .tabs a {
display: block;
padding: 11px 15px;
+ color: #fff;
}
.page.settings .tabs .active a {
background: #4e5969;
@@ -84,6 +93,7 @@
position: relative;
margin-bottom: -25px;
border: none;
+ width: 20px;
}
.page.settings .ctrlHolder {
@@ -92,9 +102,13 @@
font-size: 14px;
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:hover { background: rgba(255,255,255,0.05); }
- .page.settings .ctrlHolder.focused { background: rgba(255,255,255,0.2); }
+ .page.settings .ctrlHolder:hover { background-color: rgba(255,255,255,0.05); }
+ .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 {
float: right;
@@ -135,6 +149,8 @@
margin: 0;
padding: 6px 0 0;
}
+
+ .page.settings .xsmall { width: 20px !important; text-align: center; }
.page.settings input[type=text], .page.settings input[type=password] {
padding: 5px 3px;
@@ -165,39 +181,154 @@
.page.settings .directory {
display: inline-block;
- padding: 0 4px;
+ padding: 0 4% 0 4px;
font-size: 13px;
- width: 29.7%;
- }
- .page.settings .directory_list {
- position: absolute;
- width: 300px;
- margin: 0 0 0 16.5%;
- 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;
+ width: 26.3%;
+ background-image: url('../../images/icon.folder.gif');
+ background-repeat: no-repeat;
+ background-position: 97% center;
+ overflow: hidden;
+ vertical-align: top;
}
+ .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 {
- width: 100%;
- max-height: 200px;
+ width: 92%;
+ height: 300px;
overflow: auto;
+ margin: 0 4%;
}
- .page.settings .directory_list li {
- padding: 2px 10px;
- }
+ .page.settings .directory_list li {
+ 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 {
clear: both;
- padding: 10px;
- background: #414953;
+ padding: 4% 4% 2%;
+ 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; }
\ No newline at end of file
+
+ .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;
+}
\ No newline at end of file
diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html
index fe9b0c9a..be98a3d9 100644
--- a/couchpotato/templates/_desktop.html
+++ b/couchpotato/templates/_desktop.html
@@ -16,6 +16,7 @@
+
diff --git a/libs/README.md b/libs/README.md
deleted file mode 100644
index 49fbd340..00000000
--- a/libs/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-Dependencies
-===========
-
-Holds all dependencies that are required by CouchPotato.
diff --git a/libs/axl/axel.py b/libs/axl/axel.py
index f97e745d..2b0d265f 100644
--- a/libs/axl/axel.py
+++ b/libs/axl/axel.py
@@ -11,7 +11,10 @@
# Source: http://pypi.python.org/pypi/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):
"""
@@ -100,7 +103,7 @@ class Event(object):
self.handlers = {}
self.memoize = {}
- def handle(self, handler):
+ def handle(self, handler, priority = 0):
""" Registers a handler. The handler can be transmitted together
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}
"""
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
def unhandle(self, handler):
@@ -144,7 +147,7 @@ class Event(object):
t.daemon = True
t.start()
- for handler in self.handlers:
+ for handler in sorted(self.handlers.iterkeys(), cmp = natcmp):
self.queue.put(handler)
if self.asynchronous:
diff --git a/libs/multipartpost.py b/libs/multipartpost.py
new file mode 100644
index 00000000..38dfbd12
--- /dev/null
+++ b/libs/multipartpost.py
@@ -0,0 +1,88 @@
+#!/usr/bin/python
+
+####
+# 06/2010 Nic Wolfe
+# 02/2006 Will Holcomb
+#
+# 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
diff --git a/libs/pkg_resources.py b/libs/pkg_resources.py
new file mode 100644
index 00000000..79db00b8
--- /dev/null
+++ b/libs/pkg_resources.py
@@ -0,0 +1,2625 @@
+"""Package resource API
+--------------------
+
+A resource is a logical file contained within a package, or a logical
+subdirectory thereof. The package resource API expects resource names
+to have their path parts separated with ``/``, *not* whatever the local
+path separator is. Do not use os.path operations to manipulate resource
+names being passed into the API.
+
+The package resource API is designed to work with normal filesystem packages,
+.egg files, and unpacked .egg files. It can also work in a limited way with
+.zip files and with custom PEP 302 loaders that support the ``get_data()``
+method.
+"""
+
+import sys, os, zipimport, time, re, imp
+
+try:
+ frozenset
+except NameError:
+ from sets import ImmutableSet as frozenset
+
+# capture these to bypass sandboxing
+from os import utime, rename, unlink, mkdir
+from os import open as os_open
+from os.path import isdir, split
+
+
+def _bypass_ensure_directory(name, mode=0777):
+ # Sandbox-bypassing version of ensure_directory()
+ dirname, filename = split(name)
+ if dirname and filename and not isdir(dirname):
+ _bypass_ensure_directory(dirname)
+ mkdir(dirname, mode)
+
+
+
+
+
+
+
+_state_vars = {}
+
+def _declare_state(vartype, **kw):
+ g = globals()
+ for name, val in kw.iteritems():
+ g[name] = val
+ _state_vars[name] = vartype
+
+def __getstate__():
+ state = {}
+ g = globals()
+ for k, v in _state_vars.iteritems():
+ state[k] = g['_sget_'+v](g[k])
+ return state
+
+def __setstate__(state):
+ g = globals()
+ for k, v in state.iteritems():
+ g['_sset_'+_state_vars[k]](k, g[k], v)
+ return state
+
+def _sget_dict(val):
+ return val.copy()
+
+def _sset_dict(key, ob, state):
+ ob.clear()
+ ob.update(state)
+
+def _sget_object(val):
+ return val.__getstate__()
+
+def _sset_object(key, ob, state):
+ ob.__setstate__(state)
+
+_sget_none = _sset_none = lambda *args: None
+
+
+
+
+
+
+def get_supported_platform():
+ """Return this platform's maximum compatible version.
+
+ distutils.util.get_platform() normally reports the minimum version
+ of Mac OS X that would be required to *use* extensions produced by
+ distutils. But what we want when checking compatibility is to know the
+ version of Mac OS X that we are *running*. To allow usage of packages that
+ explicitly require a newer version of Mac OS X, we must also know the
+ current version of the OS.
+
+ If this condition occurs for any other platform with a version in its
+ platform strings, this function should be extended accordingly.
+ """
+ plat = get_build_platform(); m = macosVersionString.match(plat)
+ if m is not None and sys.platform == "darwin":
+ try:
+ plat = 'macosx-%s-%s' % ('.'.join(_macosx_vers()[:2]), m.group(3))
+ except ValueError:
+ pass # not Mac OS X
+ return plat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+__all__ = [
+ # Basic resource access and distribution/entry point discovery
+ 'require', 'run_script', 'get_provider', 'get_distribution',
+ 'load_entry_point', 'get_entry_map', 'get_entry_info', 'iter_entry_points',
+ 'resource_string', 'resource_stream', 'resource_filename',
+ 'resource_listdir', 'resource_exists', 'resource_isdir',
+
+ # Environmental control
+ 'declare_namespace', 'working_set', 'add_activation_listener',
+ 'find_distributions', 'set_extraction_path', 'cleanup_resources',
+ 'get_default_cache',
+
+ # Primary implementation classes
+ 'Environment', 'WorkingSet', 'ResourceManager',
+ 'Distribution', 'Requirement', 'EntryPoint',
+
+ # Exceptions
+ 'ResolutionError','VersionConflict','DistributionNotFound','UnknownExtra',
+ 'ExtractionError',
+
+ # Parsing functions and string utilities
+ 'parse_requirements', 'parse_version', 'safe_name', 'safe_version',
+ 'get_platform', 'compatible_platforms', 'yield_lines', 'split_sections',
+ 'safe_extra', 'to_filename',
+
+ # filesystem utilities
+ 'ensure_directory', 'normalize_path',
+
+ # Distribution "precedence" constants
+ 'EGG_DIST', 'BINARY_DIST', 'SOURCE_DIST', 'CHECKOUT_DIST', 'DEVELOP_DIST',
+
+ # "Provider" interfaces, implementations, and registration/lookup APIs
+ 'IMetadataProvider', 'IResourceProvider', 'FileMetadata',
+ 'PathMetadata', 'EggMetadata', 'EmptyProvider', 'empty_provider',
+ 'NullProvider', 'EggProvider', 'DefaultProvider', 'ZipProvider',
+ 'register_finder', 'register_namespace_handler', 'register_loader_type',
+ 'fixup_namespace_packages', 'get_importer',
+
+ # Deprecated/backward compatibility only
+ 'run_main', 'AvailableDistributions',
+]
+class ResolutionError(Exception):
+ """Abstract base for dependency resolution errors"""
+ def __repr__(self): return self.__class__.__name__+repr(self.args)
+
+class VersionConflict(ResolutionError):
+ """An already-installed version conflicts with the requested version"""
+
+class DistributionNotFound(ResolutionError):
+ """A requested distribution was not found"""
+
+class UnknownExtra(ResolutionError):
+ """Distribution doesn't have an "extra feature" of the given name"""
+_provider_factories = {}
+PY_MAJOR = sys.version[:3]
+EGG_DIST = 3
+BINARY_DIST = 2
+SOURCE_DIST = 1
+CHECKOUT_DIST = 0
+DEVELOP_DIST = -1
+
+def register_loader_type(loader_type, provider_factory):
+ """Register `provider_factory` to make providers for `loader_type`
+
+ `loader_type` is the type or class of a PEP 302 ``module.__loader__``,
+ and `provider_factory` is a function that, passed a *module* object,
+ returns an ``IResourceProvider`` for that module.
+ """
+ _provider_factories[loader_type] = provider_factory
+
+def get_provider(moduleOrReq):
+ """Return an IResourceProvider for the named module or requirement"""
+ if isinstance(moduleOrReq,Requirement):
+ return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
+ try:
+ module = sys.modules[moduleOrReq]
+ except KeyError:
+ __import__(moduleOrReq)
+ module = sys.modules[moduleOrReq]
+ loader = getattr(module, '__loader__', None)
+ return _find_adapter(_provider_factories, loader)(module)
+
+def _macosx_vers(_cache=[]):
+ if not _cache:
+ from platform import mac_ver
+ _cache.append(mac_ver()[0].split('.'))
+ return _cache[0]
+
+def _macosx_arch(machine):
+ return {'PowerPC':'ppc', 'Power_Macintosh':'ppc'}.get(machine,machine)
+
+def get_build_platform():
+ """Return this platform's string for platform-specific distributions
+
+ XXX Currently this is the same as ``distutils.util.get_platform()``, but it
+ needs some hacks for Linux and Mac OS X.
+ """
+ from distutils.util import get_platform
+ plat = get_platform()
+ if sys.platform == "darwin" and not plat.startswith('macosx-'):
+ try:
+ version = _macosx_vers()
+ machine = os.uname()[4].replace(" ", "_")
+ return "macosx-%d.%d-%s" % (int(version[0]), int(version[1]),
+ _macosx_arch(machine))
+ except ValueError:
+ # if someone is running a non-Mac darwin system, this will fall
+ # through to the default implementation
+ pass
+ return plat
+
+macosVersionString = re.compile(r"macosx-(\d+)\.(\d+)-(.*)")
+darwinVersionString = re.compile(r"darwin-(\d+)\.(\d+)\.(\d+)-(.*)")
+get_platform = get_build_platform # XXX backward compat
+
+
+
+
+
+
+
+
+
+def compatible_platforms(provided,required):
+ """Can code for the `provided` platform run on the `required` platform?
+
+ Returns true if either platform is ``None``, or the platforms are equal.
+
+ XXX Needs compatibility checks for Linux and other unixy OSes.
+ """
+ if provided is None or required is None or provided==required:
+ return True # easy case
+
+ # Mac OS X special cases
+ reqMac = macosVersionString.match(required)
+ if reqMac:
+ provMac = macosVersionString.match(provided)
+
+ # is this a Mac package?
+ if not provMac:
+ # this is backwards compatibility for packages built before
+ # setuptools 0.6. All packages built after this point will
+ # use the new macosx designation.
+ provDarwin = darwinVersionString.match(provided)
+ if provDarwin:
+ dversion = int(provDarwin.group(1))
+ macosversion = "%s.%s" % (reqMac.group(1), reqMac.group(2))
+ if dversion == 7 and macosversion >= "10.3" or \
+ dversion == 8 and macosversion >= "10.4":
+
+ #import warnings
+ #warnings.warn("Mac eggs should be rebuilt to "
+ # "use the macosx designation instead of darwin.",
+ # category=DeprecationWarning)
+ return True
+ return False # egg isn't macosx or legacy darwin
+
+ # are they the same major version and machine type?
+ if provMac.group(1) != reqMac.group(1) or \
+ provMac.group(3) != reqMac.group(3):
+ return False
+
+
+
+ # is the required OS major update >= the provided one?
+ if int(provMac.group(2)) > int(reqMac.group(2)):
+ return False
+
+ return True
+
+ # XXX Linux and other platforms' special cases should go here
+ return False
+
+
+def run_script(dist_spec, script_name):
+ """Locate distribution `dist_spec` and run its `script_name` script"""
+ ns = sys._getframe(1).f_globals
+ name = ns['__name__']
+ ns.clear()
+ ns['__name__'] = name
+ require(dist_spec)[0].run_script(script_name, ns)
+
+run_main = run_script # backward compatibility
+
+def get_distribution(dist):
+ """Return a current distribution object for a Requirement or string"""
+ if isinstance(dist,basestring): dist = Requirement.parse(dist)
+ if isinstance(dist,Requirement): dist = get_provider(dist)
+ if not isinstance(dist,Distribution):
+ raise TypeError("Expected string, Requirement, or Distribution", dist)
+ return dist
+
+def load_entry_point(dist, group, name):
+ """Return `name` entry point of `group` for `dist` or raise ImportError"""
+ return get_distribution(dist).load_entry_point(group, name)
+
+def get_entry_map(dist, group=None):
+ """Return the entry point map for `group`, or the full entry map"""
+ return get_distribution(dist).get_entry_map(group)
+
+def get_entry_info(dist, group, name):
+ """Return the EntryPoint object for `group`+`name`, or ``None``"""
+ return get_distribution(dist).get_entry_info(group, name)
+
+
+class IMetadataProvider:
+
+ def has_metadata(name):
+ """Does the package's distribution contain the named metadata?"""
+
+ def get_metadata(name):
+ """The named metadata resource as a string"""
+
+ def get_metadata_lines(name):
+ """Yield named metadata resource as list of non-blank non-comment lines
+
+ Leading and trailing whitespace is stripped from each line, and lines
+ with ``#`` as the first non-blank character are omitted."""
+
+ def metadata_isdir(name):
+ """Is the named metadata a directory? (like ``os.path.isdir()``)"""
+
+ def metadata_listdir(name):
+ """List of metadata names in the directory (like ``os.listdir()``)"""
+
+ def run_script(script_name, namespace):
+ """Execute the named script in the supplied namespace dictionary"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+class IResourceProvider(IMetadataProvider):
+ """An object that provides access to package resources"""
+
+ def get_resource_filename(manager, resource_name):
+ """Return a true filesystem path for `resource_name`
+
+ `manager` must be an ``IResourceManager``"""
+
+ def get_resource_stream(manager, resource_name):
+ """Return a readable file-like object for `resource_name`
+
+ `manager` must be an ``IResourceManager``"""
+
+ def get_resource_string(manager, resource_name):
+ """Return a string containing the contents of `resource_name`
+
+ `manager` must be an ``IResourceManager``"""
+
+ def has_resource(resource_name):
+ """Does the package contain the named resource?"""
+
+ def resource_isdir(resource_name):
+ """Is the named resource a directory? (like ``os.path.isdir()``)"""
+
+ def resource_listdir(resource_name):
+ """List of resource names in the directory (like ``os.listdir()``)"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+class WorkingSet(object):
+ """A collection of active distributions on sys.path (or a similar list)"""
+
+ def __init__(self, entries=None):
+ """Create working set from list of path entries (default=sys.path)"""
+ self.entries = []
+ self.entry_keys = {}
+ self.by_key = {}
+ self.callbacks = []
+
+ if entries is None:
+ entries = sys.path
+
+ for entry in entries:
+ self.add_entry(entry)
+
+
+ def add_entry(self, entry):
+ """Add a path item to ``.entries``, finding any distributions on it
+
+ ``find_distributions(entry, True)`` is used to find distributions
+ corresponding to the path entry, and they are added. `entry` is
+ always appended to ``.entries``, even if it is already present.
+ (This is because ``sys.path`` can contain the same value more than
+ once, and the ``.entries`` of the ``sys.path`` WorkingSet should always
+ equal ``sys.path``.)
+ """
+ self.entry_keys.setdefault(entry, [])
+ self.entries.append(entry)
+ for dist in find_distributions(entry, True):
+ self.add(dist, entry, False)
+
+
+ def __contains__(self,dist):
+ """True if `dist` is the active distribution for its project"""
+ return self.by_key.get(dist.key) == dist
+
+
+
+
+
+ def find(self, req):
+ """Find a distribution matching requirement `req`
+
+ If there is an active distribution for the requested project, this
+ returns it as long as it meets the version requirement specified by
+ `req`. But, if there is an active distribution for the project and it
+ does *not* meet the `req` requirement, ``VersionConflict`` is raised.
+ If there is no active distribution for the requested project, ``None``
+ is returned.
+ """
+ dist = self.by_key.get(req.key)
+ if dist is not None and dist not in req:
+ raise VersionConflict(dist,req) # XXX add more info
+ else:
+ return dist
+
+ def iter_entry_points(self, group, name=None):
+ """Yield entry point objects from `group` matching `name`
+
+ If `name` is None, yields all entry points in `group` from all
+ distributions in the working set, otherwise only ones matching
+ both `group` and `name` are yielded (in distribution order).
+ """
+ for dist in self:
+ entries = dist.get_entry_map(group)
+ if name is None:
+ for ep in entries.values():
+ yield ep
+ elif name in entries:
+ yield entries[name]
+
+ def run_script(self, requires, script_name):
+ """Locate distribution for `requires` and run `script_name` script"""
+ ns = sys._getframe(1).f_globals
+ name = ns['__name__']
+ ns.clear()
+ ns['__name__'] = name
+ self.require(requires)[0].run_script(script_name, ns)
+
+
+
+ def __iter__(self):
+ """Yield distributions for non-duplicate projects in the working set
+
+ The yield order is the order in which the items' path entries were
+ added to the working set.
+ """
+ seen = {}
+ for item in self.entries:
+ for key in self.entry_keys[item]:
+ if key not in seen:
+ seen[key]=1
+ yield self.by_key[key]
+
+ def add(self, dist, entry=None, insert=True):
+ """Add `dist` to working set, associated with `entry`
+
+ If `entry` is unspecified, it defaults to the ``.location`` of `dist`.
+ On exit from this routine, `entry` is added to the end of the working
+ set's ``.entries`` (if it wasn't already present).
+
+ `dist` is only added to the working set if it's for a project that
+ doesn't already have a distribution in the set. If it's added, any
+ callbacks registered with the ``subscribe()`` method will be called.
+ """
+ if insert:
+ dist.insert_on(self.entries, entry)
+
+ if entry is None:
+ entry = dist.location
+ keys = self.entry_keys.setdefault(entry,[])
+ keys2 = self.entry_keys.setdefault(dist.location,[])
+ if dist.key in self.by_key:
+ return # ignore hidden distros
+
+ self.by_key[dist.key] = dist
+ if dist.key not in keys:
+ keys.append(dist.key)
+ if dist.key not in keys2:
+ keys2.append(dist.key)
+ self._added_new(dist)
+
+ def resolve(self, requirements, env=None, installer=None):
+ """List all distributions needed to (recursively) meet `requirements`
+
+ `requirements` must be a sequence of ``Requirement`` objects. `env`,
+ if supplied, should be an ``Environment`` instance. If
+ not supplied, it defaults to all distributions available within any
+ entry or distribution in the working set. `installer`, if supplied,
+ will be invoked with each requirement that cannot be met by an
+ already-installed distribution; it should return a ``Distribution`` or
+ ``None``.
+ """
+
+ requirements = list(requirements)[::-1] # set up the stack
+ processed = {} # set of processed requirements
+ best = {} # key -> dist
+ to_activate = []
+
+ while requirements:
+ req = requirements.pop(0) # process dependencies breadth-first
+ if req in processed:
+ # Ignore cyclic or redundant dependencies
+ continue
+ dist = best.get(req.key)
+ if dist is None:
+ # Find the best distribution and add it to the map
+ dist = self.by_key.get(req.key)
+ if dist is None:
+ if env is None:
+ env = Environment(self.entries)
+ dist = best[req.key] = env.best_match(req, self, installer)
+ if dist is None:
+ raise DistributionNotFound(req) # XXX put more info here
+ to_activate.append(dist)
+ if dist not in req:
+ # Oops, the "best" so far conflicts with a dependency
+ raise VersionConflict(dist,req) # XXX put more info here
+ requirements.extend(dist.requires(req.extras)[::-1])
+ processed[req] = True
+
+ return to_activate # return list of distros to activate
+
+ def find_plugins(self,
+ plugin_env, full_env=None, installer=None, fallback=True
+ ):
+ """Find all activatable distributions in `plugin_env`
+
+ Example usage::
+
+ distributions, errors = working_set.find_plugins(
+ Environment(plugin_dirlist)
+ )
+ map(working_set.add, distributions) # add plugins+libs to sys.path
+ print "Couldn't load", errors # display errors
+
+ The `plugin_env` should be an ``Environment`` instance that contains
+ only distributions that are in the project's "plugin directory" or
+ directories. The `full_env`, if supplied, should be an ``Environment``
+ contains all currently-available distributions. If `full_env` is not
+ supplied, one is created automatically from the ``WorkingSet`` this
+ method is called on, which will typically mean that every directory on
+ ``sys.path`` will be scanned for distributions.
+
+ `installer` is a standard installer callback as used by the
+ ``resolve()`` method. The `fallback` flag indicates whether we should
+ attempt to resolve older versions of a plugin if the newest version
+ cannot be resolved.
+
+ This method returns a 2-tuple: (`distributions`, `error_info`), where
+ `distributions` is a list of the distributions found in `plugin_env`
+ that were loadable, along with any other distributions that are needed
+ to resolve their dependencies. `error_info` is a dictionary mapping
+ unloadable plugin distributions to an exception instance describing the
+ error that occurred. Usually this will be a ``DistributionNotFound`` or
+ ``VersionConflict`` instance.
+ """
+
+ plugin_projects = list(plugin_env)
+ plugin_projects.sort() # scan project names in alphabetic order
+
+ error_info = {}
+ distributions = {}
+
+ if full_env is None:
+ env = Environment(self.entries)
+ env += plugin_env
+ else:
+ env = full_env + plugin_env
+
+ shadow_set = self.__class__([])
+ map(shadow_set.add, self) # put all our entries in shadow_set
+
+ for project_name in plugin_projects:
+
+ for dist in plugin_env[project_name]:
+
+ req = [dist.as_requirement()]
+
+ try:
+ resolvees = shadow_set.resolve(req, env, installer)
+
+ except ResolutionError,v:
+ error_info[dist] = v # save error info
+ if fallback:
+ continue # try the next older version of project
+ else:
+ break # give up on this project, keep going
+
+ else:
+ map(shadow_set.add, resolvees)
+ distributions.update(dict.fromkeys(resolvees))
+
+ # success, no need to try any more versions of this project
+ break
+
+ distributions = list(distributions)
+ distributions.sort()
+
+ return distributions, error_info
+
+
+
+
+
+ def require(self, *requirements):
+ """Ensure that distributions matching `requirements` are activated
+
+ `requirements` must be a string or a (possibly-nested) sequence
+ thereof, specifying the distributions and versions required. The
+ return value is a sequence of the distributions that needed to be
+ activated to fulfill the requirements; all relevant distributions are
+ included, even if they were already activated in this working set.
+ """
+ needed = self.resolve(parse_requirements(requirements))
+
+ for dist in needed:
+ self.add(dist)
+
+ return needed
+
+ def subscribe(self, callback):
+ """Invoke `callback` for all distributions (including existing ones)"""
+ if callback in self.callbacks:
+ return
+ self.callbacks.append(callback)
+ for dist in self:
+ callback(dist)
+
+ def _added_new(self, dist):
+ for callback in self.callbacks:
+ callback(dist)
+
+ def __getstate__(self):
+ return (
+ self.entries[:], self.entry_keys.copy(), self.by_key.copy(),
+ self.callbacks[:]
+ )
+
+ def __setstate__(self, (entries, keys, by_key, callbacks)):
+ self.entries = entries[:]
+ self.entry_keys = keys.copy()
+ self.by_key = by_key.copy()
+ self.callbacks = callbacks[:]
+
+
+class Environment(object):
+ """Searchable snapshot of distributions on a search path"""
+
+ def __init__(self, search_path=None, platform=get_supported_platform(), python=PY_MAJOR):
+ """Snapshot distributions available on a search path
+
+ Any distributions found on `search_path` are added to the environment.
+ `search_path` should be a sequence of ``sys.path`` items. If not
+ supplied, ``sys.path`` is used.
+
+ `platform` is an optional string specifying the name of the platform
+ that platform-specific distributions must be compatible with. If
+ unspecified, it defaults to the current platform. `python` is an
+ optional string naming the desired version of Python (e.g. ``'2.4'``);
+ it defaults to the current version.
+
+ You may explicitly set `platform` (and/or `python`) to ``None`` if you
+ wish to map *all* distributions, not just those compatible with the
+ running platform or Python version.
+ """
+ self._distmap = {}
+ self._cache = {}
+ self.platform = platform
+ self.python = python
+ self.scan(search_path)
+
+ def can_add(self, dist):
+ """Is distribution `dist` acceptable for this environment?
+
+ The distribution must match the platform and python version
+ requirements specified when this environment was created, or False
+ is returned.
+ """
+ return (self.python is None or dist.py_version is None
+ or dist.py_version==self.python) \
+ and compatible_platforms(dist.platform,self.platform)
+
+ def remove(self, dist):
+ """Remove `dist` from the environment"""
+ self._distmap[dist.key].remove(dist)
+
+ def scan(self, search_path=None):
+ """Scan `search_path` for distributions usable in this environment
+
+ Any distributions found are added to the environment.
+ `search_path` should be a sequence of ``sys.path`` items. If not
+ supplied, ``sys.path`` is used. Only distributions conforming to
+ the platform/python version defined at initialization are added.
+ """
+ if search_path is None:
+ search_path = sys.path
+
+ for item in search_path:
+ for dist in find_distributions(item):
+ self.add(dist)
+
+ def __getitem__(self,project_name):
+ """Return a newest-to-oldest list of distributions for `project_name`
+ """
+ try:
+ return self._cache[project_name]
+ except KeyError:
+ project_name = project_name.lower()
+ if project_name not in self._distmap:
+ return []
+
+ if project_name not in self._cache:
+ dists = self._cache[project_name] = self._distmap[project_name]
+ _sort_dists(dists)
+
+ return self._cache[project_name]
+
+ def add(self,dist):
+ """Add `dist` if we ``can_add()`` it and it isn't already added"""
+ if self.can_add(dist) and dist.has_version():
+ dists = self._distmap.setdefault(dist.key,[])
+ if dist not in dists:
+ dists.append(dist)
+ if dist.key in self._cache:
+ _sort_dists(self._cache[dist.key])
+
+
+ def best_match(self, req, working_set, installer=None):
+ """Find distribution best matching `req` and usable on `working_set`
+
+ This calls the ``find(req)`` method of the `working_set` to see if a
+ suitable distribution is already active. (This may raise
+ ``VersionConflict`` if an unsuitable version of the project is already
+ active in the specified `working_set`.) If a suitable distribution
+ isn't active, this method returns the newest distribution in the
+ environment that meets the ``Requirement`` in `req`. If no suitable
+ distribution is found, and `installer` is supplied, then the result of
+ calling the environment's ``obtain(req, installer)`` method will be
+ returned.
+ """
+ dist = working_set.find(req)
+ if dist is not None:
+ return dist
+ for dist in self[req.key]:
+ if dist in req:
+ return dist
+ return self.obtain(req, installer) # try and download/install
+
+ def obtain(self, requirement, installer=None):
+ """Obtain a distribution matching `requirement` (e.g. via download)
+
+ Obtain a distro that matches requirement (e.g. via download). In the
+ base ``Environment`` class, this routine just returns
+ ``installer(requirement)``, unless `installer` is None, in which case
+ None is returned instead. This method is a hook that allows subclasses
+ to attempt other ways of obtaining a distribution before falling back
+ to the `installer` argument."""
+ if installer is not None:
+ return installer(requirement)
+
+ def __iter__(self):
+ """Yield the unique project names of the available distributions"""
+ for key in self._distmap.keys():
+ if self[key]: yield key
+
+
+
+
+ def __iadd__(self, other):
+ """In-place addition of a distribution or environment"""
+ if isinstance(other,Distribution):
+ self.add(other)
+ elif isinstance(other,Environment):
+ for project in other:
+ for dist in other[project]:
+ self.add(dist)
+ else:
+ raise TypeError("Can't add %r to environment" % (other,))
+ return self
+
+ def __add__(self, other):
+ """Add an environment or distribution to an environment"""
+ new = self.__class__([], platform=None, python=None)
+ for env in self, other:
+ new += env
+ return new
+
+
+AvailableDistributions = Environment # XXX backward compatibility
+
+
+class ExtractionError(RuntimeError):
+ """An error occurred extracting a resource
+
+ The following attributes are available from instances of this exception:
+
+ manager
+ The resource manager that raised this exception
+
+ cache_path
+ The base directory for resource extraction
+
+ original_error
+ The exception instance that caused extraction to fail
+ """
+
+
+
+
+class ResourceManager:
+ """Manage resource extraction and packages"""
+ extraction_path = None
+
+ def __init__(self):
+ self.cached_files = {}
+
+ def resource_exists(self, package_or_requirement, resource_name):
+ """Does the named resource exist?"""
+ return get_provider(package_or_requirement).has_resource(resource_name)
+
+ def resource_isdir(self, package_or_requirement, resource_name):
+ """Is the named resource an existing directory?"""
+ return get_provider(package_or_requirement).resource_isdir(
+ resource_name
+ )
+
+ def resource_filename(self, package_or_requirement, resource_name):
+ """Return a true filesystem path for specified resource"""
+ return get_provider(package_or_requirement).get_resource_filename(
+ self, resource_name
+ )
+
+ def resource_stream(self, package_or_requirement, resource_name):
+ """Return a readable file-like object for specified resource"""
+ return get_provider(package_or_requirement).get_resource_stream(
+ self, resource_name
+ )
+
+ def resource_string(self, package_or_requirement, resource_name):
+ """Return specified resource as a string"""
+ return get_provider(package_or_requirement).get_resource_string(
+ self, resource_name
+ )
+
+ def resource_listdir(self, package_or_requirement, resource_name):
+ """List the contents of the named resource directory"""
+ return get_provider(package_or_requirement).resource_listdir(
+ resource_name
+ )
+
+ def extraction_error(self):
+ """Give an error message for problems extracting file(s)"""
+
+ old_exc = sys.exc_info()[1]
+ cache_path = self.extraction_path or get_default_cache()
+
+ err = ExtractionError("""Can't extract file(s) to egg cache
+
+The following error occurred while trying to extract file(s) to the Python egg
+cache:
+
+ %s
+
+The Python egg cache directory is currently set to:
+
+ %s
+
+Perhaps your account does not have write access to this directory? You can
+change the cache directory by setting the PYTHON_EGG_CACHE environment
+variable to point to an accessible directory.
+""" % (old_exc, cache_path)
+ )
+ err.manager = self
+ err.cache_path = cache_path
+ err.original_error = old_exc
+ raise err
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ def get_cache_path(self, archive_name, names=()):
+ """Return absolute location in cache for `archive_name` and `names`
+
+ The parent directory of the resulting path will be created if it does
+ not already exist. `archive_name` should be the base filename of the
+ enclosing egg (which may not be the name of the enclosing zipfile!),
+ including its ".egg" extension. `names`, if provided, should be a
+ sequence of path name parts "under" the egg's extraction location.
+
+ This method should only be called by resource providers that need to
+ obtain an extraction location, and only for names they intend to
+ extract, as it tracks the generated names for possible cleanup later.
+ """
+ extract_path = self.extraction_path or get_default_cache()
+ target_path = os.path.join(extract_path, archive_name+'-tmp', *names)
+ try:
+ _bypass_ensure_directory(target_path)
+ except:
+ self.extraction_error()
+
+ self.cached_files[target_path] = 1
+ return target_path
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ def postprocess(self, tempname, filename):
+ """Perform any platform-specific postprocessing of `tempname`
+
+ This is where Mac header rewrites should be done; other platforms don't
+ have anything special they should do.
+
+ Resource providers should call this method ONLY after successfully
+ extracting a compressed resource. They must NOT call it on resources
+ that are already in the filesystem.
+
+ `tempname` is the current (temporary) name of the file, and `filename`
+ is the name it will be renamed to by the caller after this routine
+ returns.
+ """
+
+ if os.name == 'posix':
+ # Make the resource executable
+ mode = ((os.stat(tempname).st_mode) | 0555) & 07777
+ os.chmod(tempname, mode)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ def set_extraction_path(self, path):
+ """Set the base path where resources will be extracted to, if needed.
+
+ If you do not call this routine before any extractions take place, the
+ path defaults to the return value of ``get_default_cache()``. (Which
+ is based on the ``PYTHON_EGG_CACHE`` environment variable, with various
+ platform-specific fallbacks. See that routine's documentation for more
+ details.)
+
+ Resources are extracted to subdirectories of this path based upon
+ information given by the ``IResourceProvider``. You may set this to a
+ temporary directory, but then you must call ``cleanup_resources()`` to
+ delete the extracted files when done. There is no guarantee that
+ ``cleanup_resources()`` will be able to remove all extracted files.
+
+ (Note: you may not change the extraction path for a given resource
+ manager once resources have been extracted, unless you first call
+ ``cleanup_resources()``.)
+ """
+ if self.cached_files:
+ raise ValueError(
+ "Can't change extraction path, files already extracted"
+ )
+
+ self.extraction_path = path
+
+ def cleanup_resources(self, force=False):
+ """
+ Delete all extracted resource files and directories, returning a list
+ of the file and directory names that could not be successfully removed.
+ This function does not have any concurrency protection, so it should
+ generally only be called when the extraction path is a temporary
+ directory exclusive to a single process. This method is not
+ automatically called; you must call it explicitly or register it as an
+ ``atexit`` function if you wish to ensure cleanup of a temporary
+ directory used for extractions.
+ """
+ # XXX
+
+
+
+def get_default_cache():
+ """Determine the default cache location
+
+ This returns the ``PYTHON_EGG_CACHE`` environment variable, if set.
+ Otherwise, on Windows, it returns a "Python-Eggs" subdirectory of the
+ "Application Data" directory. On all other systems, it's "~/.python-eggs".
+ """
+ try:
+ return os.environ['PYTHON_EGG_CACHE']
+ except KeyError:
+ pass
+
+ if os.name!='nt':
+ return os.path.expanduser('~/.python-eggs')
+
+ app_data = 'Application Data' # XXX this may be locale-specific!
+ app_homes = [
+ (('APPDATA',), None), # best option, should be locale-safe
+ (('USERPROFILE',), app_data),
+ (('HOMEDRIVE','HOMEPATH'), app_data),
+ (('HOMEPATH',), app_data),
+ (('HOME',), None),
+ (('WINDIR',), app_data), # 95/98/ME
+ ]
+
+ for keys, subdir in app_homes:
+ dirname = ''
+ for key in keys:
+ if key in os.environ:
+ dirname = os.path.join(dirname, os.environ[key])
+ else:
+ break
+ else:
+ if subdir:
+ dirname = os.path.join(dirname,subdir)
+ return os.path.join(dirname, 'Python-Eggs')
+ else:
+ raise RuntimeError(
+ "Please set the PYTHON_EGG_CACHE enviroment variable"
+ )
+
+def safe_name(name):
+ """Convert an arbitrary string to a standard distribution name
+
+ Any runs of non-alphanumeric/. characters are replaced with a single '-'.
+ """
+ return re.sub('[^A-Za-z0-9.]+', '-', name)
+
+
+def safe_version(version):
+ """Convert an arbitrary string to a standard version string
+
+ Spaces become dots, and all other non-alphanumeric characters become
+ dashes, with runs of multiple dashes condensed to a single dash.
+ """
+ version = version.replace(' ','.')
+ return re.sub('[^A-Za-z0-9.]+', '-', version)
+
+
+def safe_extra(extra):
+ """Convert an arbitrary string to a standard 'extra' name
+
+ Any runs of non-alphanumeric characters are replaced with a single '_',
+ and the result is always lowercased.
+ """
+ return re.sub('[^A-Za-z0-9.]+', '_', extra).lower()
+
+
+def to_filename(name):
+ """Convert a project or version name to its filename-escaped form
+
+ Any '-' characters are currently replaced with '_'.
+ """
+ return name.replace('-','_')
+
+
+
+
+
+
+
+
+class NullProvider:
+ """Try to implement resources and metadata for arbitrary PEP 302 loaders"""
+
+ egg_name = None
+ egg_info = None
+ loader = None
+
+ def __init__(self, module):
+ self.loader = getattr(module, '__loader__', None)
+ self.module_path = os.path.dirname(getattr(module, '__file__', ''))
+
+ def get_resource_filename(self, manager, resource_name):
+ return self._fn(self.module_path, resource_name)
+
+ def get_resource_stream(self, manager, resource_name):
+ return StringIO(self.get_resource_string(manager, resource_name))
+
+ def get_resource_string(self, manager, resource_name):
+ return self._get(self._fn(self.module_path, resource_name))
+
+ def has_resource(self, resource_name):
+ return self._has(self._fn(self.module_path, resource_name))
+
+ def has_metadata(self, name):
+ return self.egg_info and self._has(self._fn(self.egg_info,name))
+
+ def get_metadata(self, name):
+ if not self.egg_info:
+ return ""
+ return self._get(self._fn(self.egg_info,name))
+
+ def get_metadata_lines(self, name):
+ return yield_lines(self.get_metadata(name))
+
+ def resource_isdir(self,resource_name):
+ return self._isdir(self._fn(self.module_path, resource_name))
+
+ def metadata_isdir(self,name):
+ return self.egg_info and self._isdir(self._fn(self.egg_info,name))
+
+
+ def resource_listdir(self,resource_name):
+ return self._listdir(self._fn(self.module_path,resource_name))
+
+ def metadata_listdir(self,name):
+ if self.egg_info:
+ return self._listdir(self._fn(self.egg_info,name))
+ return []
+
+ def run_script(self,script_name,namespace):
+ script = 'scripts/'+script_name
+ if not self.has_metadata(script):
+ raise ResolutionError("No script named %r" % script_name)
+ script_text = self.get_metadata(script).replace('\r\n','\n')
+ script_text = script_text.replace('\r','\n')
+ script_filename = self._fn(self.egg_info,script)
+ namespace['__file__'] = script_filename
+ if os.path.exists(script_filename):
+ execfile(script_filename, namespace, namespace)
+ else:
+ from linecache import cache
+ cache[script_filename] = (
+ len(script_text), 0, script_text.split('\n'), script_filename
+ )
+ script_code = compile(script_text,script_filename,'exec')
+ exec script_code in namespace, namespace
+
+ def _has(self, path):
+ raise NotImplementedError(
+ "Can't perform this operation for unregistered loader type"
+ )
+
+ def _isdir(self, path):
+ raise NotImplementedError(
+ "Can't perform this operation for unregistered loader type"
+ )
+
+ def _listdir(self, path):
+ raise NotImplementedError(
+ "Can't perform this operation for unregistered loader type"
+ )
+
+ def _fn(self, base, resource_name):
+ if resource_name:
+ return os.path.join(base, *resource_name.split('/'))
+ return base
+
+ def _get(self, path):
+ if hasattr(self.loader, 'get_data'):
+ return self.loader.get_data(path)
+ raise NotImplementedError(
+ "Can't perform this operation for loaders without 'get_data()'"
+ )
+
+register_loader_type(object, NullProvider)
+
+
+class EggProvider(NullProvider):
+ """Provider based on a virtual filesystem"""
+
+ def __init__(self,module):
+ NullProvider.__init__(self,module)
+ self._setup_prefix()
+
+ def _setup_prefix(self):
+ # we assume here that our metadata may be nested inside a "basket"
+ # of multiple eggs; that's why we use module_path instead of .archive
+ path = self.module_path
+ old = None
+ while path!=old:
+ if path.lower().endswith('.egg'):
+ self.egg_name = os.path.basename(path)
+ self.egg_info = os.path.join(path, 'EGG-INFO')
+ self.egg_root = path
+ break
+ old = path
+ path, base = os.path.split(path)
+
+
+
+
+
+
+class DefaultProvider(EggProvider):
+ """Provides access to package resources in the filesystem"""
+
+ def _has(self, path):
+ return os.path.exists(path)
+
+ def _isdir(self,path):
+ return os.path.isdir(path)
+
+ def _listdir(self,path):
+ return os.listdir(path)
+
+ def get_resource_stream(self, manager, resource_name):
+ return open(self._fn(self.module_path, resource_name), 'rb')
+
+ def _get(self, path):
+ stream = open(path, 'rb')
+ try:
+ return stream.read()
+ finally:
+ stream.close()
+
+register_loader_type(type(None), DefaultProvider)
+
+
+class EmptyProvider(NullProvider):
+ """Provider that returns nothing for all requests"""
+
+ _isdir = _has = lambda self,path: False
+ _get = lambda self,path: ''
+ _listdir = lambda self,path: []
+ module_path = None
+
+ def __init__(self):
+ pass
+
+empty_provider = EmptyProvider()
+
+
+
+
+class ZipProvider(EggProvider):
+ """Resource support for zips and eggs"""
+
+ eagers = None
+
+ def __init__(self, module):
+ EggProvider.__init__(self,module)
+ self.zipinfo = zipimport._zip_directory_cache[self.loader.archive]
+ self.zip_pre = self.loader.archive+os.sep
+
+ def _zipinfo_name(self, fspath):
+ # Convert a virtual filename (full path to file) into a zipfile subpath
+ # usable with the zipimport directory cache for our target archive
+ if fspath.startswith(self.zip_pre):
+ return fspath[len(self.zip_pre):]
+ raise AssertionError(
+ "%s is not a subpath of %s" % (fspath,self.zip_pre)
+ )
+
+ def _parts(self,zip_path):
+ # Convert a zipfile subpath into an egg-relative path part list
+ fspath = self.zip_pre+zip_path # pseudo-fs path
+ if fspath.startswith(self.egg_root+os.sep):
+ return fspath[len(self.egg_root)+1:].split(os.sep)
+ raise AssertionError(
+ "%s is not a subpath of %s" % (fspath,self.egg_root)
+ )
+
+ def get_resource_filename(self, manager, resource_name):
+ if not self.egg_name:
+ raise NotImplementedError(
+ "resource_filename() only supported for .egg, not .zip"
+ )
+ # no need to lock for extraction, since we use temp names
+ zip_path = self._resource_to_zip(resource_name)
+ eagers = self._get_eager_resources()
+ if '/'.join(self._parts(zip_path)) in eagers:
+ for name in eagers:
+ self._extract_resource(manager, self._eager_to_zip(name))
+ return self._extract_resource(manager, zip_path)
+
+ def _extract_resource(self, manager, zip_path):
+
+ if zip_path in self._index():
+ for name in self._index()[zip_path]:
+ last = self._extract_resource(
+ manager, os.path.join(zip_path, name)
+ )
+ return os.path.dirname(last) # return the extracted directory name
+
+ zip_stat = self.zipinfo[zip_path]
+ t,d,size = zip_stat[5], zip_stat[6], zip_stat[3]
+ date_time = (
+ (d>>9)+1980, (d>>5)&0xF, d&0x1F, # ymd
+ (t&0xFFFF)>>11, (t>>5)&0x3F, (t&0x1F) * 2, 0, 0, -1 # hms, etc.
+ )
+ timestamp = time.mktime(date_time)
+
+ try:
+ real_path = manager.get_cache_path(
+ self.egg_name, self._parts(zip_path)
+ )
+
+ if os.path.isfile(real_path):
+ stat = os.stat(real_path)
+ if stat.st_size==size and stat.st_mtime==timestamp:
+ # size and stamp match, don't bother extracting
+ return real_path
+
+ outf, tmpnam = _mkstemp(".$extract", dir=os.path.dirname(real_path))
+ os.write(outf, self.loader.get_data(zip_path))
+ os.close(outf)
+ utime(tmpnam, (timestamp,timestamp))
+ manager.postprocess(tmpnam, real_path)
+
+ try:
+ rename(tmpnam, real_path)
+
+ except os.error:
+ if os.path.isfile(real_path):
+ stat = os.stat(real_path)
+
+ if stat.st_size==size and stat.st_mtime==timestamp:
+ # size and stamp match, somebody did it just ahead of
+ # us, so we're done
+ return real_path
+ elif os.name=='nt': # Windows, del old file and retry
+ unlink(real_path)
+ rename(tmpnam, real_path)
+ return real_path
+ raise
+
+ except os.error:
+ manager.extraction_error() # report a user-friendly error
+
+ return real_path
+
+ def _get_eager_resources(self):
+ if self.eagers is None:
+ eagers = []
+ for name in ('native_libs.txt', 'eager_resources.txt'):
+ if self.has_metadata(name):
+ eagers.extend(self.get_metadata_lines(name))
+ self.eagers = eagers
+ return self.eagers
+
+ def _index(self):
+ try:
+ return self._dirindex
+ except AttributeError:
+ ind = {}
+ for path in self.zipinfo:
+ parts = path.split(os.sep)
+ while parts:
+ parent = os.sep.join(parts[:-1])
+ if parent in ind:
+ ind[parent].append(parts[-1])
+ break
+ else:
+ ind[parent] = [parts.pop()]
+ self._dirindex = ind
+ return ind
+
+ def _has(self, fspath):
+ zip_path = self._zipinfo_name(fspath)
+ return zip_path in self.zipinfo or zip_path in self._index()
+
+ def _isdir(self,fspath):
+ return self._zipinfo_name(fspath) in self._index()
+
+ def _listdir(self,fspath):
+ return list(self._index().get(self._zipinfo_name(fspath), ()))
+
+ def _eager_to_zip(self,resource_name):
+ return self._zipinfo_name(self._fn(self.egg_root,resource_name))
+
+ def _resource_to_zip(self,resource_name):
+ return self._zipinfo_name(self._fn(self.module_path,resource_name))
+
+register_loader_type(zipimport.zipimporter, ZipProvider)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+class FileMetadata(EmptyProvider):
+ """Metadata handler for standalone PKG-INFO files
+
+ Usage::
+
+ metadata = FileMetadata("/path/to/PKG-INFO")
+
+ This provider rejects all data and metadata requests except for PKG-INFO,
+ which is treated as existing, and will be the contents of the file at
+ the provided location.
+ """
+
+ def __init__(self,path):
+ self.path = path
+
+ def has_metadata(self,name):
+ return name=='PKG-INFO'
+
+ def get_metadata(self,name):
+ if name=='PKG-INFO':
+ return open(self.path,'rU').read()
+ raise KeyError("No metadata except PKG-INFO is available")
+
+ def get_metadata_lines(self,name):
+ return yield_lines(self.get_metadata(name))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+class PathMetadata(DefaultProvider):
+ """Metadata provider for egg directories
+
+ Usage::
+
+ # Development eggs:
+
+ egg_info = "/path/to/PackageName.egg-info"
+ base_dir = os.path.dirname(egg_info)
+ metadata = PathMetadata(base_dir, egg_info)
+ dist_name = os.path.splitext(os.path.basename(egg_info))[0]
+ dist = Distribution(basedir,project_name=dist_name,metadata=metadata)
+
+ # Unpacked egg directories:
+
+ egg_path = "/path/to/PackageName-ver-pyver-etc.egg"
+ metadata = PathMetadata(egg_path, os.path.join(egg_path,'EGG-INFO'))
+ dist = Distribution.from_filename(egg_path, metadata=metadata)
+ """
+
+ def __init__(self, path, egg_info):
+ self.module_path = path
+ self.egg_info = egg_info
+
+
+class EggMetadata(ZipProvider):
+ """Metadata provider for .egg files"""
+
+ def __init__(self, importer):
+ """Create a metadata provider from a zipimporter"""
+
+ self.zipinfo = zipimport._zip_directory_cache[importer.archive]
+ self.zip_pre = importer.archive+os.sep
+ self.loader = importer
+ if importer.prefix:
+ self.module_path = os.path.join(importer.archive, importer.prefix)
+ else:
+ self.module_path = importer.archive
+ self._setup_prefix()
+
+
+class ImpWrapper:
+ """PEP 302 Importer that wraps Python's "normal" import algorithm"""
+
+ def __init__(self, path=None):
+ self.path = path
+
+ def find_module(self, fullname, path=None):
+ subname = fullname.split(".")[-1]
+ if subname != fullname and self.path is None:
+ return None
+ if self.path is None:
+ path = None
+ else:
+ path = [self.path]
+ try:
+ file, filename, etc = imp.find_module(subname, path)
+ except ImportError:
+ return None
+ return ImpLoader(file, filename, etc)
+
+
+class ImpLoader:
+ """PEP 302 Loader that wraps Python's "normal" import algorithm"""
+
+ def __init__(self, file, filename, etc):
+ self.file = file
+ self.filename = filename
+ self.etc = etc
+
+ def load_module(self, fullname):
+ try:
+ mod = imp.load_module(fullname, self.file, self.filename, self.etc)
+ finally:
+ if self.file: self.file.close()
+ # Note: we don't set __loader__ because we want the module to look
+ # normal; i.e. this is just a wrapper for standard import machinery
+ return mod
+
+
+
+
+def get_importer(path_item):
+ """Retrieve a PEP 302 "importer" for the given path item
+
+ If there is no importer, this returns a wrapper around the builtin import
+ machinery. The returned importer is only cached if it was created by a
+ path hook.
+ """
+ try:
+ importer = sys.path_importer_cache[path_item]
+ except KeyError:
+ for hook in sys.path_hooks:
+ try:
+ importer = hook(path_item)
+ except ImportError:
+ pass
+ else:
+ break
+ else:
+ importer = None
+
+ sys.path_importer_cache.setdefault(path_item,importer)
+ if importer is None:
+ try:
+ importer = ImpWrapper(path_item)
+ except ImportError:
+ pass
+ return importer
+
+try:
+ from pkgutil import get_importer, ImpImporter
+except ImportError:
+ pass # Python 2.3 or 2.4, use our own implementation
+else:
+ ImpWrapper = ImpImporter # Python 2.5, use pkgutil's implementation
+ del ImpLoader, ImpImporter
+
+
+
+
+
+
+_declare_state('dict', _distribution_finders = {})
+
+def register_finder(importer_type, distribution_finder):
+ """Register `distribution_finder` to find distributions in sys.path items
+
+ `importer_type` is the type or class of a PEP 302 "Importer" (sys.path item
+ handler), and `distribution_finder` is a callable that, passed a path
+ item and the importer instance, yields ``Distribution`` instances found on
+ that path item. See ``pkg_resources.find_on_path`` for an example."""
+ _distribution_finders[importer_type] = distribution_finder
+
+
+def find_distributions(path_item, only=False):
+ """Yield distributions accessible via `path_item`"""
+ importer = get_importer(path_item)
+ finder = _find_adapter(_distribution_finders, importer)
+ return finder(importer, path_item, only)
+
+def find_in_zip(importer, path_item, only=False):
+ metadata = EggMetadata(importer)
+ if metadata.has_metadata('PKG-INFO'):
+ yield Distribution.from_filename(path_item, metadata=metadata)
+ if only:
+ return # don't yield nested distros
+ for subitem in metadata.resource_listdir('/'):
+ if subitem.endswith('.egg'):
+ subpath = os.path.join(path_item, subitem)
+ for dist in find_in_zip(zipimport.zipimporter(subpath), subpath):
+ yield dist
+
+register_finder(zipimport.zipimporter, find_in_zip)
+
+def StringIO(*args, **kw):
+ """Thunk to load the real StringIO on demand"""
+ global StringIO
+ try:
+ from cStringIO import StringIO
+ except ImportError:
+ from StringIO import StringIO
+ return StringIO(*args,**kw)
+
+def find_nothing(importer, path_item, only=False):
+ return ()
+register_finder(object,find_nothing)
+
+def find_on_path(importer, path_item, only=False):
+ """Yield distributions accessible on a sys.path directory"""
+ path_item = _normalize_cached(path_item)
+
+ if os.path.isdir(path_item) and os.access(path_item, os.R_OK):
+ if path_item.lower().endswith('.egg'):
+ # unpacked egg
+ yield Distribution.from_filename(
+ path_item, metadata=PathMetadata(
+ path_item, os.path.join(path_item,'EGG-INFO')
+ )
+ )
+ else:
+ # scan for .egg and .egg-info in directory
+ for entry in os.listdir(path_item):
+ lower = entry.lower()
+ if lower.endswith('.egg-info'):
+ fullpath = os.path.join(path_item, entry)
+ if os.path.isdir(fullpath):
+ # egg-info directory, allow getting metadata
+ metadata = PathMetadata(path_item, fullpath)
+ else:
+ metadata = FileMetadata(fullpath)
+ yield Distribution.from_location(
+ path_item,entry,metadata,precedence=DEVELOP_DIST
+ )
+ elif not only and lower.endswith('.egg'):
+ for dist in find_distributions(os.path.join(path_item, entry)):
+ yield dist
+ elif not only and lower.endswith('.egg-link'):
+ for line in file(os.path.join(path_item, entry)):
+ if not line.strip(): continue
+ for item in find_distributions(os.path.join(path_item,line.rstrip())):
+ yield item
+ break
+register_finder(ImpWrapper,find_on_path)
+
+_declare_state('dict', _namespace_handlers = {})
+_declare_state('dict', _namespace_packages = {})
+
+def register_namespace_handler(importer_type, namespace_handler):
+ """Register `namespace_handler` to declare namespace packages
+
+ `importer_type` is the type or class of a PEP 302 "Importer" (sys.path item
+ handler), and `namespace_handler` is a callable like this::
+
+ def namespace_handler(importer,path_entry,moduleName,module):
+ # return a path_entry to use for child packages
+
+ Namespace handlers are only called if the importer object has already
+ agreed that it can handle the relevant path item, and they should only
+ return a subpath if the module __path__ does not already contain an
+ equivalent subpath. For an example namespace handler, see
+ ``pkg_resources.file_ns_handler``.
+ """
+ _namespace_handlers[importer_type] = namespace_handler
+
+def _handle_ns(packageName, path_item):
+ """Ensure that named package includes a subpath of path_item (if needed)"""
+ importer = get_importer(path_item)
+ if importer is None:
+ return None
+ loader = importer.find_module(packageName)
+ if loader is None:
+ return None
+ module = sys.modules.get(packageName)
+ if module is None:
+ module = sys.modules[packageName] = imp.new_module(packageName)
+ module.__path__ = []; _set_parent_ns(packageName)
+ elif not hasattr(module,'__path__'):
+ raise TypeError("Not a package:", packageName)
+ handler = _find_adapter(_namespace_handlers, importer)
+ subpath = handler(importer,path_item,packageName,module)
+ if subpath is not None:
+ path = module.__path__; path.append(subpath)
+ loader.load_module(packageName); module.__path__ = path
+ return subpath
+
+def declare_namespace(packageName):
+ """Declare that package 'packageName' is a namespace package"""
+
+ imp.acquire_lock()
+ try:
+ if packageName in _namespace_packages:
+ return
+
+ path, parent = sys.path, None
+ if '.' in packageName:
+ parent = '.'.join(packageName.split('.')[:-1])
+ declare_namespace(parent)
+ __import__(parent)
+ try:
+ path = sys.modules[parent].__path__
+ except AttributeError:
+ raise TypeError("Not a package:", parent)
+
+ # Track what packages are namespaces, so when new path items are added,
+ # they can be updated
+ _namespace_packages.setdefault(parent,[]).append(packageName)
+ _namespace_packages.setdefault(packageName,[])
+
+ for path_item in path:
+ # Ensure all the parent's path items are reflected in the child,
+ # if they apply
+ _handle_ns(packageName, path_item)
+
+ finally:
+ imp.release_lock()
+
+def fixup_namespace_packages(path_item, parent=None):
+ """Ensure that previously-declared namespace packages include path_item"""
+ imp.acquire_lock()
+ try:
+ for package in _namespace_packages.get(parent,()):
+ subpath = _handle_ns(package, path_item)
+ if subpath: fixup_namespace_packages(subpath,package)
+ finally:
+ imp.release_lock()
+
+def file_ns_handler(importer, path_item, packageName, module):
+ """Compute an ns-package subpath for a filesystem or zipfile importer"""
+
+ subpath = os.path.join(path_item, packageName.split('.')[-1])
+ normalized = _normalize_cached(subpath)
+ for item in module.__path__:
+ if _normalize_cached(item)==normalized:
+ break
+ else:
+ # Only return the path if it's not already there
+ return subpath
+
+register_namespace_handler(ImpWrapper,file_ns_handler)
+register_namespace_handler(zipimport.zipimporter,file_ns_handler)
+
+
+def null_ns_handler(importer, path_item, packageName, module):
+ return None
+
+register_namespace_handler(object,null_ns_handler)
+
+
+def normalize_path(filename):
+ """Normalize a file/dir name for comparison purposes"""
+ return os.path.normcase(os.path.realpath(filename))
+
+def _normalize_cached(filename,_cache={}):
+ try:
+ return _cache[filename]
+ except KeyError:
+ _cache[filename] = result = normalize_path(filename)
+ return result
+
+def _set_parent_ns(packageName):
+ parts = packageName.split('.')
+ name = parts.pop()
+ if parts:
+ parent = '.'.join(parts)
+ setattr(sys.modules[parent], name, sys.modules[packageName])
+
+
+def yield_lines(strs):
+ """Yield non-empty/non-comment lines of a ``basestring`` or sequence"""
+ if isinstance(strs,basestring):
+ for s in strs.splitlines():
+ s = s.strip()
+ if s and not s.startswith('#'): # skip blank lines/comments
+ yield s
+ else:
+ for ss in strs:
+ for s in yield_lines(ss):
+ yield s
+
+LINE_END = re.compile(r"\s*(#.*)?$").match # whitespace and comment
+CONTINUE = re.compile(r"\s*\\\s*(#.*)?$").match # line continuation
+DISTRO = re.compile(r"\s*((\w|[-.])+)").match # Distribution or extra
+VERSION = re.compile(r"\s*(<=?|>=?|==|!=)\s*((\w|[-.])+)").match # ver. info
+COMMA = re.compile(r"\s*,").match # comma between items
+OBRACKET = re.compile(r"\s*\[").match
+CBRACKET = re.compile(r"\s*\]").match
+MODULE = re.compile(r"\w+(\.\w+)*$").match
+EGG_NAME = re.compile(
+ r"(?P[^-]+)"
+ r"( -(?P[^-]+) (-py(?P[^-]+) (-(?P.+))? )? )?",
+ re.VERBOSE | re.IGNORECASE
+).match
+
+component_re = re.compile(r'(\d+ | [a-z]+ | \.| -)', re.VERBOSE)
+replace = {'pre':'c', 'preview':'c','-':'final-','rc':'c','dev':'@'}.get
+
+def _parse_version_parts(s):
+ for part in component_re.split(s):
+ part = replace(part,part)
+ if not part or part=='.':
+ continue
+ if part[:1] in '0123456789':
+ yield part.zfill(8) # pad for numeric comparison
+ else:
+ yield '*'+part
+
+ yield '*final' # ensure that alpha/beta/candidate are before final
+
+def parse_version(s):
+ """Convert a version string to a chronologically-sortable key
+
+ This is a rough cross between distutils' StrictVersion and LooseVersion;
+ if you give it versions that would work with StrictVersion, then it behaves
+ the same; otherwise it acts like a slightly-smarter LooseVersion. It is
+ *possible* to create pathological version coding schemes that will fool
+ this parser, but they should be very rare in practice.
+
+ The returned value will be a tuple of strings. Numeric portions of the
+ version are padded to 8 digits so they will compare numerically, but
+ without relying on how numbers compare relative to strings. Dots are
+ dropped, but dashes are retained. Trailing zeros between alpha segments
+ or dashes are suppressed, so that e.g. "2.4.0" is considered the same as
+ "2.4". Alphanumeric parts are lower-cased.
+
+ The algorithm assumes that strings like "-" and any alpha string that
+ alphabetically follows "final" represents a "patch level". So, "2.4-1"
+ is assumed to be a branch or patch of "2.4", and therefore "2.4.1" is
+ considered newer than "2.4-1", which in turn is newer than "2.4".
+
+ Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that
+ come before "final" alphabetically) are assumed to be pre-release versions,
+ so that the version "2.4" is considered newer than "2.4a1".
+
+ Finally, to handle miscellaneous cases, the strings "pre", "preview", and
+ "rc" are treated as if they were "c", i.e. as though they were release
+ candidates, and therefore are not as new as a version string that does not
+ contain them, and "dev" is replaced with an '@' so that it sorts lower than
+ than any other pre-release tag.
+ """
+ parts = []
+ for part in _parse_version_parts(s.lower()):
+ if part.startswith('*'):
+ if part<'*final': # remove '-' before a prerelease tag
+ while parts and parts[-1]=='*final-': parts.pop()
+ # remove trailing zeros from each series of numeric parts
+ while parts and parts[-1]=='00000000':
+ parts.pop()
+ parts.append(part)
+ return tuple(parts)
+
+class EntryPoint(object):
+ """Object representing an advertised importable object"""
+
+ def __init__(self, name, module_name, attrs=(), extras=(), dist=None):
+ if not MODULE(module_name):
+ raise ValueError("Invalid module name", module_name)
+ self.name = name
+ self.module_name = module_name
+ self.attrs = tuple(attrs)
+ self.extras = Requirement.parse(("x[%s]" % ','.join(extras))).extras
+ self.dist = dist
+
+ def __str__(self):
+ s = "%s = %s" % (self.name, self.module_name)
+ if self.attrs:
+ s += ':' + '.'.join(self.attrs)
+ if self.extras:
+ s += ' [%s]' % ','.join(self.extras)
+ return s
+
+ def __repr__(self):
+ return "EntryPoint.parse(%r)" % str(self)
+
+ def load(self, require=True, env=None, installer=None):
+ if require: self.require(env, installer)
+ entry = __import__(self.module_name, globals(),globals(), ['__name__'])
+ for attr in self.attrs:
+ try:
+ entry = getattr(entry,attr)
+ except AttributeError:
+ raise ImportError("%r has no %r attribute" % (entry,attr))
+ return entry
+
+ def require(self, env=None, installer=None):
+ if self.extras and not self.dist:
+ raise UnknownExtra("Can't require() without a distribution", self)
+ map(working_set.add,
+ working_set.resolve(self.dist.requires(self.extras),env,installer))
+
+
+
+ #@classmethod
+ def parse(cls, src, dist=None):
+ """Parse a single entry point from string `src`
+
+ Entry point syntax follows the form::
+
+ name = some.module:some.attr [extra1,extra2]
+
+ The entry name and module name are required, but the ``:attrs`` and
+ ``[extras]`` parts are optional
+ """
+ try:
+ attrs = extras = ()
+ name,value = src.split('=',1)
+ if '[' in value:
+ value,extras = value.split('[',1)
+ req = Requirement.parse("x["+extras)
+ if req.specs: raise ValueError
+ extras = req.extras
+ if ':' in value:
+ value,attrs = value.split(':',1)
+ if not MODULE(attrs.rstrip()):
+ raise ValueError
+ attrs = attrs.rstrip().split('.')
+ except ValueError:
+ raise ValueError(
+ "EntryPoint must be in 'name=module:attrs [extras]' format",
+ src
+ )
+ else:
+ return cls(name.strip(), value.strip(), attrs, extras, dist)
+
+ parse = classmethod(parse)
+
+
+
+
+
+
+
+
+ #@classmethod
+ def parse_group(cls, group, lines, dist=None):
+ """Parse an entry point group"""
+ if not MODULE(group):
+ raise ValueError("Invalid group name", group)
+ this = {}
+ for line in yield_lines(lines):
+ ep = cls.parse(line, dist)
+ if ep.name in this:
+ raise ValueError("Duplicate entry point", group, ep.name)
+ this[ep.name]=ep
+ return this
+
+ parse_group = classmethod(parse_group)
+
+ #@classmethod
+ def parse_map(cls, data, dist=None):
+ """Parse a map of entry point groups"""
+ if isinstance(data,dict):
+ data = data.items()
+ else:
+ data = split_sections(data)
+ maps = {}
+ for group, lines in data:
+ if group is None:
+ if not lines:
+ continue
+ raise ValueError("Entry points must be listed in groups")
+ group = group.strip()
+ if group in maps:
+ raise ValueError("Duplicate group name", group)
+ maps[group] = cls.parse_group(group, lines, dist)
+ return maps
+
+ parse_map = classmethod(parse_map)
+
+
+
+
+
+
+class Distribution(object):
+ """Wrap an actual or potential sys.path entry w/metadata"""
+ def __init__(self,
+ location=None, metadata=None, project_name=None, version=None,
+ py_version=PY_MAJOR, platform=None, precedence = EGG_DIST
+ ):
+ self.project_name = safe_name(project_name or 'Unknown')
+ if version is not None:
+ self._version = safe_version(version)
+ self.py_version = py_version
+ self.platform = platform
+ self.location = location
+ self.precedence = precedence
+ self._provider = metadata or empty_provider
+
+ #@classmethod
+ def from_location(cls,location,basename,metadata=None,**kw):
+ project_name, version, py_version, platform = [None]*4
+ basename, ext = os.path.splitext(basename)
+ if ext.lower() in (".egg",".egg-info"):
+ match = EGG_NAME(basename)
+ if match:
+ project_name, version, py_version, platform = match.group(
+ 'name','ver','pyver','plat'
+ )
+ return cls(
+ location, metadata, project_name=project_name, version=version,
+ py_version=py_version, platform=platform, **kw
+ )
+ from_location = classmethod(from_location)
+
+ hashcmp = property(
+ lambda self: (
+ getattr(self,'parsed_version',()), self.precedence, self.key,
+ -len(self.location or ''), self.location, self.py_version,
+ self.platform
+ )
+ )
+ def __cmp__(self, other): return cmp(self.hashcmp, other)
+ def __hash__(self): return hash(self.hashcmp)
+
+ # These properties have to be lazy so that we don't have to load any
+ # metadata until/unless it's actually needed. (i.e., some distributions
+ # may not know their name or version without loading PKG-INFO)
+
+ #@property
+ def key(self):
+ try:
+ return self._key
+ except AttributeError:
+ self._key = key = self.project_name.lower()
+ return key
+ key = property(key)
+
+ #@property
+ def parsed_version(self):
+ try:
+ return self._parsed_version
+ except AttributeError:
+ self._parsed_version = pv = parse_version(self.version)
+ return pv
+
+ parsed_version = property(parsed_version)
+
+ #@property
+ def version(self):
+ try:
+ return self._version
+ except AttributeError:
+ for line in self._get_metadata('PKG-INFO'):
+ if line.lower().startswith('version:'):
+ self._version = safe_version(line.split(':',1)[1].strip())
+ return self._version
+ else:
+ raise ValueError(
+ "Missing 'Version:' header and/or PKG-INFO file", self
+ )
+ version = property(version)
+
+
+
+
+ #@property
+ def _dep_map(self):
+ try:
+ return self.__dep_map
+ except AttributeError:
+ dm = self.__dep_map = {None: []}
+ for name in 'requires.txt', 'depends.txt':
+ for extra,reqs in split_sections(self._get_metadata(name)):
+ if extra: extra = safe_extra(extra)
+ dm.setdefault(extra,[]).extend(parse_requirements(reqs))
+ return dm
+ _dep_map = property(_dep_map)
+
+ def requires(self,extras=()):
+ """List of Requirements needed for this distro if `extras` are used"""
+ dm = self._dep_map
+ deps = []
+ deps.extend(dm.get(None,()))
+ for ext in extras:
+ try:
+ deps.extend(dm[safe_extra(ext)])
+ except KeyError:
+ raise UnknownExtra(
+ "%s has no such extra feature %r" % (self, ext)
+ )
+ return deps
+
+ def _get_metadata(self,name):
+ if self.has_metadata(name):
+ for line in self.get_metadata_lines(name):
+ yield line
+
+ def activate(self,path=None):
+ """Ensure distribution is importable on `path` (default=sys.path)"""
+ if path is None: path = sys.path
+ self.insert_on(path)
+ if path is sys.path:
+ fixup_namespace_packages(self.location)
+ map(declare_namespace, self._get_metadata('namespace_packages.txt'))
+
+
+ def egg_name(self):
+ """Return what this distribution's standard .egg filename should be"""
+ filename = "%s-%s-py%s" % (
+ to_filename(self.project_name), to_filename(self.version),
+ self.py_version or PY_MAJOR
+ )
+
+ if self.platform:
+ filename += '-'+self.platform
+ return filename
+
+ def __repr__(self):
+ if self.location:
+ return "%s (%s)" % (self,self.location)
+ else:
+ return str(self)
+
+ def __str__(self):
+ try: version = getattr(self,'version',None)
+ except ValueError: version = None
+ version = version or "[unknown version]"
+ return "%s %s" % (self.project_name,version)
+
+ def __getattr__(self,attr):
+ """Delegate all unrecognized public attributes to .metadata provider"""
+ if attr.startswith('_'):
+ raise AttributeError,attr
+ return getattr(self._provider, attr)
+
+ #@classmethod
+ def from_filename(cls,filename,metadata=None, **kw):
+ return cls.from_location(
+ _normalize_cached(filename), os.path.basename(filename), metadata,
+ **kw
+ )
+ from_filename = classmethod(from_filename)
+
+ def as_requirement(self):
+ """Return a ``Requirement`` that matches this distribution exactly"""
+ return Requirement.parse('%s==%s' % (self.project_name, self.version))
+
+ def load_entry_point(self, group, name):
+ """Return the `name` entry point of `group` or raise ImportError"""
+ ep = self.get_entry_info(group,name)
+ if ep is None:
+ raise ImportError("Entry point %r not found" % ((group,name),))
+ return ep.load()
+
+ def get_entry_map(self, group=None):
+ """Return the entry point map for `group`, or the full entry map"""
+ try:
+ ep_map = self._ep_map
+ except AttributeError:
+ ep_map = self._ep_map = EntryPoint.parse_map(
+ self._get_metadata('entry_points.txt'), self
+ )
+ if group is not None:
+ return ep_map.get(group,{})
+ return ep_map
+
+ def get_entry_info(self, group, name):
+ """Return the EntryPoint object for `group`+`name`, or ``None``"""
+ return self.get_entry_map(group).get(name)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ def insert_on(self, path, loc = None):
+ """Insert self.location in path before its nearest parent directory"""
+
+ loc = loc or self.location
+ if not loc:
+ return
+
+ nloc = _normalize_cached(loc)
+ bdir = os.path.dirname(nloc)
+ npath= [(p and _normalize_cached(p) or p) for p in path]
+
+ bp = None
+ for p, item in enumerate(npath):
+ if item==nloc:
+ break
+ elif item==bdir and self.precedence==EGG_DIST:
+ # if it's an .egg, give it precedence over its directory
+ if path is sys.path:
+ self.check_version_conflict()
+ path.insert(p, loc)
+ npath.insert(p, nloc)
+ break
+ else:
+ if path is sys.path:
+ self.check_version_conflict()
+ path.append(loc)
+ return
+
+ # p is the spot where we found or inserted loc; now remove duplicates
+ while 1:
+ try:
+ np = npath.index(nloc, p+1)
+ except ValueError:
+ break
+ else:
+ del npath[np], path[np]
+ p = np # ha!
+
+ return
+
+
+ def check_version_conflict(self):
+ if self.key=='setuptools':
+ return # ignore the inevitable setuptools self-conflicts :(
+
+ nsp = dict.fromkeys(self._get_metadata('namespace_packages.txt'))
+ loc = normalize_path(self.location)
+ for modname in self._get_metadata('top_level.txt'):
+ if (modname not in sys.modules or modname in nsp
+ or modname in _namespace_packages
+ ):
+ continue
+
+ fn = getattr(sys.modules[modname], '__file__', None)
+ if fn and (normalize_path(fn).startswith(loc) or fn.startswith(loc)):
+ continue
+ issue_warning(
+ "Module %s was already imported from %s, but %s is being added"
+ " to sys.path" % (modname, fn, self.location),
+ )
+
+ def has_version(self):
+ try:
+ self.version
+ except ValueError:
+ issue_warning("Unbuilt egg for "+repr(self))
+ return False
+ return True
+
+ def clone(self,**kw):
+ """Copy this distribution, substituting in any changed keyword args"""
+ for attr in (
+ 'project_name', 'version', 'py_version', 'platform', 'location',
+ 'precedence'
+ ):
+ kw.setdefault(attr, getattr(self,attr,None))
+ kw.setdefault('metadata', self._provider)
+ return self.__class__(**kw)
+
+
+
+
+ #@property
+ def extras(self):
+ return [dep for dep in self._dep_map if dep]
+ extras = property(extras)
+
+
+def issue_warning(*args,**kw):
+ level = 1
+ g = globals()
+ try:
+ # find the first stack frame that is *not* code in
+ # the pkg_resources module, to use for the warning
+ while sys._getframe(level).f_globals is g:
+ level += 1
+ except ValueError:
+ pass
+ from warnings import warn
+ warn(stacklevel = level+1, *args, **kw)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def parse_requirements(strs):
+ """Yield ``Requirement`` objects for each specification in `strs`
+
+ `strs` must be an instance of ``basestring``, or a (possibly-nested)
+ iterable thereof.
+ """
+ # create a steppable iterator, so we can handle \-continuations
+ lines = iter(yield_lines(strs))
+
+ def scan_list(ITEM,TERMINATOR,line,p,groups,item_name):
+
+ items = []
+
+ while not TERMINATOR(line,p):
+ if CONTINUE(line,p):
+ try:
+ line = lines.next(); p = 0
+ except StopIteration:
+ raise ValueError(
+ "\\ must not appear on the last nonblank line"
+ )
+
+ match = ITEM(line,p)
+ if not match:
+ raise ValueError("Expected "+item_name+" in",line,"at",line[p:])
+
+ items.append(match.group(*groups))
+ p = match.end()
+
+ match = COMMA(line,p)
+ if match:
+ p = match.end() # skip the comma
+ elif not TERMINATOR(line,p):
+ raise ValueError(
+ "Expected ',' or end-of-list in",line,"at",line[p:]
+ )
+
+ match = TERMINATOR(line,p)
+ if match: p = match.end() # skip the terminator, if any
+ return line, p, items
+
+ for line in lines:
+ match = DISTRO(line)
+ if not match:
+ raise ValueError("Missing distribution spec", line)
+ project_name = match.group(1)
+ p = match.end()
+ extras = []
+
+ match = OBRACKET(line,p)
+ if match:
+ p = match.end()
+ line, p, extras = scan_list(
+ DISTRO, CBRACKET, line, p, (1,), "'extra' name"
+ )
+
+ line, p, specs = scan_list(VERSION,LINE_END,line,p,(1,2),"version spec")
+ specs = [(op,safe_version(val)) for op,val in specs]
+ yield Requirement(project_name, specs, extras)
+
+
+def _sort_dists(dists):
+ tmp = [(dist.hashcmp,dist) for dist in dists]
+ tmp.sort()
+ dists[::-1] = [d for hc,d in tmp]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+class Requirement:
+ def __init__(self, project_name, specs, extras):
+ """DO NOT CALL THIS UNDOCUMENTED METHOD; use Requirement.parse()!"""
+ self.unsafe_name, project_name = project_name, safe_name(project_name)
+ self.project_name, self.key = project_name, project_name.lower()
+ index = [(parse_version(v),state_machine[op],op,v) for op,v in specs]
+ index.sort()
+ self.specs = [(op,ver) for parsed,trans,op,ver in index]
+ self.index, self.extras = index, tuple(map(safe_extra,extras))
+ self.hashCmp = (
+ self.key, tuple([(op,parsed) for parsed,trans,op,ver in index]),
+ frozenset(self.extras)
+ )
+ self.__hash = hash(self.hashCmp)
+
+ def __str__(self):
+ specs = ','.join([''.join(s) for s in self.specs])
+ extras = ','.join(self.extras)
+ if extras: extras = '[%s]' % extras
+ return '%s%s%s' % (self.project_name, extras, specs)
+
+ def __eq__(self,other):
+ return isinstance(other,Requirement) and self.hashCmp==other.hashCmp
+
+ def __contains__(self,item):
+ if isinstance(item,Distribution):
+ if item.key != self.key: return False
+ if self.index: item = item.parsed_version # only get if we need it
+ elif isinstance(item,basestring):
+ item = parse_version(item)
+ last = None
+ for parsed,trans,op,ver in self.index:
+ action = trans[cmp(item,parsed)]
+ if action=='F': return False
+ elif action=='T': return True
+ elif action=='+': last = True
+ elif action=='-' or last is None: last = False
+ if last is None: last = True # no rules encountered
+ return last
+
+
+ def __hash__(self):
+ return self.__hash
+
+ def __repr__(self): return "Requirement.parse(%r)" % str(self)
+
+ #@staticmethod
+ def parse(s):
+ reqs = list(parse_requirements(s))
+ if reqs:
+ if len(reqs)==1:
+ return reqs[0]
+ raise ValueError("Expected only one requirement", s)
+ raise ValueError("No requirements found", s)
+
+ parse = staticmethod(parse)
+
+state_machine = {
+ # =><
+ '<' : '--T',
+ '<=': 'T-T',
+ '>' : 'F+F',
+ '>=': 'T+F',
+ '==': 'T..',
+ '!=': 'F++',
+}
+
+
+def _get_mro(cls):
+ """Get an mro for a type or classic class"""
+ if not isinstance(cls,type):
+ class cls(cls,object): pass
+ return cls.__mro__[1:]
+ return cls.__mro__
+
+def _find_adapter(registry, ob):
+ """Return an adapter factory for `ob` from `registry`"""
+ for t in _get_mro(getattr(ob, '__class__', type(ob))):
+ if t in registry:
+ return registry[t]
+
+
+def ensure_directory(path):
+ """Ensure that the parent directory of `path` exists"""
+ dirname = os.path.dirname(path)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+
+def split_sections(s):
+ """Split a string or iterable thereof into (section,content) pairs
+
+ Each ``section`` is a stripped version of the section header ("[section]")
+ and each ``content`` is a list of stripped lines excluding blank lines and
+ comment-only lines. If there are any such lines before the first section
+ header, they're returned in a first ``section`` of ``None``.
+ """
+ section = None
+ content = []
+ for line in yield_lines(s):
+ if line.startswith("["):
+ if line.endswith("]"):
+ if section or content:
+ yield section, content
+ section = line[1:-1].strip()
+ content = []
+ else:
+ raise ValueError("Invalid section heading", line)
+ else:
+ content.append(line)
+
+ # wrap up last segment
+ yield section, content
+
+def _mkstemp(*args,**kw):
+ from tempfile import mkstemp
+ old_open = os.open
+ try:
+ os.open = os_open # temporarily bypass sandboxing
+ return mkstemp(*args,**kw)
+ finally:
+ os.open = old_open # and then put it back
+
+
+# Set up global resource manager (deliberately not state-saved)
+_manager = ResourceManager()
+def _initialize(g):
+ for name in dir(_manager):
+ if not name.startswith('_'):
+ g[name] = getattr(_manager, name)
+_initialize(globals())
+
+# Prepare the master working set and make the ``require()`` API available
+_declare_state('object', working_set = WorkingSet())
+try:
+ # Does the main program list any requirements?
+ from __main__ import __requires__
+except ImportError:
+ pass # No: just use the default working set based on sys.path
+else:
+ # Yes: ensure the requirements are met, by prefixing sys.path if necessary
+ try:
+ working_set.require(__requires__)
+ except VersionConflict: # try it without defaults already on sys.path
+ working_set = WorkingSet([]) # by starting with an empty path
+ for dist in working_set.resolve(
+ parse_requirements(__requires__), Environment()
+ ):
+ working_set.add(dist)
+ for entry in sys.path: # add any missing entries from sys.path
+ if entry not in working_set.entries:
+ working_set.add_entry(entry)
+ sys.path[:] = working_set.entries # then copy back to sys.path
+
+require = working_set.require
+iter_entry_points = working_set.iter_entry_points
+add_activation_listener = working_set.subscribe
+run_script = working_set.run_script
+run_main = run_script # backward compatibility
+# Activate all distributions already on sys.path, and ensure that
+# all distributions added to the working set in the future (e.g. by
+# calling ``require()``) will get activated as well.
+add_activation_listener(lambda dist: dist.activate())
+working_set.entries=[]; map(working_set.add_entry,sys.path) # match order
+
diff --git a/libs/themoviedb/tmdb.py b/libs/themoviedb/tmdb.py
index e67115db..7df6e70d 100644
--- a/libs/themoviedb/tmdb.py
+++ b/libs/themoviedb/tmdb.py
@@ -350,7 +350,7 @@ class MovieDb:
etree = XmlHandler(url).getEt()
lookup_results = SearchResults()
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)
return lookup_results
diff --git a/libs/xmg/__init__.py b/libs/xmg/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/libs/xmg/xmg.py b/libs/xmg/xmg.py
deleted file mode 100644
index 8c791370..00000000
--- a/libs/xmg/xmg.py
+++ /dev/null
@@ -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)