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>= 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; + }, + /**/ + 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; }, + /**/ 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; })(); +/**/ + +/**/ +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)