From 7df24643ace63e9a47d36a5b117ce73a92047fb6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 21 Feb 2012 17:16:02 +0100 Subject: [PATCH 01/99] Api doc base --- couchpotato/api.py | 21 +++++-- couchpotato/core/_base/_core/main.py | 14 ++++- couchpotato/core/_base/updater/main.py | 20 ++++++- couchpotato/core/plugins/browser/main.py | 13 +++- couchpotato/core/plugins/file/main.py | 8 ++- couchpotato/core/plugins/log/main.py | 24 +++++++- couchpotato/core/plugins/manage/main.py | 7 ++- couchpotato/core/plugins/movie/main.py | 34 ++++++++++- couchpotato/core/settings/__init__.py | 37 +++++++++++- couchpotato/static/style/api.css | 75 ++++++++++++++++++++++++ couchpotato/templates/api.html | 49 ++++++++++++++++ 11 files changed, 280 insertions(+), 22 deletions(-) create mode 100644 couchpotato/static/style/api.css create mode 100644 couchpotato/templates/api.html diff --git a/couchpotato/api.py b/couchpotato/api.py index d1aa6a42..77f24a56 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -1,21 +1,30 @@ -from couchpotato.core.helpers.request import jsonified from flask.blueprints import Blueprint +from flask.templating import render_template api = Blueprint('api', __name__) +api_docs = {} +api_docs_missing = [] -def addApiView(route, func, static = False): - api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '-') if route else 'index', view_func = func) +def addApiView(route, func, static = False, docs = None): + api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func) + if docs: + api_docs[route[4:] if route[0:4] == 'api.' else route] = docs + else: + api_docs_missing.append(route) """ Api view """ def index(): - from couchpotato import app + from couchpotato import app routes = [] for route, x in sorted(app.view_functions.iteritems()): if route[0:4] == 'api.': - routes += [route[4:]] + routes += [route[4:].replace('::', '.')] - return jsonified({'routes': routes}) + if api_docs.get(''): + del api_docs[''] + del api_docs_missing[''] + return render_template('api.html', routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing)) addApiView('', index) addApiView('default', index) diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py index 7a142081..f1846cd3 100644 --- a/couchpotato/core/_base/_core/main.py +++ b/couchpotato/core/_base/_core/main.py @@ -24,9 +24,17 @@ class Core(Plugin): shutdown_started = False def __init__(self): - addApiView('app.shutdown', self.shutdown) - addApiView('app.restart', self.restart) - addApiView('app.available', self.available) + addApiView('app.shutdown', self.shutdown, docs = { + 'desc': 'Shutdown the app.', + 'return': {'type': 'string: shutdown'} + }) + addApiView('app.restart', self.restart, docs = { + 'desc': 'Restart the app.', + 'return': {'type': 'string: restart'} + }) + addApiView('app.available', self.available, docs = { + 'desc': 'Check if app available.' + }) addEvent('app.crappy_shutdown', self.crappyShutdown) addEvent('app.crappy_restart', self.crappyRestart) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 6e9236f6..57a98c68 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -29,9 +29,25 @@ class Updater(Plugin): addEvent('app.load', self.check) - addApiView('updater.info', self.getInfo) + addApiView('updater.info', self.getInfo, docs = { + 'desc': 'Get updater information', + 'return': { + 'type': 'object', + 'example': """ + { + 'repo_name': "Name of used repository", + 'last_check': "last checked for update", + 'update_version': "available update version or empty", + 'version': current_cp_version + } + """ + } + }) addApiView('updater.update', self.doUpdateView) - addApiView('updater.check', self.checkView) + addApiView('updater.check', self.checkView, docs = { + 'desc': 'Check for available update', + 'return': {'type': 'see updater.info'} + }) def getInfo(self): diff --git a/couchpotato/core/plugins/browser/main.py b/couchpotato/core/plugins/browser/main.py index ca7e3134..3c3e6e8f 100644 --- a/couchpotato/core/plugins/browser/main.py +++ b/couchpotato/core/plugins/browser/main.py @@ -11,7 +11,18 @@ if os.name == 'nt': class FileBrowser(Plugin): def __init__(self): - addApiView('directory.list', self.view) + addApiView('directory.list', self.view, docs = { + 'desc': 'Return the directory list of a given directory', + 'params': { + 'path': {'desc': 'The directory to scan'}, + 'show_hidden': {'desc': 'Also show hidden files'} + }, + 'return': {'type': 'object', 'example': """{ + 'is_root': bool, //is top most folder + 'empty': bool, //directory is empty + 'dirs': array, //directory names +}"""} + }) def getDirectories(self, path = '/', show_hidden = True): diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py index ba84baad..1ec72812 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file/main.py @@ -19,7 +19,13 @@ class FileManager(Plugin): addEvent('file.download', self.download) addEvent('file.types', self.getTypes) - addApiView('file.cache/', self.showCacheFile, static = True) + addApiView('file.cache/', self.showCacheFile, static = True, docs = { + 'desc': 'Return a file from the cp_data/cache directory', + 'params': { + 'filename': {'desc': 'path/filename of the wanted file'} + }, + 'return': {'type': 'file'} + }) def showCacheFile(self, filename = ''): diff --git a/couchpotato/core/plugins/log/main.py b/couchpotato/core/plugins/log/main.py index 00d2baf0..2a2c022f 100644 --- a/couchpotato/core/plugins/log/main.py +++ b/couchpotato/core/plugins/log/main.py @@ -12,9 +12,27 @@ log = CPLog(__name__) class Logging(Plugin): def __init__(self): - addApiView('logging.get', self.get) - addApiView('logging.clear', self.clear) - addApiView('logging.log', self.log) + addApiView('logging.get', self.get, docs = { + 'desc': 'Get the full log file by number', + 'params': { + 'nr': {'desc': 'Number of the log to get.'} + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'log': string, //Log file + 'total': int, //Total log files available +}"""} + }) + addApiView('logging.clear', self.clear, docs = { + 'desc': 'Remove all the log files' + }) + addApiView('logging.log', self.log, docs = { + 'desc': 'Get the full log file by number', + 'params': { + 'type': {'desc': 'Type of logging, default "error"'}, + '**kwargs': {'type':'object', 'desc': 'All other params will be printed in the log string.'}, + } + }) def get(self): diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index 6810199f..20fdd4c3 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -21,7 +21,12 @@ class Manage(Plugin): fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2) addEvent('manage.update', self.updateLibrary) - addApiView('manage.update', self.updateLibraryView) + addApiView('manage.update', self.updateLibraryView, docs = { + 'desc': 'Update the library by scanning for new movies', + 'params': { + 'full': {'desc': 'Do a full update or just recently changed/added movies.'}, + } + }) if not Env.get('dev'): addEvent('app.load', self.updateLibrary) diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index 44045ba9..cb9ffd9b 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -26,9 +26,37 @@ class MoviePlugin(Plugin): } def __init__(self): - addApiView('movie.search', self.search) - addApiView('movie.list', self.listView) - addApiView('movie.refresh', self.refresh) + addApiView('movie.search', self.search, docs = { + 'desc': 'Search the movie providers for a movie', + 'params': { + 'q': {'desc': 'The (partial) movie name you want to search for'}, + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'empty': bool, any movies returned or not, + 'movies': array, movies found, +}"""} + }) + addApiView('movie.list', self.listView, docs = { + 'desc': 'List movies in wanted list', + 'params': { + 'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'}, + 'limit_offset': {'desc': 'Limit the movie list. Examples: "50", "50,30"'}, + 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'}, + 'search': {'desc': 'Search movie title'}, + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'empty': bool, any movies returned or not, + 'movies': array, movies found, +}"""} + }) + addApiView('movie.refresh', self.refresh, docs = { + 'desc': 'Refresh a movie by id', + 'params': { + 'id': {'desc': 'The id of the movie that needs to be refreshed'}, + } + }) addApiView('movie.available_chars', self.charView) addApiView('movie.add', self.addView) diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index 6bed3dd6..29b6b051 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -16,8 +16,41 @@ class Settings(object): def __init__(self): - addApiView('settings', self.view) - addApiView('settings.save', self.saveView) + addApiView('settings', self.view, docs = { + 'desc': 'Return the options and its values of settings.conf. Including the default values and group ordering used on the settings page.', + 'return': {'type': 'object', 'example': """{ + // objects like in __init__.py of plugin + "options": { + "moovee" : { + "groups" : [{ + "description" : "SD movies only", + "name" : "#alt.binaries.moovee", + "options" : [{ + "default" : false, + "name" : "enabled", + "type" : "enabler" + }], + "tab" : "providers" + }], + "name" : "moovee" + } + }, + // object structured like settings.conf + "values": { + "moovee": { + "enabled": false + } + } +}"""} + }) + addApiView('settings.save', self.saveView, docs = { + 'desc': 'Save setting to config file (settings.conf)', + 'params': { + 'section': {'desc': 'The section name in settings.conf'}, + 'option': {'desc': 'The option name'}, + 'value': {'desc': 'The value you want to save'}, + } + }) def setFile(self, config_file): self.file = config_file diff --git a/couchpotato/static/style/api.css b/couchpotato/static/style/api.css new file mode 100644 index 00000000..15bddd36 --- /dev/null +++ b/couchpotato/static/style/api.css @@ -0,0 +1,75 @@ +html { + font-size: 12px; + line-height: 1.5; + font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; + font-size: 14px; +} + +h1, h2, h3, h4, h5 { + clear: both; + padding: 0; + margin: 0; + font-size: 14px; +} + +h1 { + font-size: 25px; +} + +h2 { + font-size: 20px; +} + +pre { + background: #ccc; + font-family: monospace; + margin: 0; + padding: 2%; + width: 96%; + display: block; +} + +.api { + margin-bottom: 20px; + overflow: hidden; +} + + .api .description { + color: #333; + padding: 0 0 5px; + } + + .api .params { + background: #f5f5f5; + width: 100%; + } + .api h3 { + clear: both; + float: left; + width: 100px; + } + + .api .params { + float: left; + width: 700px; + } + + .api .params .param { + vertical-align: top; + } + + .api .params .param th { + text-align: left; + width: 100px; + } + + .api .param .type { + font-style: italic; + margin-right: 10px; + width: 100px; + } + + .api .return { + float: left; + width: 700px; + } diff --git a/couchpotato/templates/api.html b/couchpotato/templates/api.html new file mode 100644 index 00000000..9852ccb5 --- /dev/null +++ b/couchpotato/templates/api.html @@ -0,0 +1,49 @@ + + + + + API documentation + + + +

API

+ {% for route in routes %} + {% if api_docs.get(route) %} +
+

{{route}}

+
{{api_docs[route].get('desc', '')}}
+ + {% if api_docs[route].get('params') %} +

Params

+ + {% for param in api_docs[route]['params'] %} + + + + + + {% endfor %} +
{{param}}{{ api_docs[route]['params'][param].get('type', 'string') }}{{ api_docs[route]['params'][param]['desc'] }}
+ {% endif %} + + {% if api_docs[route].get('return') %} +

Return

+
+
{{ api_docs[route]['return'].get('type', '{"success": True}') }}
+ {% if api_docs[route]['return'].get('example') %} +
+

Example

+
{{ api_docs[route]['return'].get('example', '')|safe }}
+
+ {% endif %} +
+ {% endif %} +
+ {% endif %} + {% endfor %} + +

Missing documentation

+
{{', '.join(api_docs_missing)}}
+ + + \ No newline at end of file From e03c7b4c1cd462f063ecedae34340cd85634ffab Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 19 Feb 2012 12:37:25 +0100 Subject: [PATCH 02/99] Merge lists, not overwrite --- couchpotato/core/helpers/variable.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index c3a9980a..f38cf39c 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -34,6 +34,8 @@ def mergeDicts(a, b): else: if isDict(current_src[key]) and isDict(current_dst[key]): stack.append((current_dst[key], current_src[key])) + elif isinstance(current_src[key], list) and isinstance(current_dst[key], list): + current_dst[key].extend(current_src[key]) else: current_dst[key] = current_src[key] return dst From e011a59a525d7d59247062568cabb9e3baccfdd4 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 19 Feb 2012 12:45:22 +0100 Subject: [PATCH 03/99] kwargs in file.download for urlopen --- couchpotato/core/plugins/file/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py index 1ec72812..e0f02408 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file/main.py @@ -35,7 +35,7 @@ class FileManager(Plugin): from flask.helpers import send_from_directory return send_from_directory(cache_dir, filename) - def download(self, url = '', dest = None, overwrite = False): + def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}): if not dest: # to Cache dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url))) @@ -44,7 +44,7 @@ class FileManager(Plugin): return dest try: - filedata = self.urlopen(url) + filedata = self.urlopen(url, **urlopen_kwargs) except: return False From 6a588167290a49a43e85075feb9c8e2c5a17fe89 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 19 Feb 2012 12:48:54 +0100 Subject: [PATCH 04/99] Initial trailer support --- couchpotato/core/plugins/trailer/__init__.py | 34 ++++++++++++++++ couchpotato/core/plugins/trailer/main.py | 32 +++++++++++++++ couchpotato/core/providers/trailer/base.py | 8 ++++ .../core/providers/trailer/hdtrailers/main.py | 40 +++++++++---------- 4 files changed, 92 insertions(+), 22 deletions(-) create mode 100644 couchpotato/core/plugins/trailer/__init__.py create mode 100644 couchpotato/core/plugins/trailer/main.py diff --git a/couchpotato/core/plugins/trailer/__init__.py b/couchpotato/core/plugins/trailer/__init__.py new file mode 100644 index 00000000..49b0cb9e --- /dev/null +++ b/couchpotato/core/plugins/trailer/__init__.py @@ -0,0 +1,34 @@ +from .main import Trailer + +def start(): + return Trailer() + +config = [{ + 'name': 'trailer', + 'groups': [ + { + 'tab': 'metadata', + 'name': 'trailer', + 'options': [ + { + 'name': 'enabled', + 'label': 'Search and download trailers', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'quality', + 'default': '720p', + 'type': 'dropdown', + 'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')], + }, + { + 'name': 'automatic', + 'default': False, + 'type': 'bool', + 'description': 'Automaticly search & download for movies in library', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/plugins/trailer/main.py b/couchpotato/core/plugins/trailer/main.py new file mode 100644 index 00000000..8f8e4ab2 --- /dev/null +++ b/couchpotato/core/plugins/trailer/main.py @@ -0,0 +1,32 @@ +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.variable import getExt +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin +import os + +log = CPLog(__name__) + + +class Trailer(Plugin): + + def __init__(self): + addEvent('renamer.after', self.searchSingle) + + def searchSingle(self, group): + + if self.isDisabled() or len(group['files']['trailer']) > 0: return + + trailers = fireEvent('trailer.search', group = group, merge = True) + + for trailer in trailers.get(self.conf('quality'), []): + destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer)) + if not os.path.isfile(destination): + fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True) + else: + log.debug('Trailer already exists: %s' % destination) + + # Download first and break + break + + def getRootName(self, data = {}): + return os.path.join(data['destination_dir'], data['filename']) diff --git a/couchpotato/core/providers/trailer/base.py b/couchpotato/core/providers/trailer/base.py index 17d2e5cb..338ca9b3 100644 --- a/couchpotato/core/providers/trailer/base.py +++ b/couchpotato/core/providers/trailer/base.py @@ -1,5 +1,13 @@ +from couchpotato.core.event import addEvent +from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import Provider +log = CPLog(__name__) + class TrailerProvider(Provider): + type = 'trailer' + + def __init__(self): + addEvent('trailer.search', self.search) diff --git a/couchpotato/core/providers/trailer/hdtrailers/main.py b/couchpotato/core/providers/trailer/hdtrailers/main.py index 5fe476dd..e6628760 100644 --- a/couchpotato/core/providers/trailer/hdtrailers/main.py +++ b/couchpotato/core/providers/trailer/hdtrailers/main.py @@ -1,4 +1,5 @@ from BeautifulSoup import SoupStrainer, BeautifulSoup +from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.logger import CPLog from couchpotato.core.providers.trailer.base import TrailerProvider from string import letters, digits @@ -16,47 +17,40 @@ class HDTrailers(TrailerProvider): } providers = ['apple.ico', 'yahoo.ico', 'moviefone.ico', 'myspace.ico', 'favicon.ico'] - def find(self, movie): + def search(self, group): - movie_name = movie['library']['titles'][0]['title'] + movie_name = group['library']['titles'][0]['title'] - url = self.url['api'] % self.movieUrlName(movie_name) - try: - data = self.urlopen(url) - except: - return {} + url = self.urls['api'] % self.movieUrlName(movie_name) + data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url) + + result_data = {} - p480 = [] - p720 = [] - p1080 = [] did_alternative = False for provider in self.providers: results = self.findByProvider(data, provider) # Find alternative if results.get('404') and not did_alternative: - results = self.findViaAlternative(movie_name) + results = self.findViaAlternative(group) did_alternative = True - p480.extend(results.get('480p')) - p720.extend(results.get('720p')) - p1080.extend(results.get('1080p')) + result_data = mergeDicts(result_data, results) - return {'480p':p480, '720p':p720, '1080p':p1080} + return result_data - def findViaAlternative(self, movie): + def findViaAlternative(self, group): results = {'480p':[], '720p':[], '1080p':[]} - url = "%s?%s" % (self.url['backup'], urlencode({'s':movie})) - try: - data = self.urlopen(url) - except: - return results + movie_name = group['library']['titles'][0]['title'] + + url = "%s?%s" % (self.url['backup'], urlencode({'s':movie_name})) + data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url) try: tables = SoupStrainer('div') html = BeautifulSoup(data, parseOnlyThese = tables) - result_table = html.findAll('h2', text = re.compile(movie)) + result_table = html.findAll('h2', text = re.compile(movie_name)) for h2 in result_table: if 'trailer' in h2.lower(): @@ -88,6 +82,8 @@ class HDTrailers(TrailerProvider): break if 'trailer' in trtext and not 'clip' in trtext and provider in trtext: nr = 0 + if 'trailer' not in tr.find('span', 'standardTrailerName').text.lower(): + continue resolutions = tr.findAll('td', attrs = {'class':'bottomTableResolution'}) for res in resolutions: results[str(res.a.contents[0])].insert(0, res.a['href']) From 3face06baf821028221db3761ec2ae37c0aeb3ef Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 19 Feb 2012 12:53:55 +0100 Subject: [PATCH 05/99] Remove nfo when not renaming as .orig.nfo --- couchpotato/core/plugins/renamer/main.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 72b09275..b67214f7 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -6,7 +6,7 @@ from couchpotato.core.helpers.request import jsonified from couchpotato.core.helpers.variable import getExt, mergeDicts 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, File from couchpotato.environment import Env import os import re @@ -123,6 +123,9 @@ class Renamer(Plugin): # 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) + if self.conf('clean_up'): + for current_file in group['files'][file_type]: + remove_files.append(current_file) continue # Subtitle extra @@ -301,11 +304,15 @@ class Renamer(Plugin): # Remove files for src in remove_files: + + if isinstance(src, File): + src = src.path + log.info('(fake) Removing "%s"' % src) # Remove matching releases for release in remove_releases: - log.info('(fake) Removing release %s' % release) + log.info('(fake) Removing release %s' % release.identifier) # Search for trailers etc fireEventAsync('renamer.after', group) From a5cc1859d127dd7cd790b498857a64b99554e6e5 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 20 Feb 2012 21:30:06 +0100 Subject: [PATCH 06/99] Floating movie navigation --- couchpotato/core/plugins/movie/static/list.js | 14 ++++++++++++-- couchpotato/core/plugins/movie/static/movie.css | 15 ++++++++++++++- couchpotato/static/style/main.css | 6 +++--- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index e90fb89f..883e15b8 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -141,6 +141,16 @@ var MovieList = new Class({ } }); + self.nav_scrollspy = new ScrollSpy({ + min: 10, + onEnter: function(){ + self.navigation.addClass('float') + }, + onLeave: function(){ + self.navigation.removeClass('float') + } + }); + }, reset: function(){ @@ -211,8 +221,8 @@ var MovieList = new Class({ loadMore: function(){ var self = this; - - self.getMovies() + if(self.offset >= self.options.limit) + self.getMovies() }, store: function(movies){ diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 7d06d709..311667b0 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -5,7 +5,7 @@ */ .movies { - padding: 20px 0; + padding: 50px 0 20px; } .movies .movie { @@ -215,7 +215,20 @@ .movies .alph_nav { overflow: hidden; + transition: box-shadow .4s linear; + position: fixed; + z-index: 2; + top: 0; + padding: 100px 7px 7px; + margin: -7px; + width: 960px; } + + .movies .alph_nav.float { + box-shadow: 0 0 10px rgba(0,0,0,0.4); + border-radius: 4px; + background: #4e5969; + } .movies .alph_nav ul { list-style: none; diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 8446492c..f5f60428 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -153,12 +153,12 @@ body > .spinner, .mask{ height: 60px; position: fixed; width: 99%; - z-index: 2; - box-shadow: 0 0 5px rgba(0,0,0,0.1); + z-index: 5; + box-shadow: 0 0 5px rgba(0,0,0,0.05); transition: box-shadow .4s cubic-bezier(0.9,0,0.1,1); } .header.with_shadow { - box-shadow: 0 0 50px rgba(0,0,0,0.3); + box-shadow: 0 0 20px rgba(0,0,0,0.3); } .header > div { From b164674ff7f44ee672ae938a0707e92b70eca6d7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 21 Feb 2012 20:42:41 +0100 Subject: [PATCH 07/99] More API documentation --- couchpotato/core/_base/updater/main.py | 15 ++++----- couchpotato/core/plugins/movie/main.py | 26 ++++++++++++--- couchpotato/core/plugins/release/main.py | 14 +++++++-- couchpotato/core/plugins/renamer/main.py | 6 ++-- couchpotato/core/plugins/status/main.py | 8 ++++- couchpotato/static/style/api.css | 40 ++++++++++++++++++------ couchpotato/templates/api.html | 10 +++--- 7 files changed, 88 insertions(+), 31 deletions(-) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 57a98c68..cf9150f4 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -33,15 +33,12 @@ class Updater(Plugin): 'desc': 'Get updater information', 'return': { 'type': 'object', - 'example': """ - { - 'repo_name': "Name of used repository", - 'last_check': "last checked for update", - 'update_version': "available update version or empty", - 'version': current_cp_version - } - """ - } + 'example': """{ + 'repo_name': "Name of used repository", + 'last_check': "last checked for update", + 'update_version': "available update version or empty", + 'version': current_cp_version +}"""} }) addApiView('updater.update', self.doUpdateView) addApiView('updater.check', self.checkView, docs = { diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index cb9ffd9b..aa578ca9 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -58,10 +58,28 @@ class MoviePlugin(Plugin): } }) addApiView('movie.available_chars', self.charView) - - addApiView('movie.add', self.addView) - addApiView('movie.edit', self.edit) - addApiView('movie.delete', self.delete) + addApiView('movie.add', self.addView, docs = { + 'desc': 'Add new movie to the wanted list', + 'params': { + 'identifier': {'desc': 'IMDB id of the movie your want to add.'}, + 'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'}, + 'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, + } + }) + addApiView('movie.edit', self.edit, docs = { + 'desc': 'Add new movie to the wanted list', + 'params': { + 'id': {'desc': 'Movie ID you want to edit'}, + 'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'}, + 'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, + } + }) + addApiView('movie.delete', self.delete, docs = { + 'desc': 'Delete a movie from the wanted list', + 'params': { + 'id': {'desc': 'Movie ID you want to delete'}, + } + }) addEvent('movie.add', self.add) addEvent('movie.get', self.get) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 9297dbb7..01e7f600 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -16,8 +16,18 @@ class Release(Plugin): def __init__(self): addEvent('release.add', self.add) - addApiView('release.download', self.download) - addApiView('release.delete', self.delete) + addApiView('release.download', self.download, docs = { + 'desc': 'Send a release manually to the downloaders', + 'params': { + 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} + } + }) + addApiView('release.delete', self.delete, docs = { + 'desc': 'Check for available update', + 'params': { + 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} + } + }) def add(self, group): db = get_session() diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index b67214f7..6541c185 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -22,7 +22,9 @@ class Renamer(Plugin): def __init__(self): - addApiView('renamer.scan', self.scanView) + addApiView('renamer.scan', self.scanView, docs = { + 'desc': 'For the renamer to check for new files to rename', + }) addEvent('renamer.scan', self.scan) addEvent('app.load', self.scan) @@ -31,7 +33,7 @@ class Renamer(Plugin): def scanView(self): - fireEvent('renamer.scan') + fireEventAsync('renamer.scan') return jsonified({ 'success': True diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py index 5256f8de..b8c4f5f8 100644 --- a/couchpotato/core/plugins/status/main.py +++ b/couchpotato/core/plugins/status/main.py @@ -29,7 +29,13 @@ class StatusPlugin(Plugin): addEvent('status.all', self.all) addEvent('app.initialize', self.fill) - addApiView('status.list', self.list) + addApiView('status.list', self.list, docs = { + 'desc': 'Check for available update', + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'list': array, statuses +}"""} + }) def list(self): diff --git a/couchpotato/static/style/api.css b/couchpotato/static/style/api.css index 15bddd36..afba2b5b 100644 --- a/couchpotato/static/style/api.css +++ b/couchpotato/static/style/api.css @@ -1,3 +1,4 @@ + html { font-size: 12px; line-height: 1.5; @@ -5,15 +6,22 @@ html { font-size: 14px; } +* { + margin: 0; + padding: 0; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + h1, h2, h3, h4, h5 { clear: both; - padding: 0; - margin: 0; font-size: 14px; } h1 { font-size: 25px; + padding: 20px 40px; } h2 { @@ -21,18 +29,23 @@ h2 { } pre { - background: #ccc; + background: #eee; font-family: monospace; margin: 0; - padding: 2%; - width: 96%; + padding: 10px; + width: 100%; display: block; + font-size: 12px; } -.api { - margin-bottom: 20px; +.api, .missing { overflow: hidden; + border-bottom: 1px solid #eee; + padding: 40px; } + .api:hover { + color: #000; + } .api .description { color: #333; @@ -40,7 +53,7 @@ pre { } .api .params { - background: #f5f5f5; + background: #fafafa; width: 100%; } .api h3 { @@ -54,6 +67,14 @@ pre { width: 700px; } + .api .params td, .api .params th { + padding: 3px 5px; + border-bottom: 1px solid #eee; + } + .api .params tr:last-child td, .api .params tr:last-child th { + border: 0; + } + .api .params .param { vertical-align: top; } @@ -67,9 +88,10 @@ pre { font-style: italic; margin-right: 10px; width: 100px; + color: #666; } .api .return { float: left; width: 700px; - } + } \ No newline at end of file diff --git a/couchpotato/templates/api.html b/couchpotato/templates/api.html index 9852ccb5..47fb6c34 100644 --- a/couchpotato/templates/api.html +++ b/couchpotato/templates/api.html @@ -6,7 +6,7 @@ -

API

+

CouchPotato API Documentation

{% for route in routes %} {% if api_docs.get(route) %}
@@ -15,7 +15,7 @@ {% if api_docs[route].get('params') %}

Params

- +
{% for param in api_docs[route]['params'] %} @@ -42,8 +42,10 @@ {% endif %} {% endfor %} -

Missing documentation

-
{{', '.join(api_docs_missing)}}
+
+

Missing documentation

+ {{', '.join(api_docs_missing)}} +
\ No newline at end of file From d568bea393c13e3d4dca0617541da04d56f090cd Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 25 Feb 2012 01:51:07 +0100 Subject: [PATCH 08/99] Don't even try to load migration when db doesn't exists --- couchpotato/runner.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 6b8196ea..ef56462b 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -128,24 +128,24 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # Load migrations - from migrate.versioning.api import version_control, db_version, version, upgrade - db = Env.get('db_path') - repo = os.path.join(base_path, 'couchpotato', 'core', 'migration') - logging.getLogger('migrate').setLevel(logging.WARNING) # Disable logging for migration - - 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) + db = Env.get('db_path') + if os.path.isfile(db): + from migrate.versioning.api import version_control, db_version, version, upgrade + repo = os.path.join(base_path, 'couchpotato', 'core', 'migration') + logging.getLogger('migrate').setLevel(logging.WARNING) # Disable logging for migration - if current_db_version < latest_db_version and not debug: - log.info('Doing database upgrade. From %d to %d' % (current_db_version, latest_db_version)) - upgrade(db, repo) + latest_db_version = version(repo) + 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) + + if current_db_version < latest_db_version and not debug: + log.info('Doing database upgrade. From %d to %d' % (current_db_version, latest_db_version)) + upgrade(db, repo) # Configure Database from couchpotato.core.settings.model import setup From 5c0805a6437256fbbe87193b739782f81b64804d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 25 Feb 2012 02:22:49 +0100 Subject: [PATCH 09/99] Duplicate options in settings --- couchpotato/core/settings/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index 29b6b051..60c77cb2 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -25,7 +25,7 @@ class Settings(object): "groups" : [{ "description" : "SD movies only", "name" : "#alt.binaries.moovee", - "options" : [{ + "options" : [{ "default" : false, "name" : "enabled", "type" : "enabler" @@ -161,7 +161,6 @@ class Settings(object): 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): @@ -169,7 +168,6 @@ class Settings(object): def view(self): - return jsonified({ 'options': self.getOptions(), 'values': self.getValues() From 4101671f4220c5b59c93e1ace7b9bae4cb6d789b Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Feb 2012 21:37:37 +0100 Subject: [PATCH 10/99] Do not initialize multiple times --- couchpotato/runner.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/couchpotato/runner.py b/couchpotato/runner.py index ef56462b..11236c16 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -130,7 +130,9 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # Load migrations initialize = True db = Env.get('db_path') - if os.path.isfile(db): + if os.path.isfile(db.replace('sqlite:///', '')): + initialize = False + from migrate.versioning.api import version_control, db_version, version, upgrade repo = os.path.join(base_path, 'couchpotato', 'core', 'migration') logging.getLogger('migrate').setLevel(logging.WARNING) # Disable logging for migration @@ -138,7 +140,6 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En latest_db_version = version(repo) 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) From ed7a220d47ac6afc46bd31150527ae9bd32e52b8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Feb 2012 21:38:10 +0100 Subject: [PATCH 11/99] NZBClub, replace spaces --- couchpotato/core/providers/nzb/nzbclub/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py index c6731eb5..78ef5d70 100644 --- a/couchpotato/core/providers/nzb/nzbclub/main.py +++ b/couchpotato/core/providers/nzb/nzbclub/main.py @@ -71,7 +71,7 @@ class NZBClub(NZBProvider, RSS): 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'size': tryInt(size) / 1024 / 1024, 'url': enclosure['url'], - 'download': enclosure['url'], + 'download': enclosure['url'].replace(' ', '_'), 'detail_url': self.getTextElement(nzb, "link"), 'description': description, } From 7a47ca96ed3fad5c5bcb2dcba000bd65c4a53c95 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Feb 2012 21:38:56 +0100 Subject: [PATCH 12/99] Use profile list as array, not object --- couchpotato/core/plugins/movie/static/search.js | 2 +- couchpotato/static/scripts/page/wanted.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js index 3f29d139..f2f14c4f 100644 --- a/couchpotato/core/plugins/movie/static/search.js +++ b/couchpotato/core/plugins/movie/static/search.js @@ -318,7 +318,7 @@ Block.Search.Item = new Class({ }).inject(self.title_select) }) - Object.each(Quality.getActiveProfiles(), function(profile){ + Quality.getActiveProfiles().each(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/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index c2ac6c08..bb68dc29 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -75,7 +75,7 @@ window.addEvent('domready', function(){ }).inject(self.title_select); }); - Object.each(Quality.getActiveProfiles(), function(profile){ + Quality.getActiveProfiles().each(function(profile){ new Element('option', { 'value': profile.id ? profile.id : profile.data.id, 'text': profile.label ? profile.label : profile.data.label From ffd64c5122d9ad4fbf4b74e40ee51b0b07423d4e Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Feb 2012 21:42:30 +0100 Subject: [PATCH 13/99] Box modeling --- .../core/plugins/movie/static/movie.css | 21 ++++++++++--------- .../core/plugins/movie/static/search.css | 10 ++++----- couchpotato/static/style/main.css | 11 ++++++++-- couchpotato/static/style/page/settings.css | 2 +- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 311667b0..087312e3 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -20,8 +20,8 @@ } .movies .data { padding: 20px; - height: 140px; - width: 800px; + height: 180px; + width: 840px; position: relative; float: right; @@ -101,8 +101,8 @@ background-repeat: no-repeat; background-position: center; display: inline-block; - width: 20px; - height: 20px; + width: 26px; + height: 26px; padding: 3px; opacity: 0; } @@ -132,6 +132,7 @@ .movies .options { position: absolute; margin-left: 120px; + width: 840px; } .movies .options .form { @@ -162,7 +163,7 @@ .movies .options .table .item > * { display: inline-block; padding: 0 5px; - width: 50px; + width: 60px; min-height: 24px; white-space: nowrap; text-overflow: ellipsis; @@ -174,10 +175,10 @@ border: 0; } .movies .options .table .provider { - width: 120px; + width: 130px; } .movies .options .table .name { - width: 360px; + width: 370px; overflow: hidden; text-align: left; padding: 0 10px; @@ -187,8 +188,8 @@ .movies .options .table .is_available { width: 80px; } .movies .options .table a { - width: 16px !important; - height: 16px; + width: 30px !important; + height: 20px; opacity: 0.8; } .movies .options .table a:hover { @@ -241,7 +242,7 @@ .movies .alph_nav li { display: inline-block; vertical-align: top; - width: 24px; + width: 23px; height: 24px; line-height: 26px; text-align: center; diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css index 6d34c72c..bea734ee 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -10,11 +10,10 @@ } .search_form input { - padding-right: 25px; padding: 4px; margin: 0; font-size: 14px; - width: 90%; + width: 100%; } .search_form .input a { width: 17px; @@ -56,7 +55,7 @@ } .search_form .results { - max-height: 550px; + max-height: 570px; overflow-x: hidden; padding: 10px 0; } @@ -106,7 +105,7 @@ .movie_result .data { padding: 0 15px; - width: 440px; + width: 470px; position: relative; min-height: 100px; top: 0; @@ -131,13 +130,12 @@ display: inline-block; margin: 15px 3% 15px 0; vertical-align: top; - border-radius: 3px; box-shadow: 0 0 3px rgba(0,0,0,0.35); } .movie_result .info { - width: 74%; + width: 80%; display: inline-block; vertical-align: top; padding: 15px 0; diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index f5f60428..239bb87b 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -26,6 +26,12 @@ body { #clean { background: transparent !important; } + +* { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} pre { white-space: pre-wrap; @@ -150,9 +156,10 @@ body > .spinner, .mask{ .header { background: #4e5969; padding:10px; - height: 60px; + height: 80px; position: fixed; - width: 99%; + margin: 0; + width: 100%; z-index: 5; box-shadow: 0 0 5px rgba(0,0,0,0.05); transition: box-shadow .4s cubic-bezier(0.9,0,0.1,1); diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index 1821c412..e85651e1 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -47,7 +47,7 @@ .page.settings .containers { - width: 75.8%; + width: 80%; float: left; padding: 20px 2%; min-height: 300px; From f5b3144c07fd4f1bac789b03f751ddd1514486fd Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Feb 2012 21:44:23 +0100 Subject: [PATCH 14/99] Use sprite for images --- .../core/plugins/movie/static/search.css | 2 +- couchpotato/static/images/checks.png | Bin 1111 -> 0 bytes couchpotato/static/images/sprite.png | Bin 0 -> 1350 bytes couchpotato/static/style/main.css | 17 ++++++------- couchpotato/static/style/page/settings.css | 24 +++--------------- 5 files changed, 13 insertions(+), 30 deletions(-) delete mode 100644 couchpotato/static/images/checks.png create mode 100644 couchpotato/static/images/sprite.png diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css index bea734ee..957a3cd4 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -22,7 +22,7 @@ margin: 0 0 -5px -20px; top: 4px; right: 5px; - background: url('../images/checks.png') right -36px no-repeat; + background: url('../images/sprite.png') right -36px no-repeat; cursor: pointer; } diff --git a/couchpotato/static/images/checks.png b/couchpotato/static/images/checks.png deleted file mode 100644 index 3561d917e9e1ead4a885b14e6a0e55bd1026b861..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1111 zcmV-d1gQIoP)J~wn6)G6X7zWOmtq!kgkmH|g3NxWD21*2KQvxIP#(_K&YbkDSXSYX0-Ceh2(?>vQ`hIOM=6?0peX95+wCE`x;n2OKbBPm z`|(hIAe=GFmzqbCBm$3m`TEV_)oT*(sAxJ8whLZCpa%!pXQ?ThFTi#?lu;?rZw!Ak zp+rK7y>?54~O z2r7pBTQlF4e29dB<32y+G9akd9YkjR?KT zY!^fkGOdW)D1xF$E>Q@v;p2Lm-DXmGt6_IcXCLWzVG=tXHQ+nQAs|yD)@}T!6OPkQE}lA=Yb-I z;xiHRz8CiaxbBq#1-r6od;`L?JwwP*>g&I5CON>GH#>*jwJ5XpDVww{(NI@q=4=^? zmtBS&RjkEY46|xru{m@fwasyjP1;qiC~K@=i4BqfbtDiVTbjnazh==k_$B?15&Au+a#f-nNp`3*Eh zcIf;2({`qAG09#-88hkjuAaAT9bZ7*$4{->Q+KX|X?!BgxEqm9*WJ^kWTxx}Jb3tc zYgu)D#jf{LDmE|?{RZpM2$W+<@IWy6$^YHrsqUd4VJQq{=D9*gS!DQPc z-cbr~hTxZZ`*a}G)P3siW|(QiFPR>4Vgh^%N-ZiXszHM4fh-G#g3|D_G|z1^@s6NI7vw0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU$=t)FDRCwC#T5D_+RTQ2VbXNq{LK-kB z@sFfxf5<}&NkJvr}j0dM~+y)8WqU>{4tibJLTZJ$LSR&$;K`@0^(}4FI6# zsxj0YwuK8up*7Q_fJR56@mCV5^dHEeo8t=w^A3-$i6v4^$!SpO9};}dH>{_;b=k*P6^3V?#6vkw;DN5kQeSyikU?|eu zcX42i26T=0zaI!i0PhdYeDmGOmoU&$)Tdu}&R_v6L}Qf8<$7jyS=ptL%U54xSf+wu znB$_DrA9`EkG_7OrXAjwgz`7SnPtA-J({Kw_=+EY`Ss<8AF2F|V_666S7b$Dr=~&; z)m2;Hh5d0TGkTz#7)CdtIG~h$dgAlFz5QOnGcXJ~y81j76%RcC^-j$-()J<+fW`~E z>c~6qJ=NOLJ@|HQT^0O(uh}No#v9$#19BJu0gF(kp(tixq%||%$6`^;fb?Koo{gFl zWbv>DNAlff+W$i|2`9i(4*750{QdNw4wyK;OipL~NTTuNCrDRJs$+ta;V*<(5d~MnNm}zfWGc(rU(wPh;%gU^Y&>tQ% zlQ)|BHsSNRSFUvJ*Cs|_iCFX}5^d)Tfc)VHbstUe{HbIzB_QF)ub({f)}fDaRw5@? zlQm;-@>qdCQOn>ImxZDhvSupgeHq^a@U^T5vTE4^J^?muFA{Rpuk{^wlb#HPx2Do! z2dbSrYVhQgBPv6>}UQekIOLQY$ScrEXJao9k zYO%Lu*R#9V{dL{n9g4&OtggLKdeHSXKG0vY|GKHjFcm45S)--xTxogv zy3+~(k3nJ_R24;n<@_d=p@)1E6FYZQZwnHSA;C>{aAfSafuSp)^TMV6C#!dCfMt9o zthjrS&O18RYvv{~0AGFm?e;V0y4s%JwYw5OAGsdguyK9s`L6cw8-Cbl766i)Xx3%; zc>qy#8h77=DSQT3-m;Zyo5m4ka4-}@<*n&Jp{4u$olTo*?Hn*KG&M4Z=PsuY&@$0p=+eeX-uAXLDfx7P3MuI z>wKPBp`e}kvnAiwSh!`V&1W{)-CepHPXI80(;(lgWt{~aq4O99K8nA$TeXljOboP{ zkVWXa;pgU@mc6?7Iht5fk-&;xGt#=N_IClpJ&-YlWaDLLA1+HT%; zZrVA(2D4M$A~;UCv8l8u=NhzpnG=mutbmn_^L|QJ$p5Cnu{U10qn|x8RwxL)5MM?8 z`@5#Gp^9A4gXMa+&ElC^6EI}5q<7Qosm?wDHrSm2`$vEQ098;G>|&dAE&u=k07*qo IM6N<$f+-@5AOHXW literal 0 HcmV?d00001 diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 239bb87b..4392e9c3 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -258,15 +258,14 @@ body > .spinner, .mask{ height: 16px; width: 16px; cursor: pointer; + background: url('../images/sprite.png') no-repeat -200px; } .check.highlighted { background-color: #424c59; } - .check.checked { - background-image: url('../images/checks.png'); - background-position: -2px 0; -} -.check input { - display: none !important; -} + .check.checked { background-position: -2px 0; } + .check.indeterminate { background-position: -1px -119px; } + .check input { + display: none !important; + } .select { cursor: pointer; @@ -281,14 +280,14 @@ body > .spinner, .mask{ box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20); - background: url('../images/checks.png') no-repeat 94% -53px, -webkit-gradient( + background: url('../images/sprite.png') no-repeat 94% -53px, -webkit-gradient( linear, left bottom, left top, color-stop(0, #406db8), color-stop(1, #5b9bd1) ); - background: url('../images/checks.png') no-repeat 94% -53px, -moz-linear-gradient( + background: url('../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient( center top, #5b9bd1 0%, #406db8 100% diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index e85651e1..c780e566 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -117,22 +117,6 @@ vertical-align: middle; padding-left: 2%; } - - .check { - display: inline-block; - vertical-align: middle; - height: 16px; - width: 16px; - cursor: pointer; - } - .check.highlighted { background-color: #424c59; } - .check.checked { - background-image: url('../../images/checks.png'); - background-position: -2px 0; -} - .check input { - display: none; - } .page .check + .formHint { float: none; @@ -354,28 +338,28 @@ border-radius: 2px; } .page .tag_input > ul:hover > li.choice { - background: url('../images/checks.png') no-repeat 94% -53px, -webkit-gradient( + background: url('../images/sprite.png') no-repeat 94% -53px, -webkit-gradient( linear, left bottom, left top, color-stop(0, rgba(255,255,255,0.1)), color-stop(1, rgba(255,255,255,0.3)) ); - background: url('../images/checks.png') no-repeat 94% -53px, -moz-linear-gradient( + background: url('../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient( center top, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.1) 100% ); } .page .tag_input > ul > li.choice:hover { - background: url('../images/checks.png') no-repeat 94% -53px, -webkit-gradient( + background: url('../images/sprite.png') no-repeat 94% -53px, -webkit-gradient( linear, left bottom, left top, color-stop(0, #406db8), color-stop(1, #5b9bd1) ); - background: url('../images/checks.png') no-repeat 94% -53px, -moz-linear-gradient( + background: url('../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient( center top, #5b9bd1 0%, #406db8 100% From 58855d35f79d6525abfd0f716af415512ddc4430 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Feb 2012 21:44:44 +0100 Subject: [PATCH 15/99] Some settings styling --- couchpotato/static/style/page/settings.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index c780e566..f17d7247 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -28,10 +28,10 @@ right top, 40% 4%, color-stop(0, rgba(0,0,0, 0.3)), - color-stop(0.9, rgba(0,0,0, 0)) + color-stop(1, rgba(0,0,0, 0)) ); background-image: -moz-linear-gradient( - 30% 0% 16deg, + 10% 0% 16deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.3) 100% ); From 08deab647c4015425103aa34565a6ea2fc205c81 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Feb 2012 21:45:16 +0100 Subject: [PATCH 16/99] Don't prefix css when developing --- couchpotato/templates/_desktop.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html index 28beaa10..7ec3fd95 100644 --- a/couchpotato/templates/_desktop.html +++ b/couchpotato/templates/_desktop.html @@ -10,8 +10,9 @@ + {% if not env.get('dev') %} - + {% endif %} From 43d491aab5541d170aefefaedd573b42805723f1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Feb 2012 21:45:45 +0100 Subject: [PATCH 17/99] Add identifier to movie lists --- couchpotato/static/scripts/page/manage.js | 2 +- couchpotato/static/scripts/page/wanted.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/static/scripts/page/manage.js b/couchpotato/static/scripts/page/manage.js index 0ed3dfc8..34529c55 100644 --- a/couchpotato/static/scripts/page/manage.js +++ b/couchpotato/static/scripts/page/manage.js @@ -17,8 +17,8 @@ Page.Manage = new Class({ }).inject(self.el); self.list = new MovieList({ + 'identifier': 'manage', 'status': 'done', - 'navigation': true, 'actions': MovieActions }); $(self.list).inject(self.el); diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index bb68dc29..9bf5e48a 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -12,8 +12,8 @@ Page.Wanted = new Class({ // Wanted movies self.wanted = new MovieList({ + 'identifier': 'wanted', 'status': 'active', - 'navigation': true, 'actions': MovieActions }); $(self.wanted).inject(self.el); From c8dc03a78ea25cf5b93c97452c40e87b21bb62ca Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Feb 2012 21:47:06 +0100 Subject: [PATCH 18/99] Api docs under /docs/ --- couchpotato/__init__.py | 16 ++++++++++++++++ couchpotato/api.py | 16 ++++------------ couchpotato/core/_base/_core/__init__.py | 2 +- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index 7058b111..f7dfb3a5 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -1,3 +1,4 @@ +from couchpotato.api import api_docs, api_docs_missing from couchpotato.core.auth import requires_auth from couchpotato.core.event import fireEvent from couchpotato.core.logger import CPLog @@ -35,6 +36,21 @@ def addView(route, func, static = False): def index(): return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) +""" Api view """ +@web.route('docs/') +@requires_auth +def apiDocs(): + from couchpotato import app + routes = [] + for route, x in sorted(app.view_functions.iteritems()): + if route[0:4] == 'api.': + routes += [route[4:].replace('::', '.')] + + if api_docs.get(''): + del api_docs[''] + del api_docs_missing[''] + return render_template('api.html', routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing)) + @app.errorhandler(404) def page_not_found(error): index_url = url_for('web.index') diff --git a/couchpotato/api.py b/couchpotato/api.py index 77f24a56..71cf9b87 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -1,5 +1,7 @@ from flask.blueprints import Blueprint +from flask.helpers import url_for from flask.templating import render_template +from werkzeug.utils import redirect api = Blueprint('api', __name__) api_docs = {} @@ -14,17 +16,7 @@ def addApiView(route, func, static = False, docs = None): """ Api view """ def index(): - - from couchpotato import app - routes = [] - for route, x in sorted(app.view_functions.iteritems()): - if route[0:4] == 'api.': - routes += [route[4:].replace('::', '.')] - - if api_docs.get(''): - del api_docs[''] - del api_docs_missing[''] - return render_template('api.html', routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing)) + index_url = url_for('web.index') + return redirect(index_url + 'docs/') addApiView('', index) -addApiView('default', index) diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py index 38631a2e..f48b157e 100644 --- a/couchpotato/core/_base/_core/__init__.py +++ b/couchpotato/core/_base/_core/__init__.py @@ -54,7 +54,7 @@ config = [{ 'name': 'api_key', 'default': uuid4().hex, 'readonly': 1, - 'description': "This is top-secret! Don't share this!", + 'description': 'Let 3rd party app do stuff. Docs', }, { 'name': 'debug', From 17d431a9e11b8e870c64cae51beccf2a30ef82aa Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Feb 2012 23:19:53 +0100 Subject: [PATCH 19/99] Mass editing of movies List movie view --- couchpotato/core/plugins/movie/main.py | 34 +-- couchpotato/core/plugins/movie/static/list.js | 201 ++++++++++++++++-- .../core/plugins/movie/static/movie.css | 196 +++++++++++++++-- .../core/plugins/movie/static/movie.js | 38 +++- couchpotato/static/style/main.css | 12 +- 5 files changed, 425 insertions(+), 56 deletions(-) diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index aa578ca9..2b377bb8 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -69,7 +69,7 @@ class MoviePlugin(Plugin): addApiView('movie.edit', self.edit, docs = { 'desc': 'Add new movie to the wanted list', 'params': { - 'id': {'desc': 'Movie ID you want to edit'}, + 'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'}, 'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'}, 'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, } @@ -77,7 +77,7 @@ class MoviePlugin(Plugin): addApiView('movie.delete', self.delete, docs = { 'desc': 'Delete a movie from the wanted list', 'params': { - 'id': {'desc': 'Movie ID you want to delete'}, + 'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'}, } }) @@ -301,19 +301,23 @@ class MoviePlugin(Plugin): params = getParams() db = get_session() - m = db.query(Movie).filter_by(id = params.get('id')).first() - m.profile_id = params.get('profile_id') + ids = params.get('id').split(',') + for movie_id in ids: - # Default title - for title in m.library.titles: - title.default = params.get('default_title').lower() == title.title.lower() + m = db.query(Movie).filter_by(id = movie_id).first() + m.profile_id = params.get('profile_id') - db.commit() + # Default title + if params.get('default_title'): + for title in m.library.titles: + title.default = params.get('default_title').lower() == title.title.lower() - fireEvent('movie.restatus', m.id) + db.commit() - movie_dict = m.to_dict(self.default_dict) - fireEventAsync('searcher.single', movie_dict) + fireEvent('movie.restatus', m.id) + + movie_dict = m.to_dict(self.default_dict) + fireEventAsync('searcher.single', movie_dict) return jsonified({ 'success': True, @@ -326,9 +330,11 @@ class MoviePlugin(Plugin): status = fireEvent('status.add', 'deleted', single = True) - movie = db.query(Movie).filter_by(id = params.get('id')).first() - movie.status_id = status.get('id') - db.commit() + ids = params.get('id').split(',') + for movie_id in ids: + movie = db.query(Movie).filter_by(id = movie_id).first() + movie.status_id = status.get('id') + db.commit() return jsonified({ 'success': True, diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index 883e15b8..e1971055 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -3,7 +3,7 @@ var MovieList = new Class({ Implements: [Options], options: { - navigation: false, + navigation: true, limit: 50 }, @@ -76,11 +76,15 @@ var MovieList = new Class({ var actions = a[info.status.identifier.capitalize()] || a.Wanted || {}; var m = new Movie(self, { - 'actions': actions + 'actions': actions, + 'view': self.current_view, + 'onSelect': self.calculateSelected.bind(self) }, info); $(m).inject(self.movie_list); m.fireEvent('injected'); + self.movies.include(m) + }); }, @@ -89,8 +93,12 @@ var MovieList = new Class({ var self = this; var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + self.current_view = self.getSavedView(); + self.el.addClass(self.current_view+'_list') + self.navigation = new Element('div.alph_nav').adopt( - self.alpha = new Element('ul.inlay', { + self.navigation_actions = new Element('ul.inlay.actions.reversed'), + self.navigation_alpha = new Element('ul.inlay.numbers', { 'events': { 'click:relay(li)': function(e, el){ self.movie_list.empty() @@ -99,24 +107,73 @@ var MovieList = new Class({ } } }), - self.search_input = new Element('input.inlay', { + self.navigation_search_input = new Element('input.inlay', { 'placeholder': 'Search', 'events': { 'keyup': self.search.bind(self), 'change': self.search.bind(self) } - })/*, - self.view = new Element('ul.inlay').adopt( - new Element('li.list'), - new Element('li.thumbnails'), - new Element('li.text') - )*/ + }), + self.mass_edit_form = new Element('div.mass_edit_form').adopt( + new Element('span.select').adopt( + self.mass_edit_select = new Element('input[type=checkbox].inlay', { + 'events': { + 'change': self.massEditToggleAll.bind(self) + } + }), + self.mass_edit_selected = new Element('span.count', {'text': 0}), + self.mass_edit_selected_label = new Element('span', {'text': 'selected'}) + ), + new Element('div.quality').adopt( + self.mass_edit_quality = new Element('select'), + new Element('a.button.orange', { + 'text': 'Change quality', + 'events': { + 'click': self.changeQualitySelected.bind(self) + } + }) + ), + new Element('div.delete').adopt( + new Element('span[text=or]'), + new Element('a.button.red', { + 'text': 'Delete', + 'events': { + 'click': self.deleteSelected.bind(self) + } + }) + ) + ) ).inject(self.el, 'top'); + // Mass edit + self.mass_edit_select_class = new Form.Check(self.mass_edit_select); + Quality.getActiveProfiles().each(function(profile){ + new Element('option', { + 'value': profile.id ? profile.id : profile.data.id, + 'text': profile.label ? profile.label : profile.data.label + }).inject(self.mass_edit_quality) + }); + + // Actions + ['mass_edit', 'thumbs', 'list'].each(function(view){ + self.navigation_actions.adopt( + new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', { + 'events': { + 'click': function(e){ + var a = 'active'; + self.navigation_actions.getElements('.'+a).removeClass(a); + self.changeView(this.get('data-view')); + this.addClass(a); + } + } + }).adopt(new Element('span')) + ) + }); + // All self.letters['all'] = new Element('li.letter_all.available.active', { 'text': 'ALL', - }).inject(self.alpha); + }).inject(self.navigation_alpha); // Chars chars.split('').each(function(c){ @@ -124,7 +181,7 @@ var MovieList = new Class({ 'text': c, 'class': 'letter_'+c, 'data-letter': c - }).inject(self.alpha); + }).inject(self.navigation_alpha); }); // Get available chars and highlight @@ -153,10 +210,106 @@ var MovieList = new Class({ }, + calculateSelected: function(){ + var self = this; + + var selected = 0, + movies = self.movies.length; + self.movies.each(function(movie){ + selected += movie.isSelected() ? 1 : 0 + }) + + var indeterminate = selected > 0 && selected < movies, + checked = selected == movies && selected > 0; + + self.mass_edit_select.set('indeterminate', indeterminate) + + self.mass_edit_select_class[checked ? 'check' : 'uncheck']() + self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate') + + self.mass_edit_selected.set('text', selected); + }, + + deleteSelected: function(){ + var self = this; + var ids = self.getSelectedMovies() + + var qObj = new Question('Are you sure you want to delete the selected movies?', 'Items using this profile, will be set to the default quality.', [{ + 'text': 'Yes, delete them', + 'class': 'delete', + 'events': { + 'click': function(e){ + (e).stop(); + Api.request('movie.delete', { + 'data': { + 'id': ids.join(',') + }, + 'onSuccess': function(){ + qObj.close(); + + self.movies.each(function(movie){ + if (movie.isSelected()){ + $(movie).destroy() + self.movies.erase(movie) + } + }); + + self.calculateSelected() + } + }); + + } + } + }, { + 'text': 'Cancel', + 'cancel': true + }]); + + }, + + changeQualitySelected: function(){ + var self = this; + var ids = self.getSelectedMovies() + + Api.request('movie.edit', { + 'data': { + 'id': ids.join(','), + 'profile_id': self.mass_edit_quality.get('value') + }, + 'onSuccess': self.search.bind(self) + }); + }, + + getSelectedMovies: function(){ + var self = this; + + var ids = [] + self.movies.each(function(movie){ + if (movie.isSelected()) + ids.include(movie.get('id')) + }); + + return ids + }, + + massEditToggleAll: function(){ + var self = this; + + var select = self.mass_edit_select.get('checked'); + + self.movies.each(function(movie){ + movie.select(select) + }); + + self.calculateSelected() + }, + reset: function(){ var self = this; - self.navigation.getElements('.active').removeClass('active') + self.movies = [] + self.calculateSelected() + self.navigation_alpha.getElements('.active').removeClass('active') self.offset = 0; self.load_more.show(); self.scrollspy.start(); @@ -172,12 +325,32 @@ var MovieList = new Class({ }, + changeView: function(new_view){ + var self = this; + + self.movies.each(function(movie){ + movie.changeView(new_view) + }); + + self.el + .removeClass(self.current_view+'_list') + .addClass(new_view+'_list') + + self.current_view = new_view; + Cookie.write(self.options.identifier+'_view', new_view, {duration: 1000}); + }, + + getSavedView: function(){ + var self = this; + return Cookie.read(self.options.identifier+'_view') || 'thumb'; + }, + search: function(){ var self = this; if(self.search_timer) clearTimeout(self.search_timer); self.search_timer = (function(){ - var search_value = self.search_input.get('value'); + var search_value = self.navigation_search_input.get('value'); if (search_value == self.last_search_value) return self.reset() diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 087312e3..adde2cb6 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -5,31 +5,61 @@ */ .movies { - padding: 50px 0 20px; + padding: 60px 0 20px; } + .movies.mass_edit_list { + padding-top: 90px; + } + .movies .movie { position: relative; border-radius: 4px; margin: 10px 0; - overflow: hidden; + width: 100%; + transition: all 0.2s linear; } - .movies .movie:hover { - border-color: #ddd #fff #fff #ddd; + .movies .movie.list_view, .movies .movie.mass_edit_view { + margin: 1px 0; + border-radius: 0; + background: no-repeat; + box-shadow: none; + border-bottom: 1px solid rgba(255,255,255,0.05); } + .movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover { + background: rgba(255,255,255,0.03); + } + .movies .data { padding: 20px; height: 180px; width: 840px; position: relative; float: right; - border-radius: 0; - -moz-border-radius: 0; - -webkit-border-radius: 0; overflow: hidden; + transition: all 0.2s linear; } + .movies .list_view .data, .movies .mass_edit_view .data { + height: 30px; + padding: 3px 10px; + width: 938px; + box-shadow: none; + border: 0; + background: none; + } + + .movies .movie .check { + display: none; + } + + .movies.mass_edit_list .movie .check { + float: left; + display: block; + margin: 7px 0 0 5px; + } + .movies .poster { float: left; width: 120px; @@ -37,8 +67,17 @@ overflow: hidden; height: 180px; border-radius: 4px 0 0 4px; + transition: all 0.2s linear; } + .movies .list_view .poster, .movies .mass_edit_view .poster { + width: 20px; + height: 30px; + } + .movies.mass_edit_list .poster { + display: none; + } + .movies .poster img, .options .poster img { width: 101%; height: 101%; @@ -49,8 +88,14 @@ font-weight: bold; margin-bottom: 10px; float: left; - width: 80%; + width: 50%; + transition: all 0.2s linear; + overflow: hidden; } + .movies .list_view .info .title, .movies .mass_edit_view .info .title { + font-size: 16px; + font-weight: normal; + } .movies .info .year { font-size: 30px; @@ -59,7 +104,12 @@ color: #bbb; width: 10%; text-align: right; + transition: all 0.2s linear; } + .movies .list_view .info .year, .movies .mass_edit_view .info .year { + font-size: 16px; + width: 6%; + } .movies .info .rating { font-size: 30px; @@ -78,12 +128,20 @@ .movies .data:hover .description { overflow: auto; } - + .movies .list_view .info .description, .movies .mass_edit_view .info .description { + display: none; + } + .movies .data .quality span { padding: 0 5px; font-weight: bold; } .movies .data .quality span:first-child {padding-left: 0;} + .movies .list_view .data .quality, .movies .mass_edit_view .data .quality { + text-align: right; + float: right; + width: 35%; + } .movies .data .quality .available { color: orange; } .movies .data .quality .snatched { color: lightgreen; } @@ -95,7 +153,10 @@ margin-top: -25px; } .movies .data:hover .action { opacity: 0.6; } - .movies .data:hover .action:hover { opacity: 1; } + .movies .data:hover .action:hover { opacity: 1; } + .movies.mass_edit_list .data .actions { + display: none; + } .movies .data .action { background-repeat: no-repeat; @@ -106,6 +167,11 @@ padding: 3px; opacity: 0; } + + .movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions { + margin: -35px -7px 0 0; + background: #4e5969; + } .movies .delete_container { clear: both; @@ -183,9 +249,9 @@ text-align: left; padding: 0 10px; } - .movies .options .table.files .name { width: 598px; } - .movies .options .table .type { width: 120px; } - .movies .options .table .is_available { width: 80px; } + .movies .options .table.files .name { width: 608px; } + .movies .options .table .type { width: 130px; } + .movies .options .table .is_available { width: 90px; } .movies .options .table a { width: 30px !important; @@ -220,17 +286,18 @@ position: fixed; z-index: 2; top: 0; - padding: 100px 7px 7px; - margin: -7px; - width: 960px; + padding: 100px 60px 7px; + width: 1082px; + margin: 0 -60px; + box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1); } .movies .alph_nav.float { - box-shadow: 0 0 10px rgba(0,0,0,0.4); - border-radius: 4px; + box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5); + border-radius: 0; background: #4e5969; } - + .movies .alph_nav ul { list-style: none; padding: 0 0 1px; @@ -250,7 +317,7 @@ color: #666; border: 1px solid transparent; } - .movies .alph_nav li:first-child { + .movies .alph_nav .numbers li:first-child { width: 34px; } .movies .alph_nav li.active, .movies .alph_nav li:hover { @@ -265,4 +332,91 @@ padding: 6px 5px; margin: 0; float: right; - } \ No newline at end of file + width: 155px; + height: 25px; + float: right; + } + + .movies .alph_nav .actions { + margin: 0 32px 0 0; + -moz-user-select: none; + } + .movies .alph_nav .actions li { + border-radius: 1px; + width: auto; + } + .movies .alph_nav .actions li.active { + background: none; + border: 1px solid transparent; + box-shadow: none; + } + .movies .alph_nav .actions li span { + display: block; + background: url('../images/sprite.png') no-repeat; + width: 25px; + height: 100%; + } + + .movies .alph_nav .actions li.mass_edit span { + background-position: 3px 3px; + } + + .movies .alph_nav .actions li.list span { + background-position: 3px -95px; + } + + .movies .alph_nav .actions li.thumbs span { + background-position: 3px -74px; + } + + .movies .alph_nav .actions li:first-child { + border-radius: 3px 0 0 3px; + } + .movies .alph_nav .actions li:last-child { + border-radius: 0 3px 3px 0; + } + + .movies .alph_nav .mass_edit_form { + clear: both; + text-align: center; + display: none; + } + .movies.mass_edit_list .mass_edit_form { + display: block; + } + .movies.mass_edit_list .mass_edit_form .select { + float: left; + margin: 5px 0 0 5px; + font-size: 14px; + } + .movies.mass_edit_list .mass_edit_form .select span { + vertical-align: middle; + opacity: 0.7; + } + .movies.mass_edit_list .mass_edit_form .select .count { + font-weight: bold; + margin: 0 3px 0 10px; + } + + .movies .alph_nav .mass_edit_form .quality { + float: left; + padding: 8px 0 0; + margin: 0 0 0 16px; + } + .movies .alph_nav .mass_edit_form .quality select { + width: 120px; + margin-right: 5px; + } + .movies .alph_nav .mass_edit_form .button { + padding: 3px 7px; + } + + .movies .alph_nav .mass_edit_form .delete { + float: left; + padding: 8px 0 0 8px; + } + + .movies .alph_nav .mass_edit_form .delete span { + margin: 0 10px 0 0; + } + \ No newline at end of file diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index 4b7ac27c..55d51d44 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -8,6 +8,7 @@ var Movie = new Class({ var self = this; self.data = data; + self.view = options.view || 'thumb'; self.profile = Quality.getProfile(data.profile_id) || {}; self.parent(self, options); @@ -17,6 +18,13 @@ var Movie = new Class({ var self = this; self.el = new Element('div.movie.inlay').adopt( + self.select_checkbox = new Element('input[type=checkbox].inlay', { + 'events': { + 'change': function(){ + self.fireEvent('select') + } + } + }), self.thumbnail = File.Select.single('poster', self.data.library.files), self.data_container = new Element('div.data.inlay.light', { 'tween': { @@ -44,6 +52,9 @@ var Movie = new Class({ ) ); + self.changeView(self.view); + self.select_checkbox_class = new Form.Check(self.select_checkbox); + // Add profile if(self.profile.data) self.profile.getTypes().each(function(type){ @@ -108,7 +119,13 @@ var Movie = new Class({ var self = this; if(direction == 'in'){ - self.el.addEvent('outerClick', self.slide.bind(self, 'out')) + self.temp_view = self.view; + self.changeView('thumb') + + self.el.addEvent('outerClick', function(){ + self.changeView(self.temp_view) + self.slide('out') + }) el.show(); self.data_container.tween('right', 0, -840); } @@ -123,8 +140,27 @@ var Movie = new Class({ } }, + changeView: function(new_view){ + var self = this; + + self.el + .removeClass(self.view+'_view') + .addClass(new_view+'_view') + + self.view = new_view; + }, + get: function(attr){ return this.data[attr] || this.data.library[attr] + }, + + select: function(bool){ + var self = this; + self.select_checkbox_class[bool ? 'check' : 'uncheck']() + }, + + isSelected: function(){ + return this.select_checkbox.get('checked'); } }); diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 4392e9c3..fef35b24 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -161,11 +161,11 @@ body > .spinner, .mask{ margin: 0; width: 100%; z-index: 5; - box-shadow: 0 0 5px rgba(0,0,0,0.05); + box-shadow: 0 20px 30px -30px rgba(0,0,0,0.05); transition: box-shadow .4s cubic-bezier(0.9,0,0.1,1); } .header.with_shadow { - box-shadow: 0 0 20px rgba(0,0,0,0.3); + box-shadow: 0 20px 30px -30px rgba(0,0,0,0.3); } .header > div { @@ -342,22 +342,22 @@ body > .spinner, .mask{ color: #fff; border: 0; border-radius:3px; - background: #282d34; + background-color: #282d34; box-shadow: inset 0 1px 8px rgba(0,0,0,0.25), 0 1px 0px rgba(255,255,255,0.25); } .inlay.light { - background: #47515f; + background-color: #47515f; outline: none; box-shadow: inset 0 1px 8px rgba(0,0,0,0.05), 0 1px 0px rgba(255,255,255,0.15); } .inlay:focus { - background: #3a4350; + background-color: #3a4350; outline: none; } -.onlay, .inlay .selected, .inlay > li:hover, .inlay > li.active { +.onlay, .inlay .selected, .inlay:not(.reversed) > li:hover, .inlay > li.active, .inlay.reversed > li { border-radius:3px; border: 1px solid #252930; box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2); From 457bf0958a959a2f1f61a6fc30f5bd17eec103e1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 26 Feb 2012 23:27:48 +0100 Subject: [PATCH 20/99] Initial view state --- couchpotato/core/plugins/movie/static/list.js | 2 +- couchpotato/core/plugins/movie/static/movie.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index e1971055..a5491b92 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -342,7 +342,7 @@ var MovieList = new Class({ getSavedView: function(){ var self = this; - return Cookie.read(self.options.identifier+'_view') || 'thumb'; + return Cookie.read(self.options.identifier+'_view') || 'thumbs'; }, search: function(){ diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index 55d51d44..d728f578 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -8,7 +8,7 @@ var Movie = new Class({ var self = this; self.data = data; - self.view = options.view || 'thumb'; + self.view = options.view || 'thumbs'; self.profile = Quality.getProfile(data.profile_id) || {}; self.parent(self, options); @@ -120,7 +120,7 @@ var Movie = new Class({ if(direction == 'in'){ self.temp_view = self.view; - self.changeView('thumb') + self.changeView('thumbs') self.el.addEvent('outerClick', function(){ self.changeView(self.temp_view) From bb61af71533fdf55e7ba1d4b69d8234f0566194c Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 27 Feb 2012 00:16:29 +0100 Subject: [PATCH 21/99] Directory select width --- couchpotato/core/plugins/movie/static/movie.css | 2 +- couchpotato/static/style/page/settings.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index adde2cb6..3188caa3 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -90,11 +90,11 @@ float: left; width: 50%; transition: all 0.2s linear; - overflow: hidden; } .movies .list_view .info .title, .movies .mass_edit_view .info .title { font-size: 16px; font-weight: normal; + text-overflow: ellipsis; } .movies .info .year { diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index f17d7247..e1b31f59 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -164,7 +164,7 @@ display: inline-block; padding: 0 4% 0 4px; font-size: 13px; - width: 26.3%; + width: 30%; background-image: url('../../images/icon.folder.gif'); background-repeat: no-repeat; background-position: 97% center; From 7c9d7ff506f06c70d088aa6abedd6a3f27b6fdde Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 27 Feb 2012 00:38:15 +0100 Subject: [PATCH 22/99] Log didn't fit anymore --- couchpotato/core/plugins/log/static/log.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css index 6a4eef56..01e9a863 100644 --- a/couchpotato/core/plugins/log/static/log.css +++ b/couchpotato/core/plugins/log/static/log.css @@ -42,7 +42,7 @@ float: left; width: 86%; line-height: 150%; - padding: 3px 1%; + padding: 3px 0; border-top: 1px solid rgba(255, 255, 255, 0.2); font-size: 11px; font-family: Lucida Console, Monaco, Nimbus Mono L; @@ -56,7 +56,7 @@ .page.log .container .time { clear: both; - width: 11%; + width: 14%; color: lightgrey; padding: 3px 0; } From da79b4cd921c1cfb12085249707c51cf417ad4fd Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 27 Feb 2012 01:06:59 +0100 Subject: [PATCH 23/99] Speed up loading of movielist with lots of releases --- couchpotato/core/plugins/movie/main.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index 2b377bb8..bfdcb376 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -104,10 +104,6 @@ class MoviePlugin(Plugin): q = db.query(Movie) \ .join(Movie.library, Library.titles) \ - .options(joinedload_all('releases.status')) \ - .options(joinedload_all('releases.quality')) \ - .options(joinedload_all('releases.files')) \ - .options(joinedload_all('releases.info')) \ .options(joinedload_all('library.titles')) \ .options(joinedload_all('library.files')) \ .options(joinedload_all('status')) \ From 5796e1f97da6a4b2f38f23def07b8fe19dc63b77 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 27 Feb 2012 20:38:18 +0100 Subject: [PATCH 24/99] Javascript library updates --- .../static/scripts/library/mootools.js | 346 +++++++++----- .../static/scripts/library/prefix_free.js | 421 +++++++++++++++++- .../static/scripts/library/templated.js | 411 ----------------- couchpotato/templates/_desktop.html | 1 - 4 files changed, 651 insertions(+), 528 deletions(-) delete mode 100644 couchpotato/static/scripts/library/templated.js diff --git a/couchpotato/static/scripts/library/mootools.js b/couchpotato/static/scripts/library/mootools.js index a4d83f8d..9917ad32 100644 --- a/couchpotato/static/scripts/library/mootools.js +++ b/couchpotato/static/scripts/library/mootools.js @@ -8,6 +8,9 @@ web build: packager build: - 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 +... +*/ + /* --- @@ -17,7 +20,7 @@ description: The heart of MooTools. license: MIT-style license. -copyright: Copyright (c) 2006-2010 [Valerio Proietti](http://mad4milk.net/). +copyright: Copyright (c) 2006-2012 [Valerio Proietti](http://mad4milk.net/). authors: The MooTools production team (http://mootools.net/developers/) @@ -33,8 +36,8 @@ provides: [Core, MooTools, Type, typeOf, instanceOf, Native] (function(){ this.MooTools = { - version: '1.4.2', - build: '552dfd4704fccffed444e0211c50831a2bfe209f' + version: '1.4.5', + build: 'ab8ea8824dc3b24b6666867a2c4ed58ebb762cf0' }; // typeOf, instanceOf @@ -61,6 +64,9 @@ var instanceOf = this.instanceOf = function(item, object){ if (constructor === object) return true; constructor = constructor.parent; } + /**/ + if (!item.hasOwnProperty) return false; + /**/ return item instanceof object; }; @@ -93,8 +99,9 @@ Function.prototype.overloadGetter = function(usePlural){ var self = this; return function(a){ var args, result; - if (usePlural || typeof a != 'string') args = a; + if (typeof a != 'string') args = a; else if (arguments.length > 1) args = arguments; + else if (usePlural) args = [a]; if (args){ result = {}; for (var i = 0; i < args.length; i++) result[args[i]] = self.call(this, args[i]); @@ -251,14 +258,18 @@ var force = function(name, object, methods){ proto = prototype[key]; if (generic) generic.protect(); - - if (isType && proto){ - delete prototype[key]; - prototype[key] = proto.protect(); - } + if (isType && proto) object.implement(key, proto.protect()); } - if (isType) object.implement(prototype); + if (isType){ + var methodsEnumerable = prototype.propertyIsEnumerable(methods[0]); + object.forEachMethod = function(fn){ + if (!methodsEnumerable) for (var i = 0, l = methods.length; i < l; i++){ + fn.call(prototype, prototype[methods[i]], methods[i]); + } + for (var key in prototype) fn.call(prototype, prototype[key], key) + }; + } return force; }; @@ -429,8 +440,9 @@ Array.implement({ filter: function(fn, bind){ var results = []; - 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]); + for (var value, i = 0, l = this.length >>> 0; i < l; i++) if (i in this){ + value = this[i]; + if (fn.call(bind, value, i, this)) results.push(value); } return results; }, @@ -1787,8 +1799,14 @@ local.setDocument = function(document){ // contains // FIXME: Add specs: local.contains should be different for xml and html documents? - features.contains = (root && this.isNativeCode(root.contains)) ? function(context, node){ + var nativeRootContains = root && this.isNativeCode(root.contains), + nativeDocumentContains = document && this.isNativeCode(document.contains); + + features.contains = (nativeRootContains && nativeDocumentContains) ? function(context, node){ return context.contains(node); + } : (nativeRootContains && !nativeDocumentContains) ? function(context, node){ + // IE8 does not have .contains on document. + return context === node || ((context === document) ? document.documentElement : context).contains(node); } : (root && root.compareDocumentPosition) ? function(context, node){ return context === node || !!(context.compareDocumentPosition(node) & 16); } : function(context, node){ @@ -2183,7 +2201,7 @@ local.matchSelector = function(node, tag, id, classes, attributes, pseudos){ var i, part, cls; if (classes) for (i = classes.length; i--;){ - cls = node.getAttribute('class') || node.className; + cls = this.getAttribute(node, 'class'); if (!(cls && classes[i].regexp.test(cls))) return false; } if (attributes) for (i = attributes.length; i--;){ @@ -2369,7 +2387,7 @@ var pseudos = { 'nth-last-of-type': local.createNTHPseudo('lastChild', 'previousSibling', 'posNTHTypeLast', true), 'index': function(node, index){ - return this['pseudo:nth-child'](node, '' + index + 1); + return this['pseudo:nth-child'](node, '' + (index + 1)); }, 'even': function(node){ @@ -2441,10 +2459,6 @@ for (var p in pseudos) local['pseudo:' + p] = pseudos[p]; var attributeGetters = local.attributeGetters = { - 'class': function(){ - return this.getAttribute('class') || this.className; - }, - 'for': function(){ return ('htmlFor' in this) ? this.htmlFor : this.getAttribute('for'); }, @@ -2479,7 +2493,7 @@ attributeGetters.MAXLENGTH = attributeGetters.maxLength = attributeGetters.maxle var Slick = local.Slick = (this.Slick || {}); -Slick.version = '1.1.6'; +Slick.version = '1.1.7'; // Slick finder @@ -2638,7 +2652,10 @@ new Type('Element', Element).mirror(function(name){ if (!Browser.Element){ Element.parent = Object; - Element.Prototype = {'$family': Function.from('element').hide()}; + Element.Prototype = { + '$constructor': Element, + '$family': Function.from('element').hide() + }; Element.mirror(function(name, method){ Element.Prototype[name] = method; @@ -2753,16 +2770,17 @@ if (object[1] == 1) Elements.implement('splice', function(){ return result; }.protect()); -Elements.implement(Array.prototype); +Array.forEachMethod(function(method, name){ + Elements.implement(name, method); +}); Array.mirror(Elements); /**/ var createElementAcceptsHTML; try { - var x = document.createElement(''); - createElementAcceptsHTML = (x.name == 'x'); -} catch(e){} + createElementAcceptsHTML = (document.createElement('').name == 'x'); +} catch (e){} var escapeQuotes = function(html){ return ('' + html).replace(/&/g, '&').replace(/"/g, '"'); @@ -2821,7 +2839,11 @@ Document.implement({ element: function(el, nocash){ Slick.uidOf(el); if (!nocash && !el.$family && !(/^(?:object|embed)$/i).test(el.tagName)){ - el._fireEvent = el.fireEvent; + var fireEvent = el.fireEvent; + // wrapping needed in IE7, or else crash + el._fireEvent = function(type, event){ + return fireEvent(type, event); + }; Object.append(el, Element.Prototype); } return el; @@ -3001,13 +3023,8 @@ Array.forEach([ properties[property.toLowerCase()] = property; }); -Object.append(properties, { - 'html': 'innerHTML', - 'text': (function(){ - var temp = document.createElement('div'); - return (temp.textContent == null) ? 'innerText': 'textContent'; - })() -}); +properties.html = 'innerHTML'; +properties.text = (document.createElement('div').textContent == null) ? 'innerText': 'textContent'; Object.forEach(properties, function(real, key){ propertySetters[key] = function(node, value){ @@ -3056,7 +3073,7 @@ Object.append(propertySetters, { }, 'value': function(node, value){ - node.value = value || ''; + node.value = (value != null) ? value : ''; } }); @@ -3072,10 +3089,31 @@ try { el.type = 'button'; } catch(e){} if (el.type != 'button') propertySetters.type = function(node, value){ node.setAttribute('type', value); }; +el = null; /* */ +/**/ +var input = document.createElement('input'); +input.value = 't'; +input.type = 'submit'; +if (input.value != 't') propertySetters.type = function(node, type){ + var value = node.value; + node.type = type; + node.value = value; +}; +input = null; +/**/ + /* getProperty, setProperty */ +/* */ +var pollutesGetAttribute = (function(div){ + div.random = 'attribute'; + return (div.getAttribute('random') == 'attribute'); +})(document.createElement('div')); + +/* */ + Element.implement({ setProperty: function(name, value){ @@ -3083,8 +3121,21 @@ Element.implement({ if (setter){ setter(this, value); } else { - if (value == null) this.removeAttribute(name); - else this.setAttribute(name, value); + /* */ + if (pollutesGetAttribute) var attributeWhiteList = this.retrieve('$attributeWhiteList', {}); + /* */ + + if (value == null){ + this.removeAttribute(name); + /* */ + if (pollutesGetAttribute) delete attributeWhiteList[name]; + /* */ + } else { + this.setAttribute(name, '' + value); + /* */ + if (pollutesGetAttribute) attributeWhiteList[name] = true; + /* */ + } } return this; }, @@ -3097,6 +3148,18 @@ Element.implement({ getProperty: function(name){ var getter = propertyGetters[name.toLowerCase()]; if (getter) return getter(this); + /* */ + if (pollutesGetAttribute){ + var attr = this.getAttributeNode(name), attributeWhiteList = this.retrieve('$attributeWhiteList', {}); + if (!attr) return null; + if (attr.expando && !attributeWhiteList[name]){ + var outer = this.outerHTML; + // segment by the opening tag and find mention of attribute name + if (outer.substr(0, outer.search(/\/?['"]?>(?![^<]*<['"])/)).indexOf(name) < 0) return null; + attributeWhiteList[name] = true; + } + } + /* */ var result = Slick.getAttribute(this, name); return (!result && !Slick.hasAttribute(this, name)) ? null : result; }, @@ -3223,7 +3286,7 @@ var get = function(uid){ }; var clean = function(item){ - var uid = item.uid; + var uid = item.uniqueNumber; if (item.removeEvents) item.removeEvents(); if (item.clearAttributes) item.clearAttributes(); if (uid != null){ @@ -3269,7 +3332,7 @@ Element.implement({ if (node.clearAttributes){ node.clearAttributes(); node.mergeAttributes(element); - node.removeAttribute('uid'); + node.removeAttribute('uniqueNumber'); if (node.options){ var no = node.options, eo = element.options; for (var j = no.length; j--;) no[j].selected = eo[j].selected; @@ -3369,60 +3432,77 @@ Element.Properties.tag = { }; -/**/ -Element.Properties.html = (function(){ +Element.Properties.html = { - var tableTest = Function.attempt(function(){ - var table = document.createElement('table'); - table.innerHTML = ''; - }); + set: function(html){ + if (html == null) html = ''; + else if (typeOf(html) == 'array') html = html.join(''); + this.innerHTML = html; + }, - var wrapper = document.createElement('div'); - - var translations = { - table: [1, '
{{param}}
', '
'], - select: [1, ''], - tbody: [2, '', '
'], - tr: [3, '', '
'] - }; - 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); + erase: function(){ + this.innerHTML = ''; } - /**/ - var html = { - 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]; - for (var i = wrap[0]; i--;) first = first.firstChild; - this.empty().adopt(first.childNodes); - } else { - this.innerHTML = html; - } - } - }; +/**/ +// technique by jdbarlett - http://jdbartlett.com/innershiv/ +var div = document.createElement('div'); +div.innerHTML = ''; +var supportsHTML5Elements = (div.childNodes.length == 1); +if (!supportsHTML5Elements){ + 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]); +} +div = null; +/**/ - html.erase = html.set; +/**/ +var supportsTableInnerHTML = Function.attempt(function(){ + var table = document.createElement('table'); + table.innerHTML = ''; + return true; +}); - return html; -})(); -/**/ +/**/ +var tr = document.createElement('tr'), html = ''; +tr.innerHTML = html; +var supportsTRInnerHTML = (tr.innerHTML == html); +tr = null; +/**/ + +if (!supportsTableInnerHTML || !supportsTRInnerHTML || !supportsHTML5Elements){ + + Element.Properties.html.set = (function(set){ + + var translations = { + table: [1, '', '
'], + select: [1, ''], + tbody: [2, '', '
'], + tr: [3, '', '
'] + }; + + translations.thead = translations.tfoot = translations.tbody; + + return function(html){ + var wrap = translations[this.get('tag')]; + if (!wrap && !supportsHTML5Elements) wrap = [0, '', '']; + if (!wrap) return set.call(this, html); + + var level = wrap[0], wrapper = document.createElement('div'), target = wrapper; + if (!supportsHTML5Elements) fragment.appendChild(wrapper); + wrapper.innerHTML = [wrap[1], html, wrap[2]].flatten().join(''); + while (level--) target = target.firstChild; + this.empty().adopt(target.childNodes); + if (!supportsHTML5Elements) fragment.removeChild(wrapper); + wrapper = null; + }; + + })(Element.Properties.html.set); +} +/*
*/ /**/ var testForm = document.createElement('form'); @@ -3454,11 +3534,11 @@ if (testForm.firstChild.value != 's') Element.Properties.value = { } }; +testForm = null; /**/ /**/ -var el = document.createElement('div'); -if (el.getAttributeNode('id')) Element.Properties.id = { +if (document.createElement('div').getAttributeNode('id')) Element.Properties.id = { set: function(id){ this.id = this.getAttributeNode('id').value = id; }, @@ -3494,6 +3574,15 @@ provides: Element.Style var html = document.html; +// +// Check for oldIE, which does not remove styles when they're set to null +var el = document.createElement('div'); +el.style.color = 'red'; +el.style.color = null; +var doesNotRemoveStyles = el.style.color == 'red'; +el = null; +// + Element.Properties.styles = {set: function(styles){ this.setStyles(styles); }}; @@ -3504,17 +3593,19 @@ var hasOpacity = (html.style.opacity != null), var setVisibility = function(element, opacity){ element.store('$opacity', opacity); - element.style.visibility = opacity > 0 ? 'visible' : 'hidden'; + element.style.visibility = opacity > 0 || opacity == null ? '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; - 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; + var style = element.style; + if (!element.currentStyle || !element.currentStyle.hasLayout) style.zoom = 1; + if (opacity == null || opacity == 1) opacity = ''; + else opacity = 'alpha(opacity=' + (opacity * 100).limit(0, 100).round() + ')'; + var filter = style.filter || element.getComputedStyle('filter') || ''; + style.filter = reAlpha.test(filter) ? filter.replace(reAlpha, opacity) : filter + opacity; + if (!style.filter) style.removeAttribute('filter'); } : setVisibility)); var getOpacity = (hasOpacity ? function(element){ @@ -3544,7 +3635,8 @@ Element.implement({ setStyle: function(property, value){ if (property == 'opacity'){ - setOpacity(this, parseFloat(value)); + if (value != null) value = parseFloat(value); + setOpacity(this, value); return this; } property = (property == 'float' ? floatName : property).camelCase(); @@ -3558,6 +3650,11 @@ Element.implement({ value = Math.round(value); } this.style[property] = value; + // + if ((value == '' || value == null) && doesNotRemoveStyles && this.style.removeAttribute){ + this.style.removeAttribute(property); + } + // return this; }, @@ -3579,16 +3676,17 @@ Element.implement({ var color = result.match(/rgba?\([\d\s,]+\)/); if (color) result = result.replace(color[0], color[0].rgbToHex()); } - if (Browser.opera || (Browser.ie && isNaN(parseFloat(result)))){ - if ((/^(height|width)$/).test(property)){ + if (Browser.opera || Browser.ie){ + if ((/^(height|width)$/).test(property) && !(/px$/.test(result))){ var values = (property == 'width') ? ['left', 'right'] : ['top', 'bottom'], size = 0; values.each(function(value){ size += this.getStyle('border-' + value + '-width').toInt() + this.getStyle('padding-' + value).toInt(); }, this); return this['offset' + property.capitalize()] - size + 'px'; } - if (Browser.opera && String(result).indexOf('px') != -1) return result; - if ((/^border(.+)Width|margin|padding/).test(property)) return '0px'; + if (Browser.ie && (/^border(.+)Width|margin|padding/).test(property) && isNaN(parseFloat(result))){ + return '0px'; + } } return result; }, @@ -3940,7 +4038,7 @@ if (!window.addEventListener){ return (this.get('tag') == 'input' && (type == 'radio' || type == 'checkbox')) ? 'propertychange' : 'change' }, condition: function(event){ - return !!(this.type != 'radio' || this.checked); + return this.type != 'radio' || (event.event.propertyName == 'checked' && this.checked); } } } @@ -4641,12 +4739,31 @@ Fx.CSS = new Class({ prepare: function(element, property, values){ values = Array.from(values); - if (values[1] == null){ - values[1] = values[0]; - values[0] = element.getStyle(property); + var from = values[0], to = values[1]; + if (to == null){ + to = from; + from = element.getStyle(property); + var unit = this.options.unit; + // adapted from: https://github.com/ryanmorr/fx/blob/master/fx.js#L299 + if (unit && from.slice(-unit.length) != unit && parseFloat(from) != 0){ + element.setStyle(property, to + unit); + var value = element.getComputedStyle(property); + // IE and Opera support pixelLeft or pixelWidth + if (!(/px$/.test(value))){ + value = element.style[('pixel-' + property).camelCase()]; + if (value == null){ + // adapted from Dean Edwards' http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + var left = element.style.left; + element.style.left = to + unit; + value = element.style.pixelLeft; + element.style.left = left; + } + } + from = (to || 1) / (parseFloat(value) || 1) * (parseFloat(from) || 0); + element.setStyle(property, from + unit); + } } - var parsed = values.map(this.parse); - return {from: parsed[0], to: parsed[1]}; + return {from: this.parse(from), to: this.parse(to)}; }, //parses a value into an array @@ -4834,24 +4951,25 @@ Element.implement({ }, fade: function(how){ - var fade = this.get('tween'), method, to, toggle; - if (how == null) how = 'toggle'; - switch (how){ - case 'in': method = 'start'; to = 1; break; - case 'out': method = 'start'; to = 0; break; - case 'show': method = 'set'; to = 1; break; - case 'hide': method = 'set'; to = 0; break; + var fade = this.get('tween'), method, args = ['opacity'].append(arguments), toggle; + if (args[1] == null) args[1] = 'toggle'; + switch (args[1]){ + case 'in': method = 'start'; args[1] = 1; break; + case 'out': method = 'start'; args[1] = 0; break; + case 'show': method = 'set'; args[1] = 1; break; + case 'hide': method = 'set'; args[1] = 0; break; case 'toggle': var flag = this.retrieve('fade:flag', this.getStyle('opacity') == 1); method = 'start'; - to = flag ? 0 : 1; + args[1] = flag ? 0 : 1; this.store('fade:flag', !flag); toggle = true; break; - default: method = 'start'; to = how; + default: method = 'start'; } if (!toggle) this.eliminate('fade:flag'); - fade[method]('opacity', to); + fade[method].apply(fade, args); + var to = args[args.length - 1]; if (method == 'set' || to != 0) this.setStyle('visibility', to == 0 ? 'hidden' : 'visible'); else fade.chain(function(){ this.element.setStyle('visibility', 'hidden'); diff --git a/couchpotato/static/scripts/library/prefix_free.js b/couchpotato/static/scripts/library/prefix_free.js index 96ed40e4..1ca634ee 100644 --- a/couchpotato/static/scripts/library/prefix_free.js +++ b/couchpotato/static/scripts/library/prefix_free.js @@ -1,2 +1,419 @@ -// StyleFix 1.0.1 & PrefixFree 1.0.4 / by Lea Verou / MIT license -(function(){function b(a,b){return[].slice.call((b||document).querySelectorAll(a))}if(!window.addEventListener)return;var a=window.StyleFix={link:function(b){try{if(b.rel!=="stylesheet"||!b.sheet.cssRules||b.hasAttribute("data-noprefix"))return}catch(c){return}var d=b.href||b.getAttribute("data-href"),e=d.replace(/[^\/]+$/,""),f=b.parentNode,g=new XMLHttpRequest;g.open("GET",d),g.onreadystatechange=function(){if(g.readyState===4){var c=g.responseText;if(c&&b.parentNode){c=a.fix(c,!0,b),e&&(c=c.replace(/url\((?:'|")?(.+?)(?:'|")?\)/gi,function(a,b){return/^([a-z]{3,10}:|\/|#)/i.test(b)?a:'url("'+e+b+'")'}),c=c.replace(RegExp("\\b(behavior:\\s*?url\\('?\"?)"+e,"gi"),"$1"));var d=document.createElement("style");d.textContent=c,d.media=b.media,d.disabled=b.disabled,d.setAttribute("data-href",b.getAttribute("href")),f.insertBefore(d,b),f.removeChild(b)}}},g.send(null),b.setAttribute("data-inprogress","")},styleElement:function(b){var c=b.disabled;b.textContent=a.fix(b.textContent,!0,b),b.disabled=c},styleAttribute:function(b){var c=b.getAttribute("style");c=a.fix(c,!1,b),b.setAttribute("style",c)},process:function(){b('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link),b("style").forEach(StyleFix.styleElement),b("[style]").forEach(StyleFix.styleAttribute)},register:function(b,c){(a.fixers=a.fixers||[]).splice(c===undefined?a.fixers.length:c,0,b)},fix:function(b,c){for(var d=0;d3){d.pop();var f=d.join("-");h(f)&&b.indexOf(f)===-1&&b.push(f)}}},h=function(a){return StyleFix.camelCase(a)in f};if(e.length>0)for(var i=0;i 3) { + parts.pop(); + + var shorthand = parts.join('-'); + + if(supported(shorthand) && properties.indexOf(shorthand) === -1) { + properties.push(shorthand); + } + } + } + }, + supported = function(property) { + return StyleFix.camelCase(property) in dummy; + } + + // Some browsers have numerical indices for the properties, some don't + if(style.length > 0) { + for(var i=0; i
", - - // The URL to get the template from *instead* of the string - templateUrl: "", - - // A node reference for where this UI widget will be placed...in reference to - element: null, - - // Should this widget be parsed for sub-widgets? - widgetsInTemplate: true, - - // Property mappings (should be an object) - // These override defaults - propertyMappings: null, - - // Default property mappings - // Thse properties on the element will be moved to the respective nodes within the template - defaultPropertyMappings: null, - - // Should messages be debug to the console - debugMode: true - }, - - // Create placeholders for attached points and events - _attachPoints: [], - _attachEvents: [], - - // Parse - parse: function() { - - // Get shortcuts to the options and element - var options = this.options, - nodeRef = options.element = options.element || new Element("div").inject(document.body); - - // *IF* a templateUrl is specified, can't do anything until template is loaded - // Defer parsing until we've got it - if(options.templateUrl && Templated.templates && !Templated.templates[options.templateUrl]) { - this.debug("[Templated:parse] Need to load template from URL: " + options.templateUrl); - this.getTemplate(); - return false; - } - - // If already data-widgetized...gtfo - if(nodeRef.retrieve("widget")) { - this.debug("[Templated:parse] Node already widgetized, leaving ", nodeRef); - return nodeRef.domNode; - } - - // Mix noderef properties with options - options.defaultPropertyMappings = options.defaultPropertyMappings || { // THESE OVERRIDE CLASSES IN THE TEMPLATE!!!! - "id": "domNode", - "style": "domNode", - "class": "domNode" - }; - Object.merge(options, this.getNodeProps(nodeRef)); - - // postMixInProperties runs after options have been mixed with defaults but before - // any templating is done - this.postMixInProperties(); - - // Build rendering - creates the actual nodes, attachpoints, and attachevents - this.buildRendering(); - - // Fire the "postCreate" method, which runs after nodes are created *but* before the nodes are rendered to the page - this.postCreate(); - - // Cleanup creation - this.cleanupCreation(); - - // "Startup": The widget is in the DOM and the widget is ready to go - this.startup(); - - // Return the domNode - this.debug("[Templated:parse] At the end of parse, this is: ", this); - return this.domNode; - }, - - // Creates build rendering - buildRendering: function() { - // Get shortcuts to the options and element - var options = this.options, nodeRef = options.element; - - // Do string substitution on the template - var template = this.template = options.template.substitute(options || {}); - - // Create the DOM node within a DIV that's not rendered to the page - var bitchNode = this.bitchNode = new Element("div", { html: template.trim() }), - domNode = this.domNode = document.id(bitchNode.childNodes[0]); - - // Look for subwidgets if told to... - if(options.widgetsInTemplate) { - this.debug("[Templated:parse] Looking for subwidgets under domNode", domNode); - this.makeSubWidgets(this.domNode); - } - - // Create the attachpoints for me, then my kiddies - this.makeAttachPoints(domNode); - if(options.widgetsInTemplate) domNode.getElements("[" + dataWidgetAttachPoint + "]").each(this.makeAttachPoints, this); - this.debug("[Templated:parse] Creating attachpoints", this._attachPoints); - - // Create the attachevents for me, then my kiddies - this.makeAttachEvents(domNode); - if(options.widgetsInTemplate) domNode.getElements("[" + dataWidgetAttachEvent + "]").each(this.makeAttachEvents, this); - this.debug("Creating attachevents", this._attachEvents); - - // Map properties to nodes within the template - // Mix the custom mappings with the default - // This needs to happen after attachpoints - var mappings = options.propertyMappings ? Object.merge(options.defaultPropertyMappings, options.propertyMappings) : options.defaultPropertyMappings; - Object.each(mappings, function(value, key) { - // Ignore the value if not present in the object - if(!this[value]) return; - // Assign the value to the key - var currentProp = nodeRef.get(key); - if(currentProp != "") this[value].set(key, currentProp); - }.bind(this)); - - // If this widget has a "containerNode", grab it's childNodes *or* inject innerHTML - if(this.containerNode) { - var kids = nodeRef.childNodes; - kids.length ? $$(kids).inject(this.containerNode) : this.containerNode.set("html", nodeRef.get("html")); - } - }, - - // "postMixInProperties" -- Fired after options have been mixed in - postMixInProperties: function() { - this.debug("[Templated:postMixInProperties] postMixInProperties!"); - }, - - // "PostCreate" -- Fired after nodes are created, attachpoints and events are found - postCreate: function() { - this.debug("[Templated:postCreate] postCreate!"); - }, - - // "CleanupCreation" -- Removes the old element, destroys bitch node - cleanupCreation: function() { - // Get hold of the dom node and bitch nodes - var domNode = this.domNode, bitchNode = this.bitchNode, nodeRef = this.options.element; - - // Put the domNode where it should go and destroy the node reference - domNode.replaces(nodeRef); - nodeRef.destroy(); - - // Mark as data-widgetized and store the widget within data - domNode.set(dataWidgetized, true); - domNode.store("widget", this); - - // Remove the bitch node - bitchNode.destroy(); - }, - - // "StartUp" -- Fired when node is in place - startup: function(){ - this.debug("[Templated:startup] startup!"); - }, - - // Focus on focus node, if present - focus: function() { - var node = this.focusNode; - node && node.focus(); - }, - - // Create subwidgets from this - makeSubWidgets: function(domNode) { - if(!domNode) domNode = this.domNode; - domNode.getElements("["+ dataWidgetType +"]:not([" + dataWidgetized + "])").each(function(node){ - // Store the subwidget's attachpoints, attachevents, class type, and properties - var points = node.get(dataWidgetAttachPoint), - events = node.get(dataWidgetAttachEvent), - widgetProps = this.getNodeProps(node), - klass = node.get(dataWidgetType).trim(); - - // Create the widget - if(scope[klass]) { - var widget = new scope[klass](Object.merge(widgetProps, { element: node })); - this.debug("[parse:makeSubWidgets] Creating child widget: ", klass, widget); - // Get access to its dom node - widgetDomNode = widget.domNode; - // Add attachments back to the widget - points && widgetDomNode.set(dataWidgetAttachPoint, points.trim()); - events && widgetDomNode.set(dataWidgetAttachEvent, events.trim()); - } - }, this); - }, - - // Makes attachpoints - makeAttachPoints: function(node) { - var points = node.get(dataWidgetAttachPoint); - if(points) { - points.trim().split(",").each(function(attach) { - attach = attach.trim(); - this[attach] = node.retrieve("widget") || node; - this.debug("[Templated:makeAttachPoints] " + attach, node); - this._attachPoints.push({ node: node, name: attach }); - }, this); - node.set(dataWidgetAttachPoint, ""); - node.set("data-widgetized-attach-point", points); - } - }, - - // Makes attachevents - makeAttachEvents: function(node) { - // Temporarily store this widget's events so they may be added to this.domNode later - var events = node.get(dataWidgetAttachEvent); - // If there are events.... - if(events) { - // For every event found.... - events.trim().split(",").each(function(event) { - // Trim the event - event = event.trim(); - // Split the event:method pair - var eventFn = event.split(":"); - // Trim and rename each piece - var nativeEvent = eventFn[0].trim(), - classEvent = eventFn[1].trim(); - this.debug("[Templated:makeAttachEvents] " + nativeEvent + " / " + classEvent,node); - - // If the method isn't found on this, create a stub for it' - if(!this[classEvent]) { - this.debug("cant find ", classEvent, " in: ", this, " creating sub for it"); - this[classEvent] = function(){}; - } - - // Bind "this" to the event - var ev = this[classEvent].bind(this); - - // Add the event to the domNode - node.addEvent(nativeEvent, ev); - - // Store the event - this._attachEvents.push({ type: nativeEvent, event: ev, node: node }); - }, this); - - // Remove the event from its former place and add to -ized data item - var set = { - "data-widgetized-attach-event": events - }; - set[dataWidgetAttachEvent] = ""; - node.set(set); - //node.set("data-widget-attach-event",""); - //node.set("data-widgetized-attach-event",events); - } - }, - - // Destroy: removes node events - destroy: function() { - // Get reference to domNode - var domNode = this.domNode, events = this._attachEvents, points = this._attachPoints; - - // Clear out children - domNode.getElements("[" + dataWidgetized + "]").each(function(widget) { - widget.destroy(); - }); - - // Remove events - if(events.length) { - events.each(function(event) { - if(event.node) event.node.removeEvents(); - }); - } - // Remove node connections - if(points.length) { - points.each(function(point) { - if(point.name != "domNode") this[point.name] = null; - }, this); - } - // Destroy the dom node and its children, fin - domNode.store("widget", null).destroy(); - }, - - // Gets the in-node properties for a widget - getNodeProps: function(node) { - var props = node.get(dataWidgetProps), - widgetProps = {}; - // Create the widget - if(props) { - // Not using JSON.parse because it's too restricting, especially with quotes - var json = "{" + props.trim() + "}"; - if(JSON && JSON.decode) { // MooTools - widgetProps = JSON.decode(json); - } - else { // Native - eval("widgetProps = " + json); - } - } - return widgetProps; - }, - - debug: function(one, two, three, four) { - if(this.options.debugMode && console && console.log) { - console.log("[" + (this.domNode ? this.domNode.id : "") + "] ", one, two || "", three || "", four || ""); - } - } - }); - - // If Request is available.... - if(Request) { - Templated.templates = {}; - // Return the template - Templated.implement({ - // Method to return cached template or retrieve new one synchronously - getTemplate: function() { - /* - var url = this.options.templateUrl; - // Try to return cached first - if(Templated.templates[url]) { - return Templated.templates[url]; - } - else { - // Send a new request - return new Request({ - url: url, - async: false, // Used to ensure that necessary templates are there - onSuccess: function(template) { - this.options.template = template; - Templated.templates[url] = template; - this.parse(); - }.bind(this) - }).send(); - } - */ - - var url = this.options.templateUrl; - // Try to return cached first - if(!Templated.templates[url]) { - // Send a new request - return new Request({ - url: url, - async: false, // Used to ensure that necessary templates are there - onSuccess: function(template) { - this.options.template = template; - Templated.templates[url] = template; - this.parse(); - }.bind(this), - onFailure: function() { - Templated.templates[url] = Templated.prototype.options.template; - } - }).send(); - } - return Templated.templates[url]; - } - }); - } - - // Allow for parsing of an element and its children - Element.implement({ - parse: function() { - - var elFn = function(element) { - // Get the widget type - var klass = element.get(dataWidgetType); - // If the class exists.... - if(klass && scope[klass]) { - // Create the new class instance - new scope[klass]({ element: element }); - } - else { - window.console && console.log && console.log("klass does not exist! ", klass); - } - }; - - // Grab this and all nodes which are not already widgetized - elFn(this); - $$(this.getElements("["+ dataWidgetType +"]:not(" + dataWidgetized + ")")).each(elFn); - } - }); - - // Get widget from id - document.widget = function(idOrNode) { - return document.id(idOrNode).retrieve("widget") || null; - }; - - -})(this); // Scope limiter \ No newline at end of file diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html index 7ec3fd95..0f6ab5b6 100644 --- a/couchpotato/templates/_desktop.html +++ b/couchpotato/templates/_desktop.html @@ -13,7 +13,6 @@ {% if not env.get('dev') %} {% endif %} - From 1ca3fa2cff460f87c162fe1faaf0c6deaa4d46ac Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 27 Feb 2012 22:31:23 +0100 Subject: [PATCH 25/99] Proper release deletion and styling --- couchpotato/core/plugins/movie/main.py | 15 +++++++ .../core/plugins/movie/static/movie.css | 37 ++++++++++++++++-- .../core/plugins/movie/static/movie.js | 21 ++++++---- couchpotato/core/plugins/release/main.py | 24 +++++++++++- couchpotato/static/images/icon.undo.png | Bin 0 -> 587 bytes 5 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 couchpotato/static/images/icon.undo.png diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index bfdcb376..295726c1 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -272,6 +272,13 @@ class MoviePlugin(Plugin): db.commit() + # Remove releases + available_status = fireEvent('status.get', 'available', single = True) + for rel in m.releases: + if rel.status_id is available_status.get('id'): + db.delete(rel) + db.commit() + movie_dict = m.to_dict(self.default_dict) if force_readd or do_search: @@ -297,12 +304,20 @@ class MoviePlugin(Plugin): params = getParams() db = get_session() + available_status = fireEvent('status.get', 'available', single = True) + ids = params.get('id').split(',') for movie_id in ids: m = db.query(Movie).filter_by(id = movie_id).first() m.profile_id = params.get('profile_id') + # Remove releases + for rel in m.releases: + if rel.status_id is available_status.get('id'): + db.delete(rel) + db.commit() + # Default title if params.get('default_title'): for title in m.library.titles: diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 3188caa3..4cf9a7b2 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -133,18 +133,38 @@ } .movies .data .quality span { - padding: 0 5px; + padding: 2px 3px; font-weight: bold; + opacity: 0.5; + font-size: 10px; + text-transform: uppercase; + text-shadow: none; + font-weight: normal; + margin: 0 2px; + border-radius: 2px; + background-color: rgba(255,255,255,0.1); } - .movies .data .quality span:first-child {padding-left: 0;} .movies .list_view .data .quality, .movies .mass_edit_view .data .quality { text-align: right; float: right; width: 35%; } + + .movies .data .quality .available, .movies .data .quality .snatched { + opacity: 1; + box-shadow: 1px 1px 0 rgba(0,0,0,0.2); + cursor: pointer; + } - .movies .data .quality .available { color: orange; } - .movies .data .quality .snatched { color: lightgreen; } + .movies .data .quality .available { background-color: #578bc3; } + .movies .data .quality .snatched { background-color: #369545; } + .movies .data .quality .finish { + background-image: url('../images/sprite.png'); + background-repeat: no-repeat; + background-position: 0 2px; + padding-left: 14px; + background-size: 14px + } .movies .data .actions { line-height: 0; @@ -218,6 +238,15 @@ .movies .options .table .item { border-bottom: 1px solid rgba(255,255,255,0.1); } + .movies .options .table .item.ignored span { + text-decoration: line-through; + color: rgba(255,255,255,0.4); + text-shadow: none; + } + .movies .options .table .item.ignored .delete { + background-image: url('../images/icon.undo.png'); + } + .movies .options .table .item:last-child { border: 0; } .movies .options .table .item:nth-child(even) { background: rgba(255,255,255,0.05); diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index d728f578..67fcb3c7 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -46,7 +46,13 @@ var Movie = new Class({ self.description = new Element('div.description', { 'text': self.data.library.plot }), - self.quality = new Element('div.quality') + self.quality = new Element('div.quality', { + 'events': { + 'click': function(e){ + self.el.getElement('.actions .releases').fireEvent('click', [e]) + } + } + }) ), self.actions = new Element('div.actions') ) @@ -69,10 +75,11 @@ var Movie = new Class({ Array.each(self.data.releases, function(release){ var q = self.quality.getElement('.q_'+ release.quality.identifier); - if(!q) + if(!q && release.status.identifier == 'snatched') var q = self.addQuality(release.quality_id) - q.addClass(release.status.identifier); + if (q) + q.addClass(release.status.identifier); }); @@ -283,8 +290,8 @@ var ReleaseAction = new Class({ 'events': { 'click': function(e){ (e).stop(); - self.del(release); - this.getParent('.item').destroy(); + self.ignore(release); + this.getParent('.item').toggleClass('ignored') } } }) @@ -314,10 +321,10 @@ var ReleaseAction = new Class({ }); }, - del: function(release){ + ignore: function(release){ var self = this; - Api.request('release.delete', { + Api.request('release.ignore', { 'data': { 'id': release.id } diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 01e7f600..f83b322f 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -23,7 +23,13 @@ class Release(Plugin): } }) addApiView('release.delete', self.delete, docs = { - 'desc': 'Check for available update', + 'desc': 'Delete releases', + 'params': { + 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} + } + }) + addApiView('release.ignore', self.ignore, docs = { + 'desc': 'Toggle ignore, for bad or wrong releases', 'params': { 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} } @@ -105,6 +111,22 @@ class Release(Plugin): 'success': True }) + def ignore(self): + + db = get_session() + id = getParam('id') + + rel = db.query(Relea).filter_by(id = id).first() + if rel: + ignored_status = fireEvent('status.get', 'ignored', single = True) + available_status = fireEvent('status.get', 'available', single = True) + rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id') + db.commit() + + return jsonified({ + 'success': True + }) + def download(self): db = get_session() diff --git a/couchpotato/static/images/icon.undo.png b/couchpotato/static/images/icon.undo.png new file mode 100644 index 0000000000000000000000000000000000000000..07f907dce23158b507d874de9cd9fc9339ea43db GIT binary patch literal 587 zcmV-R0<`^!P)aNs?80P-y3nq1b> zaC%Rg%_JVCvHDEA=}XV}VQ?)-_5T@gZbX%`hHSL*YL?xL z+3|dN1TQ~^;hX6KXLYLrTHJdj`DC;B)wn*b$HaVBmb;4%%)~t4T!J>q0s6kGbafQJ z9MZxyaRkPJ17h_*IwMRjSP*#71I{?*0G+)eQwc4#d6eL~lb)J~R2aN}LI}-WlKOAu zfJXE7TnZ?dtc#C3cAcm1P!nT4+$4;D5dkipP*_{c&>(85&dvS))z3Kp|f0&>XC=tI6u5!IN4hTzGIUE z4(c~5C~?YNiFWTE!5cB+@tc4k)yBbULGRiD&c>-o4ye(sm*_O@rD%3+Me9ICPR)*T zaBhX_R$Zihjq6gB^h4dDK$Ei!diOS=$x#M%rvf~SQ*G8&u5KtyS4cK`MM#R0iHb6R Zra!M#EiisO%cTGS002ovPDHLkV1fjJ5as{? literal 0 HcmV?d00001 From eb03c65768f310f3d72c2bb0994f63faa6b2ee8c Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 28 Feb 2012 00:30:57 +0100 Subject: [PATCH 26/99] Sort dirlisting --- couchpotato/core/plugins/browser/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/browser/main.py b/couchpotato/core/plugins/browser/main.py index 3c3e6e8f..21d3b4b7 100644 --- a/couchpotato/core/plugins/browser/main.py +++ b/couchpotato/core/plugins/browser/main.py @@ -38,7 +38,7 @@ class FileBrowser(Plugin): if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)): dirs.append(p + os.path.sep) - return dirs + return sorted(dirs) def getFiles(self): pass From 4692326ad6b211cef66cfccd46315ce4d08e5c9f Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 28 Feb 2012 00:31:12 +0100 Subject: [PATCH 27/99] Use useragent --- couchpotato/core/plugins/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 49b73228..41802e82 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -98,7 +98,7 @@ class Plugin(object): if not headers.get('Referer'): headers['Referer'] = urlparse(url).hostname if not headers.get('User-Agent'): - headers['User-Agent'] = '' + headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.2) Gecko/20100101 Firefox/10.0.2' host = urlparse(url).hostname self.wait(host) From 841e67ad2b93d46fd3028ff972462e2a8035862d Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 28 Feb 2012 00:31:23 +0100 Subject: [PATCH 28/99] Remove prefix in movie list --- couchpotato/core/plugins/movie/static/movie.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index 67fcb3c7..99b728dd 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -115,12 +115,18 @@ var Movie = new Class({ }).pop() if(title) - return title.title + return self.getUnprefixedTitle(title.title) else if(titles.length > 0) - return titles[0].title + return self.getUnprefixedTitle(titles[0].title) return 'Unknown movie' }, + + getUnprefixedTitle: function(t){ + if(t.substr(0, 4).toLowerCase() == 'the ') + t = t.substr(4) + ', The'; + return t; + }, slide: function(direction, el){ var self = this; From 10f3cb2ed03c88d1d5bf2987ba354fb8d67c94d8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 28 Feb 2012 00:31:52 +0100 Subject: [PATCH 29/99] Set proper movie_type tuple --- couchpotato/core/plugins/scanner/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index a389bb9a..f170dcd9 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -43,7 +43,7 @@ class Scanner(Plugin): 'trailer': ('video', 'trailer'), 'nfo': ('nfo', 'nfo'), 'movie': ('video', 'movie'), - 'movie': ('movie', 'movie_extra'), + 'movie_extra': ('movie', 'movie_extra'), 'backdrop': ('image', 'backdrop'), 'leftover': ('leftover', 'leftover'), } From 39e0fb4e72ae70805279a41da972d0f0a40ac6b2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 28 Feb 2012 00:33:24 +0100 Subject: [PATCH 30/99] Don't download unused images --- couchpotato/core/providers/movie/themoviedb/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/movie/themoviedb/main.py index 5f1595b7..f2c14e12 100644 --- a/couchpotato/core/providers/movie/themoviedb/main.py +++ b/couchpotato/core/providers/movie/themoviedb/main.py @@ -131,7 +131,7 @@ class TheMovieDb(MovieProvider): # Images poster = self.getImage(movie, type = 'poster', size = 'cover') - backdrop = self.getImage(movie, type = 'backdrop', size = 'w1280') + #backdrop = self.getImage(movie, type = 'backdrop', size = 'w1280') poster_original = self.getImage(movie, type = 'poster', size = 'original') backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original') @@ -152,7 +152,7 @@ class TheMovieDb(MovieProvider): 'original_title': movie.get('original_name'), 'images': { 'poster': [poster] if poster else [], - 'backdrop': [backdrop] if backdrop else [], + #'backdrop': [backdrop] if backdrop else [], 'poster_original': [poster_original] if poster_original else [], 'backdrop_original': [backdrop_original] if backdrop_original else [], }, From 930c1667337574e22fb11c90b59ed8b99218cde6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 29 Feb 2012 08:51:41 +0100 Subject: [PATCH 31/99] Add table indexes to identifiers --- couchpotato/core/settings/model.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 69aa3f17..36d58668 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -34,7 +34,7 @@ class Library(Entity): """""" year = Field(Integer) - identifier = Field(String(20)) + identifier = Field(String(20), index = True) rating = Field(Float) plot = Field(UnicodeText) @@ -52,8 +52,8 @@ class LibraryTitle(Entity): using_options(order_by = '-default') title = Field(Unicode) - simple_title = Field(Unicode) - default = Field(Boolean) + simple_title = Field(Unicode, index = True) + default = Field(Boolean, index = True) language = OneToMany('Language') libraries = ManyToOne('Library') @@ -62,7 +62,7 @@ class LibraryTitle(Entity): class Language(Entity): """""" - identifier = Field(String(20)) + identifier = Field(String(20), index = True) label = Field(Unicode) titles = ManyToOne('LibraryTitle') @@ -72,7 +72,7 @@ class Release(Entity): """Logically groups all files that belong to a certain release, such as parts of a movie, subtitles.""" - identifier = Field(String(100)) + identifier = Field(String(100), index = True) movie = ManyToOne('Movie') status = ManyToOne('Status') @@ -85,7 +85,7 @@ class Release(Entity): class ReleaseInfo(Entity): """Properties that can be bound to a file for off-line usage""" - identifier = Field(String(50)) + identifier = Field(String(50), index = True) value = Field(Unicode(255), nullable = False) release = ManyToOne('Release') @@ -107,7 +107,7 @@ class Quality(Entity): identifier = Field(String(20), unique = True) label = Field(Unicode(20)) - order = Field(Integer) + order = Field(Integer, index = True) size_min = Field(Integer) size_max = Field(Integer) @@ -121,7 +121,7 @@ class Profile(Entity): using_options(order_by = 'order') label = Field(Unicode(50)) - order = Field(Integer) + order = Field(Integer, index = True) core = Field(Boolean) hide = Field(Boolean) @@ -133,7 +133,7 @@ class ProfileType(Entity): """""" using_options(order_by = 'order') - order = Field(Integer) + order = Field(Integer, index = True) finish = Field(Boolean) wait_for = Field(Integer) @@ -170,7 +170,7 @@ class FileType(Entity): class FileProperty(Entity): """Properties that can be bound to a file for off-line usage""" - identifier = Field(String(20)) + identifier = Field(String(20), index = True) value = Field(Unicode(255), nullable = False) file = ManyToOne('File') From 560f74e5ba9ca93d5f205671271aa961fb940d11 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 29 Feb 2012 08:52:26 +0100 Subject: [PATCH 32/99] Proper movie list selection and optimization --- couchpotato/core/plugins/movie/main.py | 32 +++++++++++++------ couchpotato/core/plugins/movie/static/list.js | 9 +++--- .../core/plugins/movie/static/movie.js | 21 +++++++----- couchpotato/core/plugins/status/main.py | 1 + .../core/plugins/status/static/status.js | 8 ++++- 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index 295726c1..32cee0c5 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -101,15 +101,11 @@ class MoviePlugin(Plugin): if not isinstance(status, (list, tuple)): status = [status] - q = db.query(Movie) \ .join(Movie.library, Library.titles) \ - .options(joinedload_all('library.titles')) \ - .options(joinedload_all('library.files')) \ - .options(joinedload_all('status')) \ - .options(joinedload_all('files')) \ .filter(LibraryTitle.default == True) \ - .filter(or_(*[Movie.status.has(identifier = s) for s in status])) + .filter(or_(*[Movie.status.has(identifier = s) for s in status])) \ + .group_by(Movie.id) filter_or = [] if starts_with: @@ -130,17 +126,32 @@ class MoviePlugin(Plugin): q = q.order_by(asc(LibraryTitle.simple_title)) + q = q.subquery() + q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \ + .options(joinedload_all('releases')) \ + .options(joinedload_all('profile.types')) \ + .options(joinedload_all('library.titles')) \ + .options(joinedload_all('library.files')) \ + .options(joinedload_all('status')) \ + .options(joinedload_all('files')) \ + + if limit_offset: splt = limit_offset.split(',') limit = splt[0] offset = 0 if len(splt) is 1 else splt[1] - q = q.limit(limit).offset(offset) + q2 = q2.limit(limit).offset(offset) - results = q.all() + results = q2.all() movies = [] for movie in results: - temp = movie.to_dict(self.default_dict) + temp = movie.to_dict({ + 'profile': {'types': {}}, + 'releases': {'files':{}, 'info': {}}, + 'library': {'titles': {}, 'files':{}}, + 'files': {}, + }) movies.append(temp) return movies @@ -159,7 +170,8 @@ class MoviePlugin(Plugin): .join(Movie.library, Library.titles) \ .options(joinedload_all('library.titles')) \ .filter(LibraryTitle.default == True) \ - .filter(or_(*[Movie.status.has(identifier = s) for s in status])) + .filter(or_(*[Movie.status.has(identifier = s) for s in status])) \ + .group_by(Movie.id) results = q.all() diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index a5491b92..ca63f023 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -69,17 +69,18 @@ var MovieList = new Class({ self.scrollspy.stop(); } - Object.each(movies, function(info){ + Object.each(movies, function(movie){ // Attach proper actions - var a = self.options.actions - var actions = a[info.status.identifier.capitalize()] || a.Wanted || {}; + var a = self.options.actions, + status = Status.get(movie.status_id); + var actions = a[status.identifier.capitalize()] || a.Wanted || {}; var m = new Movie(self, { 'actions': actions, 'view': self.current_view, 'onSelect': self.calculateSelected.bind(self) - }, info); + }, movie); $(m).inject(self.movie_list); m.fireEvent('injected'); diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index 99b728dd..cd81b652 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -74,12 +74,13 @@ var Movie = new Class({ // Add done releases Array.each(self.data.releases, function(release){ - var q = self.quality.getElement('.q_'+ release.quality.identifier); - if(!q && release.status.identifier == 'snatched') - var q = self.addQuality(release.quality_id) + var q = self.quality.getElement('.q_id'+ release.quality_id), + status = Status.get(release.status_id); + if(!q && status.identifier == 'snatched') + var q = self.addQuality(release.quality_id) if (q) - q.addClass(release.status.identifier); + q.addClass(status.identifier); }); @@ -100,7 +101,7 @@ var Movie = new Class({ var q = Quality.getQuality(quality_id); return new Element('span', { 'text': q.label, - 'class': 'q_'+q.identifier + 'class': 'q_'+q.identifier + 'q_id' + q.quality_id }).inject(self.quality); }, @@ -121,7 +122,7 @@ var Movie = new Class({ return 'Unknown movie' }, - + getUnprefixedTitle: function(t){ if(t.substr(0, 4).toLowerCase() == 'the ') t = t.substr(4) + ', The'; @@ -275,11 +276,15 @@ var ReleaseAction = new Class({ ).inject(self.release_container) Array.each(self.movie.data.releases, function(release){ + + var status = Status.get(release.status_id), + quality = Quality.getProfile(release.quality_id) + new Element('div', { - 'class': 'item ' + release.status.identifier + 'class': 'item ' + 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.quality', {'text': 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')}), diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py index b8c4f5f8..9912fee2 100644 --- a/couchpotato/core/plugins/status/main.py +++ b/couchpotato/core/plugins/status/main.py @@ -20,6 +20,7 @@ class StatusPlugin(Plugin): 'wanted': 'Wanted', 'snatched': 'Snatched', 'deleted': 'Deleted', + 'ignored': 'Ignored', } def __init__(self): diff --git a/couchpotato/core/plugins/status/static/status.js b/couchpotato/core/plugins/status/static/status.js index 7967ac58..9c2167be 100644 --- a/couchpotato/core/plugins/status/static/status.js +++ b/couchpotato/core/plugins/status/static/status.js @@ -5,7 +5,13 @@ var StatusBase = new Class({ self.statuses = statuses; - } + }, + + get: function(id){ + return this.statuses.filter(function(status){ + return status.id == id + }).pick() + }, }); window.Status = new StatusBase(); From 62d89c641ad57eb89f67de7c7359ca953c5b2345 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 29 Feb 2012 23:02:57 +0100 Subject: [PATCH 33/99] Extra manage options --- couchpotato/core/plugins/movie/static/list.js | 30 +++++++- .../core/plugins/movie/static/movie.css | 70 ++++++++++++++++-- couchpotato/static/images/sprite.png | Bin 1350 -> 1864 bytes .../static/scripts/block/navigation.js | 2 +- couchpotato/static/scripts/page/manage.js | 29 ++++++-- couchpotato/static/style/main.css | 21 +++++- 6 files changed, 135 insertions(+), 17 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index ca63f023..5a5d6808 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -4,7 +4,8 @@ var MovieList = new Class({ options: { navigation: true, - limit: 50 + limit: 50, + menu: [] }, movies: [], @@ -115,6 +116,25 @@ var MovieList = new Class({ 'change': self.search.bind(self) } }), + self.navigation_menu = new Element('div.menu').adopt( + self.navigation_menu_ul = new Element('ul'), + self.navigation_menu_toggle = new Element('a.button.onlay', { + 'events': { + 'click': function(){ + self.navigation_menu_ul.toggleClass('show') + + if(self.navigation_menu_ul.hasClass('show')) + this.addEvent('outerClick', function(){ + self.navigation_menu_ul.removeClass('show') + this.removeEvents('outerClick'); + }) + else + this.removeEvents('outerClick'); + + } + } + }) + ), self.mass_edit_form = new Element('div.mass_edit_form').adopt( new Element('span.select').adopt( self.mass_edit_select = new Element('input[type=checkbox].inlay', { @@ -198,6 +218,14 @@ var MovieList = new Class({ } }); + + // Add menu or hide + if (self.options.menu.length > 0) + self.options.menu.each(function(menu_item){ + self.navigation_menu_ul.adopt(new Element('li').adopt(menu_item)); + }) + else + self.navigation_menu.hide() self.nav_scrollspy = new ScrollSpy({ min: 10, diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 4cf9a7b2..8d089f5e 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -310,7 +310,6 @@ } .movies .alph_nav { - overflow: hidden; transition: box-shadow .4s linear; position: fixed; z-index: 2; @@ -359,15 +358,14 @@ .movies .alph_nav input { padding: 6px 5px; - margin: 0; - float: right; + margin: 0 0 0 6px; + float: left; width: 155px; height: 25px; - float: right; } .movies .alph_nav .actions { - margin: 0 32px 0 0; + margin: 0 6px 0 0; -moz-user-select: none; } .movies .alph_nav .actions li { @@ -448,4 +446,64 @@ .movies .alph_nav .mass_edit_form .delete span { margin: 0 10px 0 0; } - \ No newline at end of file + + .movies .alph_nav .menu { + float: right; + } + + .movies .alph_nav .menu > a { + display: block; + background: url('../images/sprite.png') no-repeat center -137px; + height: 25px; + width: 25px; + border: 1px solid rgba(0,0,0,0.3); + } + + .movies .alph_nav .menu ul:before { + content: ' '; + height: 0; + position: absolute; + width: 0; + border: 6px solid transparent; + border-bottom-color: rgba(0,0,0,0.8); + margin: -16px 0 0 147px; + } + + .movies .alph_nav .menu ul { + display: none; + border: 1px solid #333; + background: rgba(0,0,0,0.8); + border-radius: 3px; + padding: 4px; + position: absolute; + z-index: 9; + margin: 32px 0 0 -145px; + width: 185px; + box-shadow: 0 10px 20px -10px rgba(0,0,0,0.4); + } + .movies .alph_nav .menu ul.show { + display: block; + } + .movies .alph_nav .menu ul li { + width: 100%; + height: auto; + } + + .movies .alph_nav .menu ul li a { + display: block; + border-bottom: 1px solid rgba(255,255,255,0.2); + padding: 0 10px; + box-shadow: none; + font-weight: normal; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1px; + } + + .movies .alph_nav .menu ul li:last-child a { + border: none; + color: #fff; + } + .movies .alph_nav .menu ul li a:hover { + background: rgba(255,255,255,0.1); + } \ No newline at end of file diff --git a/couchpotato/static/images/sprite.png b/couchpotato/static/images/sprite.png index 5d753c9793ad715e3a586fa50b1840ae16d48bbc..60cc5e8fd8836bdb9cc6566bad4445dcf1f9bcb2 100644 GIT binary patch delta 1827 zcmV+;2i*9^3djzS7Yd;W1^@s6B~pa4ks&942I@&fK~#9!>{<^@6IU3&YpZ}m?X*E% zreia6ZjvxXTuji3Iv88RrZZWxB|{gNxwtrWm~C;1VKbSVX4#*FWW;5YfMEd^J2MAH z!Gengsep(DMZ{7lrT<&{zt`LM^qo7(U9Sb4Oz%s-+`I4H`@Q$Q_r33X_gzVvrisaa zA_*~xG--uO&<>!X*5|j z*_2Q+KWaWRC(*ix-U44GirxSsfw6u*G@2qH+t!6u=3njt`8~4-V}!7|o|H zwO*dj$ZqNeJ{dHcY2AP^aP*gBZ(yK*>8Or9@3M#yNG(e3$AzO6t7&pCmE2xrvrnNgAC?M@K*tf~_y-+XRa+P5;PG!g2l5JgFc zhmGagS*t&WdOL7{4RlwtNHu{Ifm6Od^4;c^_D+lDvX(xtzNA^Y^x0I9Co!sjahey$ z0+4uO=kD5FxW4Xu)0L0*?#qJvWIiKjFe24t1IjP}0`>xr08@NmTyqvlQp^+|5F1P; zU|&%|{y$zrHpW&&Px}C@W|UnZ7(@PBHUFwPod|*BN9TxV0I}8X{0hlp3Uv`iD=`e2 zez4e_H2D47jLIN85;*{Ndy)-*!kV)Ly}@Ls!eQ{VVO%F6BoflHa6!sr*wc=gGd`~& zb9+2QC`66w2>o!IfZ%Gfsb8d6bz=SvY zL|wP&9OE~Sd#)xqWHhaIxxMGn$(j+iXjS2!sHZAw%yhXjY2AYg!I}NAnPT*!#zNs5*FW9>6 zX?!@22@I(|bJeOBmR#+BzR|23G}5}kVY<7guRZ^@*VC{kk#d1acszzDYt33;m=4Q) zK0o2}`3NdZC1$K#>Bp{!irH(Nsc)FGaN(jFil#HcFRu=-D?7{AJE}v^#FJRcs;H=_ zj~ot?p*r4x6eWFs^I~OXlq=_hz0Ea z*qQjkFM>UMZkso=M@MEdfoOrPL9#|6@)~FqOixU?{P$B)NA~vS zS4hsJFIy!#j-Ah!+l~uF=tZn(BBn8B&c?HvoTdRU5B{Woq@+WWvYG(qO-xKonv~T< zU=vwQ!8sfEcH&yicq~OQZpH&9#!V6Iz6X?;te!8ROsaDf)fCly=+qQrfCwgrx+%V$ zSR{41y`gB0G@2I0y0vR8v4AD?=>~`p#r-!8nN&(NHvY~0jfWO;K`0?Um-zR)W+b5# zxs|x{k%4A^&lwjON`@y85jlSrp&_HpJAc+)bdDmJ2sU97`ZpnZ@ds{28EN1dQ86OH zp!@{z7T^@%nR>mx3mvtbPUlE(Z*Mvj(f{~f1M{%l!!R2F8i#@CU@%C3vE^Q`H>gsnc1Bb4@xzA?tD}9Q zK;Y=$@(T>+8D-YOjFetTAAq z#y8Nb0XWDsC@LyKY5QE_&iBq-bdThW%JA$klxxO-_4M>~M5>gYp1zrF0_Yye(Ht^l zv)LSfW4>0`*47TSwzk?m9?#vdR9|1OV1qID2nNO@&3HLJoUVJ^FB*XPTmERY{#n>$j{F|3eUWolaq5cGcyy-*Hck9I?znuWyGED z=(6(ia*f??52)4Zq1@cubo|USzzYPu1_x~iJ^_3PpF@N*dtwZTnf?)A0BMKou(t*d ReA@s3002ovPDHLkV1gAvhlKzD delta 1309 zcmV+&1>*Y14#o{@GV6jc3`s#NDB=$lBr(y%LZShpK~rt?hY3M{hzT)96MqiXVUZ_2q{jsr-y%SqJP_WJO`8 zra}$XRa@VM{c$KWdZ3#aMmM22pp<=j;`6<|{a(Q{Fbq1n`aBgC4?O_&PR%uc()J<+ zfW`~E>c~6qJ=NOLJ@|HQT^0O(uh}No#v9$#19BJu0gF(kp(tixq%||%$6`^;fb?Ko zo{gFlWbv>DNAlff+W$i|2`9i(4*750{QdNw4wyK;OipL~NTTuNCrDRJs$+ta;V*<( z5Z$Np?q!G{g`QQSu-=%;L@24CCkdJ ziO?S&Gm|%(`ZnS7xmT`q?bjwoV2N1tClYPv3xNFL2X!A!@cgM{G9@74$FH9}^46h` zaaJNHSCchkaPnAzKT*rz6qkjf7P4k4=6xC81Ms!12eN9}0zLsYZ7&jka@4Q&9e0zS z428F*(qadytTkn?=VB?;wTFe+9@E24AV)3~i8jNk8bo3V?*&ia+MZrdsSrzaBVAaC zcOE=+xWsC)w`A9|yVw17-QOLG!~v|Xy-<44^))`wU$g(fgSa@h1cqE+zGchf>xV|K z_V9ra-~&@&bZor;`4?V)ti(*hlE4@f#>iA{+n!Z{%4M?>mCa@;MO7S|HgA?N6)Bck zqowU!X?gj&(+U8OL1G+K6-9#O{3e#6hkO$gJ9kuX3lfhZ!A*8>WbC(rp(~*C!lnKv zt9NXGWqc*9xOqy#8h77=DSQT3-m;Zyo5m4ka4-}@<*n&Jp{4u$olTo*?Hn*KG&M4Z=PsuY&@$0p=+eeX-uAX zLDfx7P3MuI>wKPnS)rhv__HP7)>yb@sLf|K*xg;a8&3c*fYTt~t7V-99HH|V20n_v zw_CN4HcSk(nUF>3y5Z;MoR+=1_c@waQjx%lUNh3V-A*)*-Okpy#m!=(NpurFJV;Kb z^Z1-ZQ<5an;cz&MjpnRW1uYYc2?R0Kk=Cr}twD-rAv-01Z{92f%+8xO*t{ncOS|4I zq0A{c+HTrz-gR!;Ilu<9Q{5ssPPnnDv?%8qw0xNpjZ>_Em5lR#N><4Kropi{Ubv&5 zJu+4(2)z(rMg9A`rm>-lT+oB%dbiEunOPGsWU{1p)9k6vJ^?n^odEksfB^tiP!;TA Tn{+M!00008NkvXXu0mjfKVgK& diff --git a/couchpotato/static/scripts/block/navigation.js b/couchpotato/static/scripts/block/navigation.js index b0674b25..b6886f8d 100644 --- a/couchpotato/static/scripts/block/navigation.js +++ b/couchpotato/static/scripts/block/navigation.js @@ -35,7 +35,7 @@ Block.Navigation = new Class({ addTab: function(tab){ var self = this - return new Element('li').adopt( + return new Element('li.tab_'+(tab.text.toLowerCase() || 'unknown')).adopt( new Element('a', tab) ).inject(self.nav) diff --git a/couchpotato/static/scripts/page/manage.js b/couchpotato/static/scripts/page/manage.js index 34529c55..8a88a367 100644 --- a/couchpotato/static/scripts/page/manage.js +++ b/couchpotato/static/scripts/page/manage.js @@ -9,27 +9,42 @@ Page.Manage = new Class({ var self = this; if(!self.list){ - self.refresh_button = new Element('a.icon.refresh', { - 'text': 'Refresh', + self.refresh_button = new Element('a', { + 'title': 'Rescan your library for new movies', + 'text': 'Full library refresh', 'events':{ - 'click': self.refresh.bind(self) + 'click': self.refresh.bind(self, true) } - }).inject(self.el); + }); + + self.refresh_quick = new Element('a', { + 'title': 'Just scan for recently changed', + 'text': 'Quick library scan', + 'events':{ + 'click': self.refresh.bind(self, false) + } + }); self.list = new MovieList({ 'identifier': 'manage', 'status': 'done', - 'actions': MovieActions + 'actions': MovieActions, + 'menu': [self.refresh_button, self.refresh_quick] }); $(self.list).inject(self.el); } }, - refresh: function(){ + refresh: function(full){ var self = this; + p(full) - Api.request('manage.update') + Api.request('manage.update', { + 'data': { + 'full': full ? 1 : null + } + }) } diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index fef35b24..a3e56166 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -307,13 +307,24 @@ body > .spinner, .mask{ overflow: hidden; font-weight: bold; } + + .select .list:before { + content: ' '; + height: 0; + position: absolute; + width: 0; + border: 6px solid transparent; + border-bottom-color: #282d34; + margin: -11px 0 0 70px; + } + .select .list { display: none; background: #282d34; border: 1px solid #1f242b; position: absolute; - margin: 25px 0 0 0; - box-shadow: 0 1px 2px rgba(0,0,0,0.4); + margin: 30px 0 0 0; + box-shadow: 0 20px 20px -10px rgba(0,0,0,0.4); border-radius:3px; z-index: 3; } @@ -374,6 +385,12 @@ body > .spinner, .mask{ rgb(73,83,98) 100% ); } +.onlay:active, .inlay.reversed > li:active { + color: #fff; + border: 1px solid transparent; + background-color: #282d34; + box-shadow: inset 0 1px 8px rgba(0,0,0,0.25), 0 1px 0px rgba(255,255,255,0.25); +} .question { display: block; From 9ca29749a6f1fac64ed87094377177e01104c0d3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 29 Feb 2012 23:26:30 +0100 Subject: [PATCH 34/99] Cleaner A-Z in movie list --- couchpotato/core/plugins/movie/static/list.js | 2 +- .../core/plugins/movie/static/movie.css | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index 5a5d6808..ddfb5cd0 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -100,7 +100,7 @@ var MovieList = new Class({ self.navigation = new Element('div.alph_nav').adopt( self.navigation_actions = new Element('ul.inlay.actions.reversed'), - self.navigation_alpha = new Element('ul.inlay.numbers', { + self.navigation_alpha = new Element('ul.numbers', { 'events': { 'click:relay(li)': function(e, el){ self.movie_list.empty() diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 8d089f5e..c7ab8c5e 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -337,23 +337,28 @@ .movies .alph_nav li { display: inline-block; vertical-align: top; - width: 23px; + width: 22px; height: 24px; line-height: 26px; text-align: center; cursor: pointer; - color: #666; + color: rgba(255,255,255,0.2); border: 1px solid transparent; + -webkit-transition: all 0.1s ease-in-out; + text-shadow: none; } .movies .alph_nav .numbers li:first-child { - width: 34px; - } - .movies .alph_nav li.active, .movies .alph_nav li:hover { - font-weight: bolder; - color: #fff; + width: 44px; } .movies .alph_nav li.available { + color: rgba(255,255,255,0.8); + font-weight: bolder; + + } + .movies .alph_nav li.active.available, .movies .alph_nav li.available:hover { color: #fff; + font-size: 24px; + line-height: 24px; } .movies .alph_nav input { From 2d3dc8f7c88dceca94675c964fb073264f962416 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 29 Feb 2012 23:27:17 +0100 Subject: [PATCH 35/99] Crossbrowser transition --- couchpotato/core/plugins/movie/static/movie.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index c7ab8c5e..5baa02d3 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -344,7 +344,7 @@ cursor: pointer; color: rgba(255,255,255,0.2); border: 1px solid transparent; - -webkit-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; text-shadow: none; } .movies .alph_nav .numbers li:first-child { From ca67370b44570baaac3163403bbf0c4a809d7689 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 4 Mar 2012 20:46:45 +0100 Subject: [PATCH 36/99] Cleaner userscript CSS --- couchpotato/core/plugins/userscript/template.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/userscript/template.js b/couchpotato/core/plugins/userscript/template.js index 64839fb7..69cb42e0 100644 --- a/couchpotato/core/plugins/userscript/template.js +++ b/couchpotato/core/plugins/userscript/template.js @@ -57,7 +57,7 @@ if (typeof GM_addStyle == 'undefined'){ // Styles GM_addStyle('\ - #cp_popup { font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; -moz-border-radius-topleft: 6px; -moz-border-radius-bottomleft: 6px; -webkit-border-top-left-radius: 6px; -webkit-border-bottom-left-radius: 6px; -moz-box-shadow: 0 0 20px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.5); position:fixed; z-index:9999; bottom:0; right:0; font-size:15px; margin: 20px 0; display: block; background:#4E5969; } \ + #cp_popup { font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; -moz-border-radius: 6px 0px 0px 6px; -webkit-border-radius: 6px 0px 0px 6px; border-radius: 6px 0px 0px 6px; -moz-box-shadow: 0 0 20px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.5); box-shadow: 0 0 20px rgba(0,0,0,0.5); position:fixed; z-index:9999; bottom:0; right:0; font-size:15px; margin: 20px 0; display: block; background:#4E5969; } \ #cp_popup:hover { } \ #cp_popup a#add_to { cursor:pointer; text-align:center; text-decoration:none; color: #000; display:block; padding:5px 0 5px 5px; } \ #cp_popup a#close_button { cursor:pointer; float: right; padding:120px 10px 10px; } \ From f160bfe77962e33139760b79c81c2a395fd86d66 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 4 Mar 2012 23:26:36 +0100 Subject: [PATCH 37/99] Reordering of settings --- couchpotato/core/_base/_core/__init__.py | 1 + .../core/downloaders/blackhole/__init__.py | 1 + .../core/plugins/automation/__init__.py | 1 + couchpotato/core/plugins/manage/__init__.py | 3 +- couchpotato/core/plugins/movie/static/list.js | 2 +- .../core/plugins/movie/static/movie.css | 66 ++----------- .../core/plugins/quality/static/quality.js | 4 +- couchpotato/core/plugins/renamer/__init__.py | 1 + couchpotato/core/plugins/searcher/__init__.py | 1 + couchpotato/core/plugins/subtitle/__init__.py | 2 + couchpotato/core/plugins/trailer/__init__.py | 4 +- .../plugins/userscript/static/userscript.js | 34 ++----- .../core/plugins/wizard/static/wizard.js | 13 ++- .../metadata/mediabrowser/__init__.py | 3 +- .../providers/metadata/sonyps3/__init__.py | 3 +- .../core/providers/metadata/wdtv/__init__.py | 3 +- .../core/providers/metadata/xbmc/__init__.py | 3 +- .../providers/movie/themoviedb/__init__.py | 2 +- .../core/providers/nzb/moovee/__init__.py | 3 +- .../core/providers/nzb/mysterbin/__init__.py | 3 +- .../core/providers/nzb/newzbin/__init__.py | 3 +- .../core/providers/nzb/newznab/__init__.py | 3 +- .../core/providers/nzb/nzbclub/__init__.py | 3 +- .../core/providers/nzb/nzbindex/__init__.py | 3 +- .../core/providers/nzb/nzbmatrix/__init__.py | 3 +- .../core/providers/nzb/nzbs/__init__.py | 3 +- .../core/providers/nzb/x264/__init__.py | 3 +- .../torrent/kickasstorrents/__init__.py | 3 +- couchpotato/static/images/sprite.png | Bin 1864 -> 1921 bytes couchpotato/static/scripts/block/more.js | 54 ++++++++++ couchpotato/static/scripts/couchpotato.js | 16 +-- couchpotato/static/scripts/page/about.js | 80 ++++++--------- couchpotato/static/scripts/page/settings.js | 93 +++++++++++++++++- couchpotato/static/style/main.css | 87 +++++++++++++++- couchpotato/static/style/page/settings.css | 38 ++++++- couchpotato/templates/_desktop.html | 1 + 36 files changed, 370 insertions(+), 176 deletions(-) create mode 100644 couchpotato/static/scripts/block/more.js diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py index f48b157e..ff2bd07d 100644 --- a/couchpotato/core/_base/_core/__init__.py +++ b/couchpotato/core/_base/_core/__init__.py @@ -6,6 +6,7 @@ def start(): config = [{ 'name': 'core', + 'order': 1, 'groups': [ { 'tab': 'general', diff --git a/couchpotato/core/downloaders/blackhole/__init__.py b/couchpotato/core/downloaders/blackhole/__init__.py index a958ecd1..3231c48d 100644 --- a/couchpotato/core/downloaders/blackhole/__init__.py +++ b/couchpotato/core/downloaders/blackhole/__init__.py @@ -5,6 +5,7 @@ def start(): config = [{ 'name': 'blackhole', + 'order': 30, 'groups': [ { 'tab': 'downloaders', diff --git a/couchpotato/core/plugins/automation/__init__.py b/couchpotato/core/plugins/automation/__init__.py index 550da5d7..8f7d80b7 100644 --- a/couchpotato/core/plugins/automation/__init__.py +++ b/couchpotato/core/plugins/automation/__init__.py @@ -5,6 +5,7 @@ def start(): config = [{ 'name': 'automation', + 'order': 30, 'groups': [ { 'tab': 'automation', diff --git a/couchpotato/core/plugins/manage/__init__.py b/couchpotato/core/plugins/manage/__init__.py index 4aa9fe80..30f6ea68 100644 --- a/couchpotato/core/plugins/manage/__init__.py +++ b/couchpotato/core/plugins/manage/__init__.py @@ -7,10 +7,9 @@ config = [{ 'name': 'manage', 'groups': [ { - 'tab': 'renamer', + 'tab': 'manage', 'label': 'movie library manager', 'description': 'Add your existing movie folders.', - 'wizard': True, 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index ddfb5cd0..76692210 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -116,7 +116,7 @@ var MovieList = new Class({ 'change': self.search.bind(self) } }), - self.navigation_menu = new Element('div.menu').adopt( + self.navigation_menu = new Element('div.more_menu').adopt( self.navigation_menu_ul = new Element('ul'), self.navigation_menu_toggle = new Element('a.button.onlay', { 'events': { diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 5baa02d3..4b414aed 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -315,7 +315,7 @@ z-index: 2; top: 0; padding: 100px 60px 7px; - width: 1082px; + width: 1080px; margin: 0 -60px; box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1); } @@ -326,7 +326,7 @@ background: #4e5969; } -.movies .alph_nav ul { +.movies .alph_nav ul.numbers, .movies .alph_nav ul.actions { list-style: none; padding: 0 0 1px; margin: 0; @@ -334,7 +334,7 @@ user-select: none; } - .movies .alph_nav li { + .movies .alph_nav .numbers li, .movies .alph_nav .actions li { display: inline-block; vertical-align: top; width: 22px; @@ -452,63 +452,11 @@ margin: 0 10px 0 0; } - .movies .alph_nav .menu { + .movies .alph_nav .more_menu { float: right; } - .movies .alph_nav .menu > a { - display: block; - background: url('../images/sprite.png') no-repeat center -137px; - height: 25px; - width: 25px; - border: 1px solid rgba(0,0,0,0.3); + .movies .alph_nav .more_menu > a { + background-position: center -157px; } - - .movies .alph_nav .menu ul:before { - content: ' '; - height: 0; - position: absolute; - width: 0; - border: 6px solid transparent; - border-bottom-color: rgba(0,0,0,0.8); - margin: -16px 0 0 147px; - } - - .movies .alph_nav .menu ul { - display: none; - border: 1px solid #333; - background: rgba(0,0,0,0.8); - border-radius: 3px; - padding: 4px; - position: absolute; - z-index: 9; - margin: 32px 0 0 -145px; - width: 185px; - box-shadow: 0 10px 20px -10px rgba(0,0,0,0.4); - } - .movies .alph_nav .menu ul.show { - display: block; - } - .movies .alph_nav .menu ul li { - width: 100%; - height: auto; - } - - .movies .alph_nav .menu ul li a { - display: block; - border-bottom: 1px solid rgba(255,255,255,0.2); - padding: 0 10px; - box-shadow: none; - font-weight: normal; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 1px; - } - - .movies .alph_nav .menu ul li:last-child a { - border: none; - color: #fff; - } - .movies .alph_nav .menu ul li a:hover { - background: rgba(255,255,255,0.1); - } \ No newline at end of file + \ No newline at end of file diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js index 87a2b5a1..314ca576 100644 --- a/couchpotato/core/plugins/quality/static/quality.js +++ b/couchpotato/core/plugins/quality/static/quality.js @@ -39,10 +39,10 @@ var QualityBase = new Class({ self.settings = App.getPage('Settings') self.settings.addEvent('create', function(){ - var tab = self.settings.createTab('profile', { + var tab = self.settings.createSubTab('profile', { 'label': 'Quality', 'name': 'profile' - }); + }, self.settings.tabs.searcher ,'searcher'); self.tab = tab.tab; self.content = tab.content; diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index ab2cd899..b1b53395 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -24,6 +24,7 @@ rename_options = { config = [{ 'name': 'renamer', + 'order': 40, 'description': 'Move and rename your downloaded movies to your movie directory.', 'groups': [ { diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py index 14a43768..8e0f2cf9 100644 --- a/couchpotato/core/plugins/searcher/__init__.py +++ b/couchpotato/core/plugins/searcher/__init__.py @@ -6,6 +6,7 @@ def start(): config = [{ 'name': 'searcher', + 'order': 20, 'groups': [ { 'tab': 'searcher', diff --git a/couchpotato/core/plugins/subtitle/__init__.py b/couchpotato/core/plugins/subtitle/__init__.py index 88728bd9..903e934e 100644 --- a/couchpotato/core/plugins/subtitle/__init__.py +++ b/couchpotato/core/plugins/subtitle/__init__.py @@ -8,7 +8,9 @@ config = [{ 'groups': [ { 'tab': 'renamer', + 'subtab': 'subtitles', 'name': 'subtitle', + 'label': 'Download subtitles after rename', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/plugins/trailer/__init__.py b/couchpotato/core/plugins/trailer/__init__.py index 49b0cb9e..033df088 100644 --- a/couchpotato/core/plugins/trailer/__init__.py +++ b/couchpotato/core/plugins/trailer/__init__.py @@ -7,8 +7,10 @@ config = [{ 'name': 'trailer', 'groups': [ { - 'tab': 'metadata', + 'tab': 'renamer', + 'subtab': 'trailer', 'name': 'trailer', + 'label': 'Download trailer after rename', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index 63f5fc96..0e2d7688 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -62,34 +62,18 @@ var UserscriptSettingTab = new Class({ self.settings = App.getPage('Settings') self.settings.addEvent('create', function(){ - var tab = self.settings.createTab('userscript', { - 'label': 'Userscript', - 'name': 'userscript' - }); - - self.tab = tab.tab; - self.content = tab.content; - - self.createUserscript(); + self.settings.createGroup({ + 'label': 'Install the Userscript' + }).inject(self.settings.tabs.automation.content, 'top').adopt( + new Element('a', { + 'text': 'Install userscript', + 'href': Api.createUrl('userscript.get')+'couchpotato.user.js', + 'target': '_self' + }) + ); }); - }, - - createUserscript: function(){ - var self = this; - - - self.settings.createGroup({ - 'label': 'Install the Userscript' - }).inject(self.content).adopt( - new Element('a', { - 'text': 'Install userscript', - 'href': Api.createUrl('userscript.get')+'couchpotato.user.js', - 'target': '_self' - }) - ); - } }); diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js index 74d2b4db..c4edd0d5 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.js +++ b/couchpotato/core/plugins/wizard/static/wizard.js @@ -29,7 +29,7 @@ Page.Wizard = new Class({ }, 'finish': { 'title': 'Finish Up', - 'description': 'Are you done? Did you fill in everything or as much as possible? Yes, ok gogogo!', + 'description': 'Are you done? Did you fill in everything as much as possible? Yes, ok gogogo!', 'content': new Element('div').adopt( new Element('a.button.green', { 'text': 'I\'m ready to start the awesomeness, wow this button is big and green!', @@ -115,7 +115,14 @@ Page.Wizard = new Class({ if(tab_navigation && group_container){ tab_navigation.inject(tabs); // Tab navigation self.el.getElement('.tab_'+group).inject(group_container); // Tab content - if(self.headers[group]) tab_navigation.getElement('a').set('text', (self.headers[group].label || group).capitalize()); + if(self.headers[group]){ + var a = tab_navigation.getElement('a'); + a.set('text', (self.headers[group].label || group).capitalize()); + var url_split = a.get('href').split('wizard')[1].split('/'); + if(url_split.length > 3) + a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', '')); + + } } else { new Element('li.t_'+group).adopt( @@ -161,7 +168,7 @@ Page.Wizard = new Class({ if(nr == 0) func(); - + var ss = new ScrollSpy( { min: function(){ var c = g.getCoordinates(); diff --git a/couchpotato/core/providers/metadata/mediabrowser/__init__.py b/couchpotato/core/providers/metadata/mediabrowser/__init__.py index 35b84278..c061ce3c 100644 --- a/couchpotato/core/providers/metadata/mediabrowser/__init__.py +++ b/couchpotato/core/providers/metadata/mediabrowser/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'mediabrowser', 'groups': [ { - 'tab': 'metadata', + 'tab': 'renamer', + 'subtab': 'metadata', 'name': 'mediabrowser_metadata', 'label': 'MediaBrowser', 'description': 'Enable metadata MediaBrowser can understand', diff --git a/couchpotato/core/providers/metadata/sonyps3/__init__.py b/couchpotato/core/providers/metadata/sonyps3/__init__.py index 88c6167f..002b8487 100644 --- a/couchpotato/core/providers/metadata/sonyps3/__init__.py +++ b/couchpotato/core/providers/metadata/sonyps3/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'sonyps3', 'groups': [ { - 'tab': 'metadata', + 'tab': 'renamer', + 'subtab': 'metadata', 'name': 'sonyps3_metadata', 'label': 'Sony PS3', 'description': 'Enable metadata your Playstation 3 can understand', diff --git a/couchpotato/core/providers/metadata/wdtv/__init__.py b/couchpotato/core/providers/metadata/wdtv/__init__.py index edb9cc26..b3dab6e7 100644 --- a/couchpotato/core/providers/metadata/wdtv/__init__.py +++ b/couchpotato/core/providers/metadata/wdtv/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'wdtv', 'groups': [ { - 'tab': 'metadata', + 'tab': 'renamer', + 'subtab': 'metadata', 'name': 'wdtv_metadata', 'label': 'WDTV', 'description': 'Enable metadata WDTV can understand', diff --git a/couchpotato/core/providers/metadata/xbmc/__init__.py b/couchpotato/core/providers/metadata/xbmc/__init__.py index d4ff12a6..2a9510e5 100644 --- a/couchpotato/core/providers/metadata/xbmc/__init__.py +++ b/couchpotato/core/providers/metadata/xbmc/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'xbmc', 'groups': [ { - 'tab': 'metadata', + 'tab': 'renamer', + 'subtab': 'metadata', 'name': 'xbmc_metadata', 'label': 'XBMC', 'description': 'Enable metadata XBMC can understand', diff --git a/couchpotato/core/providers/movie/themoviedb/__init__.py b/couchpotato/core/providers/movie/themoviedb/__init__.py index 31441fc4..66ac536a 100644 --- a/couchpotato/core/providers/movie/themoviedb/__init__.py +++ b/couchpotato/core/providers/movie/themoviedb/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'providers', 'name': 'tmdb', 'label': 'TheMovieDB', - 'advanced': True, + 'hidden': True, 'description': 'Used for all calls to TheMovieDB.', 'options': [ { diff --git a/couchpotato/core/providers/nzb/moovee/__init__.py b/couchpotato/core/providers/nzb/moovee/__init__.py index 8d86be1f..f2f85d18 100644 --- a/couchpotato/core/providers/nzb/moovee/__init__.py +++ b/couchpotato/core/providers/nzb/moovee/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'moovee', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': '#alt.binaries.moovee', 'description': 'SD movies only', 'options': [ diff --git a/couchpotato/core/providers/nzb/mysterbin/__init__.py b/couchpotato/core/providers/nzb/mysterbin/__init__.py index 07be1d4e..0c759555 100644 --- a/couchpotato/core/providers/nzb/mysterbin/__init__.py +++ b/couchpotato/core/providers/nzb/mysterbin/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'mysterbin', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'Mysterbin', 'description': '', 'options': [ diff --git a/couchpotato/core/providers/nzb/newzbin/__init__.py b/couchpotato/core/providers/nzb/newzbin/__init__.py index ea0c27df..4ebd849d 100644 --- a/couchpotato/core/providers/nzb/newzbin/__init__.py +++ b/couchpotato/core/providers/nzb/newzbin/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'newzbin', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'newzbin', 'wizard': True, 'options': [ diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py index f963e00d..212c9847 100644 --- a/couchpotato/core/providers/nzb/newznab/__init__.py +++ b/couchpotato/core/providers/nzb/newznab/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'newznab', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'newznab', 'description': 'Enable multiple NewzNab providers such as NZB.su', 'wizard': True, diff --git a/couchpotato/core/providers/nzb/nzbclub/__init__.py b/couchpotato/core/providers/nzb/nzbclub/__init__.py index 18f4e33d..9c14e10f 100644 --- a/couchpotato/core/providers/nzb/nzbclub/__init__.py +++ b/couchpotato/core/providers/nzb/nzbclub/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'nzbclub', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'NZBClub', 'description': '', 'options': [ diff --git a/couchpotato/core/providers/nzb/nzbindex/__init__.py b/couchpotato/core/providers/nzb/nzbindex/__init__.py index cf3139c9..8a3261bf 100644 --- a/couchpotato/core/providers/nzb/nzbindex/__init__.py +++ b/couchpotato/core/providers/nzb/nzbindex/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'nzbindex', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'nzbindex', 'description': 'Free provider, but less accurate.', 'options': [ diff --git a/couchpotato/core/providers/nzb/nzbmatrix/__init__.py b/couchpotato/core/providers/nzb/nzbmatrix/__init__.py index 84d17074..82b6ef6e 100644 --- a/couchpotato/core/providers/nzb/nzbmatrix/__init__.py +++ b/couchpotato/core/providers/nzb/nzbmatrix/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'nzbmatrix', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'nzbmatrix', 'label': 'NZBMatrix', 'wizard': True, diff --git a/couchpotato/core/providers/nzb/nzbs/__init__.py b/couchpotato/core/providers/nzb/nzbs/__init__.py index bd9f9a37..2ca89171 100644 --- a/couchpotato/core/providers/nzb/nzbs/__init__.py +++ b/couchpotato/core/providers/nzb/nzbs/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'nzbs', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'nzbs', 'description': 'Id and Key can be found on your nzbs.org RSS page.', 'wizard': True, diff --git a/couchpotato/core/providers/nzb/x264/__init__.py b/couchpotato/core/providers/nzb/x264/__init__.py index ef0e2f29..152be009 100644 --- a/couchpotato/core/providers/nzb/x264/__init__.py +++ b/couchpotato/core/providers/nzb/x264/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'x264', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': '#alt.binaries.hdtv.x264', 'description': 'HD movies only', 'options': [ diff --git a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py index e88ac81c..6514643e 100644 --- a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py +++ b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'kickasstorrents', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'KickAssTorrents', 'options': [ { diff --git a/couchpotato/static/images/sprite.png b/couchpotato/static/images/sprite.png index 60cc5e8fd8836bdb9cc6566bad4445dcf1f9bcb2..1d96a56a85c0c57e890cd05e3f33903131a953f7 100644 GIT binary patch delta 1884 zcmV-i2c!7N4uKDl7Yeut1^@s60gKn+ks&942O~*DK~#9!>{<;>6h{=^y*mXg%2h9@ zO|`V9)>aa#h>ZzaQOlJ!q1Bo+wn@>(rZzTKEv9X3VrWfjt7-a^kd)Z85>XnUk(ydC z3IaAB$O(vepon-J9LN11+%LDceWSB>S$1#l04vpfxtE=LGyCnlnK$pvyj_x}X=0*( zNJdN`O(03aeTiUGJ%R2hJ=8|^NqGyvp9>=JE#%o?Y z6F}mDowZ|U?wYFe^;bUHy(b<1#|s%bgTYmk4XD5X2-pie1WXBm@r@amL?+ zf3VtJH2D47jLIN8#O(n4ev%D;!Wy#}gVF4uLLu<9AzUXTWHQn^Z*Jma*wc<0Ga;{` zaCa90f95n2%+khKT`KG?FC1>-N zr}5@EA~2--^ySN6Sa`L6<3@wN&qV9{2I!8??w0J=UQfoJM9v2$6Yv=Od^P6`Q(jnIF3%Dq^p3rn+|Kym|90D4I?K$9N?~QGVFYr^;pIRYOnDy0sZA zjl9c{M@{*a_Rc>qU%gIWxY*pXI%Dk;*v2Qrj=KTL+B!NLh1BGKoq+s;pI4o&)l|Qv z&PvATBjxi;mM*NS)6^6e{kBnP0{GNKPkjWR2at<); zYHoLs4Alt+q%i4!n-@z;O3pDrsElM`qnk)BD5Jc*ybduci-k(D>;a;6_8ndy?Ca-D0;lUq&7Z-P6LS_@dys@#daT79| z2y8sFDLQ6jUQT?o8I7fg#?5HJq_`=8-S>bJk<|+YlnHf?lA4m551pD)43NO2P&dh! z6N}`7Zf`K$O)=0o&y7j$8kmCPLLm`(Fb#;F;pYhOQE(j&WXA%Ft*W?n~ zk=uwnFBxcm_L%X3p=5Xr!O8iv2z41{-i5R7l5>>6B(QOl*w=*U;1Aq{GSa})!eVg2 zp!@{zM&Lx?=>~(L9c{H-F4s_3S62!Y(RX~W0;h%rgR9EzcArEy!{PrAzu#ZX&bmWg zeJ~jOiJ~Y!)ORs}ykJ#TRjEd!aexf~H-~`e;o)I_iY@ngy~8y%HQU3f`S`(u2Q}ed zQ6O-1Eh{V2E0xOCykNlSgV85jTU$>vH$vIDYu7HFN~NkpSM;B$>+bHp32HBe;*3#X zpvE`wR3q>(b3=#Gg{P(?+XeFt}{=y zSS&iYcOF-ihw$=rB}NRHO)+23Y$V7|;M6 zA?fw{y=<@%3q3bCceuQ~ydNx|DlIKF78VwN8kjLejloDQ`T6;`tX6BFxw+Z7fB$|@ zxP`7NR+1EAR>619%@Ip4pRPfCMIiJ-BBl?&qvW zY)atB9O2r9?b-3;$47jK3*I+Itld5RW{;g%a&lxop>~@#Z5nxYl2(8O_E3WTBftP- WaVRgE1+9Jn0000{<^@6IU3&YpZ}m?X*E% zreia6ZjvxXTuji3Iv88RrZZWxB|{gNxwtrWm~C;1VKbSVX4#*FWW;5YfMEd^J2MAH z!Gengsep(DMZ{7lrT<&{zt`LM^qo7(U9Sb4Oz%s-+`I4H`@Q$Q_r33X_gzVvrisaa zA_*~xG--uO&<>!X*5|j z*_2Q+KWaWRC(*ix-U44GirxSsfw6u*G@2qH+t!6u=3njt`8~4-V}!7|o|H zwO*dj$ZqNeJ{dHcY2AP^aP*gBZ(yK*>8Or9@3M#yNG(e3$AzO6t7&pCmE2xrvrnNgAC?M@K*tf~_y-+XRa+P5;PG!g2l5JgFc zhmGagS*t&WdOL7{4RlwtNHu{Ifm6Od^4;c^_D+lDvX(xtzNA^Y^x0I9Co!sjahey$ z0+4uO=kD5FxW4Xu)0L0*?#qJvWIiKjFe24t1IjP}0`>xr08@NmTyqvlQp^+|5F1P; zU|&%|{y$zrHpW&&Px}C@W|UnZ7(@PBHUFwPod|*BN9TxV0I}8X{0hlp3Uv`iD=`e2 zez4e_H2D47jLIN85;*{Ndy)-*!kV)Ly}@Ls!eQ{VVO%F6BoflHa6!sr*wc=gGd`~& zb9+2QC`66w2>o!IfZ%Gfsb8d6bz=SvY zL|wP&9OE~Sd#)xqWHhaIxxMGn$(j+iXjS2!sHZAw%yhXjY2AYg!I}NAnPT*!#zNs5*FW9>6 zX?!@22@I(|bJeOBmR#+BzR|23G}5}kVY<7guRZ^@*VC{kk#d1acszzDYt33;m=4Q) zK0o2}`3NdZC1$K#>Bp{!irH(Nsc)FGaN(jFil#HcFRu=-D?7{AJE}v^#FJRcs;H=_ zj~ot?p*r4x6eWFs^I~OXlq=_hz0Ea z*qQjkFM>UMZkso=M@MEdfoOrPL9#|6@)~FqOixU?{P$B)NA~vS zS4hsJFIy!#j-Ah!+l~uF=tZn(BBn8B&c?HvoTdRU5B{Woq@+WWvYG(qO-xKonv~T< zU=vwQ!8sfEcH&yicq~OQZpH&9#!V6Iz6X?;te!8ROsaDf)fCly=+qQrfCwgrx+%V$ zSR{41y`gB0G@2I0y0vR8v4AD?=>~`p#r-!8nN&(NHvY~0jfWO;K`0?Um-zR)W+b5# zxs|x{k%4A^&lwjON`@y85jlSrp&_HpJAc+)bdDmJ2sU97`ZpnZ@ds{28EN1dQ86OH zp!@{z7T^@%nR>mx3mvtbPUlE(Z*Mvj(f{~f1M{%l!!R2F8i#@CU@%C3vE^Q`H>gsnc1Bb4@xzA?tD}9Q zK;Y=$@(T>+8D-YOjFetTAAq z#y8Nb0XWDsC@LyKY5QE_&iBq-bdThW%JA$klxxO-_4M>~M5>gYp1zrF0_Yye(Ht^l zv)LSfW4>0`*47TSwzk?m9?#vdR9|1OV1qID2nNO@&3HLJoUVJ^FB*XPTmERY{#n>$j{F|3eUWolaq5cGcyy-*Hck9I?znuWyGED z=(6(ia*f??52)4Zq1@cubo|USzzYPu1_x~iJ^_3PpF@N*dtwZTnf?)A0BMKou(t*d ReA@s3002ovPDHLkV1i6uheZGY diff --git a/couchpotato/static/scripts/block/more.js b/couchpotato/static/scripts/block/more.js new file mode 100644 index 00000000..d0926d05 --- /dev/null +++ b/couchpotato/static/scripts/block/more.js @@ -0,0 +1,54 @@ +Block.More = new Class({ + + Extends: BlockBase, + + create: function(){ + var self = this; + + self.el = new Element('div.more_menu').adopt( + self.more_option_ul = new Element('ul').adopt( + new Element('li').adopt( + new Element('a.orange', { + 'text': 'Restart', + 'events': { + 'click': App.restart.bind(App) + } + }) + ), + new Element('li').adopt( + new Element('a.red', { + 'text': 'Shutdown', + 'events': { + 'click': App.shutdown.bind(App) + } + }) + ) + ), + new Element('a.button.onlay', { + 'events': { + 'click': function(){ + self.more_option_ul.toggleClass('show') + + if(self.more_option_ul.hasClass('show')) + this.addEvent('outerClick', function(){ + self.more_option_ul.removeClass('show') + this.removeEvents('outerClick'); + }) + else + this.removeEvents('outerClick'); + + } + } + }) + ) + + }, + + addLink: function(tab, position){ + var self = this + + return new Element('li').adopt(tab).inject(self.more_option_ul, position || 'bottom') + + } + +}); \ No newline at end of file diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index 8fbd00cb..380d5fe6 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -59,13 +59,14 @@ var CouchPotato = new Class({ $(self.block.header).addClass('header').adopt( new Element('div').adopt( self.block.navigation = new Block.Navigation(self, {}), - self.block.search = new Block.Search(self, {}) + self.block.search = new Block.Search(self, {}), + self.block.more = new Block.More(self, {}) ) ), self.content = new Element('div.content'), self.block.footer = new Block.Footer(self, {}) ); - + new ScrollSpy({ min: 10, onLeave: function(){ @@ -108,7 +109,7 @@ var CouchPotato = new Class({ try { var page = self.pages[page_name] || self.pages.Wanted; - page.open(action, params); + page.open(action, params, current_url); page.show(); } catch(e){ @@ -150,7 +151,7 @@ var CouchPotato = new Class({ var self = this; (function(){ - + Api.request('app.available', { 'onFailure': function(){ self.checkAvailable.delay(1000, self); @@ -161,7 +162,7 @@ var CouchPotato = new Class({ self.fireEvent('load'); } }); - + }).delay(delay || 0) }, @@ -214,7 +215,7 @@ var Route = new Class({ self.page = (url.length > 0) ? url.shift() : self.defaults.page self.action = (url.length > 0) ? url.shift() : self.defaults.action - self.params = self.defaults.params + self.params = Object.merge({}, self.defaults.params); if(url.length > 1){ var key url.each(function(el, nr){ @@ -226,6 +227,9 @@ var Route = new Class({ } }) } + else if(url.length == 1){ + self.params[url] = true; + } return self }, diff --git a/couchpotato/static/scripts/page/about.js b/couchpotato/static/scripts/page/about.js index 20ebcb22..000500be 100644 --- a/couchpotato/static/scripts/page/about.js +++ b/couchpotato/static/scripts/page/about.js @@ -38,6 +38,36 @@ var AboutSettingTab = new Class({ today = new Date(), one_day = 1000*60*60*24; + self.settings.createGroup({ + 'label': 'About This CouchPotato', + 'name': 'variables' + }).inject(self.content).adopt( + new Element('dl.info').adopt( + new Element('dt[text=Version]'), + self.version_text = new Element('dd.version', { + 'text': 'Getting version...', + 'events': { + 'click': self.checkForUpdate.bind(self), + 'mouseenter': function(){ + this.set('text', 'Check for updates') + }, + 'mouseleave': function(){ + self.fillVersion(Updater.getInfo()) + } + } + }), + new Element('dt[text=Directories]'), + new Element('dd', {'text': App.getOption('app_dir')}), + new Element('dd', {'text': App.getOption('data_dir')}), + new Element('dt[text=Startup Args]'), + new Element('dd', {'html': App.getOption('args')}), + new Element('dd', {'html': App.getOption('options')}) + ) + ); + + if(!self.fillVersion(Updater.getInfo())) + Updater.addEvent('loaded', self.fillVersion.bind(self)) + self.settings.createGroup({ 'name': 'Help Support CouchPotato' }).inject(self.content).adopt( @@ -78,56 +108,6 @@ var AboutSettingTab = new Class({ }) ); - - self.settings.createGroup({ - 'label': 'About This CouchPotato', - 'name': 'variables' - }).inject(self.content).adopt( - new Element('dl.info').adopt( - new Element('dt[text=Version]'), - self.version_text = new Element('dd.version', { - 'text': 'Getting version...', - 'events': { - 'click': self.checkForUpdate.bind(self), - 'mouseenter': function(){ - this.set('text', 'Check for updates') - }, - 'mouseleave': function(){ - self.fillVersion(Updater.getInfo()) - } - } - }), - new Element('dt[text=Directories]'), - new Element('dd', {'text': App.getOption('app_dir')}), - new Element('dd', {'text': App.getOption('data_dir')}), - new Element('dt[text=Startup Args]'), - new Element('dd', {'html': App.getOption('args')}), - new Element('dd', {'html': App.getOption('options')}) - ) - ); - - if(!self.fillVersion(Updater.getInfo())) - Updater.addEvent('loaded', self.fillVersion.bind(self)) - - self.settings.createGroup({ - 'name': 'actions' - }).inject(self.content).adopt( - new Element('div').adopt( - new Element('a.button.red', { - 'text': 'Shutdown', - 'events': { - 'click': App.shutdown.bind(App) - } - }), - new Element('a.button.orange', { - 'text': 'Restart', - 'events': { - 'click': App.restart.bind(App) - } - }) - ) - ); - }, fillVersion: function(json){ diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index c4dede3d..0a15efe6 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -8,6 +8,21 @@ Page.Settings = new Class({ tabs: {}, current: 'about', + has_tab: false, + + initialize: function(options){ + var self = this; + self.parent(options); + + // Add to more menu + if(self.name == 'settings') + App.getBlock('more').addLink(new Element('a', { + 'href': App.createUrl(self.name), + 'text': self.name.capitalize(), + 'title': self.title + }), 'top') + + }, open: function(action, params){ var self = this; @@ -40,8 +55,26 @@ Page.Settings = new Class({ var c = 'active'; var t = self.tabs[tab_name] || self.tabs[self.action] || self.tabs.general; + + // Subtab + var subtab = null + Object.each(self.params, function(param, subtab_name){ + subtab = subtab_name; + }) + + self.el.getElements('li.'+c+' , .tab_content.'+c).each(function(active){ + active.removeClass(c); + }); + + if (t.subtabs[subtab]){ + t.tab[a](c); + t.subtabs[subtab].tab[a](c); + t.subtabs[subtab].content[a](c); + } + else { t.tab[a](c); t.content[a](c); + } return t }, @@ -107,10 +140,20 @@ Page.Settings = new Class({ new Form.Check(self.advanced_toggle); // Add content to tabs + var options = []; Object.each(json.options, function(section, section_name){ + section['section_name'] = section_name; + options.include(section); + }) + + options.sort(function(a, b){ + return (a.order || 100) - (b.order || 100) + }).each(function(section){ + var section_name = section.section_name; // Add groups to content section.groups.sortBy('order').each(function(group){ + if(group.hidden) return; if(self.wizard_only && !group.wizard) return; @@ -118,17 +161,28 @@ Page.Settings = new Class({ // Create tab if(!self.tabs[group.tab] || !self.tabs[group.tab].groups) self.createTab(group.tab, {}); + var content_container = self.tabs[group.tab].content + + // Create subtab + if(group.subtab){ + if (!self.tabs[group.tab].subtabs[group.subtab]) + self.createSubTab(group.subtab, {}, self.tabs[group.tab], group.tab); + var content_container = self.tabs[group.tab].subtabs[group.subtab].content + } // Create the group if(!self.tabs[group.tab].groups[group.name]){ var group_el = self.createGroup(group) - .inject(self.tabs[group.tab].content) + .inject(content_container) .addClass('section_'+section_name); self.tabs[group.tab].groups[group.name] = group_el } // Add options to group - group.options.sortBy('order').each(function(option){ + group.options.sort(function(a, b){ + return (a.order || 100) - (b.order || 100) + }).each(function(option){ + if(option.hidden) return; var class_name = (option.type || 'string').capitalize(); var input = new Option[class_name](section_name, option.name, self.getValue(section_name, option.name), option); input.inject(self.tabs[group.tab].groups[group.name]); @@ -164,6 +218,7 @@ Page.Settings = new Class({ self.tabs[tab_name] = Object.merge(self.tabs[tab_name], { 'tab': tab_el, + 'subtabs': {}, 'content': new Element('div.tab_content.tab_'+tab_name).inject(self.containers), 'groups': {} }) @@ -172,11 +227,43 @@ Page.Settings = new Class({ }, + createSubTab: function(tab_name, tab, parent_tab, parent_tab_name){ + var self = this; + + if(parent_tab.subtabs[tab_name]) + return parent_tab.subtabs[tab_name] + + if(!parent_tab.subtabs_el) + parent_tab.subtabs_el = new Element('ul.subtabs').inject(parent_tab.tab); + + var label = (tab.label || tab.name || tab_name).capitalize() + var tab_el = new Element('li.t_'+tab_name).adopt( + new Element('a', { + 'href': App.createUrl(self.name+'/'+parent_tab_name+'/'+tab_name), + 'text': label + }).adopt() + ).inject(parent_tab.subtabs_el); + + if(!parent_tab.subtabs[tab_name]) + parent_tab.subtabs[tab_name] = { + 'label': label + } + + parent_tab.subtabs[tab_name] = Object.merge(parent_tab.subtabs[tab_name], { + 'tab': tab_el, + 'content': new Element('div.tab_content.tab_'+tab_name).inject(self.containers), + 'groups': {} + }); + + return parent_tab.subtabs[tab_name] + + }, + createGroup: function(group){ var self = this; var group_el = new Element('fieldset', { - 'class': (group.advanced ? 'inlineLabels advanced' : 'inlineLabels') + ' group_' + (group.name || '') + 'class': (group.advanced ? 'inlineLabels advanced' : 'inlineLabels') + ' group_' + (group.name || '') + ' subtab_' + (group.subtab || '') }).adopt( new Element('h2', { 'text': (group.label || group.name).capitalize() diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index a3e56166..808da171 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -175,7 +175,7 @@ body > .spinner, .mask{ } .header .navigation { display: inline-block; - width: 75%; + width: 66.7%; } .header .navigation ul { margin: 0; @@ -196,10 +196,12 @@ body > .spinner, .mask{ } .header .navigation li:first-child a { padding-left: 10px; } .header .navigation li a.logLink { font-size: 13px; padding: 23px 20px 15px; } - .header .navigation li a#showConfig { - background: url('../../media/images/gear.png') no-repeat center; - height: 35px; - width: 10px; + .header .navigation li.tab_settings { + } + .header .navigation li.tab_settings a { + background: url('../images/gear.png') no-repeat center; + width: 35px; + text-indent: -1000px; } .header .navigation li span { @@ -236,6 +238,14 @@ body > .spinner, .mask{ } .header:hover .navigation .backtotop { color: #fff; } + .header .more_menu { + float: right; + margin-top: 20px; + } + + .header .more_menu .red { color: red; } + .header .more_menu .orange { color: orange; } + .header .message.update { text-align: center; position: relative; @@ -436,3 +446,70 @@ body > .spinner, .mask{ margin-top: 20px; background-color: #4c5766; } + + + .more_menu > a { + display: block; + background: url('../images/sprite.png') no-repeat center -137px; + height: 25px; + width: 25px; + border: 1px solid rgba(0,0,0,0.3); + } + + .more_menu ul { + display: none; + border: 1px solid #333; + background: rgba(0,0,0,0.8); + border-radius: 3px; + padding: 4px !important; + position: absolute; + z-index: 9; + margin: 32px 0 0 -145px; + width: 185px; + box-shadow: 0 10px 20px -10px rgba(0,0,0,0.4); + list-style: none; + text-align: center; + } + + .more_menu ul:before { + content: ' '; + height: 0; + position: relative; + width: 0; + border: 6px solid transparent; + border-bottom-color: rgba(0,0,0,0.8); + display: block; + top: -16px; + left: 146px; + } + .more_menu ul.show { + display: block; + } + .more_menu ul li { + width: 100%; + height: auto; + } + + .more_menu ul li a { + display: block; + border-bottom: 1px solid rgba(255,255,255,0.2); + box-shadow: none; + font-weight: normal; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1px; + padding: 3px 0; + color: #fff; + } + + .more_menu ul li:first-child { + margin-top: -12px; + } + + .more_menu ul li:last-child a { + border: none; + color: #fff; + } + .more_menu ul li a:hover { + background: rgba(255,255,255,0.1); + } \ No newline at end of file diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index e1b31f59..40ae4700 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -16,7 +16,7 @@ .page.settings .tabs { float: left; width: 20%; - font-size: 25px; + font-size: 20px; text-align: right; list-style: none; padding: 40px 0; @@ -39,11 +39,41 @@ .page.settings .tabs a { display: block; padding: 11px 15px; - color: #fff; + font-weight: normal; + transition: all 0.1s ease-in-out; + color: rgba(255, 255, 255, 0.8); } - .page.settings .tabs .active a { - background: #4e5969; + .page.settings .tabs a:hover, .page.settings .tabs .active a { + background: rgb(78, 89, 105); + font-weight: bold; + font-size: 25px; + color: #fff; + } + + .page.settings .tabs .subtabs { + list-style: none; + padding: 0; + overflow: hidden; + transition: all 1s ease-in-out; + max-height: 0; } + .page.settings .tabs > .active .subtabs { + max-height: 300px; + } + + .page.settings .tabs .subtabs a { + font-size: 15px; + padding: 1px 15px; + font-weight: normal; + color: rgba(255, 255, 255, 0.8); + background: rgba(78, 89, 105, 0.4); + } + + .page.settings .tabs .subtabs .active a { + font-weight: bold; + color: #fff; + background: rgb(78, 89, 105); + } .page.settings .containers { diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html index 0f6ab5b6..acd49b2a 100644 --- a/couchpotato/templates/_desktop.html +++ b/couchpotato/templates/_desktop.html @@ -29,6 +29,7 @@ + From 58f132546d0847a9b70d57e30c0e060ad3044575 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 4 Mar 2012 23:29:12 +0100 Subject: [PATCH 38/99] Move logs to more menu --- couchpotato/core/plugins/log/static/log.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index 0bbb2682..8f5baf5e 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -2,8 +2,22 @@ Page.Log = new Class({ Extends: PageBase, - name: 'log', + name: 'logs', title: 'Show recent logs.', + has_tab: false, + + initialize: function(options){ + var self = this; + self.parent(options) + + + App.getBlock('more').addLink(new Element('a', { + 'href': App.createUrl(self.name), + 'text': self.name.capitalize(), + 'title': self.title + })) + + }, indexAction: function(){ var self = this; From 18510cbd3875b1050a03916362bab1b431bdb932 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 Mar 2012 07:50:49 +0100 Subject: [PATCH 39/99] More menu styling Logs link fix --- couchpotato/core/plugins/log/static/log.js | 2 +- couchpotato/core/plugins/movie/static/list.js | 6 ++--- couchpotato/static/scripts/block/more.js | 6 ++--- couchpotato/static/style/main.css | 22 ++++++++++--------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index 8f5baf5e..ff772829 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -2,7 +2,7 @@ Page.Log = new Class({ Extends: PageBase, - name: 'logs', + name: 'log', title: 'Show recent logs.', has_tab: false, diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index 76692210..e13fe2ee 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -121,11 +121,11 @@ var MovieList = new Class({ self.navigation_menu_toggle = new Element('a.button.onlay', { 'events': { 'click': function(){ - self.navigation_menu_ul.toggleClass('show') + self.navigation_menu.toggleClass('show') - if(self.navigation_menu_ul.hasClass('show')) + if(self.navigation_menu.hasClass('show')) this.addEvent('outerClick', function(){ - self.navigation_menu_ul.removeClass('show') + self.navigation_menu.removeClass('show') this.removeEvents('outerClick'); }) else diff --git a/couchpotato/static/scripts/block/more.js b/couchpotato/static/scripts/block/more.js index d0926d05..c9cd11b1 100644 --- a/couchpotato/static/scripts/block/more.js +++ b/couchpotato/static/scripts/block/more.js @@ -27,11 +27,11 @@ Block.More = new Class({ new Element('a.button.onlay', { 'events': { 'click': function(){ - self.more_option_ul.toggleClass('show') + self.el.toggleClass('show') - if(self.more_option_ul.hasClass('show')) + if(self.el.hasClass('show')) this.addEvent('outerClick', function(){ - self.more_option_ul.removeClass('show') + self.el.removeClass('show') this.removeEvents('outerClick'); }) else diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 808da171..e5826221 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -195,15 +195,6 @@ body > .spinner, .mask{ padding: 15px; } .header .navigation li:first-child a { padding-left: 10px; } - .header .navigation li a.logLink { font-size: 13px; padding: 23px 20px 15px; } - .header .navigation li.tab_settings { - } - .header .navigation li.tab_settings a { - background: url('../images/gear.png') no-repeat center; - width: 35px; - text-indent: -1000px; - } - .header .navigation li span { display: block; margin-top: 5px; @@ -242,6 +233,13 @@ body > .spinner, .mask{ float: right; margin-top: 20px; } + .header .more_menu ul { + width: 100px; + margin-left: -60px; + } + .header .more_menu ul:before { + margin-left: -84px; + } .header .more_menu .red { color: red; } .header .more_menu .orange { color: orange; } @@ -454,6 +452,10 @@ body > .spinner, .mask{ height: 25px; width: 25px; border: 1px solid rgba(0,0,0,0.3); + transition: all 0.3s ease-in-out; + } + .more_menu.show > a, .more_menu > a:hover { + background-color: #406db8; } .more_menu ul { @@ -482,7 +484,7 @@ body > .spinner, .mask{ top: -16px; left: 146px; } - .more_menu ul.show { + .more_menu.show ul { display: block; } .more_menu ul li { From d8a126cac4dbb9442f7066bc8cc1e53a9888110e Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 Mar 2012 10:17:30 +0100 Subject: [PATCH 40/99] Letterboxd userscript support --- .../core/providers/userscript/letterboxd/__init__.py | 6 ++++++ couchpotato/core/providers/userscript/letterboxd/main.py | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 couchpotato/core/providers/userscript/letterboxd/__init__.py create mode 100644 couchpotato/core/providers/userscript/letterboxd/main.py diff --git a/couchpotato/core/providers/userscript/letterboxd/__init__.py b/couchpotato/core/providers/userscript/letterboxd/__init__.py new file mode 100644 index 00000000..c8c17977 --- /dev/null +++ b/couchpotato/core/providers/userscript/letterboxd/__init__.py @@ -0,0 +1,6 @@ +from .main import Letterboxd + +def start(): + return Letterboxd() + +config = [] diff --git a/couchpotato/core/providers/userscript/letterboxd/main.py b/couchpotato/core/providers/userscript/letterboxd/main.py new file mode 100644 index 00000000..c0d91d79 --- /dev/null +++ b/couchpotato/core/providers/userscript/letterboxd/main.py @@ -0,0 +1,6 @@ +from couchpotato.core.providers.userscript.base import UserscriptBase + + +class Letterboxd(UserscriptBase): + + includes = ['*://letterboxd.com/film/*'] From 2f69a0694c83eeb13f58f22c7443ea2ed4185e64 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 Mar 2012 11:40:26 +0100 Subject: [PATCH 41/99] Proper link to userscript when updating --- couchpotato/core/plugins/userscript/static/userscript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index 0e2d7688..f167739b 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -90,7 +90,7 @@ window.addEvent('load', function(){ if(your_version && your_version < latest_version && checked_already < latest_version){ if(confirm("Update to the latest Userscript?\nYour version: " + your_version + ', new version: ' + latest_version )){ - document.location = Api.getOption('url')+'userscript.get/?couchpotato.user.js'; + document.location = Api.getOption('url')+'userscript.get/couchpotato.user.js'; } Cookie.write(key, latest_version, {duration: 100}); } From 84246a679934c7d32cc9db5b38ccf147f3a87540 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 Mar 2012 18:54:32 +0100 Subject: [PATCH 42/99] Try and get imdb url before sending it to CP --- couchpotato/core/plugins/userscript/main.py | 4 +++- .../core/plugins/userscript/template.js | 24 +++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py index 569f36de..ae3447aa 100644 --- a/couchpotato/core/plugins/userscript/main.py +++ b/couchpotato/core/plugins/userscript/main.py @@ -15,6 +15,8 @@ log = CPLog(__name__) class Userscript(Plugin): + version = 1 + def __init__(self): addApiView('userscript.get/', self.getUserScript, static = True) addApiView('userscript', self.iFrame) @@ -42,7 +44,7 @@ class Userscript(Plugin): versions = fireEvent('userscript.get_provider_version') - version = 0 + version = self.version for v in versions: version += v diff --git a/couchpotato/core/plugins/userscript/template.js b/couchpotato/core/plugins/userscript/template.js index 69cb42e0..e2e9b85e 100644 --- a/couchpotato/core/plugins/userscript/template.js +++ b/couchpotato/core/plugins/userscript/template.js @@ -1,7 +1,7 @@ // ==UserScript== // @name CouchPotato UserScript // @description Add movies like a real CouchPotato -// @version {{version}} +// @version {{version}} // @match {{host}}* {% for include in includes %} @@ -72,8 +72,12 @@ var close_img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8 var osd = function(){ var navbar, newElement; + var createApiUrl = function(url){ + return host + api + "?url=" + escape(url) + }; + var iframe = create('iframe', { - 'src': host + api + "?url=" + escape(document.location.href), + 'src': createApiUrl(document.location.href), 'frameborder': 0, 'scrolling': 'no' }); @@ -85,6 +89,16 @@ var osd = function(){ 'innerHTML': '', 'id': 'add_to', 'onclick': function(){ + + // Try and get imdb url + try { + var regex = new RegExp(/tt(\d+)/); + var imdb_id = document.body.innerHTML.match(regex)[0]; + if (imdb_id) + iframe.setAttribute('src', createApiUrl('http://imdb.com/title/'+imdb_id+'/')) + } + catch(e){} + popup.innerHTML = ''; popup.appendChild(create('a', { 'innerHTML': '', @@ -103,10 +117,10 @@ var osd = function(){ }; var setVersion = function(){ - document.body.setAttribute('data-userscript_version', version) + document.body.setAttribute('data-userscript_version', version) }; if(document.location.href.indexOf(host) == -1) - osd(); + osd(); else - setVersion(); \ No newline at end of file + setVersion(); \ No newline at end of file From 80ecfdba008f2cbb6eed481581b5dd81d5510a15 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 Mar 2012 19:02:53 +0100 Subject: [PATCH 43/99] Show pid and parent_id in about page --- couchpotato/environment.py | 12 ++++++++++++ couchpotato/static/scripts/page/about.js | 2 ++ couchpotato/templates/_desktop.html | 1 + 3 files changed, 15 insertions(+) diff --git a/couchpotato/environment.py b/couchpotato/environment.py index a4d50747..7fe691e8 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -1,6 +1,7 @@ from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.loader import Loader from couchpotato.core.settings import Settings +import os class Env(object): @@ -71,3 +72,14 @@ class Env(object): @staticmethod def addEvent(*args, **kwargs): return addEvent(*args, **kwargs) + + @staticmethod + def getPid(): + try: + try: + parent = os.getppid() + except: + parent = None + return '%d %s' % (os.getpid(), '(%d)' % parent if parent else '') + except: + return 0 diff --git a/couchpotato/static/scripts/page/about.js b/couchpotato/static/scripts/page/about.js index 000500be..05e13329 100644 --- a/couchpotato/static/scripts/page/about.js +++ b/couchpotato/static/scripts/page/about.js @@ -56,6 +56,8 @@ var AboutSettingTab = new Class({ } } }), + new Element('dt[text=ID]'), + new Element('dd', {'text': App.getOption('pid')}), new Element('dt[text=Directories]'), new Element('dd', {'text': App.getOption('app_dir')}), new Element('dd', {'text': App.getOption('data_dir')}), diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html index acd49b2a..306eeb3c 100644 --- a/couchpotato/templates/_desktop.html +++ b/couchpotato/templates/_desktop.html @@ -88,6 +88,7 @@ 'options': "{{ env.get('options')|safe }}", 'app_dir': {{ env.get('app_dir')|tojson|safe }}, 'data_dir': {{ env.get('data_dir')|tojson|safe }}, + 'pid': {{ env.getPid()|tojson|safe }}, 'userscript_version': {{ fireEvent('userscript.get_version', single = True)|tojson|safe }} }); }) From 59373b07935fbd3f1e2961f1c10fa8d5cf26422e Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 Mar 2012 19:52:37 +0100 Subject: [PATCH 44/99] Only show parent id > 1 --- couchpotato/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/environment.py b/couchpotato/environment.py index 7fe691e8..53cb5408 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -80,6 +80,6 @@ class Env(object): parent = os.getppid() except: parent = None - return '%d %s' % (os.getpid(), '(%d)' % parent if parent else '') + return '%d %s' % (os.getpid(), '(%d)' % parent if parent and parent > 1 else '') except: return 0 From 70783d3b603ff7d9c09c95da20467a710bdf6f85 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 5 Mar 2012 21:12:39 +0100 Subject: [PATCH 45/99] Move check for updates to more menu --- couchpotato/static/scripts/couchpotato.js | 16 ++++++++++++++++ couchpotato/static/scripts/page/about.js | 15 +++------------ couchpotato/static/style/main.css | 6 +++--- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index 380d5fe6..5d1c86de 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -67,6 +67,13 @@ var CouchPotato = new Class({ self.block.footer = new Block.Footer(self, {}) ); + self.block.more.addLink(new Element('a', { + 'text': 'Check for updates', + 'events': { + 'click': self.checkForUpdate.bind(self) + } + })) + new ScrollSpy({ min: 10, onLeave: function(){ @@ -147,6 +154,15 @@ var CouchPotato = new Class({ self.checkAvailable(1000); }, + checkForUpdate: function(func){ + var self = this; + + Updater.check(func) + + self.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates'); + self.checkAvailable(3000); + }, + checkAvailable: function(delay){ var self = this; diff --git a/couchpotato/static/scripts/page/about.js b/couchpotato/static/scripts/page/about.js index 05e13329..ad0dd5b9 100644 --- a/couchpotato/static/scripts/page/about.js +++ b/couchpotato/static/scripts/page/about.js @@ -47,7 +47,9 @@ var AboutSettingTab = new Class({ self.version_text = new Element('dd.version', { 'text': 'Getting version...', 'events': { - 'click': self.checkForUpdate.bind(self), + 'click': App.checkForUpdate.bind(App, function(json){ + self.fillVersion(json) + }), 'mouseenter': function(){ this.set('text', 'Check for updates') }, @@ -117,17 +119,6 @@ var AboutSettingTab = new Class({ var self = this; var date = new Date(json.version.date * 1000); self.version_text.set('text', json.version.hash + ' ('+date.toUTCString()+')'); - }, - - checkForUpdate: function(){ - var self = this; - - Updater.check(function(json){ - self.fillVersion(json) - }) - - App.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates'); - App.checkAvailable(3000); } }); diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index e5826221..a46d9c33 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -234,11 +234,11 @@ body > .spinner, .mask{ margin-top: 20px; } .header .more_menu ul { - width: 100px; - margin-left: -60px; + width: 150px; + margin-left: -110px; } .header .more_menu ul:before { - margin-left: -84px; + margin-left: -34px; } .header .more_menu .red { color: red; } From f2b1c6b6889706dada2b0b520acd3aa07dc7b213 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 Mar 2012 07:52:02 +0100 Subject: [PATCH 46/99] Better search result popup --- .../core/plugins/movie/static/search.css | 83 +++++++++++++----- .../core/plugins/movie/static/search.js | 50 +++++++---- couchpotato/static/images/sprite.png | Bin 1921 -> 1970 bytes 3 files changed, 90 insertions(+), 43 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css index 957a3cd4..e0ef4b84 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -14,59 +14,93 @@ margin: 0; font-size: 14px; width: 100%; + height: 24px; } + + .search_form .input .enter { + background: #369545 url('../images/sprite.png') right -188px no-repeat; + padding: 0 20px 0 4px; + border-radius: 2px; + text-transform: uppercase; + font-size: 10px; + margin-left: -78px; + display: inline-block; + opacity: 0; + position: relative; + top: -2px; + cursor: pointer; + vertical-align: middle; + visibility: hidden; + } + .search_form.focused .input .enter { + visibility: visible; + } + .search_form.focused.filled .input .enter { + opacity: 1; + } + .search_form .input a { width: 17px; height: 20px; display: inline-block; - margin: 0 0 -5px -20px; + margin: -2px 0 0 2px; top: 4px; right: 5px; - background: url('../images/sprite.png') right -36px no-repeat; + background: url('../images/sprite.png') left -37px no-repeat; cursor: pointer; + opacity: 0; + transition: all 0.2s ease-in-out; + vertical-align: middle; + } + + .search_form.filled .input a { + opacity: 1; } .search_form .results_container { position: absolute; background: #5c697b; - margin: 6px 0 0 -246px; + margin: 6px 0 0 -230px; width: 470px; min-height: 140px; - border-radius: 3px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - - box-shadow: 0 0 50px rgba(0,0,0,0.55); + box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55); + display: none; } + .search_form.shown.filled .results_container { + display: block; + } + + .search_form .results_container:before { + content: ' '; + height: 0; + position: relative; + width: 0; + border: 10px solid transparent; + border-bottom-color: #5c697b; + display: block; + top: -20px; + left: 346px; + } + .search_form .spinner { background: rgba(0,0,0,0.8) url('../images/spinner.gif') no-repeat center 70px; } - .search_form .pointer { - border-right: 10px solid transparent; - border-left: 10px solid transparent; - border-bottom: 10px solid #5c697b; - display: block; - position: absolute; - width: 0px; - left: 50%; - margin: -9px 0 0 110px; - } - .search_form .results { max-height: 570px; overflow-x: hidden; padding: 10px 0; + margin-top: -18px; } .movie_result { overflow: hidden; - min-height: 140px; + height: 140px; } .movie_result .options { - height: 139px; + height: 140px; border: 1px solid transparent; border-width: 1px 0; border-radius: 0; @@ -107,10 +141,9 @@ padding: 0 15px; width: 470px; position: relative; - min-height: 100px; + height: 140px; top: 0; - margin: -143px 0 0 0; - min-height: 140px; + margin: -140px 0 0 0; background: #5c697b; cursor: pointer; @@ -139,6 +172,8 @@ display: inline-block; vertical-align: top; padding: 15px 0; + height: 120px; + overflow: hidden; } .movie_result .info .tagline { diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js index f2f14c4f..1f7abb04 100644 --- a/couchpotato/core/plugins/movie/static/search.js +++ b/couchpotato/core/plugins/movie/static/search.js @@ -10,12 +10,23 @@ 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', + 'placeholder': 'Search & add a new movie', 'events': { 'keyup': self.keyup.bind(self), - 'focus': self.hideResults.bind(self, false) + 'focus': function(){ + self.el.addClass('focused') + }, + 'blur': function(){ + self.el.removeClass('focused') + } } }), + new Element('span.enter', { + 'events': { + 'click': self.keyup.bind(self) + }, + 'text':'Enter' + }), new Element('a', { 'events': { 'click': self.clear.bind(self) @@ -32,9 +43,8 @@ Block.Search = new Class({ } } }).adopt( - new Element('div.pointer'), self.results = new Element('div.results') - ).hide() + ) ); self.spinner = new Spinner(self.result_container); @@ -50,6 +60,7 @@ Block.Search = new Class({ self.movies = [] self.results.empty() + self.el.removeClass('filled') }, hideResults: function(bool){ @@ -57,7 +68,7 @@ Block.Search = new Class({ if(self.hidden == bool) return; - self.result_container[bool ? 'hide' : 'show'](); + self.el[bool ? 'removeClass' : 'addClass']('shown'); if(bool){ History.removeEvent('change', self.hideResults.bind(self, !bool)); @@ -74,16 +85,14 @@ Block.Search = new Class({ keyup: function(e){ var self = this; - if(['up', 'down'].indexOf(e.key) > -1){ - p('select item') - } - else if(self.q() != self.last_q) { + self.el[self.q() ? 'addClass' : 'removeClass']('filled') + + if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click')) self.autocomplete() - } }, - autocomplete: function(delay){ + autocomplete: function(){ var self = this; if(!self.q()){ @@ -91,10 +100,7 @@ Block.Search = new Class({ return } - self.spinner.show() - - if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer) - self.autocomplete_timer = self.list.delay((delay || 300), self) + self.list() }, list: function(){ @@ -108,6 +114,7 @@ Block.Search = new Class({ self.hideResults(false) if(!cache){ + self.spinner.show() self.api_request = Api.request('movie.search', { 'data': { 'q': q @@ -138,9 +145,15 @@ Block.Search = new Class({ self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m }); - + if(q != self.q()) self.list() + + // Calculate result heights + var w = window.getSize(), + rc = self.result_container.getCoordinates(); + + self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px') }, @@ -293,10 +306,9 @@ Block.Search.Item = new Class({ }) : null, self.info.in_wanted ? new Element('span.in_wanted', { 'text': 'Already in wanted list: ' + self.info.in_wanted.label - }) : null, - self.info.in_library ? new Element('span.in_library', { + }) : (self.info.in_library ? new Element('span.in_library', { 'text': 'Already in library: ' + self.info.in_library.label - }) : null, + }) : null), self.title_select = new Element('select', { 'name': 'title' }), diff --git a/couchpotato/static/images/sprite.png b/couchpotato/static/images/sprite.png index 1d96a56a85c0c57e890cd05e3f33903131a953f7..8a61b25e9c136d0290f9d6d38f88430c1fb70318 100644 GIT binary patch delta 1840 zcmV-02haF{53&!C7Yfk`1^@s6E^vu+ks&AnRFO(xf4g>B_U8_cN^xKAW#`_^emigG z&3iL%7bi&)ix$bSDAMEw!_b(GVcc+w&GD9V&|Qf-bRQ#9ur^)CJiBwS+%@F&>uz+u z2cqd?0mDsZ>qfh4$ZT^Ade2o|$N|}-z|$fH!}Y!WJIyv{&^TaqohUBZ3^EmP465S; z!wr4?fBTFk%b6=pSLe~P+xp&5`b-v5-)jmS{pHvj6wq{3#~gPBs|Rwc&5kLR%1l*a zVsrbATkpu^vL!N^?2y;%!`j<*2exm^seyJb;9-O_qWg5OaQyB=5csUpQ>Wj2ZfWwj z3b{NM>WLsh$Oi^YMH%U+CYGCK=+MTSF!1kM{0MhyU?>Mowd})no!HC;$ZP1s(z>_`tZv zj7@Ti2|geum_o$91VR2kUPCd;RwPe51!hxV6^O==|5nYvD$d42;P}xsb8Od#JHDi%5_fRre;G3w6+C89S418v#QA_jk&Ic0^7}!m4M~-Q zOrUUkHi=ijxEK0(UAN>MBR7u;R}=3ynO6&QW2q z*v`PNnzY!RZI`uI<~KBIXIt&gdL%Pj9hctU@j;B>W{&|jP5pA#!mc~L^(Kphe}vt2 z8*l?E-`4lFNUO)8B+Lse$40dVMK_CO3!^%<*;oXe0yP5HI8dHe>xV*v2Qrj=KTL+B!NL`PAf` zfc%1=*PO4_RKKLoN~X?7%IB9YUtCqEsVOY_Z8P5laH)x&`UrI%KrY%%RnMSYc!1h@ z2YGkf3^1IUQ5iKE1Z(F_%gAuRCdb;8JnfW>0pqo?iB}Vzw3ig8yufk=_1firrc;Hb=_fDcgrYr^f}2(N*9 zK|hHu7yW)luxD>?dIjf9fBG_2A|qJ&e7Wt2K*SDWdE+sSQDZii+2k}0I6U~{;^GcP zWi|oK8yg!N7nRvWVB?uh(J>qIa^jlJNGwG(Zbkwo#Z3w9fd`bZte!8RMAbP;YD#K8 zbZSa5KmwCO-6UU543ZDJy+NUywAx11+BK`K;ef?->wB>v!Tp(re?l(D>gxWcKjR_A zTo6i#&%yqEugNB~Be!AqUNVr(G2;S5$?zDAmGfs1>N3i_^Jm>9=O}?mVB;pSuL;q? zAGir+q=9D$Vz9xW`~>i3;6&h=27{p;ZM9r3*HBkiR|*u-cYLn`rwW3>RpoZOPotaR z@c)P3?=NO%-Jz~Ne;5q@L=c1@>bqz_POz$~s#K%VIKTven?pe4@bECflzYA2;hLJ7 zokD6pe)#ZVjnFF!1dgs{Wo3G$Qn{8B3>bYd`ebWs>sk6nC_DG;*`rgbRCVZz{?m2c z-QBlA?d4FMH3|&W_y(S81Rkbu$ji$^X?rzx?|r8l{YP?Ie`RPp0Og8NV4a6g> zDZHiu*1iq~e>6abNqW70KND=kLeI_39WE~~?+42#N=r+Pg@uI%dJIuxFj7l?e*PV+ z)f#ASZgw6#c+exX(3Oi8E!v-zm8F?GckXBL@$tK0kR8;l1+ufVk3ySwGcz;Kr=_K# z@p?*dp##kXUW(m&kFF>xD$+U}j(|p^=~t`ODO8(he}LzUdJPWR4txsu5Va2x&+JJt zKmwD%rtaB^{W&X>5?C^aP&>h%ojiGR#D}=xePhJhP3Sj!%*2wDCG+vM+p=ZL1m@4A zlZU^rJsFB~v3T566dASgtDNC9=j0%MK|9t}0|XqyHuEhh(;(?vDV`uJ~s`K?%KH9w}9sb7)899T&Rg(>m_o$9!h-yNyoO?wtw^5s0hrA&t3Wh{{I_cURdG5N0>_W8Ax|%2o5S@L zf09KM>NrL#GxnQ*u-aWT`2E|A${;($?Ew3Jk`2NdvlxTX?4Uv+@U$UZCnIDs(mHQ$ z;$zs;jv6x|uc2^zJVY=^jd&3H;d=t2v&p6&gP}MfLAjZEFamShZDo9JTWkV?pMSdN ziypoHn9DV2MXc|>b!5kf+rGq6i9fgse~g(72ag%}ipYaQoDVn@&6tI--w)z8G%g3( zK#}%r60d-9FZA)cZpk@DZyxttO|su)UNPwQo<}=tM%burnhCNRv(>&{aIy0^M}^5^ zI|aLH+G2OMUDRHh+t8?;X|+4+k<4s$TzG%m2Qgtcdn~Xi>X)-z@ij?V6u?AKmT#-2pZ2PPBn z7@qW%t9&6kr11ItgwN+As1OyKx@?&ryCN!LuW_cjcILc!^D8KtP6NkyB}7qv*v_ZQ zW#m;uPtUrw87qyv%aBJ+`IYw0e?Ko@y-r`a*xa%@W9<^y#wWv$y8+4CIyxJL)a0Fj z{DPlXovqbWzogDe#^)pD^GlX4tg6%06c+uqQD_4A)I?8x1fK_xi+1DcY1oAa@Xk9Z zxZ7rdk$6UBcrqx?&YO|Zk$_E(wJCYp2QmhX*T(L>n((CEv^edK2n^J{e{l5Z(X&EN zM|G%8G>N73;^N|JZg-Fj)d>cqFzK5YOG--4F+iw{WMQM5NG>R&yu7>)F)E9NO0nz# zqILLsM$9xK0lPnTCLQ=Cu!qlW3ug9cZYC3m7Fil3s}~BM$~+9b5C5-;w1*?S2I>X# zB&J;S`>C)!dwatxByZA}f2|T7#mX1TZAS$nb`Z-Ok7IT@z7!} z2qna45&yo|RfB!Gi}i;a*W7aC9vzE7L2L%GJDJ!03a~CtF)vPct_{*|}@iE}crHszX=wpQ-Ea z?!F0XFNNZaQDC6PH}F&=@Gx^jUS1wb+pCE??>p7#Kaw*le?!{=C|8UE>+I}o<*JmD zlCptq0_Z=IqcLR2Znrx}y{)dStn6=UYI1lyp1WbGy1H7$24nssFz6Mn)9D65X67?m z)Qj&63JR_>PqkPqI=JKjC!j0-f19*gtt}}j=~uXCr;we+*VfinM)2N=?&#<+R8&-? z2$}|1`x+R~e*hgJ>Gk@(Y_JgvJvTRZxV*f)A1t3LEiE+`78V+qF+`2QNG#48{9cVi6V&cwwbVX57k=EgG1T-2=zgn$M!EK%aWS%SPHF#(%@Co1pcpoC3*^^>` z1SWw!xMwHs=d5f>;K&@|+J)`e@#Duwe25F)H%6@8J^f~nomg^mWImyGn>KA4d3KUk gfCTnXg8d`F0Aq0|FPa6degFUf07*qoM6N<$f{gNj8~^|S From d0aca6116536918bf599f1dfa8cdb16a0cf274fa Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 Mar 2012 07:55:30 +0100 Subject: [PATCH 47/99] Try and stop running daemon first --- CouchPotato.py | 1 + 1 file changed, 1 insertion(+) diff --git a/CouchPotato.py b/CouchPotato.py index 33ad4e06..9a0d9fb7 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -85,6 +85,7 @@ class Loader(object): # remove old pidfile first try: if self.runAsDaemon(): + self.daemon.stop() self.daemon.delpid() except: self.log.critical(traceback.format_exc()) From aa0f7bfb36cf3aecda71a5487afe996e0ee912d1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 Mar 2012 08:02:25 +0100 Subject: [PATCH 48/99] Don't return rotten tomatoes results with imdbapi for now --- couchpotato/core/providers/movie/imdbapi/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/providers/movie/imdbapi/main.py b/couchpotato/core/providers/movie/imdbapi/main.py index f238e7c4..ea41f548 100644 --- a/couchpotato/core/providers/movie/imdbapi/main.py +++ b/couchpotato/core/providers/movie/imdbapi/main.py @@ -14,7 +14,7 @@ class IMDBAPI(MovieProvider): urls = { 'search': 'http://www.imdbapi.com/?%s', - 'info': 'http://www.imdbapi.com/?i=%s&tomatoes=true', + 'info': 'http://www.imdbapi.com/?i=%s', } http_time_between_calls = 0 From d2fd098030e605c6672f23053b70ae2c3128df6d Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 Mar 2012 16:15:47 +0100 Subject: [PATCH 49/99] Notify my Windows Phone support Thanks to https://github.com/yngvebn --- .../core/notifications/notifymywp/__init__.py | 43 ++++++ .../core/notifications/notifymywp/main.py | 24 ++++ libs/pynmwp/__init__.py | 134 ++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 couchpotato/core/notifications/notifymywp/__init__.py create mode 100644 couchpotato/core/notifications/notifymywp/main.py create mode 100644 libs/pynmwp/__init__.py diff --git a/couchpotato/core/notifications/notifymywp/__init__.py b/couchpotato/core/notifications/notifymywp/__init__.py new file mode 100644 index 00000000..76228e6a --- /dev/null +++ b/couchpotato/core/notifications/notifymywp/__init__.py @@ -0,0 +1,43 @@ +from .main import NotifyMyWP + +def start(): + return NotifyMyWP() + +config = [{ + 'name': 'notifymywp', + 'groups': [ + { + 'tab': 'notifications', + 'name': 'notifymywp', + 'label': 'Notify My Windows Phone', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'api_key', + 'description': 'Multiple keys seperated by a comma. Maximum of 5.' + }, + { + 'name': 'dev_key', + 'advanced': True, + }, + { + 'name': 'priority', + 'default': 0, + 'type': 'dropdown', + 'values': [('Very Low', -2), ('Moderate', -1), ('Normal', 0), ('High', 1), ('Emergency', 2)], + }, + { + 'name': 'on_snatch', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/notifymywp/main.py b/couchpotato/core/notifications/notifymywp/main.py new file mode 100644 index 00000000..4445af66 --- /dev/null +++ b/couchpotato/core/notifications/notifymywp/main.py @@ -0,0 +1,24 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification +from pynmwp import PyNMWP + +log = CPLog(__name__) + + +class NotifyMyWP(Notification): + + def notify(self, message = '', data = {}): + + if self.isDisabled(): + return + + keys = self.conf('api_key').split(',') + p = PyNMWP(keys, self.conf('dev_key')) + + response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1) + + for key in keys: + if not response[key]['Code'] == u'200': + log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s' % (key, response[key]['message'])) + + return response diff --git a/libs/pynmwp/__init__.py b/libs/pynmwp/__init__.py new file mode 100644 index 00000000..de724b9d --- /dev/null +++ b/libs/pynmwp/__init__.py @@ -0,0 +1,134 @@ +from xml.dom.minidom import parseString +from httplib import HTTPSConnection +from urllib import urlencode + +__version__ = "0.1" + +API_SERVER = 'notifymywindowsphone.com' +ADD_PATH = '/publicapi/notify' + +USER_AGENT = "PyNMWP/v%s" % __version__ + +def uniq_preserve(seq): # Dave Kirby + # Order preserving + seen = set() + return [x for x in seq if x not in seen and not seen.add(x)] + +def uniq(seq): + # Not order preserving + return {}.fromkeys(seq).keys() + +class PyNMWP(object): + """PyNMWP(apikey=[], developerkey=None) +takes 2 optional arguments: + - (opt) apykey: might me a string containing 1 key or an array of keys + - (opt) developerkey: where you can store your developer key +""" + + def __init__(self, apikey = [], developerkey = None): + self._developerkey = None + self.developerkey(developerkey) + if apikey: + if type(apikey) == str: + apikey = [apikey] + self._apikey = uniq(apikey) + + def addkey(self, key): + "Add a key (register ?)" + if type(key) == str: + if not key in self._apikey: + self._apikey.append(key) + elif type(key) == list: + for k in key: + if not k in self._apikey: + self._apikey.append(k) + + def delkey(self, key): + "Removes a key (unregister ?)" + if type(key) == str: + if key in self._apikey: + self._apikey.remove(key) + elif type(key) == list: + for k in key: + if key in self._apikey: + self._apikey.remove(k) + + def developerkey(self, developerkey): + "Sets the developer key (and check it has the good length)" + if type(developerkey) == str and len(developerkey) == 48: + self._developerkey = developerkey + + def push(self, application = "", event = "", description = "", url = "", priority = 0, batch_mode = False): + """Pushes a message on the registered API keys. +takes 5 arguments: + - (req) application: application name [256] + - (req) event: event name [1000] + - (req) description: description [10000] + - (opt) url: url [512] + - (opt) priority: from -2 (lowest) to 2 (highest) (def:0) + - (opt) batch_mode: call API 5 by 5 (def:False) + +Warning: using batch_mode will return error only if all API keys are bad + cf: http://nma.usk.bz/api.php +""" + datas = { + 'application': application[:256].encode('utf8'), + 'event': event[:1024].encode('utf8'), + 'description': description[:10000].encode('utf8'), + 'priority': priority + } + + if url: + datas['url'] = url[:512] + + if self._developerkey: + datas['developerkey'] = self._developerkey + + results = {} + + if not batch_mode: + for key in self._apikey: + datas['apikey'] = key + res = self.callapi('POST', ADD_PATH, datas) + results[key] = res + else: + for i in range(0, len(self._apikey), 5): + datas['apikey'] = ",".join(self._apikey[i:i + 5]) + res = self.callapi('POST', ADD_PATH, datas) + results[datas['apikey']] = res + return results + + def callapi(self, method, path, args): + headers = { 'User-Agent': USER_AGENT } + if method == "POST": + headers['Content-type'] = "application/x-www-form-urlencoded" + http_handler = HTTPSConnection(API_SERVER) + http_handler.request(method, path, urlencode(args), headers) + resp = http_handler.getresponse() + + try: + res = self._parse_reponse(resp.read()) + except Exception, e: + res = {'type': "pynmwperror", + 'code': 600, + 'message': str(e) + } + pass + + return res + + def _parse_reponse(self, response): + root = parseString(response).firstChild + for elem in root.childNodes: + if elem.nodeType == elem.TEXT_NODE: continue + if elem.tagName == 'success': + res = dict(elem.attributes.items()) + res['message'] = "" + res['type'] = elem.tagName + return res + if elem.tagName == 'error': + res = dict(elem.attributes.items()) + res['message'] = elem.firstChild.nodeValue + res['type'] = elem.tagName + return res + From d03d1565f3e7a86e784e53dc8ec9a748ce737c88 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 Mar 2012 17:59:25 +0100 Subject: [PATCH 50/99] Styling fix for numberlist --- couchpotato/core/plugins/movie/static/movie.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 4b414aed..1df14ef0 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -348,7 +348,8 @@ text-shadow: none; } .movies .alph_nav .numbers li:first-child { - width: 44px; + width: 43px; + margin-left: 7px; } .movies .alph_nav li.available { color: rgba(255,255,255,0.8); From 0ce183968df53768835bb342be06b64c9e3bfc38 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 Mar 2012 18:04:22 +0100 Subject: [PATCH 51/99] styling, element alignment --- couchpotato/core/plugins/movie/static/movie.css | 2 +- couchpotato/static/style/main.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 1df14ef0..aad77aaf 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -43,7 +43,7 @@ } .movies .list_view .data, .movies .mass_edit_view .data { height: 30px; - padding: 3px 10px; + padding: 3px 0 3px 10px; width: 938px; box-shadow: none; border: 0; diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index a46d9c33..21886ecf 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -454,7 +454,7 @@ body > .spinner, .mask{ border: 1px solid rgba(0,0,0,0.3); transition: all 0.3s ease-in-out; } - .more_menu.show > a, .more_menu > a:hover { + .more_menu.show > a:not(:active), .more_menu > a:hover:not(:active) { background-color: #406db8; } From 6e8a1950cdefe67ce60309f129e8900ecb16461a Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 Mar 2012 19:14:52 +0100 Subject: [PATCH 52/99] Spinner overlay color --- couchpotato/core/plugins/movie/static/search.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css index e0ef4b84..54c9bbcd 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -84,7 +84,7 @@ } .search_form .spinner { - background: rgba(0,0,0,0.8) url('../images/spinner.gif') no-repeat center 70px; + background: rgba(92,105,123,0.7) url('../images/spinner.gif') no-repeat 345px 10px; } .search_form .results { From af46827eb27952dc6cf71d5f53a3db8acacf75a1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 6 Mar 2012 23:37:55 +0100 Subject: [PATCH 53/99] Better score calculating --- couchpotato/core/plugins/score/main.py | 8 +++++- couchpotato/core/plugins/score/scores.py | 25 +++++++++++++++++-- couchpotato/core/providers/base.py | 2 +- .../core/providers/nzb/nzbclub/main.py | 8 +++--- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py index ee7c9806..49dbba41 100644 --- a/couchpotato/core/plugins/score/main.py +++ b/couchpotato/core/plugins/score/main.py @@ -3,7 +3,7 @@ from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \ - sizeScore + sizeScore, providerScore, duplicateScore log = CPLog(__name__) @@ -31,4 +31,10 @@ class Score(Plugin): except: pass + # Provider score + score += providerScore(nzb['provider']) + + # Duplicates in name + score += duplicateScore(nzb['name'], movie['library']['titles'][0]['title']) + return score diff --git a/couchpotato/core/plugins/score/scores.py b/couchpotato/core/plugins/score/scores.py index ccf993ec..1a2f2b88 100644 --- a/couchpotato/core/plugins/score/scores.py +++ b/couchpotato/core/plugins/score/scores.py @@ -27,14 +27,14 @@ def nameScore(name, year): score = 0 name = name.lower() - #give points for the cool stuff + # give points for the cool stuff for value in name_scores: v = value.split(':') add = int(v.pop()) if v.pop() in name: score = score + add - #points if the year is correct + # points if the year is correct if str(year) in name: score = score + 5 @@ -58,3 +58,24 @@ def nameRatioScore(nzb_name, movie_name): def sizeScore(size): return 0 if size else -20 + + +def providerScore(provider): + if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']: + return 30 + + if provider in ['Newznab', 'Moovee', 'X264']: + return 10 + + return 0 + + +def duplicateScore(nzb_name, movie_name): + + nzb_words = re.split('\W+', simplifyString(nzb_name)) + movie_words = re.split('\W+', simplifyString(movie_name)) + + # minus for duplicates + duplicates = [x for i, x in enumerate(nzb_words) if nzb_words[i:].count(x) > 1] + + return len(list(set(duplicates) - set(movie_words))) * -4 diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 148b2e7c..0d00179b 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -102,4 +102,4 @@ class YarrProvider(Provider): return [self.cat_backup_id] def found(self, new): - log.info('Found: score(%(score)s): %(name)s' % new) + log.info('Found: score(%(score)s) on %(provider)s: %(name)s' % new) diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py index 78ef5d70..07ae2c2d 100644 --- a/couchpotato/core/providers/nzb/nzbclub/main.py +++ b/couchpotato/core/providers/nzb/nzbclub/main.py @@ -59,9 +59,7 @@ class NZBClub(NZBProvider, RSS): size = enclosure['length'] date = self.getTextElement(nzb, "pubDate") - description = '' - if 'nfo files' in self.getTextElement(nzb, "description"): - description = toUnicode(self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), timeout = 25920000)) + description = toUnicode(self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), timeout = 25920000)) new = { 'id': nzbclub_id, @@ -77,6 +75,10 @@ class NZBClub(NZBProvider, RSS): } new['score'] = fireEvent('score.calculate', new, movie, single = True) + if 'ARCHIVE inside ARCHIVE' in description: + log.info('Wrong: Seems to be passworded files: %s' % new['name']) + continue + is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, imdb_results = False, single_category = False, single = True) From e2675bd28c087f4d2effab07f52e981d58074d37 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 Mar 2012 00:50:24 +0100 Subject: [PATCH 54/99] Remove getppid script --- couchpotato/core/_base/_core/getppid.py | 45 ------------------------- couchpotato/core/_base/_core/main.py | 5 +-- 2 files changed, 1 insertion(+), 49 deletions(-) delete mode 100644 couchpotato/core/_base/_core/getppid.py diff --git a/couchpotato/core/_base/_core/getppid.py b/couchpotato/core/_base/_core/getppid.py deleted file mode 100644 index 6854e291..00000000 --- a/couchpotato/core/_base/_core/getppid.py +++ /dev/null @@ -1,45 +0,0 @@ -from ctypes import * -from ctypes.wintypes import * -import win32process - - -class PROCESSENTRY32(Structure): - _fields_ = ( - ('dwSize', DWORD,), - ('cntUsage', DWORD,), - ('th32ProcessID', DWORD,), - ('th32DefaultHeapID', POINTER(ULONG),), - ('th32ModuleID', DWORD,), - ('cntThreads', DWORD,), - ('th32ParentProcessID', DWORD,), - ('pcPriClassBase', LONG,), - ('dwFlags', DWORD,), - ('szExeFile', c_char * MAX_PATH,), - ) - - -def getppid(pid): - """the Windows version of os.getppid""" - pe = PROCESSENTRY32() - pe.dwSize = sizeof(PROCESSENTRY32) - - snapshot = windll.kernel32.CreateToolhelp32Snapshot(2, 0) - try: - if not windll.kernel32.Process32First(snapshot, byref(pe)): - raise WindowsError - while pe.th32ProcessID != pid: - if not windll.kernel32.Process32Next(snapshot, byref(pe)): - raise WindowsError - result = pe.th32ParentProcessID - finally: - windll.kernel32.CloseHandle(snapshot) - - if result not in win32process.EnumProcesses(): - result = 1 - - return result - - -import os -if not hasattr(os, 'getppid'): - os.getppid = getppid diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py index f1846cd3..d334553d 100644 --- a/couchpotato/core/_base/_core/main.py +++ b/couchpotato/core/_base/_core/main.py @@ -12,12 +12,9 @@ import time import traceback import webbrowser -if os.name == 'nt': - import getppid - - log = CPLog(__name__) + class Core(Plugin): ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown'] From 9df218d0a89704a0926a892b487f8202d836f5fc Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 8 Mar 2012 00:51:12 +0100 Subject: [PATCH 55/99] Enable octal and decimal permission settings --- couchpotato/core/_base/_core/__init__.py | 6 +++--- couchpotato/environment.py | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py index ff2bd07d..d90fa59b 100644 --- a/couchpotato/core/_base/_core/__init__.py +++ b/couchpotato/core/_base/_core/__init__.py @@ -81,13 +81,13 @@ config = [{ }, { 'name': 'permission_folder', - 'default': 0755, + 'default': '0755', 'label': 'Folder CHMOD', - 'description': 'Permission (decimal) for creating/copying folders. 0755 => 593, 0777 => 511', + 'description': 'Can be either decimal (493) or octal (leading zero: 0755)', }, { 'name': 'permission_file', - 'default': 0755, + 'default': '0755', 'label': 'File CHMOD', 'description': 'Same as Folder CHMOD but for files', }, diff --git a/couchpotato/environment.py b/couchpotato/environment.py index 53cb5408..1ac62b6a 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -62,8 +62,12 @@ class Env(object): return s @staticmethod - def getPermission(type): - return int(Env.get('settings').get('permission_%s' % type, default = 0777)) + def getPermission(setting_type): + perm = Env.get('settings').get('permission_%s' % setting_type, default = '0777') + if perm[0] == '0': + return oct(int(perm, 8)) + else: + return oct(int(perm)) @staticmethod def fireEvent(*args, **kwargs): From e7be68bf1035baf630aa7532284fde6f426aa3d6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 9 Mar 2012 12:38:59 +0100 Subject: [PATCH 56/99] Return int for permission settings --- couchpotato/environment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/environment.py b/couchpotato/environment.py index 1ac62b6a..ac256c18 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -65,9 +65,9 @@ class Env(object): def getPermission(setting_type): perm = Env.get('settings').get('permission_%s' % setting_type, default = '0777') if perm[0] == '0': - return oct(int(perm, 8)) + return int(perm, 8) else: - return oct(int(perm)) + return int(perm) @staticmethod def fireEvent(*args, **kwargs): From bfbd3b26a918da66401a40fcbe78d681e9da1246 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 9 Mar 2012 12:40:36 +0100 Subject: [PATCH 57/99] Don't show xbmc error message when not available --- couchpotato/core/notifications/xbmc/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index 4fe0a45c..2651caec 100644 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -30,7 +30,7 @@ class XBMC(Notification): } try: - self.urlopen(url, headers = headers) + self.urlopen(url, headers = headers, show_error = False) except: log.error("Couldn't sent command to XBMC") return False From 088dc5386c9351a044b5150c6bff1674bd7ffb14 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 10 Mar 2012 17:07:19 +0100 Subject: [PATCH 58/99] Better shutdown of process --- CouchPotato.py | 3 ++- couchpotato/core/_base/_core/main.py | 7 +++++++ couchpotato/runner.py | 24 +++++++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CouchPotato.py b/CouchPotato.py index 9a0d9fb7..3bc7916f 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -85,7 +85,8 @@ class Loader(object): # remove old pidfile first try: if self.runAsDaemon(): - self.daemon.stop() + try: self.daemon.stop() + except: pass self.daemon.delpid() except: self.log.critical(traceback.format_exc()) diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py index d334553d..b22f9ba5 100644 --- a/couchpotato/core/_base/_core/main.py +++ b/couchpotato/core/_base/_core/main.py @@ -56,6 +56,9 @@ class Core(Plugin): }) def crappyShutdown(self): + if self.shutdown_started: + return + try: self.urlopen('%s/app.shutdown' % self.createApiUrl(), show_error = False) return True @@ -64,6 +67,9 @@ class Core(Plugin): return False def crappyRestart(self): + if self.shutdown_started: + return + try: self.urlopen('%s/app.restart' % self.createApiUrl(), show_error = False) return True @@ -82,6 +88,7 @@ class Core(Plugin): def initShutdown(self, restart = False): if self.shutdown_started: log.info('Already shutting down') + return log.info('Shutting down' if not restart else 'Restarting') diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 11236c16..217f52ba 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -186,4 +186,26 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En if fire_load: fireEventAsync('app.load') # Go go go! - app.run(**config) + try_restart = True + restart_tries = 5 + while try_restart: + try: + app.run(**config) + except Exception, e: + try: + nr, msg = e + if nr == 48: + log.info('Already in use, try %s more time after few seconds' % restart_tries) + time.sleep(1) + restart_tries -= 1 + + if restart_tries > 0: + continue + else: + return + except: + pass + + raise + + try_restart = False From b3ba7328542b3ddad5d296849c971717187d1f4f Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 10 Mar 2012 17:35:19 +0100 Subject: [PATCH 59/99] Don't ask password on userscript iframe --- couchpotato/core/plugins/userscript/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py index ae3447aa..98c959bb 100644 --- a/couchpotato/core/plugins/userscript/main.py +++ b/couchpotato/core/plugins/userscript/main.py @@ -8,6 +8,7 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env from flask.globals import request from flask.helpers import url_for +from flask.templating import render_template import os log = CPLog(__name__) @@ -51,7 +52,7 @@ class Userscript(Plugin): return version def iFrame(self): - return index() + return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) def getViaUrl(self): From 3f08238adb7e893ead093a040bc8174f430ac420 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 10 Mar 2012 23:12:34 +0100 Subject: [PATCH 60/99] Don't show scheduler warnings --- couchpotato/core/_base/scheduler/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/_base/scheduler/main.py b/couchpotato/core/_base/scheduler/main.py index 896e1cae..fb3b01ee 100644 --- a/couchpotato/core/_base/scheduler/main.py +++ b/couchpotato/core/_base/scheduler/main.py @@ -15,7 +15,7 @@ class Scheduler(Plugin): def __init__(self): - logging.getLogger('apscheduler').setLevel(logging.WARNING) + logging.getLogger('apscheduler').setLevel(logging.ERROR) addEvent('schedule.cron', self.cron) addEvent('schedule.interval', self.interval) From 3f84f2226249592e21c61b844cae300ee81bf145 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 10 Mar 2012 23:13:06 +0100 Subject: [PATCH 61/99] Don't return imdb empty poster --- couchpotato/core/providers/movie/imdbapi/main.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/providers/movie/imdbapi/main.py b/couchpotato/core/providers/movie/imdbapi/main.py index ea41f548..1c817274 100644 --- a/couchpotato/core/providers/movie/imdbapi/main.py +++ b/couchpotato/core/providers/movie/imdbapi/main.py @@ -32,8 +32,11 @@ class IMDBAPI(MovieProvider): if cached: result = self.parseMovie(cached) - log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') - return [result] + if result.get('titles') and len(result.get('titles')) > 0: + log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') + return [result] + + return [] return [] @@ -61,7 +64,7 @@ class IMDBAPI(MovieProvider): 'titles': [movie.get('Title', '')], 'original_title': movie.get('Title', ''), 'images': { - 'poster': [movie.get('Poster', '')], + 'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [], }, 'rating': { 'imdb': (tryFloat(movie.get('Rating', 0)), tryInt(movie.get('Votes', ''))), From 9a88cc8e0bf90f95ceb95fd80af6fc0bd7621fb4 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 10 Mar 2012 23:19:51 +0100 Subject: [PATCH 62/99] Prio tmdb searches before imdbapi --- couchpotato/core/providers/movie/themoviedb/main.py | 4 ++-- couchpotato/core/providers/userscript/tmdb/main.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/movie/themoviedb/main.py index f2c14e12..2838c4ce 100644 --- a/couchpotato/core/providers/movie/themoviedb/main.py +++ b/couchpotato/core/providers/movie/themoviedb/main.py @@ -11,8 +11,8 @@ class TheMovieDb(MovieProvider): def __init__(self): addEvent('movie.by_hash', self.byHash) - addEvent('movie.search', self.search) - addEvent('movie.info', self.getInfo) + addEvent('movie.search', self.search, priority = 1) + addEvent('movie.info', self.getInfo, priority = 1) addEvent('movie.info_by_tmdb', self.getInfoByTMDBId) # Use base wrapper diff --git a/couchpotato/core/providers/userscript/tmdb/main.py b/couchpotato/core/providers/userscript/tmdb/main.py index d58d8197..6205851e 100644 --- a/couchpotato/core/providers/userscript/tmdb/main.py +++ b/couchpotato/core/providers/userscript/tmdb/main.py @@ -10,4 +10,7 @@ class TMDB(UserscriptBase): def getMovie(self, url): match = re.search('(?P\d+)', url) movie = fireEvent('movie.info_by_tmdb', id = match.group('id'), merge = True) - return self.getInfo(movie['imdb']) + + if movie['imdb']: + return self.getInfo(movie['imdb']) + From 93eecc614c4bf1905dd224252f7aca8ff2b7322c Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 10 Mar 2012 23:21:00 +0100 Subject: [PATCH 63/99] Use spin.js for loaders --- .../core/plugins/movie/static/movie.css | 2 +- .../core/plugins/movie/static/search.css | 8 +- .../core/plugins/movie/static/search.js | 34 +- couchpotato/static/images/spinner.gif | Bin 673 -> 0 bytes couchpotato/static/scripts/couchpotato.js | 44 +- .../static/scripts/library/mootools_more.js | 1501 +++++------------ couchpotato/static/scripts/library/spin.js | 301 ++++ couchpotato/static/style/main.css | 15 +- couchpotato/templates/_desktop.html | 1 + 9 files changed, 785 insertions(+), 1121 deletions(-) delete mode 100644 couchpotato/static/images/spinner.gif create mode 100644 couchpotato/static/scripts/library/spin.js diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index aad77aaf..cdef5f22 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -88,7 +88,7 @@ font-weight: bold; margin-bottom: 10px; float: left; - width: 50%; + width: 90%; transition: all 0.2s linear; } .movies .list_view .info .title, .movies .mass_edit_view .info .title { diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css index 54c9bbcd..9f4866b4 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -82,10 +82,6 @@ top: -20px; left: 346px; } - - .search_form .spinner { - background: rgba(92,105,123,0.7) url('../images/spinner.gif') no-repeat 345px 10px; - } .search_form .results { max-height: 570px; @@ -197,3 +193,7 @@ .movie_result .info h2 span:before { content: "("; } .movie_result .info h2 span:after { content: ")"; } + +.search_form .mask { + border-radius: 3px; +} \ No newline at end of file diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js index 1f7abb04..63d55dd3 100644 --- a/couchpotato/core/plugins/movie/static/search.js +++ b/couchpotato/core/plugins/movie/static/search.js @@ -47,7 +47,7 @@ Block.Search = new Class({ ) ); - self.spinner = new Spinner(self.result_container); + self.mask = new Element('div.mask').inject(self.result_container).fade('hide'); }, @@ -111,10 +111,14 @@ Block.Search = new Class({ var q = self.q(); var cache = self.cache[q]; - self.hideResults(false) + self.hideResults(false); if(!cache){ - self.spinner.show() + self.positionMask().fade('in'); + + if(!self.spinner) + self.spinner = createSpinner(self.mask); + self.api_request = Api.request('movie.search', { 'data': { 'q': q @@ -132,7 +136,7 @@ Block.Search = new Class({ fill: function(q, json){ var self = this; - self.spinner.hide(); + self.positionMask() self.cache[q] = json self.movies = {} @@ -148,13 +152,27 @@ Block.Search = new Class({ if(q != self.q()) self.list() - + // Calculate result heights var w = window.getSize(), rc = self.result_container.getCoordinates(); - - self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px') + self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px') + self.mask.hide() + + }, + + positionMask: function(){ + var self = this; + + var s = self.result_container.getSize() + + return self.mask.setStyles({ + 'width': s.x, + 'height': s.y + }).position({ + 'relativeTo': self.result_container + }) }, loading: function(bool){ @@ -273,8 +291,6 @@ Block.Search.Item = new Class({ 'title': self.title_select.get('value'), 'profile_id': self.profile_select.get('value') }, - 'useSpinner': true, - 'spinnerTarget': self.options, 'onComplete': function(){ self.options.empty(); self.options.adopt( diff --git a/couchpotato/static/images/spinner.gif b/couchpotato/static/images/spinner.gif deleted file mode 100644 index d0bce1542342e912da81a2c260562df172f30d73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 673 zcmZ?wbhEHb6krfw_{6~Q|Nnmm28Kh24mmkF0U1e2Nli^nlO|14{Lk&@8WQa67~pE8 zXTZz|lvDgC+Z`3#dv5h=E26FfcG1 zbL_hF&)}42ws10s6^G;;cE1^EoUR)U5A70}d2pLv!jVIT7j&Z~EblI3x0K*v_sV|m z0kj3v921Z^em#l`(k(o@H$3ZdDRc@9NidXDNbqrumReCGv$gd8+e8WW28HVqkJ_9i zH>s*<31KtHjANIPvi2#*6BEu%3Dak5O_t&NBI)H?V$TxT}#l{vOTn5naXTfF^&~Hhq+NX@#Ccc>y7T?;vjI&jdhsDsPJyAw*m0Qz>i}K7# zL9w50Ng{fT}A5JUe8lRK1h7_Y2;BWJDd=c6f&i?Wv5(5q?6|P zQw{>maxZP<537OA37Uk}7@%_$4o$EWe_Zl>&#id|lE-BpDC#+Fn|msJ%_2h{Hg1vP z#N8WAzfWasG}yq|xqE)DrWaOofX=z|?*pgc%{ig5vl!pqDlC|q&~Z0$&Rvsft&VO- z4MZj+%-+Vx%W}v;V76hyp=;+R;x+~t^Q%*xuFTQAF2})fSfTHDAs>sO!OBw`)&)o$ c0!CNZt))x~rAZP^^P&YOFfdqy5)K#u0POD40{{R3 diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index 5d1c86de..c0f6a6a0 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -185,21 +185,24 @@ var CouchPotato = new Class({ blockPage: function(message, title){ var self = this; - if(!self.mask){ - var body = $(document.body); - self.mask = new Spinner(document.body, { - 'message': new Element('div').adopt( - new Element('h1', {'text': title || 'Unavailable'}), - new Element('div', {'text': message || 'Something must have crashed.. check the logs ;)'}) - ) - }); - } - self.mask.show(); + var body = $(document.body); + self.mask = new Element('div.mask').adopt( + new Element('div').adopt( + new Element('h1', {'text': title || 'Unavailable'}), + new Element('div', {'text': message || 'Something must have crashed.. check the logs ;)'}) + ) + ).fade('hide').inject(document.body).fade('in'); + + createSpinner(self.mask, { + 'top': -50 + }); }, unBlockPage: function(){ var self = this; - self.mask.hide(); + self.mask.get('tween').start('opacity', 0).chain(function(){ + this.element.destroy() + }); }, createUrl: function(action, params){ @@ -363,3 +366,22 @@ function randomString(length, extra) { })(); +var createSpinner = function(target, options){ + var opts = Object.merge({ + lines: 12, + length: 5, + width: 4, + radius: 9, + color: '#fff', + speed: 1.9, + trail: 53, + shadow: false, + hwaccel: true, + className: 'spinner', + zIndex: 2e9, + top: 'auto', + left: 'auto' + }, options); + + return new Spinner(opts).spin(target); +} \ No newline at end of file diff --git a/couchpotato/static/scripts/library/mootools_more.js b/couchpotato/static/scripts/library/mootools_more.js index e62145e0..4ad1700f 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/710fc92ae4753344d23cbd5fc7e44420 -// Or build this file again with packager using: packager build More/Events.Pseudos More/Element.Forms More/Element.Shortcuts More/Fx.Scroll 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/d7fedf16aa88d2a757ac66071dd56b3a +// Or build this file again with packager using: packager build More/Events.Pseudos More/Element.Forms More/Element.Position More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical /* --- @@ -487,6 +487,412 @@ Element.implement({ }); +/* +--- + +script: Element.Measure.js + +name: Element.Measure + +description: Extends the Element native object to include methods useful in measuring dimensions. + +credits: "Element.measure / .expose methods by Daniel Steigerwald License: MIT-style license. Copyright: Copyright (c) 2008 Daniel Steigerwald, daniel.steigerwald.cz" + +license: MIT-style license + +authors: + - Aaron Newton + +requires: + - Core/Element.Style + - Core/Element.Dimensions + - /MooTools.More + +provides: [Element.Measure] + +... +*/ + +(function(){ + +var getStylesList = function(styles, planes){ + var list = []; + Object.each(planes, function(directions){ + Object.each(directions, function(edge){ + styles.each(function(style){ + list.push(style + '-' + edge + (style == 'border' ? '-width' : '')); + }); + }); + }); + return list; +}; + +var calculateEdgeSize = function(edge, styles){ + var total = 0; + Object.each(styles, function(value, style){ + if (style.test(edge)) total = total + value.toInt(); + }); + return total; +}; + +var isVisible = function(el){ + return !!(!el || el.offsetHeight || el.offsetWidth); +}; + + +Element.implement({ + + measure: function(fn){ + if (isVisible(this)) return fn.call(this); + var parent = this.getParent(), + toMeasure = []; + while (!isVisible(parent) && parent != document.body){ + toMeasure.push(parent.expose()); + parent = parent.getParent(); + } + var restore = this.expose(), + result = fn.call(this); + restore(); + toMeasure.each(function(restore){ + restore(); + }); + return result; + }, + + expose: function(){ + if (this.getStyle('display') != 'none') return function(){}; + var before = this.style.cssText; + this.setStyles({ + display: 'block', + position: 'absolute', + visibility: 'hidden' + }); + return function(){ + this.style.cssText = before; + }.bind(this); + }, + + getDimensions: function(options){ + options = Object.merge({computeSize: false}, options); + var dim = {x: 0, y: 0}; + + var getSize = function(el, options){ + return (options.computeSize) ? el.getComputedSize(options) : el.getSize(); + }; + + var parent = this.getParent('body'); + + if (parent && this.getStyle('display') == 'none'){ + dim = this.measure(function(){ + return getSize(this, options); + }); + } else if (parent){ + try { //safari sometimes crashes here, so catch it + dim = getSize(this, options); + }catch(e){} + } + + return Object.append(dim, (dim.x || dim.x === 0) ? { + width: dim.x, + height: dim.y + } : { + x: dim.width, + y: dim.height + } + ); + }, + + getComputedSize: function(options){ + + + options = Object.merge({ + styles: ['padding','border'], + planes: { + height: ['top','bottom'], + width: ['left','right'] + }, + mode: 'both' + }, options); + + var styles = {}, + size = {width: 0, height: 0}, + dimensions; + + if (options.mode == 'vertical'){ + delete size.width; + delete options.planes.width; + } else if (options.mode == 'horizontal'){ + delete size.height; + delete options.planes.height; + } + + getStylesList(options.styles, options.planes).each(function(style){ + styles[style] = this.getStyle(style).toInt(); + }, this); + + Object.each(options.planes, function(edges, plane){ + + var capitalized = plane.capitalize(), + style = this.getStyle(plane); + + if (style == 'auto' && !dimensions) dimensions = this.getDimensions(); + + style = styles[plane] = (style == 'auto') ? dimensions[plane] : style.toInt(); + size['total' + capitalized] = style; + + edges.each(function(edge){ + var edgesize = calculateEdgeSize(edge, styles); + size['computed' + edge.capitalize()] = edgesize; + size['total' + capitalized] += edgesize; + }); + + }, this); + + return Object.append(size, styles); + } + +}); + +})(); + + +/* +--- + +script: Element.Position.js + +name: Element.Position + +description: Extends the Element native object to include methods useful positioning elements relative to others. + +license: MIT-style license + +authors: + - Aaron Newton + - Jacob Thornton + +requires: + - Core/Options + - Core/Element.Dimensions + - Element.Measure + +provides: [Element.Position] + +... +*/ + +(function(original){ + +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){ + if (options && (options.x != null || options.y != null)){ + return (original ? original.apply(this, arguments) : this); + } + var position = this.setStyle('position', 'absolute').calculatePosition(options); + return (options && options.returnPos) ? position : this.setStyles(position); + }, + + calculatePosition: function(options){ + return local.getPosition(this, options); + } + +}); + +})(Element.prototype.position); + + /* --- @@ -1768,1094 +2174,3 @@ Request.implement({ }); - -/* ---- - -script: Class.Refactor.js - -name: Class.Refactor - -description: Extends a class onto itself with new property, preserving any items attached to the class's namespace. - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Class - - /MooTools.More - -# Some modules declare themselves dependent on Class.Refactor -provides: [Class.refactor, Class.Refactor] - -... -*/ - -Class.refactor = function(original, refactors){ - - Object.each(refactors, function(item, name){ - var origin = original.prototype[name]; - origin = (origin && origin.$origin) || origin || function(){}; - original.implement(name, (typeof item == 'function') ? function(){ - var old = this.previous; - this.previous = origin; - var value = item.apply(this, arguments); - this.previous = old; - return value; - } : item); - }); - - return original; - -}; - - -/* ---- - -script: Class.Binds.js - -name: Class.Binds - -description: Automagically binds specified methods in a class to the instance of the class. - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Class - - /MooTools.More - -provides: [Class.Binds] - -... -*/ - -Class.Mutators.Binds = function(binds){ - if (!this.prototype.initialize) this.implement('initialize', function(){}); - return Array.from(binds).concat(this.prototype.Binds || []); -}; - -Class.Mutators.initialize = function(initialize){ - return function(){ - Array.from(this.Binds).each(function(name){ - var original = this[name]; - if (original) this[name] = original.bind(this); - }, this); - return initialize.apply(this, arguments); - }; -}; - - -/* ---- - -script: Element.Measure.js - -name: Element.Measure - -description: Extends the Element native object to include methods useful in measuring dimensions. - -credits: "Element.measure / .expose methods by Daniel Steigerwald License: MIT-style license. Copyright: Copyright (c) 2008 Daniel Steigerwald, daniel.steigerwald.cz" - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Element.Style - - Core/Element.Dimensions - - /MooTools.More - -provides: [Element.Measure] - -... -*/ - -(function(){ - -var getStylesList = function(styles, planes){ - var list = []; - Object.each(planes, function(directions){ - Object.each(directions, function(edge){ - styles.each(function(style){ - list.push(style + '-' + edge + (style == 'border' ? '-width' : '')); - }); - }); - }); - return list; -}; - -var calculateEdgeSize = function(edge, styles){ - var total = 0; - Object.each(styles, function(value, style){ - if (style.test(edge)) total = total + value.toInt(); - }); - return total; -}; - -var isVisible = function(el){ - return !!(!el || el.offsetHeight || el.offsetWidth); -}; - - -Element.implement({ - - measure: function(fn){ - if (isVisible(this)) return fn.call(this); - var parent = this.getParent(), - toMeasure = []; - while (!isVisible(parent) && parent != document.body){ - toMeasure.push(parent.expose()); - parent = parent.getParent(); - } - var restore = this.expose(), - result = fn.call(this); - restore(); - toMeasure.each(function(restore){ - restore(); - }); - return result; - }, - - expose: function(){ - if (this.getStyle('display') != 'none') return function(){}; - var before = this.style.cssText; - this.setStyles({ - display: 'block', - position: 'absolute', - visibility: 'hidden' - }); - return function(){ - this.style.cssText = before; - }.bind(this); - }, - - getDimensions: function(options){ - options = Object.merge({computeSize: false}, options); - var dim = {x: 0, y: 0}; - - var getSize = function(el, options){ - return (options.computeSize) ? el.getComputedSize(options) : el.getSize(); - }; - - var parent = this.getParent('body'); - - if (parent && this.getStyle('display') == 'none'){ - dim = this.measure(function(){ - return getSize(this, options); - }); - } else if (parent){ - try { //safari sometimes crashes here, so catch it - dim = getSize(this, options); - }catch(e){} - } - - return Object.append(dim, (dim.x || dim.x === 0) ? { - width: dim.x, - height: dim.y - } : { - x: dim.width, - y: dim.height - } - ); - }, - - getComputedSize: function(options){ - - - options = Object.merge({ - styles: ['padding','border'], - planes: { - height: ['top','bottom'], - width: ['left','right'] - }, - mode: 'both' - }, options); - - var styles = {}, - size = {width: 0, height: 0}, - dimensions; - - if (options.mode == 'vertical'){ - delete size.width; - delete options.planes.width; - } else if (options.mode == 'horizontal'){ - delete size.height; - delete options.planes.height; - } - - getStylesList(options.styles, options.planes).each(function(style){ - styles[style] = this.getStyle(style).toInt(); - }, this); - - Object.each(options.planes, function(edges, plane){ - - var capitalized = plane.capitalize(), - style = this.getStyle(plane); - - if (style == 'auto' && !dimensions) dimensions = this.getDimensions(); - - style = styles[plane] = (style == 'auto') ? dimensions[plane] : style.toInt(); - size['total' + capitalized] = style; - - edges.each(function(edge){ - var edgesize = calculateEdgeSize(edge, styles); - size['computed' + edge.capitalize()] = edgesize; - size['total' + capitalized] += edgesize; - }); - - }, this); - - return Object.append(size, styles); - } - -}); - -})(); - - -/* ---- - -script: Element.Position.js - -name: Element.Position - -description: Extends the Element native object to include methods useful positioning elements relative to others. - -license: MIT-style license - -authors: - - Aaron Newton - - Jacob Thornton - -requires: - - Core/Options - - Core/Element.Dimensions - - Element.Measure - -provides: [Element.Position] - -... -*/ - -(function(original){ - -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){ - if (options && (options.x != null || options.y != null)){ - return (original ? original.apply(this, arguments) : this); - } - var position = this.setStyle('position', 'absolute').calculatePosition(options); - return (options && options.returnPos) ? position : this.setStyles(position); - }, - - calculatePosition: function(options){ - return local.getPosition(this, options); - } - -}); - -})(Element.prototype.position); - - -/* ---- - -script: Class.Occlude.js - -name: Class.Occlude - -description: Prevents a class from being applied to a DOM element twice. - -license: MIT-style license. - -authors: - - Aaron Newton - -requires: - - Core/Class - - Core/Element - - /MooTools.More - -provides: [Class.Occlude] - -... -*/ - -Class.Occlude = new Class({ - - occlude: function(property, element){ - element = document.id(element || this.element); - var instance = element.retrieve(property || this.property); - if (instance && !this.occluded) - return (this.occluded = instance); - - this.occluded = false; - element.store(property || this.property, this); - return this.occluded; - } - -}); - - -/* ---- - -script: IframeShim.js - -name: IframeShim - -description: Defines IframeShim, a class for obscuring select lists and flash objects in IE. - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Element.Event - - Core/Element.Style - - Core/Options - - Core/Events - - /Element.Position - - /Class.Occlude - -provides: [IframeShim] - -... -*/ - -var IframeShim = new Class({ - - Implements: [Options, Events, Class.Occlude], - - options: { - className: 'iframeShim', - src: 'javascript:false;document.write("");', - display: false, - zIndex: null, - margin: 0, - offset: {x: 0, y: 0}, - browsers: (Browser.ie6 || (Browser.firefox && Browser.version < 3 && Browser.Platform.mac)) - }, - - property: 'IframeShim', - - initialize: function(element, options){ - this.element = document.id(element); - if (this.occlude()) return this.occluded; - this.setOptions(options); - this.makeShim(); - return this; - }, - - makeShim: function(){ - if (this.options.browsers){ - var zIndex = this.element.getStyle('zIndex').toInt(); - - if (!zIndex){ - zIndex = 1; - var pos = this.element.getStyle('position'); - if (pos == 'static' || !pos) this.element.setStyle('position', 'relative'); - this.element.setStyle('zIndex', zIndex); - } - zIndex = ((this.options.zIndex != null || this.options.zIndex === 0) && zIndex > this.options.zIndex) ? this.options.zIndex : zIndex - 1; - if (zIndex < 0) zIndex = 1; - this.shim = new Element('iframe', { - src: this.options.src, - scrolling: 'no', - frameborder: 0, - styles: { - zIndex: zIndex, - position: 'absolute', - border: 'none', - filter: 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)' - }, - 'class': this.options.className - }).store('IframeShim', this); - var inject = (function(){ - this.shim.inject(this.element, 'after'); - this[this.options.display ? 'show' : 'hide'](); - this.fireEvent('inject'); - }).bind(this); - if (!IframeShim.ready) window.addEvent('load', inject); - else inject(); - } else { - this.position = this.hide = this.show = this.dispose = Function.from(this); - } - }, - - position: function(){ - if (!IframeShim.ready || !this.shim) return this; - var size = this.element.measure(function(){ - return this.getSize(); - }); - if (this.options.margin != undefined){ - size.x = size.x - (this.options.margin * 2); - size.y = size.y - (this.options.margin * 2); - this.options.offset.x += this.options.margin; - this.options.offset.y += this.options.margin; - } - this.shim.set({width: size.x, height: size.y}).position({ - relativeTo: this.element, - offset: this.options.offset - }); - return this; - }, - - hide: function(){ - if (this.shim) this.shim.setStyle('display', 'none'); - return this; - }, - - show: function(){ - if (this.shim) this.shim.setStyle('display', 'block'); - return this.position(); - }, - - dispose: function(){ - if (this.shim) this.shim.dispose(); - return this; - }, - - destroy: function(){ - if (this.shim) this.shim.destroy(); - return this; - } - -}); - -window.addEvent('load', function(){ - IframeShim.ready = true; -}); - - -/* ---- - -script: Mask.js - -name: Mask - -description: Creates a mask element to cover another. - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Options - - Core/Events - - Core/Element.Event - - /Class.Binds - - /Element.Position - - /IframeShim - -provides: [Mask] - -... -*/ - -var Mask = new Class({ - - Implements: [Options, Events], - - Binds: ['position'], - - options: {/* - onShow: function(){}, - onHide: function(){}, - onDestroy: function(){}, - onClick: function(event){}, - inject: { - where: 'after', - target: null, - }, - hideOnClick: false, - id: null, - destroyOnHide: false,*/ - style: {}, - 'class': 'mask', - maskMargins: false, - useIframeShim: true, - iframeShimOptions: {} - }, - - initialize: function(target, options){ - this.target = document.id(target) || document.id(document.body); - this.target.store('mask', this); - this.setOptions(options); - this.render(); - this.inject(); - }, - - render: function(){ - this.element = new Element('div', { - 'class': this.options['class'], - id: this.options.id || 'mask-' + String.uniqueID(), - styles: Object.merge({}, this.options.style, { - display: 'none' - }), - events: { - click: function(event){ - this.fireEvent('click', event); - if (this.options.hideOnClick) this.hide(); - }.bind(this) - } - }); - - this.hidden = true; - }, - - toElement: function(){ - return this.element; - }, - - inject: function(target, where){ - where = where || (this.options.inject ? this.options.inject.where : '') || this.target == document.body ? 'inside' : 'after'; - target = target || (this.options.inject && this.options.inject.target) || this.target; - - this.element.inject(target, where); - - if (this.options.useIframeShim){ - this.shim = new IframeShim(this.element, this.options.iframeShimOptions); - - this.addEvents({ - show: this.shim.show.bind(this.shim), - hide: this.shim.hide.bind(this.shim), - destroy: this.shim.destroy.bind(this.shim) - }); - } - }, - - position: function(){ - this.resize(this.options.width, this.options.height); - - this.element.position({ - relativeTo: this.target, - position: 'topLeft', - ignoreMargins: !this.options.maskMargins, - ignoreScroll: this.target == document.body - }); - - return this; - }, - - resize: function(x, y){ - var opt = { - styles: ['padding', 'border'] - }; - if (this.options.maskMargins) opt.styles.push('margin'); - - var dim = this.target.getComputedSize(opt); - if (this.target == document.body){ - this.element.setStyles({width: 0, height: 0}); - var win = window.getScrollSize(); - if (dim.totalHeight < win.y) dim.totalHeight = win.y; - if (dim.totalWidth < win.x) dim.totalWidth = win.x; - } - this.element.setStyles({ - width: Array.pick([x, dim.totalWidth, dim.x]), - height: Array.pick([y, dim.totalHeight, dim.y]) - }); - - return this; - }, - - show: function(){ - if (!this.hidden) return this; - - window.addEvent('resize', this.position); - this.position(); - this.showMask.apply(this, arguments); - - return this; - }, - - showMask: function(){ - this.element.setStyle('display', 'block'); - this.hidden = false; - this.fireEvent('show'); - }, - - hide: function(){ - if (this.hidden) return this; - - window.removeEvent('resize', this.position); - this.hideMask.apply(this, arguments); - if (this.options.destroyOnHide) return this.destroy(); - - return this; - }, - - hideMask: function(){ - this.element.setStyle('display', 'none'); - this.hidden = true; - this.fireEvent('hide'); - }, - - toggle: function(){ - this[this.hidden ? 'show' : 'hide'](); - }, - - destroy: function(){ - this.hide(); - this.element.destroy(); - this.fireEvent('destroy'); - this.target.eliminate('mask'); - } - -}); - -Element.Properties.mask = { - - set: function(options){ - var mask = this.retrieve('mask'); - if (mask) mask.destroy(); - return this.eliminate('mask').store('mask:options', options); - }, - - get: function(){ - var mask = this.retrieve('mask'); - if (!mask){ - mask = new Mask(this, this.retrieve('mask:options')); - this.store('mask', mask); - } - return mask; - } - -}; - -Element.implement({ - - mask: function(options){ - if (options) this.set('mask', options); - this.get('mask').show(); - return this; - }, - - unmask: function(){ - this.get('mask').hide(); - return this; - } - -}); - - -/* ---- - -script: Spinner.js - -name: Spinner - -description: Adds a semi-transparent overlay over a dom element with a spinnin ajax icon. - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Fx.Tween - - Core/Request - - /Class.refactor - - /Mask - -provides: [Spinner] - -... -*/ - -var Spinner = new Class({ - - Extends: Mask, - - Implements: Chain, - - options: {/* - message: false,*/ - 'class': 'spinner', - containerPosition: {}, - content: { - 'class': 'spinner-content' - }, - messageContainer: { - 'class': 'spinner-msg' - }, - img: { - 'class': 'spinner-img' - }, - fxOptions: { - link: 'chain' - } - }, - - initialize: function(target, options){ - this.target = document.id(target) || document.id(document.body); - this.target.store('spinner', this); - this.setOptions(options); - this.render(); - this.inject(); - - // Add this to events for when noFx is true; parent methods handle hide/show. - var deactivate = function(){ this.active = false; }.bind(this); - this.addEvents({ - hide: deactivate, - show: deactivate - }); - }, - - render: function(){ - this.parent(); - - this.element.set('id', this.options.id || 'spinner-' + String.uniqueID()); - - this.content = document.id(this.options.content) || new Element('div', this.options.content); - this.content.inject(this.element); - - if (this.options.message){ - this.msg = document.id(this.options.message) || new Element('p', this.options.messageContainer).appendText(this.options.message); - this.msg.inject(this.content); - } - - if (this.options.img){ - this.img = document.id(this.options.img) || new Element('div', this.options.img); - this.img.inject(this.content); - } - - this.element.set('tween', this.options.fxOptions); - }, - - show: function(noFx){ - if (this.active) return this.chain(this.show.bind(this)); - if (!this.hidden){ - this.callChain.delay(20, this); - return this; - } - - this.active = true; - - return this.parent(noFx); - }, - - showMask: function(noFx){ - var pos = function(){ - this.content.position(Object.merge({ - relativeTo: this.element - }, this.options.containerPosition)); - }.bind(this); - - if (noFx){ - this.parent(); - pos(); - } else { - if (!this.options.style.opacity) this.options.style.opacity = this.element.getStyle('opacity').toFloat(); - this.element.setStyles({ - display: 'block', - opacity: 0 - }).tween('opacity', this.options.style.opacity); - pos(); - this.hidden = false; - this.fireEvent('show'); - this.callChain(); - } - }, - - hide: function(noFx){ - if (this.active) return this.chain(this.hide.bind(this)); - if (this.hidden){ - this.callChain.delay(20, this); - return this; - } - this.active = true; - return this.parent(noFx); - }, - - hideMask: function(noFx){ - if (noFx) return this.parent(); - this.element.tween('opacity', 0).get('tween').chain(function(){ - this.element.setStyle('display', 'none'); - this.hidden = true; - this.fireEvent('hide'); - this.callChain(); - }.bind(this)); - }, - - destroy: function(){ - this.content.destroy(); - this.parent(); - this.target.eliminate('spinner'); - } - -}); - -Request = Class.refactor(Request, { - - options: { - useSpinner: false, - spinnerOptions: {}, - spinnerTarget: false - }, - - initialize: function(options){ - this._send = this.send; - this.send = function(options){ - var spinner = this.getSpinner(); - if (spinner) spinner.chain(this._send.pass(options, this)).show(); - else this._send(options); - return this; - }; - this.previous(options); - }, - - getSpinner: function(){ - if (!this.spinner){ - var update = document.id(this.options.spinnerTarget) || document.id(this.options.update); - if (this.options.useSpinner && update){ - update.set('spinner', this.options.spinnerOptions); - var spinner = this.spinner = update.get('spinner'); - ['complete', 'exception', 'cancel'].each(function(event){ - this.addEvent(event, spinner.hide.bind(spinner)); - }, this); - } - } - return this.spinner; - } - -}); - -Element.Properties.spinner = { - - set: function(options){ - var spinner = this.retrieve('spinner'); - if (spinner) spinner.destroy(); - return this.eliminate('spinner').store('spinner:options', options); - }, - - get: function(){ - var spinner = this.retrieve('spinner'); - if (!spinner){ - spinner = new Spinner(this, this.retrieve('spinner:options')); - this.store('spinner', spinner); - } - return spinner; - } - -}; - -Element.implement({ - - spin: function(options){ - if (options) this.set('spinner', options); - this.get('spinner').show(); - return this; - }, - - unspin: function(){ - this.get('spinner').hide(); - return this; - } - -}); - diff --git a/couchpotato/static/scripts/library/spin.js b/couchpotato/static/scripts/library/spin.js new file mode 100644 index 00000000..6c2d0d59 --- /dev/null +++ b/couchpotato/static/scripts/library/spin.js @@ -0,0 +1,301 @@ +//fgnass.github.com/spin.js#v1.2.4 +(function(window, document, undefined) { + +/** + * Copyright (c) 2011 Felix Gnass [fgnass at neteye dot de] + * Licensed under the MIT license + */ + + var prefixes = ['webkit', 'Moz', 'ms', 'O']; /* Vendor prefixes */ + var animations = {}; /* Animation rules keyed by their name */ + var useCssAnimations; + + /** + * Utility function to create elements. If no tag name is given, + * a DIV is created. Optionally properties can be passed. + */ + function createEl(tag, prop) { + var el = document.createElement(tag || 'div'); + var n; + + for(n in prop) { + el[n] = prop[n]; + } + return el; + } + + /** + * Appends children and returns the parent. + */ + function ins(parent /* child1, child2, ...*/) { + for (var i=1, n=arguments.length; i> 1) : o.left+mid) + 'px', + top: (o.top == 'auto' ? tp.y-ep.y + (target.offsetHeight >> 1) : o.top+mid) + 'px' + }); + } + + el.setAttribute('aria-role', 'progressbar'); + self.lines(el, self.opts); + + if (!useCssAnimations) { + // No CSS animation support, use setTimeout() instead + var i = 0; + var fps = o.fps; + var f = fps/o.speed; + var ostep = (1-o.opacity)/(f*o.trail / 100); + var astep = f/o.lines; + + !function anim() { + i++; + for (var s=o.lines; s; s--) { + var alpha = Math.max(1-(i+s*astep)%f * ostep, o.opacity); + self.opacity(el, o.lines-s, alpha, o); + } + self.timeout = self.el && setTimeout(anim, ~~(1000/fps)); + }(); + } + return self; + }, + stop: function() { + var el = this.el; + if (el) { + clearTimeout(this.timeout); + if (el.parentNode) el.parentNode.removeChild(el); + this.el = undefined; + } + return this; + }, + lines: function(el, o) { + var i = 0; + var seg; + + function fill(color, shadow) { + return css(createEl(), { + position: 'absolute', + width: (o.length+o.width) + 'px', + height: o.width + 'px', + background: color, + boxShadow: shadow, + transformOrigin: 'left', + transform: 'rotate(' + ~~(360/o.lines*i) + 'deg) translate(' + o.radius+'px' +',0)', + borderRadius: (o.width>>1) + 'px' + }); + } + for (; i < o.lines; i++) { + seg = css(createEl(), { + position: 'absolute', + top: 1+~(o.width/2) + 'px', + transform: o.hwaccel ? 'translate3d(0,0,0)' : '', + opacity: o.opacity, + animation: useCssAnimations && addAnimation(o.opacity, o.trail, i, o.lines) + ' ' + 1/o.speed + 's linear infinite' + }); + if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'})); + ins(el, ins(seg, fill(o.color, '0 0 1px rgba(0,0,0,.1)'))); + } + return el; + }, + opacity: function(el, i, val) { + if (i < el.childNodes.length) el.childNodes[i].style.opacity = val; + } + }; + + ///////////////////////////////////////////////////////////////////////// + // VML rendering for IE + ///////////////////////////////////////////////////////////////////////// + + /** + * Check and init VML support + */ + !function() { + var s = css(createEl('group'), {behavior: 'url(#default#VML)'}); + var i; + + if (!vendor(s, 'transform') && s.adj) { + + // VML support detected. Insert CSS rules ... + for (i=4; i--;) sheet.addRule(['group', 'roundrect', 'fill', 'stroke'][i], 'behavior:url(#default#VML)'); + + Spinner.prototype.lines = function(el, o) { + var r = o.length+o.width; + var s = 2*r; + + function grp() { + return css(createEl('group', {coordsize: s +' '+s, coordorigin: -r +' '+-r}), {width: s, height: s}); + } + + var margin = -(o.width+o.length)*2+'px'; + var g = css(grp(), {position: 'absolute', top: margin, left: margin}); + + var i; + + function seg(i, dx, filter) { + ins(g, + ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}), + ins(css(createEl('roundrect', {arcsize: 1}), { + width: r, + height: o.width, + left: o.radius, + top: -o.width>>1, + filter: filter + }), + createEl('fill', {color: o.color, opacity: o.opacity}), + createEl('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change + ) + ) + ); + } + + if (o.shadow) { + for (i = 1; i <= o.lines; i++) { + seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)'); + } + } + for (i = 1; i <= o.lines; i++) seg(i); + return ins(el, g); + }; + Spinner.prototype.opacity = function(el, i, val, o) { + var c = el.firstChild; + o = o.shadow && o.lines || 0; + if (c && i+o < c.childNodes.length) { + c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild; + if (c) c.opacity = val; + } + }; + } + else { + useCssAnimations = vendor(s, 'animation'); + } + }(); + + window.Spinner = Spinner; + +})(window, document); diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 21886ecf..96058721 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -84,7 +84,7 @@ a:hover { color: #f3f3f3; } .content { clear:both; - padding: 80px 10px 10px; + padding: 80px 0 10px; } .footer { @@ -117,7 +117,16 @@ form { body > .spinner, .mask{ background: rgba(0,0,0, 0.9); z-index: 100; + text-align: center; } + body > .mask { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; + padding: 200px; + } .button { background: #5082bc url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAyCAYAAACd+7GKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAClJREFUeNpi/v//vwMTAwPDfzjBgMpFI/7hFSOT9Y8qRuF3JLoHAQIMAHYtMmRA+CugAAAAAElFTkSuQmCC") repeat-x; @@ -155,7 +164,7 @@ body > .spinner, .mask{ /*** Navigation ***/ .header { background: #4e5969; - padding:10px; + padding: 10px 0; height: 80px; position: fixed; margin: 0; @@ -175,7 +184,7 @@ body > .spinner, .mask{ } .header .navigation { display: inline-block; - width: 66.7%; + width: 67.2%; } .header .navigation ul { margin: 0; diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html index 306eeb3c..8df06c4b 100644 --- a/couchpotato/templates/_desktop.html +++ b/couchpotato/templates/_desktop.html @@ -20,6 +20,7 @@ + From bb27d0a18b24e96a60b5d7ad44f983039b3bf609 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 Mar 2012 00:03:59 +0100 Subject: [PATCH 64/99] Remove non-used items from imdbapi --- couchpotato/core/providers/movie/imdbapi/main.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/providers/movie/imdbapi/main.py b/couchpotato/core/providers/movie/imdbapi/main.py index 1c817274..158359b0 100644 --- a/couchpotato/core/providers/movie/imdbapi/main.py +++ b/couchpotato/core/providers/movie/imdbapi/main.py @@ -47,8 +47,9 @@ class IMDBAPI(MovieProvider): if cached: result = self.parseMovie(cached) - log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') - return result + if result.get('titles') and len(result.get('titles')) > 0: + log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') + return result return {} @@ -60,6 +61,11 @@ class IMDBAPI(MovieProvider): if isinstance(movie, (str, unicode)): movie = json.loads(movie) + tmp_movie = movie.copy() + for key in tmp_movie: + if tmp_movie.get(key).lower() == 'n/a': + del movie[key] + movie_data = { 'titles': [movie.get('Title', '')], 'original_title': movie.get('Title', ''), From 7675ef80fe1c3658856c2d4c4f12a72ab1cf912e Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 Mar 2012 00:21:29 +0100 Subject: [PATCH 65/99] Don't type behind searchbox --- couchpotato/core/plugins/movie/static/search.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css index 9f4866b4..b3ef1e50 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -10,12 +10,15 @@ } .search_form input { - padding: 4px; + padding: 4px 20px 4px 4px; margin: 0; font-size: 14px; width: 100%; height: 24px; } + .search_form input:focus { + padding-right: 83px; + } .search_form .input .enter { background: #369545 url('../images/sprite.png') right -188px no-repeat; From d1315ffc815ccc4441cc2def02ee70decdd1c4ba Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 Mar 2012 00:25:52 +0100 Subject: [PATCH 66/99] Catch metadata exception and continue --- couchpotato/core/plugins/scanner/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index f170dcd9..2d9607cb 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -365,7 +365,6 @@ class Scanner(Plugin): def getMeta(self, filename): try: - p = enzyme.parse(filename) return { 'video': p.video[0].codec, @@ -377,6 +376,8 @@ class Scanner(Plugin): log.debug('Failed to parse meta for %s' % filename) except NoParserError: log.debug('No parser found for %s' % filename) + except: + log.debug('Failed parsing %s' % filename) return {} From 8803323ae4d7d6116b4e553223b34744adcb5871 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 Mar 2012 00:53:03 +0100 Subject: [PATCH 67/99] Rating and director XBMC metadata fix --- couchpotato/core/providers/metadata/xbmc/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/providers/metadata/xbmc/main.py b/couchpotato/core/providers/metadata/xbmc/main.py index d1f0feed..2908c6f6 100644 --- a/couchpotato/core/providers/metadata/xbmc/main.py +++ b/couchpotato/core/providers/metadata/xbmc/main.py @@ -48,7 +48,7 @@ class XBMC(MetaDataBase): pass # Other values - types = ['rating', 'year', 'mpaa', 'originaltitle:original_title', 'outline', 'plot', 'tagline', 'premiered:released'] + types = ['year', 'mpaa', 'originaltitle:original_title', 'outline', 'plot', 'tagline', 'premiered:released'] for type in types: if ':' in type: @@ -73,7 +73,7 @@ class XBMC(MetaDataBase): votes.text = str(v) break except: - log.error('Failed adding rating info from %s: %s' % (rating_type, traceback.format_exc())) + log.debug('Failed adding rating info from %s: %s' % (rating_type, traceback.format_exc())) # Genre for genre in movie_info.get('genres', []): @@ -87,9 +87,9 @@ class XBMC(MetaDataBase): name.text = toUnicode(actor) # Directors - for director in movie_info.get('directors', []): + for director_name in movie_info.get('directors', []): director = SubElement(nfoxml, 'director') - director.text = toUnicode(director) + director.text = toUnicode(director_name) # Writers for writer in movie_info.get('writers', []): From 34611e1061d5e85795f90c7b88c6ce600663f8e8 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 Mar 2012 02:12:03 +0100 Subject: [PATCH 68/99] Better priority merging --- couchpotato/core/event.py | 11 ++++++----- libs/axl/axel.py | 12 ++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py index 6f588481..88c15454 100644 --- a/couchpotato/core/event.py +++ b/couchpotato/core/event.py @@ -1,8 +1,7 @@ from axl.axel import Event -from couchpotato.core.helpers.variable import mergeDicts +from couchpotato.core.helpers.variable import mergeDicts, natcmp from couchpotato.core.logger import CPLog import threading -import time import traceback log = CPLog(__name__) @@ -36,7 +35,7 @@ def addEvent(name, handler, priority = 100): return h - e.handle(handler, priority = priority) + e.handle(createHandle, priority = priority) def removeEvent(name, handler): e = events[name] @@ -84,7 +83,8 @@ def fireEvent(name, *args, **kwargs): results = None # Loop over results, stop when first not None result is found. - for r in result: + for r_key in sorted(result.iterkeys(), cmp = natcmp): + r = result[r_key] if r[0] is True and r[1] is not None: results = r[1] break @@ -95,7 +95,8 @@ def fireEvent(name, *args, **kwargs): else: results = [] - for r in result: + for r_key in sorted(result.iterkeys(), cmp = natcmp): + r = result[r_key] if r[0] == True and r[1]: results.append(r[1]) elif r[1]: diff --git a/libs/axl/axel.py b/libs/axl/axel.py index 59ffa1be..5607450d 100644 --- a/libs/axl/axel.py +++ b/libs/axl/axel.py @@ -141,7 +141,7 @@ class Event(object): def fire(self, *args, **kwargs): """ Stores all registered handlers in a queue for processing """ self.queue = Queue.Queue() - self.result = [] + self.result = {} if self.handlers: @@ -158,12 +158,12 @@ class Event(object): if self.asynchronous: handler_, memoize, timeout = self.handlers[handler] - self.result.append((None, None, handler_)) + self.result[handler] = (None, None, handler_) if not self.asynchronous: self.queue.join() - return tuple(self.result) or None + return self.result or None def count(self): """ Returns the count of registered handlers """ @@ -187,12 +187,12 @@ class Event(object): try: r = self._memoize(memoize, timeout, handler, *args, **kwargs) if not self.asynchronous: - self.result.append(tuple(r)) + self.result[h_] = tuple(r) except Exception: if not self.asynchronous: - self.result.append((False, self._error(sys.exc_info()), - handler)) + self.result[h_] = (False, self._error(sys.exc_info()), + handler) else: self.error_handler(sys.exc_info()) finally: From eceaf465f01f2939c6fca41fe0c71fbb26fdf7da Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 Mar 2012 02:46:08 +0100 Subject: [PATCH 69/99] Better git version check --- libs/git/repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/git/repository.py b/libs/git/repository.py index 669e8a8b..b6609e3f 100755 --- a/libs/git/repository.py +++ b/libs/git/repository.py @@ -144,7 +144,7 @@ class LocalRepository(Repository): def getGitVersion(self): if self._version is None: version_output = self._getOutputAssertSuccess("version") - version_match = re.match(r"git\s+version\s+(\S+)$", version_output, re.I) + version_match = re.match(r"git\s+version\s+(\S+)[\s\(]?", version_output, re.I) if version_match is None: raise GitException("Cannot extract git version (unfamiliar output format %r?)" % version_output) self._version = version_match.group(1) From 63225f7e5cb2e8c3ed72576961a8b70bc63adf56 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 12 Mar 2012 21:43:34 +0100 Subject: [PATCH 70/99] Library info as json --- couchpotato/core/plugins/library/main.py | 3 +-- couchpotato/core/providers/metadata/base.py | 7 +------ couchpotato/core/settings/model.py | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py index 4ceca115..4cc3bb26 100644 --- a/couchpotato/core/plugins/library/main.py +++ b/couchpotato/core/plugins/library/main.py @@ -5,7 +5,6 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Library, LibraryTitle, File from string import ascii_letters -import json import traceback log = CPLog(__name__) @@ -75,7 +74,7 @@ class LibraryPlugin(Plugin): library.tagline = toUnicode(info.get('tagline', '')) library.year = info.get('year', 0) library.status_id = done_status.get('id') - library.info = toUnicode(json.dumps(info)) + library.info = info db.commit() # Titles diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py index 5d99ea9b..d18ecbf7 100644 --- a/couchpotato/core/providers/metadata/base.py +++ b/couchpotato/core/providers/metadata/base.py @@ -2,7 +2,6 @@ from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -import json import os import shutil import traceback @@ -31,11 +30,7 @@ class MetaDataBase(Plugin): root = self.getRootName(release) - try: - movie_info = json.loads(release['library'].get('info')) - except: - log.error('Failed to parse movie info: %s' % traceback.format_exc()) - movie_info = {} + movie_info = release['library'].get('info') for file_type in ['nfo', 'thumbnail', 'fanart']: try: diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 36d58668..0ea5f4ec 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -1,10 +1,12 @@ +from couchpotato.core.helpers.encoding import toUnicode from elixir.entity import Entity from elixir.fields import Field from elixir.options import options_defaults, using_options from elixir.relationships import ManyToMany, OneToMany, ManyToOne from libs.elixir.relationships import OneToOne from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \ - String + String, TypeDecorator +import json options_defaults["shortnames"] = True @@ -16,6 +18,16 @@ options_defaults["shortnames"] = True __session__ = None +class JsonType(TypeDecorator): + impl = UnicodeText + + def process_bind_param(self, value, dialect): + return toUnicode(json.dumps(value)) + + def process_result_value(self, value, dialect): + return json.loads(value) + + class Movie(Entity): """Movie Resource a movie could have multiple releases The files belonging to the movie object are global for the whole movie @@ -35,11 +47,10 @@ class Library(Entity): year = Field(Integer) identifier = Field(String(20), index = True) - rating = Field(Float) plot = Field(UnicodeText) tagline = Field(UnicodeText(255)) - info = Field(UnicodeText) + info = Field(JsonType) status = ManyToOne('Status') movies = OneToMany('Movie') From 2323136221c2b1735f235d45fe0b81f24fe649a6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 13 Mar 2012 08:04:04 +0100 Subject: [PATCH 71/99] Don't load empty json --- couchpotato/core/settings/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 0ea5f4ec..886b3584 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -25,7 +25,7 @@ class JsonType(TypeDecorator): return toUnicode(json.dumps(value)) def process_result_value(self, value, dialect): - return json.loads(value) + return json.loads(value if value else '{}') class Movie(Entity): From 15175a0330cc64c5563c314c0d38bcc4759a2436 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 14 Mar 2012 20:33:22 +0100 Subject: [PATCH 72/99] Notification system --- couchpotato/core/_base/updater/main.py | 21 +- couchpotato/core/notifications/core/main.py | 65 +- .../notifications/core/static/notification.js | 83 +- couchpotato/core/plugins/movie/static/list.js | 24 +- .../core/plugins/movie/static/movie.css | 8 +- .../core/plugins/movie/static/movie.js | 8 +- .../core/plugins/movie/static/search.css | 7 +- .../core/plugins/movie/static/search.js | 4 +- .../core/plugins/profile/static/profile.css | 2 - .../core/plugins/profile/static/profile.js | 2 +- .../core/plugins/quality/static/quality.css | 2 - .../core/plugins/wizard/static/wizard.css | 2 - .../core/plugins/wizard/static/wizard.js | 2 +- couchpotato/core/settings/model.py | 16 +- couchpotato/static/images/sprite.png | Bin 1970 -> 2291 bytes couchpotato/static/scripts/block/menu.js | 43 + couchpotato/static/scripts/block/more.js | 54 - couchpotato/static/scripts/couchpotato.js | 30 +- .../static/scripts/library/mootools_more.js | 991 +++++++++++++++++- couchpotato/static/scripts/page/settings.js | 6 +- couchpotato/static/scripts/page/wanted.js | 14 +- couchpotato/static/style/api.css | 1 - couchpotato/static/style/main.css | 85 +- couchpotato/static/style/page/settings.css | 6 - couchpotato/templates/_desktop.html | 2 +- 25 files changed, 1313 insertions(+), 165 deletions(-) create mode 100644 couchpotato/static/scripts/block/menu.js delete mode 100644 couchpotato/static/scripts/block/more.js diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index cf9150f4..8bf26359 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -5,6 +5,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env from git.repository import LocalRepository +from datetime import datetime import os import time import traceback @@ -48,12 +49,15 @@ class Updater(Plugin): def getInfo(self): - return jsonified({ + return jsonified(self.info()) + + def info(self): + return { 'repo_name': self.repo_name, 'last_check': self.last_check, 'update_version': self.update_version, 'version': self.getVersion() - }) + } def getVersion(self): @@ -91,14 +95,14 @@ class Updater(Plugin): log.info('Versions, local:%s, remote:%s' % (local.hash[:8], remote.hash[:8])) if local.getDate() < remote.getDate(): + self.update_version = { + 'hash': remote.hash[:8], + 'date': remote.getDate(), + } if self.conf('automatic') and not self.update_failed: if self.doUpdate(): fireEventAsync('app.crappy_restart') else: - self.update_version = { - 'hash': remote.hash[:8], - 'date': remote.getDate(), - } if self.conf('notification'): fireEvent('updater.available', message = 'A new update is available', data = self.getVersion()) @@ -119,11 +123,16 @@ class Updater(Plugin): self.repo.saveStash() log.info('Updating to latest version') + info = self.info() self.repo.pull() # Delete leftover .pyc files self.deletePyc() + # Notify before returning and restarting + version_date = datetime.fromtimestamp(info['update_version']['date']) + fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info) + return True except: log.error('Failed updating via GIT: %s' % traceback.format_exc()) diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index a5a478b2..df53347d 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -1,8 +1,13 @@ +from couchpotato import get_session from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.request import jsonified +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.request import jsonified, getParam +from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification +from couchpotato.core.settings.model import Notification as Notif +from sqlalchemy.sql.expression import or_ import time log = CPLog(__name__) @@ -11,28 +16,65 @@ log = CPLog(__name__) class CoreNotifier(Notification): messages = [] + listen_to = [ + 'movie.downloaded', 'movie.snatched', + 'updater.available', 'updater.updated', + ] def __init__(self): + super(CoreNotifier, self).__init__() addEvent('notify', self.notify) addEvent('notify.frontend', self.frontend) - addApiView('core_notifier.listener', self.listener) + addApiView('notification.markread', self.markAsRead, docs = { + 'desc': 'Mark notifications as read', + 'params': { + 'id': {'desc': 'Notification id you want to mark as read.', 'type': 'int (comma separated)'}, + }, + }) + addApiView('notification.listener', self.listener) self.registerEvents() - def registerEvents(self): # Library update, frontend refresh addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data)) - def notify(self, message = '', data = {}): - self.add(data = { - 'message': message, - 'raw': data, + def markAsRead(self): + ids = getParam('ids').split(',') + + db = get_session() + + q = db.query(Notif) \ + .filter(or_(*[Notif.id == tryInt(s) for s in ids])) + q.update({Notif.read: True}) + + db.commit() + + return jsonified({ + 'success': True }) + def notify(self, message = '', data = {}): + + db = get_session() + + n = Notif( + message = toUnicode(message), + data = data + ) + db.add(n) + db.commit() + + ndict = n.to_dict() + ndict['type'] = 'notification' + ndict['time'] = time.time() + self.messages.append(ndict) + + db.remove() + def frontend(self, type = 'notification', data = {}): self.messages.append({ 'time': time.time(), @@ -48,6 +90,15 @@ class CoreNotifier(Notification): if message['time'] > (time.time() - 15): messages.append(message) + # Get unread + if getParam('init'): + db = get_session() + notifications = db.query(Notif).filter_by(read = False).all() + for n in notifications: + ndict = n.to_dict() + ndict['type'] = 'notification' + messages.append(ndict) + self.messages = [] return jsonified({ 'success': True, diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index e96fdba2..617ef2ac 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -10,19 +10,90 @@ var NotificationBase = new Class({ // Listener App.addEvent('load', self.startInterval.bind(self)); App.addEvent('unload', self.stopTimer.bind(self)); - self.addEvent('notification', self.notify.bind(self)) + App.addEvent('notification', self.notify.bind(self)); // Add test buttons to settings page App.addEvent('load', self.addTestButtons.bind(self)); + // Notification bar + self.notifications = [] + App.addEvent('load', function(){ + + App.block.notification = new Block.Menu(self, { + 'class': 'notification_menu', + 'onOpen': self.markAsRead.bind(self) + }) + $(App.block.notification).inject(App.getBlock('search'), 'after'); + self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide(); + + App.getBlock('notification').addLink(new Element('a.more', { + 'href': App.createUrl('notifications'), + 'text': 'See more notifications' + })); + }) + + }, + + notify: function(result){ + var self = this; + + var added = new Date(); + added.setTime(result.added*1000) + + result.el = App.getBlock('notification').addLink( + new Element('span').adopt( + new Element('span.message', {'text': result.message}), + new Element('span.added', {'text': added.timeDiffInWords(), 'title': added}) + ) + , 'top'); + self.notifications.include(result); + + if(!result.read) + self.setBadge(self.notifications.filter(function(n){ return !n.read}).length) + + if(self.notifications.length >= 5){ + var n = self.notifications[self.notifications.length-5]; + n.el.destroy(); + } + + }, + + setBadge: function(value){ + var self = this; + self.badge.set('text', value) + self.badge[value ? 'show' : 'hide']() + }, + + markAsRead: function(){ + var self = this; + + var rn = self.notifications.filter(function(n){ + return !n.read + }) + + var ids = [] + rn.each(function(n){ + ids.include(n.id) + }) + + Api.request('notification.markread', { + 'data': { + 'ids': ids.join(',') + }, + 'onSuccess': function(){ + self.setBadge('') + } + }) + }, startInterval: function(){ var self = this; - self.request = Api.request('core_notifier.listener', { + self.request = Api.request('notification.listener', { 'initialDelay': 100, 'delay': 3000, + 'data': {'init':true}, 'onSuccess': self.processData.bind(self) }) @@ -40,16 +111,12 @@ var NotificationBase = new Class({ this.request.stopTimer() }, - notify: function(data){ - var self = this; - - }, - processData: function(json){ var self = this; + self.request.options.data = {} Array.each(json.result, function(result){ - App.fireEvent(result.type, result.data) + App.fireEvent(result.type, result) }) }, diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index e13fe2ee..9ddffd97 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -116,25 +116,7 @@ var MovieList = new Class({ 'change': self.search.bind(self) } }), - self.navigation_menu = new Element('div.more_menu').adopt( - self.navigation_menu_ul = new Element('ul'), - self.navigation_menu_toggle = new Element('a.button.onlay', { - 'events': { - 'click': function(){ - self.navigation_menu.toggleClass('show') - - if(self.navigation_menu.hasClass('show')) - this.addEvent('outerClick', function(){ - self.navigation_menu.removeClass('show') - this.removeEvents('outerClick'); - }) - else - this.removeEvents('outerClick'); - - } - } - }) - ), + self.navigation_menu = new Block.Menu(self), self.mass_edit_form = new Element('div.mass_edit_form').adopt( new Element('span.select').adopt( self.mass_edit_select = new Element('input[type=checkbox].inlay', { @@ -222,7 +204,7 @@ var MovieList = new Class({ // Add menu or hide if (self.options.menu.length > 0) self.options.menu.each(function(menu_item){ - self.navigation_menu_ul.adopt(new Element('li').adopt(menu_item)); + self.navigation_menu.addLink(menu_item); }) else self.navigation_menu.hide() @@ -268,7 +250,7 @@ var MovieList = new Class({ 'class': 'delete', 'events': { 'click': function(e){ - (e).stop(); + (e).preventDefault(); Api.request('movie.delete', { 'data': { 'id': ids.join(',') diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index cdef5f22..36ff17c3 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -1,9 +1,3 @@ -/* @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: 60px 0 20px; } @@ -454,7 +448,7 @@ } .movies .alph_nav .more_menu { - float: right; + margin-left: 48px; } .movies .alph_nav .more_menu > a { diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index cd81b652..53cd9e31 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -230,7 +230,7 @@ var IMDBAction = new Class({ gotoIMDB: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); window.open('http://www.imdb.com/title/'+self.id+'/'); } @@ -258,7 +258,7 @@ var ReleaseAction = new Class({ show: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); if(!self.options_container){ self.options_container = new Element('div.options').adopt( @@ -292,7 +292,7 @@ var ReleaseAction = new Class({ new Element('a.download.icon', { 'events': { 'click': function(e){ - (e).stop(); + (e).preventDefault(); self.download(release); } } @@ -300,7 +300,7 @@ var ReleaseAction = new Class({ new Element('a.delete.icon', { 'events': { 'click': function(e){ - (e).stop(); + (e).preventDefault(); self.ignore(release); this.getParent('.item').toggleClass('ignored') } diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css index b3ef1e50..2be6ce1e 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -1,11 +1,6 @@ -/* @override - http://localhost:5000/static/movie_plugin/search.css - http://192.168.1.20:5000/static/movie_plugin/search.css - http://127.0.0.1:5000/static/movie_plugin/search.css -*/ - .search_form { display: inline-block; + vertical-align: middle; width: 25%; } diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js index 63d55dd3..a94b3438 100644 --- a/couchpotato/core/plugins/movie/static/search.js +++ b/couchpotato/core/plugins/movie/static/search.js @@ -53,7 +53,7 @@ Block.Search = new Class({ clear: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); self.input.set('value', ''); self.input.focus() @@ -283,7 +283,7 @@ Block.Search.Item = new Class({ add: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); Api.request('movie.add', { 'data': { diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css index c1f57196..b763cd80 100644 --- a/couchpotato/core/plugins/profile/static/profile.css +++ b/couchpotato/core/plugins/profile/static/profile.css @@ -1,5 +1,3 @@ -/* @override http://192.168.1.20:5000/static/profile_plugin/profile.css */ - .add_new_profile { padding: 20px; display: block; diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js index 7a6571e1..470fda8f 100644 --- a/couchpotato/core/plugins/profile/static/profile.js +++ b/couchpotato/core/plugins/profile/static/profile.js @@ -156,7 +156,7 @@ var Profile = new Class({ 'class': 'delete', 'events': { 'click': function(e){ - (e).stop(); + (e).preventDefault(); Api.request('profile.delete', { 'data': { 'id': self.data.id diff --git a/couchpotato/core/plugins/quality/static/quality.css b/couchpotato/core/plugins/quality/static/quality.css index e2081738..a66fefc4 100644 --- a/couchpotato/core/plugins/quality/static/quality.css +++ b/couchpotato/core/plugins/quality/static/quality.css @@ -1,5 +1,3 @@ -/* @override http://127.0.0.1:5000/static/quality_plugin/quality.css */ - .group_sizes { } diff --git a/couchpotato/core/plugins/wizard/static/wizard.css b/couchpotato/core/plugins/wizard/static/wizard.css index 8197b58a..3b19cf19 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.css +++ b/couchpotato/core/plugins/wizard/static/wizard.css @@ -1,5 +1,3 @@ -/* @override http://127.0.0.1:5000/static/wizard/wizard.css */ - .page.wizard h1 { padding: 10px 30px; margin: 0; diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js index c4edd0d5..e1e07a87 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.js +++ b/couchpotato/core/plugins/wizard/static/wizard.js @@ -35,7 +35,7 @@ Page.Wizard = new Class({ 'text': 'I\'m ready to start the awesomeness, wow this button is big and green!', 'events': { 'click': function(e){ - (e).stop(); + (e).preventDefault(); Api.request('settings.save', { 'data': { 'section': 'core', diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 886b3584..92d3ecdb 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -7,6 +7,7 @@ from libs.elixir.relationships import OneToOne from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \ String, TypeDecorator import json +import time options_defaults["shortnames"] = True @@ -33,7 +34,7 @@ class Movie(Entity): The files belonging to the movie object are global for the whole movie such as trailers, nfo, thumbnails""" - last_edit = Field(Integer) + last_edit = Field(Integer, default = lambda: int(time.time())) library = ManyToOne('Library') status = ManyToOne('Status') @@ -191,8 +192,8 @@ class History(Entity): """History of actions that are connected to a certain release, such as, renamed to, downloaded, deleted, download subtitles etc""" - added = Field(Integer) - message = Field(UnicodeText()) + added = Field(Integer, default = lambda: int(time.time())) + message = Field(UnicodeText) type = Field(Unicode(50)) release = ManyToOne('Release') @@ -207,6 +208,15 @@ class RenameHistory(Entity): file = ManyToOne('File') +class Notification(Entity): + using_options(order_by = 'added') + + added = Field(Integer, default = lambda: int(time.time())) + read = Field(Boolean, default = False) + message = Field(Unicode(255)) + data = Field(JsonType) + + class Folder(Entity): """Renamer destination folders.""" diff --git a/couchpotato/static/images/sprite.png b/couchpotato/static/images/sprite.png index 8a61b25e9c136d0290f9d6d38f88430c1fb70318..6af04a7e88dde73701b9889744c1c3c71f15b29b 100644 GIT binary patch delta 2257 zcmV;?2rl=s5AzX_7YgYJ1^@s6##s!rks&942$e}hK~#9!>|70O6vq|by*pw%wat|{ zCR8P*wFQI-D4s5Dg)gM(Xs zl#5$xr^d!MDF^sC^JC7JIEOi(eLmm+oqd14z3qE2OV{h&+dJDd6n5mNoq03+&Agd^ zH*aR%&Y5OemYS)cqGmAMy%p>*-Cv`!vw3Ou=Z|;P4ABZy@0q@dV##IJzEq1wdJt zCj~~^hlY1KobHnsO_v|xc-L)%KOb^`x>?(xGycY#Z$C?b9>6?)ziR|7Pe6CeAeX|k-&p-BV<9EJX zyyUkUwK^N}nFPbAM@F3WYf7K|8RYwc;(Vank_p8G$_6U@&1I=jv;g=yoM@uzB{Bds>DY(1y|{C2>zQ*`f4XB=DWr3yl$^s5ipdAm5C8;@0F44M zQea|zCJbsqj1&+bOe3RTDM9{!tcGTit|*rF9vICOvp{wY`QM89&!+dYVd3~ga5OxK z6&iEIA*RMcU(+**y(yQG!{97J8O>FWi-tJUc1ZVdq#4y zix@|Z)9pP8yK2_$3-nwtUYvKn%Q(m53!Foq%i}-$qvwB|m2$Jk1DjR;jfzLDHwMo+ z-F_B!*F8YzF@D`PbZOInbJbrYo8z>=Q1rYNE55q$n&sp3wjn2L8yaCPef^ho-}?3v zVoB6uU@8fV5iVW1Dw<@InrJjeMWa!QNix~Xo_HcgOc9f|)o5w!m@|L=qfHFUE(gPS zHOVkB*v>CgtLSU?fq|!=TC>t2It@j{RA24w`^V*LAF*dIbYEJ3z2>RMU>m;#cHHNY z*JJ7Hk`hyN0BR5ZW!0$;L)*IYiY4THq+R;h;|p6m4efRHe_by%0AgZdt)C+20W{HL zB>O>P!sBG;osisZ^T22_qAD^Nj9}-@@z`|0?vA;sSlWAX4VbQu-FY#grX8$&*pU_( zhqjYwt<0e*#~ z+tY2~1gn9q;A-OH^(RiIY}xm!zDbKZefc7>8O(es-*!fSAaVz>n(3&USikOO+_nnx^8lZqFOWj>Qop?bV8jB=Sy<{|Y6|7#h(vuEYR=;hKN-*NrG&E{8 z)!F$0ca4XCl`}!;5PyjJ?{`gMz#X}ly7iHP<@cEw7&^nVD1pzP7qH3bd6)LOE5=a) zQ^2Mj@9|XvElUXo z&w{bBu||9u4*&1PVzCCk*B$cCB@&50F$@!fd@BckC<@lv+Pci)aE$N);N>I)c6@xC z;qxPr$as5u`wOYW{Otby`wgi+Q6M-zj~_p7(`vP=MZtjZ!szF(T)FZ-_aejt+qZ8w z7Zen9;uF(cUVnf8brAb;h~`fM12JBOS{*>++zT}|HR!f4r*3`kEXOqRIgz342*jHv zf%Wx&^<5E)R8mq>#a97LBOm*a3qGGOFzIV`b93{s$z<|}!{OV@Qd?VF0UwM@BN()b zHJi;L;JL(<7F+SVg9i_O#MSC{yUp;}1T-6;Bz-d&jYjW+1qxV(}nPbO}IqK@_>|7sW zVNg2=wv?`wYW~Fdjrb+ zpscLy)bi!avA@2Ma-u`X11hF&eMi^S*Vh~Uet+CxFbtQMmzR(-i-6|IS`Cq~8R%Vq zpnYT?BJbH#)&K=e0lRn4PQvS~{4iU<^QS78vSmk(9GP$t7p!lVT)8`XXOHh#3J7>n zsd5`OY`BB|Gv&CWfGJ=Km;yF+FjnY*n+_N)=Fv+BjGZYlr?enwWPsQd;-&@0?-P0^ z#H43hHE3lzDPRhi0(QS15j8S}*1D{|`F9iO& zJ$v>PVVbz16Ne5RLRWPqCG#H{)>c(j)!J;fA)Y8VxQNwi9nk4?f24S~@#XHi;}6Yr zTeog4t*oqEn3tEAguYYh^?J)ZfddCRAn)%$PNCAu9jl9g@Hb(zs9V1%wE$h0ngNOf fQvs6*{}EsSVCcNM23v$A00000NkvXXu0mjfoeg6{ delta 1933 zcmV;82XgrH5wZ`E7Yfk`1^@s6E^vu+ks&942UJN!K~#9!>{<;>6h{=^y*mXg%2h9@ zO|`V9)>aa#h>ZzaQOlJ!q1Bo+wn@>(rZzTKEv9X3VrWfjt7-a^kd)Z85>XnUk(ydC z3IaAB$O(vepon-J9LN11+%LDceWSZ}S@!1+j!JP~?q%oR%zis>=FNLEZx<&?5{njp z$*?HW0<%IO=jyx zyKBg7a}0XVRb9vd*`vVIA_c?sz5P4QHfPW{V0E1+F4+t+6>toy;{(GDef|54Cd-*C zO;_j9vfKLJPx?$2Qr~L|9R20k8x+ugbX3P2cLl2la;wdbDV54hRbpau`;A-g$mFsm zGMVg<*XzUD+jR%FZ_BBHb}ryygfpW1bgyvy?n4mxtkP4b-+XRq^0x}PJQnJSAVJ6n z224d6>8n15dIxZT33OjGwwl1Pz=>ZU`EFBVOPf`DRYzXbT+uFB@@x{w<7m}?IL(V= z0&r?z=j_^@yRPbD{k4zw?n{UN@q9*3W3bg^0xBo~1ndPK0w(ytxWQ(zT{#*qJ3&A%$n#zNru(KY1hMQn4pzCyCFLLJL!WyXH< z4_3R21iycmQ5j@M*d1Wrk267k7-JS=Fq$1iC$7$inK2>qyg0;03Yq#lEzI3Yo~m3}Y+bJ}fXTy9%z0)n4^y6=k~z5ayDHE2bw z@4j3?>CuO4Z6J-(axF{Hfoz@gRI7EwQuBI>>SQfVY1lHz^c zwO8ghG-_vC?aq26Gg}>(-rw;-jNoRE0X9wja@NAGJH7QLi-UySbsKO4D&N-kwd8Ew z_B6FQ4hsyaK6B;D7ZzWC@3_^V?=zA5z5%kMv%4kxwbzp=Pa@|6lks>APx|ULz7QEw z_{_JRcqD+rQI1IKtRL=b-1&Zo*{_;o{1&xZ9G ztBst?kV8%Rwf4?GuU@}NUb@`evNmJ=GT6o^!;ZTF$=W(P8~N0K!>+Ia_gciRjwoSIP? zH5mkJ=S|DVaKI+V+LS!)l#Bu6wXum;6P~n}6sNsmfq}Z0jvqgMp6}_X4z-CSv6Nn1 zTwKlW4w9id-hd>3kiL1bq@?5m4TQ=_CK%mBazPp8<>hsVQCTchie(QFZJ@5_#7rX` zum@vj(t%$Bd-&WoZ)T5VXEK3Ek)?q%dZFN`%%gx0Q2%Sf?coTofqFqdi7prYenzlo zZ*O`9=S=!CRU#u;`Fy$Uh(N>+VtL~+jZtGZmf7Sq4LCf1_~YW@4n<`)0n8g48ygpu z*+gLDnN86#8}oAFn$1WoMKo?k0w%>x3G9Ifl(4LxFQ7!#IZA3uYCd#oN-;nJlS17j zUrr2?54ycUp_{bYM%CIitF7UH#dPa?u^_?ynTA3x$Li|-ra$8$#as|dh|j_PeXq$T zv?I4+_g*r8kjydT0z=907>t$kXAtT#%DnSu-6iKJfk|NFCb6#x(ZL_M31y^#X9!}j z!JzyE@Mhpd;F$)4p&f0tTrSs8S65dG6w!BluL7qEg27egcDqlbo8j>Phu`lnW@g=? zu09wH{zMRjAL_ekKu)l#s;X3@(Kx^afSW@=j#S=!*W+b=}?Fw?XaYP@FXi z4Al4ro@xXhrfZ%{Y_0x4v)ukKP**OSF4y{^nU~fy&`ow-5|)!Tt-VG z-FuI&C@Lz_IvkFGMx*IhtJNt~n`eOMi+T+X+75gQ_z<-Z5zp*NF+c*7z^3lmiTycv zE0YpfGKWw*!JeHwd2+;uxZr(b#M({hH+#&)l9MI#@wMBsWy=KS&!ppy1SWw=U=rBa z!AN!jZaiQ(S4J-!Ffv+djwv80M1V*Eb;AN`+T00000NkvXXu0mjfA*QMa diff --git a/couchpotato/static/scripts/block/menu.js b/couchpotato/static/scripts/block/menu.js new file mode 100644 index 00000000..f8ca6f11 --- /dev/null +++ b/couchpotato/static/scripts/block/menu.js @@ -0,0 +1,43 @@ +Block.Menu = new Class({ + + Extends: BlockBase, + + options: { + 'class': 'menu' + }, + + create: function(){ + var self = this; + + self.el = new Element('div', { + 'class': 'more_menu '+self.options['class'] + }).adopt( + self.more_option_ul = new Element('ul'), + new Element('a.button.onlay', { + 'events': { + 'click': function(){ + self.el.toggleClass('show') + self.fireEvent(self.el.hasClass('show') ? 'open' : 'close') + + if(self.el.hasClass('show')) + this.addEvent('outerClick', function(){ + self.el.removeClass('show') + this.removeEvents('outerClick'); + }) + else + this.removeEvents('outerClick'); + + } + } + }) + ) + + }, + + addLink: function(tab, position){ + var self = this; + var el = new Element('li').adopt(tab).inject(self.more_option_ul, position || 'bottom'); + return el; + } + +}); \ No newline at end of file diff --git a/couchpotato/static/scripts/block/more.js b/couchpotato/static/scripts/block/more.js deleted file mode 100644 index c9cd11b1..00000000 --- a/couchpotato/static/scripts/block/more.js +++ /dev/null @@ -1,54 +0,0 @@ -Block.More = new Class({ - - Extends: BlockBase, - - create: function(){ - var self = this; - - self.el = new Element('div.more_menu').adopt( - self.more_option_ul = new Element('ul').adopt( - new Element('li').adopt( - new Element('a.orange', { - 'text': 'Restart', - 'events': { - 'click': App.restart.bind(App) - } - }) - ), - new Element('li').adopt( - new Element('a.red', { - 'text': 'Shutdown', - 'events': { - 'click': App.shutdown.bind(App) - } - }) - ) - ), - new Element('a.button.onlay', { - 'events': { - 'click': function(){ - self.el.toggleClass('show') - - if(self.el.hasClass('show')) - this.addEvent('outerClick', function(){ - self.el.removeClass('show') - this.removeEvents('outerClick'); - }) - else - this.removeEvents('outerClick'); - - } - } - }) - ) - - }, - - addLink: function(tab, position){ - var self = this - - return new Element('li').adopt(tab).inject(self.more_option_ul, position || 'bottom') - - } - -}); \ No newline at end of file diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index c0f6a6a0..e4263884 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -43,7 +43,7 @@ var CouchPotato = new Class({ pushState: function(e){ var self = this; if((!e.meta && Browser.Platform.mac) || (!e.control && !Browser.Platform.mac)){ - (e).stop(); + (e).preventDefault(); var url = e.target.get('href'); if(History.getPath() != url) History.push(url); @@ -60,19 +60,34 @@ var CouchPotato = new Class({ new Element('div').adopt( self.block.navigation = new Block.Navigation(self, {}), self.block.search = new Block.Search(self, {}), - self.block.more = new Block.More(self, {}) + self.block.more = new Block.Menu(self, {}) ) ), self.content = new Element('div.content'), self.block.footer = new Block.Footer(self, {}) ); - self.block.more.addLink(new Element('a', { + [new Element('a.orange', { + 'text': 'Restart', + 'events': { + 'click': App.restart.bind(App) + } + }), + new Element('a.red', { + 'text': 'Shutdown', + 'events': { + 'click': App.shutdown.bind(App) + } + }), + new Element('a', { 'text': 'Check for updates', 'events': { 'click': self.checkForUpdate.bind(self) } - })) + })].each(function(a){ + self.block.more.addLink(a) + }) + new ScrollSpy({ min: 10, @@ -207,6 +222,13 @@ var CouchPotato = new Class({ createUrl: function(action, params){ return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '') + }, + + notify: function(options){ + return this.growl.notify({ + title: "this scrolls away", + text: "test - hello there. mouseover to pause away action" + }); } }); diff --git a/couchpotato/static/scripts/library/mootools_more.js b/couchpotato/static/scripts/library/mootools_more.js index 4ad1700f..d2d70369 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/d7fedf16aa88d2a757ac66071dd56b3a -// Or build this file again with packager using: packager build More/Events.Pseudos More/Element.Forms More/Element.Position More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical +// Load this file's selection again by visiting: http://mootools.net/more/43db227db7a621ebb062ee621432ae3d +// Or build this file again with packager using: packager build More/Events.Pseudos More/Date More/Date.Extras More/Element.Forms More/Element.Position More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical /* --- @@ -195,6 +195,993 @@ Events.implement(Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent)); })(); +/* +--- + +script: Object.Extras.js + +name: Object.Extras + +description: Extra Object generics, like getFromPath which allows a path notation to child elements. + +license: MIT-style license + +authors: + - Aaron Newton + +requires: + - Core/Object + - /MooTools.More + +provides: [Object.Extras] + +... +*/ + +(function(){ + +var defined = function(value){ + return value != null; +}; + +var hasOwnProperty = Object.prototype.hasOwnProperty; + +Object.extend({ + + getFromPath: function(source, parts){ + if (typeof parts == 'string') parts = parts.split('.'); + for (var i = 0, l = parts.length; i < l; i++){ + if (hasOwnProperty.call(source, parts[i])) source = source[parts[i]]; + else return null; + } + return source; + }, + + cleanValues: function(object, method){ + method = method || defined; + for (var key in object) if (!method(object[key])){ + delete object[key]; + } + return object; + }, + + erase: function(object, key){ + if (hasOwnProperty.call(object, key)) delete object[key]; + return object; + }, + + run: function(object){ + var args = Array.slice(arguments, 1); + for (var key in object) if (object[key].apply){ + object[key].apply(object, args); + } + return object; + } + +}); + +})(); + + +/* +--- + +script: Locale.js + +name: Locale + +description: Provides methods for localization. + +license: MIT-style license + +authors: + - Aaron Newton + - Arian Stolwijk + +requires: + - Core/Events + - /Object.Extras + - /MooTools.More + +provides: [Locale, Lang] + +... +*/ + +(function(){ + +var current = null, + locales = {}, + inherits = {}; + +var getSet = function(set){ + if (instanceOf(set, Locale.Set)) return set; + else return locales[set]; +}; + +var Locale = this.Locale = { + + define: function(locale, set, key, value){ + var name; + if (instanceOf(locale, Locale.Set)){ + name = locale.name; + if (name) locales[name] = locale; + } else { + name = locale; + if (!locales[name]) locales[name] = new Locale.Set(name); + locale = locales[name]; + } + + if (set) locale.define(set, key, value); + + + + if (!current) current = locale; + + return locale; + }, + + use: function(locale){ + locale = getSet(locale); + + if (locale){ + current = locale; + + this.fireEvent('change', locale); + + + } + + return this; + }, + + getCurrent: function(){ + return current; + }, + + get: function(key, args){ + return (current) ? current.get(key, args) : ''; + }, + + inherit: function(locale, inherits, set){ + locale = getSet(locale); + + if (locale) locale.inherit(inherits, set); + return this; + }, + + list: function(){ + return Object.keys(locales); + } + +}; + +Object.append(Locale, new Events); + +Locale.Set = new Class({ + + sets: {}, + + inherits: { + locales: [], + sets: {} + }, + + initialize: function(name){ + this.name = name || ''; + }, + + define: function(set, key, value){ + var defineData = this.sets[set]; + if (!defineData) defineData = {}; + + if (key){ + if (typeOf(key) == 'object') defineData = Object.merge(defineData, key); + else defineData[key] = value; + } + this.sets[set] = defineData; + + return this; + }, + + get: function(key, args, _base){ + var value = Object.getFromPath(this.sets, key); + if (value != null){ + var type = typeOf(value); + if (type == 'function') value = value.apply(null, Array.from(args)); + else if (type == 'object') value = Object.clone(value); + return value; + } + + // get value of inherited locales + var index = key.indexOf('.'), + set = index < 0 ? key : key.substr(0, index), + names = (this.inherits.sets[set] || []).combine(this.inherits.locales).include('en-US'); + if (!_base) _base = []; + + for (var i = 0, l = names.length; i < l; i++){ + if (_base.contains(names[i])) continue; + _base.include(names[i]); + + var locale = locales[names[i]]; + if (!locale) continue; + + value = locale.get(key, args, _base); + if (value != null) return value; + } + + return ''; + }, + + inherit: function(names, set){ + names = Array.from(names); + + if (set && !this.inherits.sets[set]) this.inherits.sets[set] = []; + + var l = names.length; + while (l--) (set ? this.inherits.sets[set] : this.inherits.locales).unshift(names[l]); + + return this; + } + +}); + + + +})(); + + +/* +--- + +name: Locale.en-US.Date + +description: Date messages for US English. + +license: MIT-style license + +authors: + - Aaron Newton + +requires: + - /Locale + +provides: [Locale.en-US.Date] + +... +*/ + +Locale.define('en-US', 'Date', { + + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + months_abbr: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + days_abbr: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + + // Culture's date order: MM/DD/YYYY + dateOrder: ['month', 'date', 'year'], + shortDate: '%m/%d/%Y', + shortTime: '%I:%M%p', + AM: 'AM', + PM: 'PM', + firstDayOfWeek: 0, + + // Date.Extras + ordinal: function(dayOfMonth){ + // 1st, 2nd, 3rd, etc. + return (dayOfMonth > 3 && dayOfMonth < 21) ? 'th' : ['th', 'st', 'nd', 'rd', 'th'][Math.min(dayOfMonth % 10, 4)]; + }, + + lessThanMinuteAgo: 'less than a minute ago', + minuteAgo: 'about a minute ago', + minutesAgo: '{delta} minutes ago', + hourAgo: 'about an hour ago', + hoursAgo: 'about {delta} hours ago', + dayAgo: '1 day ago', + daysAgo: '{delta} days ago', + weekAgo: '1 week ago', + weeksAgo: '{delta} weeks ago', + monthAgo: '1 month ago', + monthsAgo: '{delta} months ago', + yearAgo: '1 year ago', + yearsAgo: '{delta} years ago', + + lessThanMinuteUntil: 'less than a minute from now', + minuteUntil: 'about a minute from now', + minutesUntil: '{delta} minutes from now', + hourUntil: 'about an hour from now', + hoursUntil: 'about {delta} hours from now', + dayUntil: '1 day from now', + daysUntil: '{delta} days from now', + weekUntil: '1 week from now', + weeksUntil: '{delta} weeks from now', + monthUntil: '1 month from now', + monthsUntil: '{delta} months from now', + yearUntil: '1 year from now', + yearsUntil: '{delta} years from now' + +}); + + +/* +--- + +script: Date.js + +name: Date + +description: Extends the Date native object to include methods useful in managing dates. + +license: MIT-style license + +authors: + - Aaron Newton + - Nicholas Barthelemy - https://svn.nbarthelemy.com/date-js/ + - Harald Kirshner - mail [at] digitarald.de; http://digitarald.de + - Scott Kyle - scott [at] appden.com; http://appden.com + +requires: + - Core/Array + - Core/String + - Core/Number + - MooTools.More + - Locale + - Locale.en-US.Date + +provides: [Date] + +... +*/ + +(function(){ + +var Date = this.Date; + +var DateMethods = Date.Methods = { + ms: 'Milliseconds', + year: 'FullYear', + min: 'Minutes', + mo: 'Month', + sec: 'Seconds', + hr: 'Hours' +}; + +['Date', 'Day', 'FullYear', 'Hours', 'Milliseconds', 'Minutes', 'Month', 'Seconds', 'Time', 'TimezoneOffset', + 'Week', 'Timezone', 'GMTOffset', 'DayOfYear', 'LastMonth', 'LastDayOfMonth', 'UTCDate', 'UTCDay', 'UTCFullYear', + 'AMPM', 'Ordinal', 'UTCHours', 'UTCMilliseconds', 'UTCMinutes', 'UTCMonth', 'UTCSeconds', 'UTCMilliseconds'].each(function(method){ + Date.Methods[method.toLowerCase()] = method; +}); + +var pad = function(n, digits, string){ + if (digits == 1) return n; + return n < Math.pow(10, digits - 1) ? (string || '0') + pad(n, digits - 1, string) : n; +}; + +Date.implement({ + + set: function(prop, value){ + prop = prop.toLowerCase(); + var method = DateMethods[prop] && 'set' + DateMethods[prop]; + if (method && this[method]) this[method](value); + return this; + }.overloadSetter(), + + get: function(prop){ + prop = prop.toLowerCase(); + var method = DateMethods[prop] && 'get' + DateMethods[prop]; + if (method && this[method]) return this[method](); + return null; + }.overloadGetter(), + + clone: function(){ + return new Date(this.get('time')); + }, + + increment: function(interval, times){ + interval = interval || 'day'; + times = times != null ? times : 1; + + switch (interval){ + case 'year': + return this.increment('month', times * 12); + case 'month': + var d = this.get('date'); + this.set('date', 1).set('mo', this.get('mo') + times); + return this.set('date', d.min(this.get('lastdayofmonth'))); + case 'week': + return this.increment('day', times * 7); + case 'day': + return this.set('date', this.get('date') + times); + } + + if (!Date.units[interval]) throw new Error(interval + ' is not a supported interval'); + + return this.set('time', this.get('time') + times * Date.units[interval]()); + }, + + decrement: function(interval, times){ + return this.increment(interval, -1 * (times != null ? times : 1)); + }, + + isLeapYear: function(){ + return Date.isLeapYear(this.get('year')); + }, + + clearTime: function(){ + return this.set({hr: 0, min: 0, sec: 0, ms: 0}); + }, + + diff: function(date, resolution){ + if (typeOf(date) == 'string') date = Date.parse(date); + + return ((date - this) / Date.units[resolution || 'day'](3, 3)).round(); // non-leap year, 30-day month + }, + + getLastDayOfMonth: function(){ + return Date.daysInMonth(this.get('mo'), this.get('year')); + }, + + getDayOfYear: function(){ + return (Date.UTC(this.get('year'), this.get('mo'), this.get('date') + 1) + - Date.UTC(this.get('year'), 0, 1)) / Date.units.day(); + }, + + setDay: function(day, firstDayOfWeek){ + if (firstDayOfWeek == null){ + firstDayOfWeek = Date.getMsg('firstDayOfWeek'); + if (firstDayOfWeek === '') firstDayOfWeek = 1; + } + + day = (7 + Date.parseDay(day, true) - firstDayOfWeek) % 7; + var currentDay = (7 + this.get('day') - firstDayOfWeek) % 7; + + return this.increment('day', day - currentDay); + }, + + getWeek: function(firstDayOfWeek){ + if (firstDayOfWeek == null){ + firstDayOfWeek = Date.getMsg('firstDayOfWeek'); + if (firstDayOfWeek === '') firstDayOfWeek = 1; + } + + var date = this, + dayOfWeek = (7 + date.get('day') - firstDayOfWeek) % 7, + dividend = 0, + firstDayOfYear; + + if (firstDayOfWeek == 1){ + // ISO-8601, week belongs to year that has the most days of the week (i.e. has the thursday of the week) + var month = date.get('month'), + startOfWeek = date.get('date') - dayOfWeek; + + if (month == 11 && startOfWeek > 28) return 1; // Week 1 of next year + + if (month == 0 && startOfWeek < -2){ + // Use a date from last year to determine the week + date = new Date(date).decrement('day', dayOfWeek); + dayOfWeek = 0; + } + + firstDayOfYear = new Date(date.get('year'), 0, 1).get('day') || 7; + if (firstDayOfYear > 4) dividend = -7; // First week of the year is not week 1 + } else { + // In other cultures the first week of the year is always week 1 and the last week always 53 or 54. + // Days in the same week can have a different weeknumber if the week spreads across two years. + firstDayOfYear = new Date(date.get('year'), 0, 1).get('day'); + } + + dividend += date.get('dayofyear'); + dividend += 6 - dayOfWeek; // Add days so we calculate the current date's week as a full week + dividend += (7 + firstDayOfYear - firstDayOfWeek) % 7; // Make up for first week of the year not being a full week + + return (dividend / 7); + }, + + getOrdinal: function(day){ + return Date.getMsg('ordinal', day || this.get('date')); + }, + + getTimezone: function(){ + return this.toString() + .replace(/^.*? ([A-Z]{3}).[0-9]{4}.*$/, '$1') + .replace(/^.*?\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\)$/, '$1$2$3'); + }, + + getGMTOffset: function(){ + var off = this.get('timezoneOffset'); + return ((off > 0) ? '-' : '+') + pad((off.abs() / 60).floor(), 2) + pad(off % 60, 2); + }, + + setAMPM: function(ampm){ + ampm = ampm.toUpperCase(); + var hr = this.get('hr'); + if (hr > 11 && ampm == 'AM') return this.decrement('hour', 12); + else if (hr < 12 && ampm == 'PM') return this.increment('hour', 12); + return this; + }, + + getAMPM: function(){ + return (this.get('hr') < 12) ? 'AM' : 'PM'; + }, + + parse: function(str){ + this.set('time', Date.parse(str)); + return this; + }, + + isValid: function(date){ + if (!date) date = this; + return typeOf(date) == 'date' && !isNaN(date.valueOf()); + }, + + format: function(format){ + if (!this.isValid()) return 'invalid date'; + + if (!format) format = '%x %X'; + if (typeof format == 'string') format = formats[format.toLowerCase()] || format; + if (typeof format == 'function') return format(this); + + var d = this; + return format.replace(/%([a-z%])/gi, + function($0, $1){ + switch ($1){ + case 'a': return Date.getMsg('days_abbr')[d.get('day')]; + case 'A': return Date.getMsg('days')[d.get('day')]; + case 'b': return Date.getMsg('months_abbr')[d.get('month')]; + case 'B': return Date.getMsg('months')[d.get('month')]; + case 'c': return d.format('%a %b %d %H:%M:%S %Y'); + case 'd': return pad(d.get('date'), 2); + case 'e': return pad(d.get('date'), 2, ' '); + case 'H': return pad(d.get('hr'), 2); + case 'I': return pad((d.get('hr') % 12) || 12, 2); + case 'j': return pad(d.get('dayofyear'), 3); + case 'k': return pad(d.get('hr'), 2, ' '); + case 'l': return pad((d.get('hr') % 12) || 12, 2, ' '); + case 'L': return pad(d.get('ms'), 3); + case 'm': return pad((d.get('mo') + 1), 2); + case 'M': return pad(d.get('min'), 2); + case 'o': return d.get('ordinal'); + case 'p': return Date.getMsg(d.get('ampm')); + case 's': return Math.round(d / 1000); + case 'S': return pad(d.get('seconds'), 2); + case 'T': return d.format('%H:%M:%S'); + case 'U': return pad(d.get('week'), 2); + case 'w': return d.get('day'); + case 'x': return d.format(Date.getMsg('shortDate')); + case 'X': return d.format(Date.getMsg('shortTime')); + case 'y': return d.get('year').toString().substr(2); + case 'Y': return d.get('year'); + case 'z': return d.get('GMTOffset'); + case 'Z': return d.get('Timezone'); + } + return $1; + } + ); + }, + + toISOString: function(){ + return this.format('iso8601'); + } + +}).alias({ + toJSON: 'toISOString', + compare: 'diff', + strftime: 'format' +}); + +// The day and month abbreviations are standardized, so we cannot use simply %a and %b because they will get localized +var rfcDayAbbr = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + rfcMonthAbbr = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + +var formats = { + db: '%Y-%m-%d %H:%M:%S', + compact: '%Y%m%dT%H%M%S', + 'short': '%d %b %H:%M', + 'long': '%B %d, %Y %H:%M', + rfc822: function(date){ + return rfcDayAbbr[date.get('day')] + date.format(', %d ') + rfcMonthAbbr[date.get('month')] + date.format(' %Y %H:%M:%S %Z'); + }, + rfc2822: function(date){ + return rfcDayAbbr[date.get('day')] + date.format(', %d ') + rfcMonthAbbr[date.get('month')] + date.format(' %Y %H:%M:%S %z'); + }, + iso8601: function(date){ + return ( + date.getUTCFullYear() + '-' + + pad(date.getUTCMonth() + 1, 2) + '-' + + pad(date.getUTCDate(), 2) + 'T' + + pad(date.getUTCHours(), 2) + ':' + + pad(date.getUTCMinutes(), 2) + ':' + + pad(date.getUTCSeconds(), 2) + '.' + + pad(date.getUTCMilliseconds(), 3) + 'Z' + ); + } +}; + +var parsePatterns = [], + nativeParse = Date.parse; + +var parseWord = function(type, word, num){ + var ret = -1, + translated = Date.getMsg(type + 's'); + switch (typeOf(word)){ + case 'object': + ret = translated[word.get(type)]; + break; + case 'number': + ret = translated[word]; + if (!ret) throw new Error('Invalid ' + type + ' index: ' + word); + break; + case 'string': + var match = translated.filter(function(name){ + return this.test(name); + }, new RegExp('^' + word, 'i')); + if (!match.length) throw new Error('Invalid ' + type + ' string'); + if (match.length > 1) throw new Error('Ambiguous ' + type); + ret = match[0]; + } + + return (num) ? translated.indexOf(ret) : ret; +}; + +var startCentury = 1900, + startYear = 70; + +Date.extend({ + + getMsg: function(key, args){ + return Locale.get('Date.' + key, args); + }, + + units: { + ms: Function.from(1), + second: Function.from(1000), + minute: Function.from(60000), + hour: Function.from(3600000), + day: Function.from(86400000), + week: Function.from(608400000), + month: function(month, year){ + var d = new Date; + return Date.daysInMonth(month != null ? month : d.get('mo'), year != null ? year : d.get('year')) * 86400000; + }, + year: function(year){ + year = year || new Date().get('year'); + return Date.isLeapYear(year) ? 31622400000 : 31536000000; + } + }, + + daysInMonth: function(month, year){ + return [31, Date.isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; + }, + + isLeapYear: function(year){ + return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0); + }, + + parse: function(from){ + var t = typeOf(from); + if (t == 'number') return new Date(from); + if (t != 'string') return from; + from = from.clean(); + if (!from.length) return null; + + var parsed; + parsePatterns.some(function(pattern){ + var bits = pattern.re.exec(from); + return (bits) ? (parsed = pattern.handler(bits)) : false; + }); + + if (!(parsed && parsed.isValid())){ + parsed = new Date(nativeParse(from)); + if (!(parsed && parsed.isValid())) parsed = new Date(from.toInt()); + } + return parsed; + }, + + parseDay: function(day, num){ + return parseWord('day', day, num); + }, + + parseMonth: function(month, num){ + return parseWord('month', month, num); + }, + + parseUTC: function(value){ + var localDate = new Date(value); + var utcSeconds = Date.UTC( + localDate.get('year'), + localDate.get('mo'), + localDate.get('date'), + localDate.get('hr'), + localDate.get('min'), + localDate.get('sec'), + localDate.get('ms') + ); + return new Date(utcSeconds); + }, + + orderIndex: function(unit){ + return Date.getMsg('dateOrder').indexOf(unit) + 1; + }, + + defineFormat: function(name, format){ + formats[name] = format; + return this; + }, + + + + defineParser: function(pattern){ + parsePatterns.push((pattern.re && pattern.handler) ? pattern : build(pattern)); + return this; + }, + + defineParsers: function(){ + Array.flatten(arguments).each(Date.defineParser); + return this; + }, + + define2DigitYearStart: function(year){ + startYear = year % 100; + startCentury = year - startYear; + return this; + } + +}).extend({ + defineFormats: Date.defineFormat.overloadSetter() +}); + +var regexOf = function(type){ + return new RegExp('(?:' + Date.getMsg(type).map(function(name){ + return name.substr(0, 3); + }).join('|') + ')[a-z]*'); +}; + +var replacers = function(key){ + switch (key){ + case 'T': + return '%H:%M:%S'; + case 'x': // iso8601 covers yyyy-mm-dd, so just check if month is first + return ((Date.orderIndex('month') == 1) ? '%m[-./]%d' : '%d[-./]%m') + '([-./]%y)?'; + case 'X': + return '%H([.:]%M)?([.:]%S([.:]%s)?)? ?%p? ?%z?'; + } + return null; +}; + +var keys = { + d: /[0-2]?[0-9]|3[01]/, + H: /[01]?[0-9]|2[0-3]/, + I: /0?[1-9]|1[0-2]/, + M: /[0-5]?\d/, + s: /\d+/, + o: /[a-z]*/, + p: /[ap]\.?m\.?/, + y: /\d{2}|\d{4}/, + Y: /\d{4}/, + z: /Z|[+-]\d{2}(?::?\d{2})?/ +}; + +keys.m = keys.I; +keys.S = keys.M; + +var currentLanguage; + +var recompile = function(language){ + currentLanguage = language; + + keys.a = keys.A = regexOf('days'); + keys.b = keys.B = regexOf('months'); + + parsePatterns.each(function(pattern, i){ + if (pattern.format) parsePatterns[i] = build(pattern.format); + }); +}; + +var build = function(format){ + if (!currentLanguage) return {format: format}; + + var parsed = []; + var re = (format.source || format) // allow format to be regex + .replace(/%([a-z])/gi, + function($0, $1){ + return replacers($1) || $0; + } + ).replace(/\((?!\?)/g, '(?:') // make all groups non-capturing + .replace(/ (?!\?|\*)/g, ',? ') // be forgiving with spaces and commas + .replace(/%([a-z%])/gi, + function($0, $1){ + var p = keys[$1]; + if (!p) return $1; + parsed.push($1); + return '(' + p.source + ')'; + } + ).replace(/\[a-z\]/gi, '[a-z\\u00c0-\\uffff;\&]'); // handle unicode words + + return { + format: format, + re: new RegExp('^' + re + '$', 'i'), + handler: function(bits){ + bits = bits.slice(1).associate(parsed); + var date = new Date().clearTime(), + year = bits.y || bits.Y; + + if (year != null) handle.call(date, 'y', year); // need to start in the right year + if ('d' in bits) handle.call(date, 'd', 1); + if ('m' in bits || bits.b || bits.B) handle.call(date, 'm', 1); + + for (var key in bits) handle.call(date, key, bits[key]); + return date; + } + }; +}; + +var handle = function(key, value){ + if (!value) return this; + + switch (key){ + case 'a': case 'A': return this.set('day', Date.parseDay(value, true)); + case 'b': case 'B': return this.set('mo', Date.parseMonth(value, true)); + case 'd': return this.set('date', value); + case 'H': case 'I': return this.set('hr', value); + case 'm': return this.set('mo', value - 1); + case 'M': return this.set('min', value); + case 'p': return this.set('ampm', value.replace(/\./g, '')); + case 'S': return this.set('sec', value); + case 's': return this.set('ms', ('0.' + value) * 1000); + case 'w': return this.set('day', value); + case 'Y': return this.set('year', value); + case 'y': + value = +value; + if (value < 100) value += startCentury + (value < startYear ? 100 : 0); + return this.set('year', value); + case 'z': + if (value == 'Z') value = '+00'; + var offset = value.match(/([+-])(\d{2}):?(\d{2})?/); + offset = (offset[1] + '1') * (offset[2] * 60 + (+offset[3] || 0)) + this.getTimezoneOffset(); + return this.set('time', this - offset * 60000); + } + + return this; +}; + +Date.defineParsers( + '%Y([-./]%m([-./]%d((T| )%X)?)?)?', // "1999-12-31", "1999-12-31 11:59pm", "1999-12-31 23:59:59", ISO8601 + '%Y%m%d(T%H(%M%S?)?)?', // "19991231", "19991231T1159", compact + '%x( %X)?', // "12/31", "12.31.99", "12-31-1999", "12/31/2008 11:59 PM" + '%d%o( %b( %Y)?)?( %X)?', // "31st", "31st December", "31 Dec 1999", "31 Dec 1999 11:59pm" + '%b( %d%o)?( %Y)?( %X)?', // Same as above with month and day switched + '%Y %b( %d%o( %X)?)?', // Same as above with year coming first + '%o %b %d %X %z %Y', // "Thu Oct 22 08:11:23 +0000 2009" + '%T', // %H:%M:%S + '%H:%M( ?%p)?' // "11:05pm", "11:05 am" and "11:05" +); + +Locale.addEvent('change', function(language){ + if (Locale.get('Date')) recompile(language); +}).fireEvent('change', Locale.getCurrent()); + +})(); + + +/* +--- + +script: Date.Extras.js + +name: Date.Extras + +description: Extends the Date native object to include extra methods (on top of those in Date.js). + +license: MIT-style license + +authors: + - Aaron Newton + - Scott Kyle + +requires: + - /Date + +provides: [Date.Extras] + +... +*/ + +Date.implement({ + + timeDiffInWords: function(to){ + return Date.distanceOfTimeInWords(this, to || new Date); + }, + + timeDiff: function(to, separator){ + if (to == null) to = new Date; + var delta = ((to - this) / 1000).floor().abs(); + + var vals = [], + durations = [60, 60, 24, 365, 0], + names = ['s', 'm', 'h', 'd', 'y'], + value, duration; + + for (var item = 0; item < durations.length; item++){ + if (item && !delta) break; + value = delta; + if ((duration = durations[item])){ + value = (delta % duration); + delta = (delta / duration).floor(); + } + vals.unshift(value + (names[item] || '')); + } + + return vals.join(separator || ':'); + } + +}).extend({ + + distanceOfTimeInWords: function(from, to){ + return Date.getTimePhrase(((to - from) / 1000).toInt()); + }, + + getTimePhrase: function(delta){ + var suffix = (delta < 0) ? 'Until' : 'Ago'; + if (delta < 0) delta *= -1; + + var units = { + minute: 60, + hour: 60, + day: 24, + week: 7, + month: 52 / 12, + year: 12, + eon: Infinity + }; + + var msg = 'lessThanMinute'; + + for (var unit in units){ + var interval = units[unit]; + if (delta < 1.5 * interval){ + if (delta > 0.75 * interval) msg = unit; + break; + } + delta /= interval; + msg = unit + 's'; + } + + delta = delta.round(); + return Date.getMsg(msg + suffix, delta).substitute({delta: delta}); + } + +}).defineParsers( + + { + // "today", "tomorrow", "yesterday" + re: /^(?:tod|tom|yes)/i, + handler: function(bits){ + var d = new Date().clearTime(); + switch (bits[0]){ + case 'tom': return d.increment(); + case 'yes': return d.decrement(); + default: return d; + } + } + }, + + { + // "next Wednesday", "last Thursday" + re: /^(next|last) ([a-z]+)$/i, + handler: function(bits){ + var d = new Date().clearTime(); + var day = d.getDay(); + var newDay = Date.parseDay(bits[2], true); + var addDays = newDay - day; + if (newDay <= day) addDays += 7; + if (bits[1] == 'last') addDays -= 7; + return d.set('date', d.getDate() + addDays); + } + } + +).alias('timeAgoInWords', 'timeDiffInWords'); + + /* --- diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index 0a15efe6..442d61af 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -628,7 +628,7 @@ Option.Directory = new Class({ self.dir_list = new Element('ul', { 'events': { 'click:relay(li)': function(e, el){ - (e).stop(); + (e).preventDefault(); self.selectDirectory(el.get('data-value')) }, 'mousewheel': function(e){ @@ -678,7 +678,7 @@ Option.Directory = new Class({ hideBrowser: function(e, save){ var self = this; - (e).stop(); + (e).preventDefault(); if(save) self.save() @@ -1241,7 +1241,7 @@ Option.Combined = new Class({ deleteCombinedItem: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); var item = e.target.getParent(); diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index 9bf5e48a..8d1dd931 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -49,7 +49,7 @@ window.addEvent('domready', function(){ editMovie: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); if(!self.options_container){ self.options_container = new Element('div.options').adopt( @@ -89,7 +89,7 @@ window.addEvent('domready', function(){ }, save: function(e){ - (e).stop(); + (e).preventDefault(); var self = this; Api.request('movie.edit', { @@ -129,7 +129,7 @@ window.addEvent('domready', function(){ doRefresh: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); Api.request('movie.refresh', { 'data': { @@ -160,7 +160,7 @@ window.addEvent('domready', function(){ showConfirm: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); if(!self.delete_container){ self.delete_container = new Element('div.delete_container').adopt( @@ -188,13 +188,13 @@ window.addEvent('domready', function(){ hideConfirm: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); self.movie.slide('out'); }, del: function(e){ - (e).stop(); + (e).preventDefault(); var self = this; var movie = $(self.movie); @@ -253,7 +253,7 @@ window.addEvent('domready', function(){ showFiles: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); if(!self.options_container){ self.options_container = new Element('div.options').adopt( diff --git a/couchpotato/static/style/api.css b/couchpotato/static/style/api.css index afba2b5b..c6354098 100644 --- a/couchpotato/static/style/api.css +++ b/couchpotato/static/style/api.css @@ -1,4 +1,3 @@ - html { font-size: 12px; line-height: 1.5; diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 96058721..fd66f0eb 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -1,10 +1,3 @@ -/* @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 - http://127.0.0.1:5000/v2/_api_/static/style/main.css -*/ - html { color: #fff; font-size: 12px; @@ -184,6 +177,7 @@ body > .spinner, .mask{ } .header .navigation { display: inline-block; + vertical-align: middle; width: 67.2%; } .header .navigation ul { @@ -239,8 +233,7 @@ body > .spinner, .mask{ .header:hover .navigation .backtotop { color: #fff; } .header .more_menu { - float: right; - margin-top: 20px; + margin-left: 12px; } .header .more_menu ul { width: 150px; @@ -252,6 +245,51 @@ body > .spinner, .mask{ .header .more_menu .red { color: red; } .header .more_menu .orange { color: orange; } + + .badge { + position: absolute; + width: 14px; + height: 14px; + text-align: center; + line-height: 14px; + border-radius: 50%; + font-size: 8px; + margin: -5px 0 0 15px; + box-shadow: inset 0 1px 0 rgba(255,255,255,.6), 0 0 3px rgba(0,0,0,.7); + background: -webkit-gradient(linear, left bottom, left top, from(rgba(255,255,255,.3)), to(rgba(255,255,255,.1))); + background: -moz-linear-gradient(center bottom, rgba(255,255,255,.3) 0%, rgba(255,255,255,.1) 100%); + background-color: #1b79b8; + text-shadow: none; + } + + .header .notification_menu ul { + width: 300px; + margin-left: -260px; + text-align: left; + } + .header .notification_menu ul:before { + left: 296px; + } + + .header .notification_menu > a { + background-position: center -209px; + } + + .header .notification_menu li > span { + padding: 5px; + display: block; + border-bottom: 1px solid rgba(0,0,0,0.2); + } + .header .notification_menu li .added { + display: block; + font-size: 10px; + color: #aaa; + text-align: ; + } + + .header .notification_menu li .more { + text-align: center; + } .header .message.update { text-align: center; @@ -454,6 +492,10 @@ body > .spinner, .mask{ background-color: #4c5766; } + .more_menu { + display: inline-block; + vertical-align: middle; + } .more_menu > a { display: block; @@ -470,16 +512,30 @@ body > .spinner, .mask{ .more_menu ul { display: none; border: 1px solid #333; - background: rgba(0,0,0,0.8); + background: rgba(255,255,255,0.98); border-radius: 3px; padding: 4px !important; position: absolute; z-index: 9; margin: 32px 0 0 -145px; width: 185px; - box-shadow: 0 10px 20px -10px rgba(0,0,0,0.4); + box-shadow: 0 10px 10px -5px rgba(0,0,0,0.4); list-style: none; text-align: center; + color: #000; + text-shadow: none; + background-image: -webkit-gradient( + linear, + left bottom, + right top, + color-stop(0, rgb(200,200,200)), + color-stop(1, rgb(255,255,255)) + ); + background-image: -moz-linear-gradient( + left bottom, + rgb(200,200,200) 0%, + rgb(255,255,255) 100% + ); } .more_menu ul:before { @@ -488,7 +544,7 @@ body > .spinner, .mask{ position: relative; width: 0; border: 6px solid transparent; - border-bottom-color: rgba(0,0,0,0.8); + border-bottom-color: #fff; display: block; top: -16px; left: 146px; @@ -510,7 +566,7 @@ body > .spinner, .mask{ text-transform: uppercase; letter-spacing: 1px; padding: 3px 0; - color: #fff; + color: #000; } .more_menu ul li:first-child { @@ -519,8 +575,7 @@ body > .spinner, .mask{ .more_menu ul li:last-child a { border: none; - color: #fff; } .more_menu ul li a:hover { - background: rgba(255,255,255,0.1); + background: rgba(0,0,0,0.05); } \ No newline at end of file diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index 40ae4700..98ac0332 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -1,9 +1,3 @@ -/* @override - http://localhost:5000/static/style/page/settings.css - http://192.168.1.20:5000/static/style/page/settings.css - http://127.0.0.1:5000/static/style/page/settings.css -*/ - .page.settings:after { content: "."; display: block; diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html index 8df06c4b..751850f7 100644 --- a/couchpotato/templates/_desktop.html +++ b/couchpotato/templates/_desktop.html @@ -30,7 +30,7 @@ - + From 43529a9e64a0e1b04bf4457e707a40d145c1a84d Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 15 Mar 2012 20:56:36 +0100 Subject: [PATCH 73/99] No option for CP provider --- .../core/providers/automation/cp/__init__.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/couchpotato/core/providers/automation/cp/__init__.py b/couchpotato/core/providers/automation/cp/__init__.py index 914c1f53..a4b55a83 100644 --- a/couchpotato/core/providers/automation/cp/__init__.py +++ b/couchpotato/core/providers/automation/cp/__init__.py @@ -3,21 +3,4 @@ from .main import CP def start(): return CP() -config = [{ - 'name': 'cp', - 'groups': [ - { - 'tab': 'automation', - 'name': 'couchpotato_automation', - 'label': 'CouchPotato', - 'description': 'Enable automatic movie adding from CouchPotato', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - ], - }, - ], -}] +config = [] From a9cc7a457b7f01f4fbacc0c8715969712e61db6d Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 15 Mar 2012 22:16:22 +0100 Subject: [PATCH 74/99] List notifications --- couchpotato/core/notifications/core/main.py | 44 ++++++++++++++++++- .../notifications/core/static/notification.js | 30 ++++++------- couchpotato/core/plugins/movie/main.py | 2 +- .../core/plugins/movie/static/movie.css | 23 ++++++++-- .../core/plugins/movie/static/movie.js | 6 ++- couchpotato/static/scripts/block/menu.js | 4 +- couchpotato/static/style/main.css | 42 +++++++++++------- 7 files changed, 109 insertions(+), 42 deletions(-) diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index df53347d..c3a16214 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -33,6 +33,19 @@ class CoreNotifier(Notification): 'id': {'desc': 'Notification id you want to mark as read.', 'type': 'int (comma separated)'}, }, }) + + addApiView('notification.list', self.listView, docs = { + 'desc': 'Get list of notifications', + 'params': { + 'limit_offset': {'desc': 'Limit and offset the notification list. Examples: "50" or "50,30"'}, + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'empty': bool, any notification returned or not, + 'notifications': array, notifications found, +}"""} + }) + addApiView('notification.listener', self.listener) self.registerEvents() @@ -57,6 +70,32 @@ class CoreNotifier(Notification): 'success': True }) + def listView(self): + + db = get_session() + limit_offset = getParam('limit_offset', None) + + q = db.query(Notif) + + if limit_offset: + splt = limit_offset.split(',') + limit = splt[0] + offset = 0 if len(splt) is 1 else splt[1] + q = q.limit(limit).offset(offset) + + results = q.all() + notifications = [] + for n in results: + ndict = n.to_dict() + ndict['type'] = 'notification' + notifications.append(ndict) + + return jsonified({ + 'success': True, + 'empty': len(notifications) == 0, + 'notifications': notifications + }) + def notify(self, message = '', data = {}): db = get_session() @@ -93,7 +132,10 @@ class CoreNotifier(Notification): # Get unread if getParam('init'): db = get_session() - notifications = db.query(Notif).filter_by(read = False).all() + + notifications = db.query(Notif) \ + .filter(or_(Notif.read == False, Notif.added > (time.time() - 259200))) \ + .all() for n in notifications: ndict = n.to_dict() ndict['type'] = 'notification' diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index 617ef2ac..371b95ed 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -26,10 +26,10 @@ var NotificationBase = new Class({ $(App.block.notification).inject(App.getBlock('search'), 'after'); self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide(); - App.getBlock('notification').addLink(new Element('a.more', { + /* App.getBlock('notification').addLink(new Element('a.more', { 'href': App.createUrl('notifications'), - 'text': 'See more notifications' - })); + 'text': 'Show older notifications' + })); */ }) }, @@ -41,7 +41,7 @@ var NotificationBase = new Class({ added.setTime(result.added*1000) result.el = App.getBlock('notification').addLink( - new Element('span').adopt( + new Element('span.'+(result.read ? 'read' : '' )).adopt( new Element('span.message', {'text': result.message}), new Element('span.added', {'text': added.timeDiffInWords(), 'title': added}) ) @@ -51,11 +51,6 @@ var NotificationBase = new Class({ if(!result.read) self.setBadge(self.notifications.filter(function(n){ return !n.read}).length) - if(self.notifications.length >= 5){ - var n = self.notifications[self.notifications.length-5]; - n.el.destroy(); - } - }, setBadge: function(value){ @@ -76,14 +71,15 @@ var NotificationBase = new Class({ ids.include(n.id) }) - Api.request('notification.markread', { - 'data': { - 'ids': ids.join(',') - }, - 'onSuccess': function(){ - self.setBadge('') - } - }) + if(ids.length > 0) + Api.request('notification.markread', { + 'data': { + 'ids': ids.join(',') + }, + 'onSuccess': function(){ + self.setBadge('') + } + }) }, diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index 32cee0c5..433ecaa9 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -41,7 +41,7 @@ class MoviePlugin(Plugin): 'desc': 'List movies in wanted list', 'params': { 'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'}, - 'limit_offset': {'desc': 'Limit the movie list. Examples: "50", "50,30"'}, + 'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'}, 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'}, 'search': {'desc': 'Search movie title'}, }, diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 36ff17c3..b66d0467 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -89,6 +89,7 @@ font-size: 16px; font-weight: normal; text-overflow: ellipsis; + width: 64%; } .movies .info .year { @@ -125,12 +126,22 @@ .movies .list_view .info .description, .movies .mass_edit_view .info .description { display: none; } - + + .movies .data .quality { + display: block; + min-height: 20px; + vertical-align: mid; + } + .movies .data .quality span { padding: 2px 3px; font-weight: bold; opacity: 0.5; font-size: 10px; + height: 16px; + line-height: 12px; + vertical-align: middle; + display: inline-block; text-transform: uppercase; text-shadow: none; font-weight: normal; @@ -141,7 +152,7 @@ .movies .list_view .data .quality, .movies .mass_edit_view .data .quality { text-align: right; float: right; - width: 35%; + width: 30%; } .movies .data .quality .available, .movies .data .quality .snatched { @@ -152,6 +163,10 @@ .movies .data .quality .available { background-color: #578bc3; } .movies .data .quality .snatched { background-color: #369545; } + .movies .data .quality .done { + background-color: #369545; + opacity: 1; + } .movies .data .quality .finish { background-image: url('../images/sprite.png'); background-repeat: no-repeat; @@ -183,7 +198,7 @@ } .movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions { - margin: -35px -7px 0 0; + margin: -34px 2px 0 0; background: #4e5969; } @@ -272,7 +287,7 @@ text-align: left; padding: 0 10px; } - .movies .options .table.files .name { width: 608px; } + .movies .options .table.files .name { width: 605px; } .movies .options .table .type { width: 130px; } .movies .options .table .is_available { width: 90px; } diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index 53cd9e31..ee26ab95 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -49,7 +49,9 @@ var Movie = new Class({ self.quality = new Element('div.quality', { 'events': { 'click': function(e){ - self.el.getElement('.actions .releases').fireEvent('click', [e]) + var releases = self.el.getElement('.actions .releases'); + if(releases) + releases.fireEvent('click', [e]) } } }) @@ -101,7 +103,7 @@ var Movie = new Class({ var q = Quality.getQuality(quality_id); return new Element('span', { 'text': q.label, - 'class': 'q_'+q.identifier + 'q_id' + q.quality_id + 'class': 'q_'+q.identifier + ' q_id' + q.id }).inject(self.quality); }, diff --git a/couchpotato/static/scripts/block/menu.js b/couchpotato/static/scripts/block/menu.js index f8ca6f11..4dc143d4 100644 --- a/couchpotato/static/scripts/block/menu.js +++ b/couchpotato/static/scripts/block/menu.js @@ -12,7 +12,9 @@ Block.Menu = new Class({ self.el = new Element('div', { 'class': 'more_menu '+self.options['class'] }).adopt( - self.more_option_ul = new Element('ul'), + self.wrapper = new Element('div.wrapper').adopt( + self.more_option_ul = new Element('ul') + ), new Element('a.button.onlay', { 'events': { 'click': function(){ diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index fd66f0eb..9975da70 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -235,11 +235,11 @@ body > .spinner, .mask{ .header .more_menu { margin-left: 12px; } - .header .more_menu ul { + .header .more_menu .wrapper { width: 150px; margin-left: -110px; } - .header .more_menu ul:before { + .header .more_menu .wrapper:before { margin-left: -34px; } @@ -262,15 +262,21 @@ body > .spinner, .mask{ text-shadow: none; } - .header .notification_menu ul { + .header .notification_menu .wrapper { width: 300px; margin-left: -260px; text-align: left; } - .header .notification_menu ul:before { + + .header .notification_menu .wrapper:before { left: 296px; } + .header .notification_menu ul { + max-height: 300px; + overflow: auto; + } + .header .notification_menu > a { background-position: center -209px; } @@ -280,6 +286,8 @@ body > .spinner, .mask{ display: block; border-bottom: 1px solid rgba(0,0,0,0.2); } + .header .notification_menu li > span { color: #777; } + .header .notification_menu li:last-child > span { border: 0; } .header .notification_menu li .added { display: block; font-size: 10px; @@ -509,7 +517,7 @@ body > .spinner, .mask{ background-color: #406db8; } - .more_menu ul { + .more_menu .wrapper { display: none; border: 1px solid #333; background: rgba(255,255,255,0.98); @@ -520,7 +528,6 @@ body > .spinner, .mask{ margin: 32px 0 0 -145px; width: 185px; box-shadow: 0 10px 10px -5px rgba(0,0,0,0.4); - list-style: none; text-align: center; color: #000; text-shadow: none; @@ -538,7 +545,7 @@ body > .spinner, .mask{ ); } - .more_menu ul:before { + .more_menu .wrapper:before { content: ' '; height: 0; position: relative; @@ -549,15 +556,22 @@ body > .spinner, .mask{ top: -16px; left: 146px; } - .more_menu.show ul { + .more_menu.show .wrapper { display: block; } - .more_menu ul li { + + .more_menu ul { + padding: 0; + margin: -12px 0 0 0; + list-style: none; + } + + .more_menu .wrapper li { width: 100%; height: auto; } - .more_menu ul li a { + .more_menu .wrapper li a { display: block; border-bottom: 1px solid rgba(255,255,255,0.2); box-shadow: none; @@ -568,14 +582,10 @@ body > .spinner, .mask{ padding: 3px 0; color: #000; } - - .more_menu ul li:first-child { - margin-top: -12px; - } - .more_menu ul li:last-child a { + .more_menu .wrapper li:last-child a { border: none; } - .more_menu ul li a:hover { + .more_menu .wrapper li a:hover { background: rgba(0,0,0,0.05); } \ No newline at end of file From 02fb1a5741f19df46e88c28de4e8098a1561199d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 18 Mar 2012 16:02:26 +0100 Subject: [PATCH 75/99] Userscript bookmarklet --- .../core/plugins/userscript/bookmark.js | 43 +++++++++++++++ couchpotato/core/plugins/userscript/main.py | 11 ++++ .../plugins/userscript/static/userscript.js | 41 ++++++++++++-- .../plugins/userscript/static/userscript.png | Bin 0 -> 101726 bytes .../core/plugins/userscript/template.js | 51 ++++++++++-------- couchpotato/static/style/page/settings.css | 40 ++++++++++++++ couchpotato/templates/_desktop.html | 1 + 7 files changed, 161 insertions(+), 26 deletions(-) create mode 100644 couchpotato/core/plugins/userscript/bookmark.js create mode 100644 couchpotato/core/plugins/userscript/static/userscript.png diff --git a/couchpotato/core/plugins/userscript/bookmark.js b/couchpotato/core/plugins/userscript/bookmark.js new file mode 100644 index 00000000..3e0e517a --- /dev/null +++ b/couchpotato/core/plugins/userscript/bookmark.js @@ -0,0 +1,43 @@ +var includes = {{includes|tojson}}; +var excludes = {{excludes|tojson}}; + +var specialChars = '\\{}+.():-|^$'; +var makeRegex = function(pattern) { + pattern = pattern.split(''); + var i, len = pattern.length; + for( i = 0; i < len; i++) { + var character = pattern[i]; + if(specialChars.indexOf(character) > -1) { + pattern[i] = '\\' + character; + } else if(character === '?') { + pattern[i] = '.'; + } else if(character === '*') { + pattern[i] = '.*'; + } + } + return new RegExp('^' + pattern.join('') + '$'); +}; + +var isCorrectUrl = function() { + for(i in includes) { + var reg = includes[i] + if (makeRegex(reg).test(document.location.href)) + return true; + } + return false; +} +var addUserscript = function() { + // Add window param + document.body.setAttribute('cp_auto_open', true) + + // Load userscript + var e = document.createElement('script'); + e.setAttribute('type', 'text/javascript'); + e.setAttribute('charset', 'UTF-8'); + e.setAttribute('src', '{{host}}/userscript.get/couchpotato.js?r=' + Math.random() * 99999999); + document.body.appendChild(e) +} +if(isCorrectUrl()) + addUserscript() +else + alert('Can\'t find a proper movie on this page..') diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py index 98c959bb..1f9c0eb0 100644 --- a/couchpotato/core/plugins/userscript/main.py +++ b/couchpotato/core/plugins/userscript/main.py @@ -22,9 +22,20 @@ class Userscript(Plugin): addApiView('userscript.get/', self.getUserScript, static = True) addApiView('userscript', self.iFrame) addApiView('userscript.add_via_url', self.getViaUrl) + addApiView('userscript.bookmark', self.bookmark) addEvent('userscript.get_version', self.getVersion) + def bookmark(self): + + params = { + 'includes': fireEvent('userscript.get_includes', merge = True), + 'excludes': fireEvent('userscript.get_excludes', merge = True), + 'host': fireEvent('app.api_url', single = True) + } + + return self.renderTemplate(__file__, 'bookmark.js', **params) + def getUserScript(self, filename = ''): params = { diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index f167739b..f47825ea 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -63,15 +63,48 @@ var UserscriptSettingTab = new Class({ self.settings = App.getPage('Settings') self.settings.addEvent('create', function(){ + // See if userscript can be installed + var userscript = false; + try { + if(Components.interfaces.gmIGreasemonkeyService) + userscript = true + } + catch(e){ + userscript = Browser.chrome === true; + } + self.settings.createGroup({ - 'label': 'Install the Userscript' + 'name': 'userscript', + 'label': 'Install the bookmarklet' + (userscript ? ' or userscript' : ''), + 'description': 'Easily add movies via imdb.com, appletrailers and more' }).inject(self.settings.tabs.automation.content, 'top').adopt( - new Element('a', { + (userscript ? [new Element('a.userscript.button', { 'text': 'Install userscript', 'href': Api.createUrl('userscript.get')+'couchpotato.user.js', 'target': '_self' - }) - ); + }), new Element('span.or[text=or]')] : null), + new Element('span.bookmarklet').adopt( + new Element('a.button.green', { + 'text': '+CouchPotato', + 'href': "javascript:void((function(){var e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','" + + Api.getOption('host') + '/userscript.bookmark/' + + "?r='+Math.random()*99999999);document.body.appendChild(e)})());", + 'target': '', + 'events': { + 'click': function(e){ + (e).stop() + alert('Drag it to your bookmark ;)') + } + } + }), + new Element('span', { + 'text': '⇽ Drag this to your bookmarks' + }) + ) + ).setStyles({ + 'background-image': "url('"+Api.createUrl('static/userscript/userscript.png')+"')" + }); + }); } diff --git a/couchpotato/core/plugins/userscript/static/userscript.png b/couchpotato/core/plugins/userscript/static/userscript.png new file mode 100644 index 0000000000000000000000000000000000000000..c8e7657783a20c6e920521f23e94fcde73964a25 GIT binary patch literal 101726 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 EKNb^AV*mgE literal 0 HcmV?d00001 diff --git a/couchpotato/core/plugins/userscript/template.js b/couchpotato/core/plugins/userscript/template.js index e2e9b85e..158638e7 100644 --- a/couchpotato/core/plugins/userscript/template.js +++ b/couchpotato/core/plugins/userscript/template.js @@ -85,35 +85,42 @@ var osd = function(){ var popup = create('div', { 'id': 'cp_popup' }); + + var onclick = function(){ + + // Try and get imdb url + try { + var regex = new RegExp(/tt(\d+)/); + var imdb_id = document.body.innerHTML.match(regex)[0]; + if (imdb_id) + iframe.setAttribute('src', createApiUrl('http://imdb.com/title/'+imdb_id+'/')) + } + catch(e){} + + popup.innerHTML = ''; + popup.appendChild(create('a', { + 'innerHTML': '', + 'id': 'close_button', + 'onclick': function(){ + popup.innerHTML = ''; + popup.appendChild(add_button); + } + })); + popup.appendChild(iframe) + } + var add_button = create('a', { 'innerHTML': '', 'id': 'add_to', - 'onclick': function(){ - - // Try and get imdb url - try { - var regex = new RegExp(/tt(\d+)/); - var imdb_id = document.body.innerHTML.match(regex)[0]; - if (imdb_id) - iframe.setAttribute('src', createApiUrl('http://imdb.com/title/'+imdb_id+'/')) - } - catch(e){} - - popup.innerHTML = ''; - popup.appendChild(create('a', { - 'innerHTML': '', - 'id': 'close_button', - 'onclick': function(){ - popup.innerHTML = ''; - popup.appendChild(add_button); - } - })); - popup.appendChild(iframe) - } + 'onclick': onclick }); popup.appendChild(add_button); document.body.parentNode.insertBefore(popup, document.body); + + // Auto fold open + if(document.body.getAttribute('cp_auto_open')) + onclick() }; var setVersion = function(){ diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index 98ac0332..361a2e34 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -1,3 +1,6 @@ +/* @override + http://127.0.0.1:5000/_api_/static/style/page/settings.css */ + .page.settings:after { content: "."; display: block; @@ -550,4 +553,41 @@ .page .tab_about .group_actions a { margin: 0 10px; font-size: 20px; + } + +.group_userscript { + background: center bottom no-repeat; + min-height: 360px; + font-size: 20px; + font-weight: normal; +} + + .group_userscript h2 .hint { + display: block; + margin: 0 !important; + } + + .group_userscript .userscript { + float: left; + margin: 14px 0 0 25px; + height: 36px; + line-height: 25px; + } + + .group_userscript .or { + float: left; + margin: 20px 10px; + } + + .group_userscript .bookmarklet { + display: block; + display: block; + float: left; + padding: 20px 15px 0 0 ; + border-radius: 5px; + } + + .group_userscript .bookmarklet span { + margin-left: 10px; + display: inline-block; } \ No newline at end of file diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html index 751850f7..63aa4dd8 100644 --- a/couchpotato/templates/_desktop.html +++ b/couchpotato/templates/_desktop.html @@ -51,6 +51,7 @@ new Uniform(); Api.setup({ + 'host': {{ fireEvent('app.api_url', single = True)|tojson|safe }}, 'url': {{ url_for('api.index')|tojson|safe }}, 'path_sep': {{ sep|tojson|safe }}, 'is_remote': false From 59c2e101d7d2b6d0390f7e5e1b07bde4e44792f7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 18 Mar 2012 17:08:22 +0100 Subject: [PATCH 76/99] Reference to self --- couchpotato/static/scripts/couchpotato.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index e4263884..b615f341 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -70,13 +70,13 @@ var CouchPotato = new Class({ [new Element('a.orange', { 'text': 'Restart', 'events': { - 'click': App.restart.bind(App) + 'click': self.restart.bind(self) } }), new Element('a.red', { 'text': 'Shutdown', 'events': { - 'click': App.shutdown.bind(App) + 'click': self.shutdown.bind(self) } }), new Element('a', { From 4cca9fbb6a9b5e7ec9b6b6780d56ed03b5ddce9f Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 18 Mar 2012 17:25:23 +0100 Subject: [PATCH 77/99] Use host from current page, nog from config --- couchpotato/core/plugins/userscript/static/userscript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index f47825ea..bdf0f27c 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -87,7 +87,7 @@ var UserscriptSettingTab = new Class({ new Element('a.button.green', { 'text': '+CouchPotato', 'href': "javascript:void((function(){var e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','" + - Api.getOption('host') + '/userscript.bookmark/' + + window.location.protocol + '//' + window.location.host + Api.createUrl('userscript.bookmark') + "?r='+Math.random()*99999999);document.body.appendChild(e)})());", 'target': '', 'events': { From 7dd89247f2e244d4ae1b4f1b3f015de4f522ec9b Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 18 Mar 2012 17:44:20 +0100 Subject: [PATCH 78/99] Post hostname with bookmark --- couchpotato/core/plugins/userscript/bookmark.js | 2 +- couchpotato/core/plugins/userscript/main.py | 2 +- couchpotato/core/plugins/userscript/static/userscript.js | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/userscript/bookmark.js b/couchpotato/core/plugins/userscript/bookmark.js index 3e0e517a..5ee8c376 100644 --- a/couchpotato/core/plugins/userscript/bookmark.js +++ b/couchpotato/core/plugins/userscript/bookmark.js @@ -34,7 +34,7 @@ var addUserscript = function() { var e = document.createElement('script'); e.setAttribute('type', 'text/javascript'); e.setAttribute('charset', 'UTF-8'); - e.setAttribute('src', '{{host}}/userscript.get/couchpotato.js?r=' + Math.random() * 99999999); + e.setAttribute('src', '{{host}}couchpotato.js?r=' + Math.random() * 99999999); document.body.appendChild(e) } if(isCorrectUrl()) diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py index 1f9c0eb0..160d85ab 100644 --- a/couchpotato/core/plugins/userscript/main.py +++ b/couchpotato/core/plugins/userscript/main.py @@ -31,7 +31,7 @@ class Userscript(Plugin): params = { 'includes': fireEvent('userscript.get_includes', merge = True), 'excludes': fireEvent('userscript.get_excludes', merge = True), - 'host': fireEvent('app.api_url', single = True) + 'host': getParam('host', None), } return self.renderTemplate(__file__, 'bookmark.js', **params) diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index bdf0f27c..21edc636 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -72,6 +72,8 @@ var UserscriptSettingTab = new Class({ catch(e){ userscript = Browser.chrome === true; } + + var host_url = window.location.protocol + '//' + window.location.host; self.settings.createGroup({ 'name': 'userscript', @@ -87,8 +89,9 @@ var UserscriptSettingTab = new Class({ new Element('a.button.green', { 'text': '+CouchPotato', 'href': "javascript:void((function(){var e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','" + - window.location.protocol + '//' + window.location.host + Api.createUrl('userscript.bookmark') + - "?r='+Math.random()*99999999);document.body.appendChild(e)})());", + host_url + Api.createUrl('userscript.bookmark') + + "?host="+ encodeURI(host_url + Api.createUrl('userscript.get')) + + "&r='+Math.random()*99999999);document.body.appendChild(e)})());", 'target': '', 'events': { 'click': function(e){ From 7a246bc190965bb363f9efd2ac2e7755c2998353 Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Apr 2012 21:34:12 +0200 Subject: [PATCH 79/99] Safe filename for nzb files --- couchpotato/core/downloaders/base.py | 9 ++++++--- couchpotato/core/downloaders/nzbget/main.py | 2 +- couchpotato/core/downloaders/sabnzbd/main.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index e90f951a..e1e851f3 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -18,9 +18,12 @@ class Downloader(Plugin): def download(self, data = {}): pass - def createFileName(self, data, filename, movie): - name = os.path.join('%s%s' % (toSafeString(data.get('name')), self.cpTag(movie))) - if data.get('type') == 'nzb' and "DOCTYPE nzb" not in filename: + def createNzbName(self, data, movie): + return '%s%s' % (toSafeString(data.get('name')), self.cpTag(movie)) + + def createFileName(self, data, filedata, movie): + name = os.path.join(self.createNzbName(data, movie)) + if data.get('type') == 'nzb' and "DOCTYPE nzb" not in filedata: return '%s.%s' % (name, 'rar') return '%s.%s' % (name, data.get('type')) diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index 083f312d..b65e2511 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -22,7 +22,7 @@ class NZBGet(Downloader): log.info('Sending "%s" to NZBGet.' % data.get('name')) url = self.url % {'host': self.conf('host'), 'password': self.conf('password')} - nzb_name = data.get('name') + '.nzb' + nzb_name = '%s.nzb' % self.createNzbName(data, movie) rpc = xmlrpclib.ServerProxy(url) try: diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index d2b47786..41ac9206 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -39,7 +39,7 @@ class Sabnzbd(Downloader): 'apikey': self.conf('api_key'), 'cat': self.conf('category'), 'mode': 'addurl', - 'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)), + 'nzbname': self.createNzbName(data, movie), } if isfunction(data.get('download')): From 8c0de491dc428752e10919cb876101985577fd4d Mon Sep 17 00:00:00 2001 From: Ruud Date: Tue, 3 Apr 2012 23:18:55 +0200 Subject: [PATCH 80/99] Correct type check fix for downloaders --- couchpotato/core/downloaders/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index e1e851f3..39ee6129 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -33,10 +33,10 @@ class Downloader(Plugin): return '' - def isCorrectType(self, type): - is_correct = type in self.type + def isCorrectType(self, item_type): + is_correct = item_type in self.type if not is_correct: log.debug("Downloader doesn't support this type") - return bool + return is_correct From ee4e91d31816918cba7c2744c01a2373dfde439a Mon Sep 17 00:00:00 2001 From: "Michael J. Cohen" Date: Tue, 3 Apr 2012 19:34:21 -0400 Subject: [PATCH 81/99] Default exception handler no longer assumes all remaining exceptions take (errno, string) pairs. Previously any exception that made it all the way up to the default exception handler would be expected to take (errno, string) pairs, as is the python standard for exceptions thrown by system calls. All exceptions that don't take enough arguments throw a ValueError. Based on the errno tested, it appears that this code is meant to silently ignore when a socket receives a SIGINT (from e.g. a timeout.) This seems to be the only instance where handling EINTR in this manner is desired - though having this bubble up this far seems odd. The existing code would also handle any other EINTR, though, which includes those raised by OSError, WindowsError, and anything that subclasses EnvironmentError, barring KeyboardError because it is handled separately. This is a bug as there is already some use of the signals module elsewhere in CouchPotato.py to trap SIGINT and SIGTERM outside of system calls, and most of these other EINTRs should be handled by code lower down the stack. A default exception handler is also added, so that unhandled exceptions will be logged, and raised. --- CouchPotato.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CouchPotato.py b/CouchPotato.py index 3bc7916f..57f6461c 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -4,6 +4,7 @@ from os.path import dirname import logging import os import signal +import socket import subprocess import sys import traceback @@ -121,9 +122,20 @@ if __name__ == '__main__': pass except SystemExit: raise - except Exception as (nr, msg): + except socket.error as (nr, msg): + # log when socket receives SIGINT, but continue. + # previous code would have skipped over other types of IO errors too. if nr != 4: try: l.log.critical(traceback.format_exc()) except: print traceback.format_exc() + raise + except: + try: + # if this fails we will have two tracebacks + # one for failing to log, and one for the exception that got us here. + l.log.critical(traceback.format_exc()) + except: + print traceback.format_exc() + raise \ No newline at end of file From e99cf6757e31802e25b104c8b87f7b238fec147d Mon Sep 17 00:00:00 2001 From: "Michael J. Cohen" Date: Tue, 3 Apr 2012 20:35:25 -0400 Subject: [PATCH 82/99] Throw a useful error in the log when FileBrowser fails to load because of missing pywin32 This is a quick hack so that anyone else who runs this from source doesn't have to spend the time I spent tracking down why directory.list failed silently. There are two options that are much cleaner that come to mind: - Subclass ImportException so as to differentiate missing requirements from parse errors etc. - Provide a method for plugins to list their requirements, so that the loader can be the one to use imp.find_module(). Using imp.find_module() seems wise, either way. --- couchpotato/core/loader.py | 9 +++++++-- couchpotato/core/plugins/browser/main.py | 10 +++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/loader.py b/couchpotato/core/loader.py index a5816cc2..ee7afcb1 100644 --- a/couchpotato/core/loader.py +++ b/couchpotato/core/loader.py @@ -51,8 +51,13 @@ class Loader(object): did_save += self.loadSettings(m, module_name, save = False) self.loadPlugins(m, plugin.get('name')) - except ImportError: - log.debug('Import error, remove the empty folder: %s' % plugin.get('module')) + except ImportError as e: + # todo:: subclass ImportError for missing requirements. + if (e.message.lower().startswith("missing")): + log.error(e.message) + pass + # todo:: this needs to be more descriptive. + log.error('Import error, remove the empty folder: %s' % plugin.get('module')) except: log.error('Can\'t import %s: %s' % (module_name, traceback.format_exc())) diff --git a/couchpotato/core/plugins/browser/main.py b/couchpotato/core/plugins/browser/main.py index 21d3b4b7..887edc30 100644 --- a/couchpotato/core/plugins/browser/main.py +++ b/couchpotato/core/plugins/browser/main.py @@ -6,7 +6,15 @@ import os import string if os.name == 'nt': - import win32file + import imp + try: + imp.find_module('win32file') + except: + # todo:: subclass ImportError for missing dependencies, vs. broken plugins? + raise ImportError("Missing the win32file module, which is a part of the prerequisite \ + pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/"); + else: + import win32file class FileBrowser(Plugin): From ed280b988bbb666f639a76e9e1d47ad7a5b78550 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 4 Apr 2012 22:41:54 +0200 Subject: [PATCH 83/99] Bluray groups for nzb providers --- couchpotato/core/providers/nzb/newzbin/main.py | 1 + couchpotato/core/providers/nzb/newznab/main.py | 1 + 2 files changed, 2 insertions(+) diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py index 81672d2d..96a34a3f 100644 --- a/couchpotato/core/providers/nzb/newzbin/main.py +++ b/couchpotato/core/providers/nzb/newzbin/main.py @@ -26,6 +26,7 @@ class Newzbin(NZBProvider, RSS): 1024: ['r5'], } cat_ids = [ + ([262144], ['bd50']), ([2097152], ['1080p']), ([524288], ['720p']), ([262144], ['brrip']), diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index 9a7943e5..9b43443e 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -22,6 +22,7 @@ class Newznab(NZBProvider, RSS): cat_ids = [ ([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), ([2040], ['720p', '1080p']), + ([2050], ['bd50']), ] cat_backup_id = 2000 From b595cf8ebc7f458b640eb411ad65b1f56d2012b2 Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 4 Apr 2012 23:53:34 +0200 Subject: [PATCH 84/99] Don't allow empty titles in library --- couchpotato/core/plugins/library/main.py | 2 ++ couchpotato/core/plugins/searcher/main.py | 12 +++++++++--- couchpotato/core/providers/movie/imdbapi/main.py | 5 ++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py index 4cc3bb26..9a4cde04 100644 --- a/couchpotato/core/plugins/library/main.py +++ b/couchpotato/core/plugins/library/main.py @@ -84,6 +84,8 @@ class LibraryPlugin(Plugin): titles = info.get('titles', []) log.debug('Adding titles: %s' % titles) for title in titles: + if not title: + continue t = LibraryTitle( title = toUnicode(title), simple_title = self.simplifyTitle(title), diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 4b4ee339..f3dc28fa 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -33,13 +33,19 @@ class Searcher(Plugin): ).all() for movie in movies: - - self.single(movie.to_dict({ + movie_dict = movie.to_dict({ 'profile': {'types': {'quality': {}}}, 'releases': {'status': {}, 'quality': {}}, 'library': {'titles': {}, 'files':{}}, 'files': {} - })) + }) + + try: + self.single(movie_dict) + except IndexError: + fireEvent('library.update', movie_dict['library']['identifier'], force = True) + except: + log.error('Search failed for %s: %s' % (movie_dict['library']['identifier'], traceback.format_exc())) # Break if CP wants to shut down if self.shuttingDown(): diff --git a/couchpotato/core/providers/movie/imdbapi/main.py b/couchpotato/core/providers/movie/imdbapi/main.py index 158359b0..604b37c1 100644 --- a/couchpotato/core/providers/movie/imdbapi/main.py +++ b/couchpotato/core/providers/movie/imdbapi/main.py @@ -61,13 +61,16 @@ class IMDBAPI(MovieProvider): if isinstance(movie, (str, unicode)): movie = json.loads(movie) + if movie.get('Response') == 'Parse Error': + return movie_data + tmp_movie = movie.copy() for key in tmp_movie: if tmp_movie.get(key).lower() == 'n/a': del movie[key] movie_data = { - 'titles': [movie.get('Title', '')], + 'titles': [movie.get('Title')] if movie.get('Title') else [], 'original_title': movie.get('Title', ''), 'images': { 'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [], From 705e3058bf3367a206fa5ad49dee6d47f2951b08 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Apr 2012 22:38:48 +0200 Subject: [PATCH 85/99] Decode sets to lists before json encoding --- couchpotato/core/settings/model.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 92d3ecdb..4dd54d32 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -3,7 +3,6 @@ from elixir.entity import Entity from elixir.fields import Field from elixir.options import options_defaults, using_options from elixir.relationships import ManyToMany, OneToMany, ManyToOne -from libs.elixir.relationships import OneToOne from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \ String, TypeDecorator import json @@ -18,12 +17,18 @@ options_defaults["shortnames"] = True # http://elixir.ematia.de/trac/wiki/Recipes/MultipleDatabasesOneMetadata __session__ = None +class SetEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, set): + return list(obj) + return json.JSONEncoder.default(self, obj) + class JsonType(TypeDecorator): impl = UnicodeText def process_bind_param(self, value, dialect): - return toUnicode(json.dumps(value)) + return toUnicode(json.dumps(value, cls = SetEncoder)) def process_result_value(self, value, dialect): return json.loads(value if value else '{}') From de50351d5caa8840f14b1ff89af3ca536dd839a6 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Apr 2012 22:39:02 +0200 Subject: [PATCH 86/99] Add dvdr group for newznab --- couchpotato/core/providers/nzb/newznab/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index 9b43443e..c17e6163 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -20,6 +20,7 @@ class Newznab(NZBProvider, RSS): } cat_ids = [ + ([2010], ['dvdr']), ([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), ([2040], ['720p', '1080p']), ([2050], ['bd50']), From 8a2e01560642eb580ff4416dfb8ef02a4a607ff1 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Apr 2012 22:40:22 +0200 Subject: [PATCH 87/99] Remove empty and processed folders --- couchpotato/core/plugins/renamer/main.py | 41 +++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 6541c185..e8d70188 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -67,6 +67,8 @@ class Renamer(Plugin): nfo_name = self.conf('nfo_name') separator = self.conf('separator') + db = get_session() + for group_identifier in groups: group = groups[group_identifier] @@ -125,7 +127,7 @@ class Renamer(Plugin): # 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) - if self.conf('clean_up'): + if self.conf('cleanup'): for current_file in group['files'][file_type]: remove_files.append(current_file) continue @@ -225,7 +227,6 @@ class Renamer(Plugin): cd += 1 # 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) @@ -310,11 +311,26 @@ class Renamer(Plugin): if isinstance(src, File): src = src.path - log.info('(fake) Removing "%s"' % src) + log.info('Removing "%s"' % src) + try: + os.remove(src) + except: + log.error('Failed removing %s: %s', (src, traceback.format_exc())) # Remove matching releases for release in remove_releases: - log.info('(fake) Removing release %s' % release.identifier) + log.debug('Removing release %s' % release.identifier) + try: + db.delete(release) + except: + log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc())) + + if group['dirname'] and group['parentdir']: + try: + log.info('Deleting folder: %s' % group['parentdir']) + self.deleteEmptyFolder(group['parentdir']) + except: + log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc())) # Search for trailers etc fireEventAsync('renamer.after', group) @@ -380,3 +396,20 @@ class Renamer(Plugin): def replaceDoubles(self, string): return string.replace(' ', ' ').replace(' .', '.') + + def deleteEmptyFolder(self, folder): + + for root, dirs, files in os.walk(folder): + + for dir_name in dirs: + full_path = os.path.join(root, dir_name) + if len(os.listdir(full_path)) == 0: + try: + os.rmdir(full_path) + except: + log.error('Couldn\'t remove empty directory %s: %s' % (full_path, traceback.format_exc())) + + try: + os.rmdir(folder) + except: + log.error('Couldn\'t remove empty directory %s: %s' % (folder, traceback.format_exc())) From 8a544c262553d77676ad3b5609fe46177db5302c Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Apr 2012 23:04:44 +0200 Subject: [PATCH 88/99] Manual update fix --- .../core/_base/updater/static/updater.js | 26 ++----------------- couchpotato/static/scripts/couchpotato.js | 4 +-- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js index 48d077c1..202540d3 100644 --- a/couchpotato/core/_base/updater/static/updater.js +++ b/couchpotato/core/_base/updater/static/updater.js @@ -76,32 +76,10 @@ var UpdaterBase = new Class({ 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) - + App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating'); + App.checkAvailable.delay(500, App); } - } }); } diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index b615f341..edafdf4c 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -161,10 +161,10 @@ var CouchPotato = new Class({ self.checkAvailable(1000); }, - restart: function(){ + restart: function(message, title){ var self = this; - self.blockPage('Restarting... please wait. If this takes to long, something must have gone wrong.'); + self.blockPage(message || 'Restarting... please wait. If this takes to long, something must have gone wrong.', title); Api.request('app.restart'); self.checkAvailable(1000); }, From 6b739ee52c38c37294b03d15de636c78ede8be53 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Apr 2012 23:28:04 +0200 Subject: [PATCH 89/99] Extra br50 tags --- couchpotato/core/plugins/quality/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 892fefc6..5736ddd7 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -16,7 +16,7 @@ log = CPLog(__name__) class QualityPlugin(Plugin): qualities = [ - {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate']}, + {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts']}, {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']}, @@ -155,6 +155,10 @@ class QualityPlugin(Plugin): log.debug('Found %s via alt %s in %s' % (quality['identifier'], quality.get('alternative'), cur_file)) return self.setCache(hash, quality) + for tag in quality.get('tags', []): + if isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words): + return self.setCache(hash, quality) + if list(set(quality.get('tags', [])) & set(words)): log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), cur_file)) return self.setCache(hash, quality) From 8a344d764a0b53610df8e370d3352e25faa63b67 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 7 Apr 2012 00:03:51 +0200 Subject: [PATCH 90/99] Rottentomatoes userscript --- .../userscript/rottentomatoes/__init__.py | 6 ++++++ .../userscript/rottentomatoes/main.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 couchpotato/core/providers/userscript/rottentomatoes/__init__.py create mode 100644 couchpotato/core/providers/userscript/rottentomatoes/main.py diff --git a/couchpotato/core/providers/userscript/rottentomatoes/__init__.py b/couchpotato/core/providers/userscript/rottentomatoes/__init__.py new file mode 100644 index 00000000..ee8266eb --- /dev/null +++ b/couchpotato/core/providers/userscript/rottentomatoes/__init__.py @@ -0,0 +1,6 @@ +from .main import RottenTomatoes + +def start(): + return RottenTomatoes() + +config = [] diff --git a/couchpotato/core/providers/userscript/rottentomatoes/main.py b/couchpotato/core/providers/userscript/rottentomatoes/main.py new file mode 100644 index 00000000..cb36b6cf --- /dev/null +++ b/couchpotato/core/providers/userscript/rottentomatoes/main.py @@ -0,0 +1,19 @@ +from BeautifulSoup import BeautifulSoup +from couchpotato.core.event import fireEvent +from couchpotato.core.providers.userscript.base import UserscriptBase + +class RottenTomatoes(UserscriptBase): + + includes = ['http*://www.rottentomatoes.com/m/*'] + + def getMovie(self, url): + + try: + data = self.urlopen(url) + except: + return + + html = BeautifulSoup(data) + title = html.find('span', {'itemprop':'name'}).text + info = fireEvent('scanner.name_year', title, single = True) + return self.search(info['name'], info['year']) From 35769b3d91794ca8c5481c745eb858e27514f3d3 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 7 Apr 2012 13:33:40 +0200 Subject: [PATCH 91/99] Cache userscript url download --- couchpotato/core/providers/userscript/allocine/main.py | 2 +- .../core/providers/userscript/appletrailers/main.py | 2 +- couchpotato/core/providers/userscript/base.py | 8 ++++++-- .../core/providers/userscript/rottentomatoes/main.py | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/providers/userscript/allocine/main.py b/couchpotato/core/providers/userscript/allocine/main.py index 91b44d95..8213ac2f 100644 --- a/couchpotato/core/providers/userscript/allocine/main.py +++ b/couchpotato/core/providers/userscript/allocine/main.py @@ -11,7 +11,7 @@ class AlloCine(UserscriptBase): return 'Url isn\'t from a movie' try: - data = self.urlopen(url) + data = self.getUrl(url) except: return diff --git a/couchpotato/core/providers/userscript/appletrailers/main.py b/couchpotato/core/providers/userscript/appletrailers/main.py index d7ce8ab3..693065d1 100644 --- a/couchpotato/core/providers/userscript/appletrailers/main.py +++ b/couchpotato/core/providers/userscript/appletrailers/main.py @@ -9,7 +9,7 @@ class AppleTrailers(UserscriptBase): def getMovie(self, url): try: - data = self.urlopen(url) + data = self.getUrl(url) except: return diff --git a/couchpotato/core/providers/userscript/base.py b/couchpotato/core/providers/userscript/base.py index d4b3b9f3..571b76c0 100644 --- a/couchpotato/core/providers/userscript/base.py +++ b/couchpotato/core/providers/userscript/base.py @@ -1,5 +1,6 @@ from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.variable import getImdb +from couchpotato.core.helpers.encoding import simplifyString +from couchpotato.core.helpers.variable import getImdb, md5 from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from urlparse import urlparse @@ -42,9 +43,12 @@ class UserscriptBase(Plugin): return + def getUrl(self, url): + return self.getCache(md5(simplifyString(url)), url = url) + def getMovie(self, url): try: - data = self.urlopen(url) + data = self.getUrl(url) except: data = '' return self.getInfo(getImdb(data)) diff --git a/couchpotato/core/providers/userscript/rottentomatoes/main.py b/couchpotato/core/providers/userscript/rottentomatoes/main.py index cb36b6cf..bbce0d84 100644 --- a/couchpotato/core/providers/userscript/rottentomatoes/main.py +++ b/couchpotato/core/providers/userscript/rottentomatoes/main.py @@ -9,7 +9,7 @@ class RottenTomatoes(UserscriptBase): def getMovie(self, url): try: - data = self.urlopen(url) + data = self.getUrl(url) except: return From 4726a4684bb218dfd80d287287675289f66d5c19 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 7 Apr 2012 13:41:29 +0200 Subject: [PATCH 92/99] Invalid rottentamoties url --- couchpotato/core/providers/userscript/rottentomatoes/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/providers/userscript/rottentomatoes/main.py b/couchpotato/core/providers/userscript/rottentomatoes/main.py index bbce0d84..1d685903 100644 --- a/couchpotato/core/providers/userscript/rottentomatoes/main.py +++ b/couchpotato/core/providers/userscript/rottentomatoes/main.py @@ -4,7 +4,7 @@ from couchpotato.core.providers.userscript.base import UserscriptBase class RottenTomatoes(UserscriptBase): - includes = ['http*://www.rottentomatoes.com/m/*'] + includes = ['*://www.rottentomatoes.com/m/*'] def getMovie(self, url): From d0796ee423798eb3587a02872177c74e6c0eb08a Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 7 Apr 2012 14:05:50 +0200 Subject: [PATCH 93/99] Saver search for imdb id in userscript --- couchpotato/core/plugins/userscript/main.py | 2 +- couchpotato/core/plugins/userscript/template.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py index 160d85ab..f3a6f5d7 100644 --- a/couchpotato/core/plugins/userscript/main.py +++ b/couchpotato/core/plugins/userscript/main.py @@ -16,7 +16,7 @@ log = CPLog(__name__) class Userscript(Plugin): - version = 1 + version = 2 def __init__(self): addApiView('userscript.get/', self.getUserScript, static = True) diff --git a/couchpotato/core/plugins/userscript/template.js b/couchpotato/core/plugins/userscript/template.js index 158638e7..d2d58adf 100644 --- a/couchpotato/core/plugins/userscript/template.js +++ b/couchpotato/core/plugins/userscript/template.js @@ -90,7 +90,7 @@ var osd = function(){ // Try and get imdb url try { - var regex = new RegExp(/tt(\d+)/); + var regex = new RegExp(/tt(\d{7})/); var imdb_id = document.body.innerHTML.match(regex)[0]; if (imdb_id) iframe.setAttribute('src', createApiUrl('http://imdb.com/title/'+imdb_id+'/')) From 6199c0385c022f7a787f66361afbf2c7f19d7c55 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 7 Apr 2012 14:24:47 +0200 Subject: [PATCH 94/99] Prevent userscript from caching when updating --- couchpotato/core/plugins/userscript/main.py | 5 ++--- couchpotato/core/plugins/userscript/static/userscript.js | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py index f3a6f5d7..359fd552 100644 --- a/couchpotato/core/plugins/userscript/main.py +++ b/couchpotato/core/plugins/userscript/main.py @@ -1,4 +1,3 @@ -from couchpotato import index from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.request import getParam, jsonified @@ -19,7 +18,7 @@ class Userscript(Plugin): version = 2 def __init__(self): - addApiView('userscript.get/', self.getUserScript, static = True) + addApiView('userscript.get//', self.getUserScript, static = True) addApiView('userscript', self.iFrame) addApiView('userscript.add_via_url', self.getViaUrl) addApiView('userscript.bookmark', self.bookmark) @@ -36,7 +35,7 @@ class Userscript(Plugin): return self.renderTemplate(__file__, 'bookmark.js', **params) - def getUserScript(self, filename = ''): + def getUserScript(self, random = '', filename = ''): params = { 'includes': fireEvent('userscript.get_includes', merge = True), diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index 21edc636..126e711f 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -82,7 +82,7 @@ var UserscriptSettingTab = new Class({ }).inject(self.settings.tabs.automation.content, 'top').adopt( (userscript ? [new Element('a.userscript.button', { 'text': 'Install userscript', - 'href': Api.createUrl('userscript.get')+'couchpotato.user.js', + 'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js', 'target': '_self' }), new Element('span.or[text=or]')] : null), new Element('span.bookmarklet').adopt( @@ -90,7 +90,7 @@ var UserscriptSettingTab = new Class({ 'text': '+CouchPotato', 'href': "javascript:void((function(){var e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','" + host_url + Api.createUrl('userscript.bookmark') + - "?host="+ encodeURI(host_url + Api.createUrl('userscript.get')) + + "?host="+ encodeURI(host_url + Api.createUrl('userscript.get')+randomString()+'/') + "&r='+Math.random()*99999999);document.body.appendChild(e)})());", 'target': '', 'events': { @@ -126,7 +126,7 @@ window.addEvent('load', function(){ if(your_version && your_version < latest_version && checked_already < latest_version){ if(confirm("Update to the latest Userscript?\nYour version: " + your_version + ', new version: ' + latest_version )){ - document.location = Api.getOption('url')+'userscript.get/couchpotato.user.js'; + document.location = Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js'; } Cookie.write(key, latest_version, {duration: 100}); } From 5f319635c2dcabe7082d321d85b4226b311e940b Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 7 Apr 2012 16:51:25 +0200 Subject: [PATCH 95/99] API introduction --- couchpotato/__init__.py | 2 +- couchpotato/core/helpers/request.py | 2 +- couchpotato/static/scripts/api.js | 2 +- couchpotato/templates/api.html | 13 +++++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index f7dfb3a5..799a2984 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -49,7 +49,7 @@ def apiDocs(): if api_docs.get(''): del api_docs[''] del api_docs_missing[''] - return render_template('api.html', routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing)) + return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing)) @app.errorhandler(404) def page_not_found(error): diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py index 09ee6b8f..a9dff599 100644 --- a/couchpotato/core/helpers/request.py +++ b/couchpotato/core/helpers/request.py @@ -70,7 +70,7 @@ def jsonify(mimetype, *args, **kwargs): def jsonified(*args, **kwargs): from couchpotato.environment import Env - callback = getParam('json_callback', None) + callback = getParam('callback_func', None) if callback: return padded_jsonify(callback, *args, **kwargs) else: diff --git a/couchpotato/static/scripts/api.js b/couchpotato/static/scripts/api.js index f39ec6f2..f14eb14d 100644 --- a/couchpotato/static/scripts/api.js +++ b/couchpotato/static/scripts/api.js @@ -11,7 +11,7 @@ var ApiClass = new Class({ var r_type = self.options.is_remote ? 'JSONP' : 'JSON'; return new Request[r_type](Object.merge({ - 'callbackKey': 'json_callback', + 'callbackKey': 'callback_func', 'method': 'get', 'url': self.createUrl(type), }, options)).send() diff --git a/couchpotato/templates/api.html b/couchpotato/templates/api.html index 47fb6c34..ec067f95 100644 --- a/couchpotato/templates/api.html +++ b/couchpotato/templates/api.html @@ -7,6 +7,19 @@

CouchPotato API Documentation

+
+ You can access the API via
{{ fireEvent('app.api_url', single = True)|safe }}/
+ To see it in action, have a look at the webinterface with Firebug (on firefox) or the development tools included in Chrome. + All the data that you see there are from the API. +
+
+ A normal API call: +
{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/
+
+ You can also use the API over another domain using JSONP, the callback function should be in 'callback_func' +
{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/?callback_func=myfunction
+
+ {% for route in routes %} {% if api_docs.get(route) %}
From 24a65ea92753ab708606bff7240a176ce531c456 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 7 Apr 2012 19:15:08 +0200 Subject: [PATCH 96/99] Disabled notifications still enabled --- couchpotato/core/notifications/base.py | 13 +++++++------ couchpotato/core/notifications/growl/main.py | 2 +- couchpotato/core/notifications/notifymywp/main.py | 5 ++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py index 9dd3cfe3..254059e0 100644 --- a/couchpotato/core/notifications/base.py +++ b/couchpotato/core/notifications/base.py @@ -23,14 +23,15 @@ class Notification(Plugin): # Attach listeners for listener in self.listen_to: if not listener in self.dont_listen_to: + addEvent(listener, self.createNotifyHandler(listener)) - # Add on snatch default - def notify(message, data): - if not self.conf('on_snatch', default = 1) and listener == 'movie.snatched': - return - return self.notify(message = message, data = data) + def createNotifyHandler(self, listener): + def notify(message, data): + if not self.conf('on_snatch', default = True) and listener == 'movie.snatched': + return + return self.notify(message = message, data = data) - addEvent(listener, notify) + return notify def notify(self, message = '', data = {}): pass diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl/main.py index 7bd6056d..29ade280 100644 --- a/couchpotato/core/notifications/growl/main.py +++ b/couchpotato/core/notifications/growl/main.py @@ -34,7 +34,7 @@ class Growl(Notification): except: log.error('Failed register of growl: %s' % traceback.format_exc()) - def notify(self, type = '', message = '', data = {}): + def notify(self, message = '', data = {}): if self.isDisabled(): return self.register() diff --git a/couchpotato/core/notifications/notifymywp/main.py b/couchpotato/core/notifications/notifymywp/main.py index 4445af66..7c294bfc 100644 --- a/couchpotato/core/notifications/notifymywp/main.py +++ b/couchpotato/core/notifications/notifymywp/main.py @@ -8,9 +8,7 @@ log = CPLog(__name__) class NotifyMyWP(Notification): def notify(self, message = '', data = {}): - - if self.isDisabled(): - return + if self.isDisabled(): return keys = self.conf('api_key').split(',') p = PyNMWP(keys, self.conf('dev_key')) @@ -20,5 +18,6 @@ class NotifyMyWP(Notification): for key in keys: if not response[key]['Code'] == u'200': log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s' % (key, response[key]['message'])) + return False return response From 71e5fb5346c6ceb6d09961de0b4eb4695b6911cd Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 7 Apr 2012 19:30:55 +0200 Subject: [PATCH 97/99] Whitespace cleanup --- couchpotato/core/plugins/file/static/file.js | 4 +- couchpotato/core/plugins/log/static/log.css | 2 +- couchpotato/core/plugins/log/static/log.js | 8 +- couchpotato/core/plugins/movie/static/list.js | 2 +- .../core/plugins/movie/static/movie.css | 89 +++++++------- .../core/plugins/movie/static/search.css | 10 +- .../core/plugins/profile/static/profile.css | 30 ++--- .../core/plugins/quality/static/quality.css | 4 +- .../core/plugins/quality/static/quality.js | 2 +- .../core/plugins/status/static/status.js | 2 +- .../plugins/userscript/static/userscript.js | 2 +- .../core/plugins/wizard/static/wizard.css | 6 +- couchpotato/static/scripts/block.js | 8 +- couchpotato/static/scripts/page.js | 8 +- couchpotato/static/scripts/page/manage.js | 2 +- couchpotato/static/scripts/page/settings.js | 6 +- couchpotato/static/style/main.css | 68 +++++------ couchpotato/static/style/page/settings.css | 109 +++++++++--------- couchpotato/static/style/uniform.css | 44 +++---- couchpotato/static/style/uniform.generic.css | 40 +++---- 20 files changed, 222 insertions(+), 224 deletions(-) diff --git a/couchpotato/core/plugins/file/static/file.js b/couchpotato/core/plugins/file/static/file.js index 50458db6..2093e2fe 100644 --- a/couchpotato/core/plugins/file/static/file.js +++ b/couchpotato/core/plugins/file/static/file.js @@ -2,7 +2,7 @@ var File = new Class({ initialize: function(file){ var self = this; - + if(!file){ self.el = new Element('div'); return @@ -17,7 +17,7 @@ var File = new Class({ createImage: function(){ var self = this; - + var file_name = self.data.path.replace(/^.*[\\\/]/, ''); self.el = new Element('div', { diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css index 01e9a863..ec1f838c 100644 --- a/couchpotato/core/plugins/log/static/log.css +++ b/couchpotato/core/plugins/log/static/log.css @@ -60,6 +60,6 @@ color: lightgrey; padding: 3px 0; } - + .page.log .container .time:last-child { display: none; } diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index ff772829..1668ded6 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -5,18 +5,18 @@ Page.Log = new Class({ name: 'log', title: 'Show recent logs.', has_tab: false, - + initialize: function(options){ var self = this; self.parent(options) - - + + App.getBlock('more').addLink(new Element('a', { 'href': App.createUrl(self.name), 'text': self.name.capitalize(), 'title': self.title })) - + }, indexAction: function(){ diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index 9ddffd97..d70db1f5 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -200,7 +200,7 @@ var MovieList = new Class({ } }); - + // Add menu or hide if (self.options.menu.length > 0) self.options.menu.each(function(menu_item){ diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index b66d0467..55dce2e3 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -24,7 +24,7 @@ .movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover { background: rgba(255,255,255,0.03); } - + .movies .data { padding: 20px; height: 180px; @@ -43,11 +43,11 @@ border: 0; background: none; } - + .movies .movie .check { display: none; } - + .movies.mass_edit_list .movie .check { float: left; display: block; @@ -62,21 +62,21 @@ height: 180px; border-radius: 4px 0 0 4px; transition: all 0.2s linear; - + } - .movies .list_view .poster, .movies .mass_edit_view .poster { + .movies .list_view .poster, .movies .mass_edit_view .poster { width: 20px; height: 30px; } .movies.mass_edit_list .poster { display: none; } - + .movies .poster img, .options .poster img { width: 101%; height: 101%; } - + .movies .info .title { font-size: 30px; font-weight: bold; @@ -91,7 +91,7 @@ text-overflow: ellipsis; width: 64%; } - + .movies .info .year { font-size: 30px; margin-bottom: 10px; @@ -105,7 +105,7 @@ font-size: 16px; width: 6%; } - + .movies .info .rating { font-size: 30px; margin-bottom: 10px; @@ -114,7 +114,7 @@ width: 5%; padding: 0 0 0 3%; } - + .movies .info .description { clear: both; height: 80px; @@ -126,13 +126,13 @@ .movies .list_view .info .description, .movies .mass_edit_view .info .description { display: none; } - + .movies .data .quality { display: block; min-height: 20px; vertical-align: mid; } - + .movies .data .quality span { padding: 2px 3px; font-weight: bold; @@ -154,39 +154,39 @@ float: right; width: 30%; } - + .movies .data .quality .available, .movies .data .quality .snatched { opacity: 1; box-shadow: 1px 1px 0 rgba(0,0,0,0.2); cursor: pointer; } - + .movies .data .quality .available { background-color: #578bc3; } .movies .data .quality .snatched { background-color: #369545; } .movies .data .quality .done { background-color: #369545; opacity: 1; } - .movies .data .quality .finish { + .movies .data .quality .finish { background-image: url('../images/sprite.png'); background-repeat: no-repeat; background-position: 0 2px; padding-left: 14px; background-size: 14px } - + .movies .data .actions { line-height: 0; clear: both; float: right; margin-top: -25px; } - .movies .data:hover .action { opacity: 0.6; } + .movies .data:hover .action { opacity: 0.6; } .movies .data:hover .action:hover { opacity: 1; } .movies.mass_edit_list .data .actions { display: none; } - + .movies .data .action { background-repeat: no-repeat; background-position: center; @@ -196,12 +196,12 @@ padding: 3px; opacity: 0; } - + .movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions { margin: -34px 2px 0 0; background: #4e5969; } - + .movies .delete_container { clear: both; text-align: center; @@ -223,23 +223,23 @@ color: #fff; background-color: #d32917; } - + .movies .options { position: absolute; margin-left: 120px; width: 840px; } - + .movies .options .form { margin: 70px 20px 0; float: left; font-size: 20px; } - + .movies .options .form select { margin-right: 20px; } - + .movies .options .table { height: 180px; overflow: auto; @@ -255,15 +255,15 @@ .movies .options .table .item.ignored .delete { background-image: url('../images/icon.undo.png'); } - + .movies .options .table .item:last-child { border: 0; } - .movies .options .table .item:nth-child(even) { + .movies .options .table .item:nth-child(even) { background: rgba(255,255,255,0.05); } - .movies .options .table .item:not(.head):hover { + .movies .options .table .item:not(.head):hover { background: rgba(255,255,255,0.03); } - + .movies .options .table .item > * { display: inline-block; padding: 0 5px; @@ -290,7 +290,7 @@ .movies .options .table.files .name { width: 605px; } .movies .options .table .type { width: 130px; } .movies .options .table .is_available { width: 90px; } - + .movies .options .table a { width: 30px !important; height: 20px; @@ -307,7 +307,7 @@ padding-bottom: 4px; height: auto; } - + .movies .load_more { display: block; padding: 10px; @@ -363,14 +363,14 @@ .movies .alph_nav li.available { color: rgba(255,255,255,0.8); font-weight: bolder; - + } .movies .alph_nav li.active.available, .movies .alph_nav li.available:hover { color: #fff; font-size: 24px; line-height: 24px; } - + .movies .alph_nav input { padding: 6px 5px; margin: 0 0 0 6px; @@ -378,16 +378,16 @@ width: 155px; height: 25px; } - + .movies .alph_nav .actions { margin: 0 6px 0 0; -moz-user-select: none; } - .movies .alph_nav .actions li { + .movies .alph_nav .actions li { border-radius: 1px; width: auto; } - .movies .alph_nav .actions li.active { + .movies .alph_nav .actions li.active { background: none; border: 1px solid transparent; box-shadow: none; @@ -398,15 +398,15 @@ width: 25px; height: 100%; } - + .movies .alph_nav .actions li.mass_edit span { background-position: 3px 3px; } - + .movies .alph_nav .actions li.list span { background-position: 3px -95px; } - + .movies .alph_nav .actions li.thumbs span { background-position: 3px -74px; } @@ -417,7 +417,7 @@ .movies .alph_nav .actions li:last-child { border-radius: 0 3px 3px 0; } - + .movies .alph_nav .mass_edit_form { clear: both; text-align: center; @@ -439,7 +439,7 @@ font-weight: bold; margin: 0 3px 0 10px; } - + .movies .alph_nav .mass_edit_form .quality { float: left; padding: 8px 0 0; @@ -452,21 +452,20 @@ .movies .alph_nav .mass_edit_form .button { padding: 3px 7px; } - + .movies .alph_nav .mass_edit_form .delete { float: left; padding: 8px 0 0 8px; } - + .movies .alph_nav .mass_edit_form .delete span { margin: 0 10px 0 0; } - + .movies .alph_nav .more_menu { margin-left: 48px; } - + .movies .alph_nav .more_menu > a { background-position: center -157px; } - \ 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 2be6ce1e..4f43ffe1 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -14,7 +14,7 @@ .search_form input:focus { padding-right: 83px; } - + .search_form .input .enter { background: #369545 url('../images/sprite.png') right -188px no-repeat; padding: 0 20px 0 4px; @@ -36,7 +36,7 @@ .search_form.focused.filled .input .enter { opacity: 1; } - + .search_form .input a { width: 17px; height: 20px; @@ -50,7 +50,7 @@ transition: all 0.2s ease-in-out; vertical-align: middle; } - + .search_form.filled .input a { opacity: 1; } @@ -68,7 +68,7 @@ .search_form.shown.filled .results_container { display: block; } - + .search_form .results_container:before { content: ' '; height: 0; @@ -146,7 +146,7 @@ } .movie_result:last-child .data { border-bottom: 0; } - + .movie_result .in_wanted, .movie_result .in_library { position: absolute; margin-top: 105px; diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css index b763cd80..9d50d2fd 100644 --- a/couchpotato/core/plugins/profile/static/profile.css +++ b/couchpotato/core/plugins/profile/static/profile.css @@ -16,7 +16,7 @@ padding: 14px; background-position: center; } - + .profile .qualities { min-height: 80px; } @@ -40,36 +40,36 @@ .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; @@ -80,7 +80,7 @@ cursor: -webkit-grab; margin: 0; } - + .profile .types .type .delete { background-position: left center; height: 20px; @@ -88,13 +88,13 @@ visibility: hidden; cursor: pointer; } - + .profile .types .type:hover:not(.is_empty) .delete { visibility: visible; } - + #profile_ordering { - + } #profile_ordering ul { @@ -112,19 +112,19 @@ 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; + line-height: 20px; } - + #profile_ordering li .handle { background: url('./handle.png') center; width: 20px; diff --git a/couchpotato/core/plugins/quality/static/quality.css b/couchpotato/core/plugins/quality/static/quality.css index a66fefc4..f71f007e 100644 --- a/couchpotato/core/plugins/quality/static/quality.css +++ b/couchpotato/core/plugins/quality/static/quality.css @@ -1,5 +1,5 @@ .group_sizes { - + } .group_sizes .head { @@ -15,7 +15,7 @@ .group_sizes .label { max-width: 120px; } - + .group_sizes .min, .group_sizes .max { text-align: center; width: 50px; diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js index 314ca576..bd2ff2ac 100644 --- a/couchpotato/core/plugins/quality/static/quality.js +++ b/couchpotato/core/plugins/quality/static/quality.js @@ -93,7 +93,7 @@ var QualityBase = new Class({ var data = data || {'id': randomString()} var profile = new Profile(data) self.profiles.include(profile) - + return profile; }, diff --git a/couchpotato/core/plugins/status/static/status.js b/couchpotato/core/plugins/status/static/status.js index 9c2167be..2b8d30f3 100644 --- a/couchpotato/core/plugins/status/static/status.js +++ b/couchpotato/core/plugins/status/static/status.js @@ -6,7 +6,7 @@ var StatusBase = new Class({ self.statuses = statuses; }, - + get: function(id){ return this.statuses.filter(function(status){ return status.id == id diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index 126e711f..d6d5983c 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -72,7 +72,7 @@ var UserscriptSettingTab = new Class({ catch(e){ userscript = Browser.chrome === true; } - + var host_url = window.location.protocol + '//' + window.location.host; self.settings.createGroup({ diff --git a/couchpotato/core/plugins/wizard/static/wizard.css b/couchpotato/core/plugins/wizard/static/wizard.css index 3b19cf19..d1aa99c8 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.css +++ b/couchpotato/core/plugins/wizard/static/wizard.css @@ -31,14 +31,14 @@ margin: 0; display: block; } - + .page.wizard .tabs li { display: inline-block; } .page.wizard .tabs li a { padding: 20px 30px; } - + .page.wizard .tab_wrapper .pointer { border-right: 10px solid transparent; border-left: 10px solid transparent; @@ -47,7 +47,7 @@ position: absolute; top: 60px; } - + .page.wizard .tab_content { margin: 20px 0 160px; } diff --git a/couchpotato/static/scripts/block.js b/couchpotato/static/scripts/block.js index da816d24..82193ca5 100644 --- a/couchpotato/static/scripts/block.js +++ b/couchpotato/static/scripts/block.js @@ -7,13 +7,13 @@ var BlockBase = new Class({ initialize: function(parent, options){ var self = this; self.setOptions(options); - + self.page = parent; self.create(); }, - + create: function(){ this.el = new Element('div.block'); }, @@ -21,11 +21,11 @@ var BlockBase = new Class({ getParent: function(){ return this.page }, - + hide: function(){ this.el.hide(); }, - + show: function(){ this.el.show(); }, diff --git a/couchpotato/static/scripts/page.js b/couchpotato/static/scripts/page.js index af77cfe5..589fa3ed 100644 --- a/couchpotato/static/scripts/page.js +++ b/couchpotato/static/scripts/page.js @@ -46,7 +46,7 @@ var PageBase = new Class({ self.fireEvent('error'); } }, - + openUrl: function(url){ if(History.getPath() != url) History.push(url); @@ -59,15 +59,15 @@ var PageBase = new Class({ getName: function(){ return this.name }, - + show: function(){ this.el.addClass('active'); }, - + hide: function(){ this.el.removeClass('active'); }, - + toElement: function(){ return this.el } diff --git a/couchpotato/static/scripts/page/manage.js b/couchpotato/static/scripts/page/manage.js index 8a88a367..74dba986 100644 --- a/couchpotato/static/scripts/page/manage.js +++ b/couchpotato/static/scripts/page/manage.js @@ -16,7 +16,7 @@ Page.Manage = new Class({ 'click': self.refresh.bind(self, true) } }); - + self.refresh_quick = new Element('a', { 'title': 'Just scan for recently changed', 'text': 'Quick library scan', diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index 442d61af..94643cc7 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -9,11 +9,11 @@ Page.Settings = new Class({ tabs: {}, current: 'about', has_tab: false, - + initialize: function(options){ var self = this; self.parent(options); - + // Add to more menu if(self.name == 'settings') App.getBlock('more').addLink(new Element('a', { @@ -21,7 +21,7 @@ Page.Settings = new Class({ 'text': self.name.capitalize(), 'title': self.title }), 'top') - + }, open: function(action, params){ diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 9975da70..a128ee32 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -13,13 +13,14 @@ body { background: #4e5969; overflow-y: scroll; height: 100%; + text-align: justify; } body.noscroll { overflow: hidden; } #clean { background: transparent !important; } - + * { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; @@ -99,9 +100,9 @@ a:hover { color: #f3f3f3; } right: 0; padding: 10px 10px 10px 40px; background: #f7f7f7 url('../images/toTop.gif') no-repeat 10px center; - border-radius: 5px 0 0 0; + border-radius: 5px 0 0 0; } - + form { padding:0; margin:0; @@ -202,19 +203,19 @@ body > .spinner, .mask{ display: block; margin-top: 5px; } - + .header .navigation li.disabled { color: #e5e5e5; } - + .header .navigation li a:link, .header .navigation li a:visited { color: #fff; } - + .header .navigation li a:hover, .header .navigation li a:active { color: #b1d8dc; } - + .header .navigation .backtotop { opacity: 0; display: block; @@ -231,7 +232,7 @@ body > .spinner, .mask{ font-weight: normal; } .header:hover .navigation .backtotop { color: #fff; } - + .header .more_menu { margin-left: 12px; } @@ -242,10 +243,10 @@ body > .spinner, .mask{ .header .more_menu .wrapper:before { margin-left: -34px; } - + .header .more_menu .red { color: red; } .header .more_menu .orange { color: orange; } - + .badge { position: absolute; width: 14px; @@ -261,30 +262,31 @@ body > .spinner, .mask{ background-color: #1b79b8; text-shadow: none; } - + .header .notification_menu .wrapper { width: 300px; margin-left: -260px; text-align: left; } - + .header .notification_menu .wrapper:before { left: 296px; } - + .header .notification_menu ul { max-height: 300px; overflow: auto; } - + .header .notification_menu > a { background-position: center -209px; } - + .header .notification_menu li > span { padding: 5px; display: block; border-bottom: 1px solid rgba(0,0,0,0.2); + word-wrap: break-word; } .header .notification_menu li > span { color: #777; } .header .notification_menu li:last-child > span { border: 0; } @@ -294,11 +296,11 @@ body > .spinner, .mask{ color: #aaa; text-align: ; } - + .header .notification_menu li .more { text-align: center; } - + .header .message.update { text-align: center; position: relative; @@ -340,9 +342,9 @@ body > .spinner, .mask{ display: inline-block; padding: 0 30px 0 20px; border-radius:30px; - + box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20); - + background: url('../images/sprite.png') no-repeat 94% -53px, -webkit-gradient( linear, left bottom, @@ -356,12 +358,12 @@ body > .spinner, .mask{ #406db8 100% ); } - + .select .selection .selectionDisplay { display: inline-block; padding-right: 15px; border-right: 1px solid rgba(0,0,0,0.2); - + box-shadow: 1px 0 0 rgba(255,255,255,0.15); } @@ -370,7 +372,7 @@ body > .spinner, .mask{ overflow: hidden; font-weight: bold; } - + .select .list:before { content: ' '; height: 0; @@ -380,7 +382,7 @@ body > .spinner, .mask{ border-bottom-color: #282d34; margin: -11px 0 0 70px; } - + .select .list { display: none; background: #282d34; @@ -391,7 +393,7 @@ body > .spinner, .mask{ border-radius:3px; z-index: 3; } - .select.active .list { + .select.active .list { display: block; } .select .list ul { @@ -409,7 +411,7 @@ body > .spinner, .mask{ background: rgba(255,255,255,0.1); border-color: transparent; } - + .select input { display: none; } .inlay { @@ -425,7 +427,7 @@ body > .spinner, .mask{ outline: none; box-shadow: inset 0 1px 8px rgba(0,0,0,0.05), 0 1px 0px rgba(255,255,255,0.15); } - + .inlay:focus { background-color: #3a4350; outline: none; @@ -491,7 +493,7 @@ body > .spinner, .mask{ .question .answer:hover { background: #f1f1f1; } - + .question .answer.delete { background-color: #a82f12; } @@ -516,7 +518,7 @@ body > .spinner, .mask{ .more_menu.show > a:not(:active), .more_menu > a:hover:not(:active) { background-color: #406db8; } - + .more_menu .wrapper { display: none; border: 1px solid #333; @@ -544,7 +546,7 @@ body > .spinner, .mask{ rgb(255,255,255) 100% ); } - + .more_menu .wrapper:before { content: ' '; height: 0; @@ -559,18 +561,18 @@ body > .spinner, .mask{ .more_menu.show .wrapper { display: block; } - + .more_menu ul { padding: 0; margin: -12px 0 0 0; list-style: none; } - + .more_menu .wrapper li { width: 100%; height: auto; } - + .more_menu .wrapper li a { display: block; border-bottom: 1px solid rgba(255,255,255,0.2); @@ -582,7 +584,7 @@ body > .spinner, .mask{ padding: 3px 0; color: #000; } - + .more_menu .wrapper li:last-child a { border: none; } diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index 361a2e34..464be778 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -1,6 +1,3 @@ -/* @override - http://127.0.0.1:5000/_api_/static/style/page/settings.css */ - .page.settings:after { content: "."; display: block; @@ -19,7 +16,7 @@ padding: 40px 0; margin: 0; min-height: 470px; - + background-image: -webkit-gradient( linear, right top, @@ -46,7 +43,7 @@ font-size: 25px; color: #fff; } - + .page.settings .tabs .subtabs { list-style: none; padding: 0; @@ -57,7 +54,7 @@ .page.settings .tabs > .active .subtabs { max-height: 300px; } - + .page.settings .tabs .subtabs a { font-size: 15px; padding: 1px 15px; @@ -65,27 +62,27 @@ color: rgba(255, 255, 255, 0.8); background: rgba(78, 89, 105, 0.4); } - + .page.settings .tabs .subtabs .active a { font-weight: bold; color: #fff; background: rgb(78, 89, 105); } - + .page.settings .containers { width: 80%; float: left; padding: 20px 2%; min-height: 300px; - } + } - .page .advanced { + .page .advanced { display: none; color: #edc07f; } .page.show_advanced .advanced { display: block; } - + .page.settings .tab_content { display: none; } @@ -106,7 +103,7 @@ font-size: 12px; margin-left: 10px; } - + .page fieldset.disabled .ctrlHolder { display: none; } @@ -153,14 +150,14 @@ height: 24px; vertical-align: middle; } - + .page .ctrlHolder label { font-weight: bold; width: 20%; margin: 0; padding: 6px 0 0; } - + .page .xsmall { width: 20px !important; text-align: center; } .page input[type=text], .page input[type=password] { @@ -174,7 +171,7 @@ .page .input.medium { width: 15% } .page .input.large { width: 25% } .page .input.xlarge { width: 30% } - + .page .advanced_toggle { clear: both; display: block; @@ -183,10 +180,10 @@ margin: 0; } .page .advanced_toggle span { padding: 0 5px; } - .page.show_advanced .advanced_toggle { + .page.show_advanced .advanced_toggle { color: #edc07f; } - + .page .directory { display: inline-block; padding: 0 4% 0 4px; @@ -206,7 +203,7 @@ white-space: nowrap; cursor: pointer; } - + .page .directory_list { z-index: 2; position: absolute; @@ -216,7 +213,7 @@ border-radius: 3px; box-shadow: 0 0 50px rgba(0,0,0,0.55); } - + .page .directory_list .pointer { border-right: 6px solid transparent; border-left: 6px solid transparent; @@ -226,7 +223,7 @@ width: 0px; margin: -6px 0 0 38%; } - + .page .directory_list ul { width: 92%; height: 300px; @@ -234,7 +231,7 @@ margin: 0 4%; font-size: 16px; } - + .page .directory_list li { padding: 4px 10px; cursor: pointer; @@ -245,17 +242,17 @@ .page .directory_list li:last-child { border-bottom: 1px solid rgba(255,255,255,0.1); } - + .page .directory_list li:hover { background-color: #515c68; } - + .page .directory_list .actions { clear: both; padding: 4% 4% 2%; min-height: 25px; } - + .page .directory_list .actions label { float: right; width: auto; @@ -264,7 +261,7 @@ .page .directory_list .actions .inlay { margin: -2px 0 0 7px; } - + .page .directory_list .actions .back { font-weight: bold; width: 160px; @@ -273,7 +270,7 @@ line-height: 120%; vertical-align: top; } - + .page .directory_list .actions:last-child { float: right; padding: 4%; @@ -283,23 +280,23 @@ padding: 0 5px; text-shadow: none; } - + .page .directory_list .actions:last-child > .clear { left: -90%; position: relative; background-color: #af3128; } - + .page .directory_list .actions:last-child > .cancel { font-weight: bold; color: #ddd; } - + .page .directory_list .actions:last-child > .save { background: #9dc156; } - - + + .page .multi_directory.is_empty .delete { visibility: hidden; } @@ -315,18 +312,18 @@ background-position: center; margin-left: 5px; } - - + + .page .tag_input select { width: 20%; display: inline-block; } - + .page .tag_input .selection { border-radius: 0 10px 10px 0; height: 26px; } - + .page .tag_input > input { display: none; } @@ -345,7 +342,7 @@ border-radius: 3px 0 0 3px; } .page .tag_input:hover .formHint { display: none; } - + .page .tag_input > ul > li { display: inline-block; min-height: 20px; @@ -392,12 +389,12 @@ #406db8 100% ); } - + .page .tag_input .select { display: none; } .page .tag_input:hover .select { display: inline-block; } - + .page .tag_input li input { background: 0; border: 0; @@ -410,13 +407,13 @@ padding-left: 2px; min-width: 0; } - + .page .tag_input li:not(.choice) span { white-space: pre; position: absolute; top: -9999px; } - + .page .tag_input .delete { display: none; height: 10px; @@ -440,11 +437,11 @@ background-size: 65%; } .page .tag_input .choice:hover .delete { display: inline-block; } - .page .tag_input .choice .delete:hover { + .page .tag_input .choice .delete:hover { height: 14px; margin-top: -13px; } - + .page .combined_table .head { margin: 0 0 0 60px; } @@ -462,17 +459,17 @@ .page .combined_table .head abbr.host { margin-right: 197px; } - + .page .combined_table .ctrlHolder { padding-top: 2px; padding-bottom: 3px; } .page .combined_table .ctrlHolder.hide { display: none; } - + .page .combined_table .ctrlHolder > * { margin: 0 10px 0 0; } - + .page .combined_table .ctrlHolder .delete { display: none; width: 22px; @@ -483,7 +480,7 @@ .page .combined_table .ctrlHolder:hover .delete { display: inline-block; } - + .page .combined_table .ctrlHolder.is_empty .delete, .page.settings .combined_table .ctrlHolder.is_empty .check { visibility: hidden; } @@ -522,7 +519,7 @@ .page .tab_about .donate form { padding: 10px 0 0; } - + .page .tab_about .info { padding: 20px 30px; margin: 0; @@ -535,7 +532,7 @@ width: 17%; font-weight: bold; } - + .page .tab_about .info dd { float: right; width: 80%; @@ -544,12 +541,12 @@ font-style: italic; } .page .tab_about .info dd.version { cursor: pointer; } - + .page .tab_about .group_actions > div { padding: 30px; text-align: center; } - + .page .tab_about .group_actions a { margin: 0 10px; font-size: 20px; @@ -561,24 +558,24 @@ font-size: 20px; font-weight: normal; } - + .group_userscript h2 .hint { display: block; margin: 0 !important; } - + .group_userscript .userscript { float: left; margin: 14px 0 0 25px; height: 36px; line-height: 25px; } - + .group_userscript .or { float: left; margin: 20px 10px; } - + .group_userscript .bookmarklet { display: block; display: block; @@ -586,8 +583,8 @@ padding: 20px 15px 0 0 ; border-radius: 5px; } - + .group_userscript .bookmarklet span { margin-left: 10px; - display: inline-block; + display: inline-block; } \ No newline at end of file diff --git a/couchpotato/static/style/uniform.css b/couchpotato/static/style/uniform.css index a64359cf..91bc83fc 100644 --- a/couchpotato/static/style/uniform.css +++ b/couchpotato/static/style/uniform.css @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------------ Copyright (c) 2010, Dragan Babic - + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without @@ -10,10 +10,10 @@ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND @@ -28,22 +28,22 @@ /* ------------------------------------------------------------------------------ */ .uniForm{ margin: 0; padding: 0; position: relative; z-index: 1; } /* reset stuff */ - + /* Some generals and more resets */ .uniForm fieldset{ border: none; margin: 0; padding: 0; } .uniForm fieldset legend{ margin: 0; padding: 0; } - + /* This are the main units that contain form elements */ .uniForm .ctrlHolder, .uniForm .buttonHolder{ margin: 0; padding: 0; clear: both; } - - /* Clear all floats */ + + /* Clear all floats */ .uniForm:after, - .uniForm .buttonHolder:after, - .uniForm .ctrlHolder:after, + .uniForm .buttonHolder:after, + .uniForm .ctrlHolder:after, .uniForm .ctrlHolder .multiField:after, .uniForm .inlineLabel:after{ content: "."; display: block; height: 0; line-height: 0; font-size: 0; clear: both; min-height: 0; visibility: hidden; } - + .uniForm label, .uniForm button{ cursor: pointer; } @@ -55,17 +55,17 @@ .uniForm label, .uniForm .label{ display: block; float: none; margin: 0 0 .5em 0; padding: 0; line-height: 100%; width: auto; } - + /* Float the input elements */ .uniForm .textInput, .uniForm .fileUpload, .uniForm .selectInput, .uniForm select, .uniForm textarea{ float: left; width: 53%; margin: 0; } - + /* Postition the hints */ .uniForm .formHint{ float: right; width: 43%; margin: 0; clear: none; } - + /* Position the elements inside combo boxes (multiple inputs/selects/checkboxes/radio buttons per unit) */ .uniForm ul{ float: left; width: 53%; margin: 0; padding: 0; } .uniForm ul li{ margin: 0 0 .5em 0; list-style: none; } @@ -79,7 +79,7 @@ .uniForm ul.alternate .textInput, .uniForm ul.alternate .selectInput, .uniForm ul.alternate select{ width: 98%; margin-top: .5em; display: block; float: none; } - + /* Required fields asterisk styling */ .uniForm label em, .uniForm .label em{ float: left; width: 1em; margin: 0 0 0 -1em; } @@ -93,7 +93,7 @@ .uniForm .inlineLabels label, .uniForm .inlineLabels .label{ float: left; margin: .3em 2% 0 0; padding: 0; line-height: 1; position: relative; width: 32%; } - + /* Float the input elements */ .uniForm .inlineLabels .textInput, .uniForm .inlineLabels .fileUpload, @@ -103,7 +103,7 @@ /* Postition the hints */ .uniForm .inlineLabels .formHint{ clear: both; float: none; width: auto; margin-left: 34%; position: static; } - + /* Position the elements inside combo boxes (multiple inputs/selects/checkboxes/radio buttons per unit) */ .uniForm .inlineLabels ul{ float: left; width: 66%; } .uniForm .inlineLabels ul li{ margin: .5em 0; } @@ -113,7 +113,7 @@ .uniForm .inlineLabels ul li label .textInput, .uniForm .inlineLabels ul li label textarea, .uniForm .inlineLabels ul li label select{ float: none; display: block; width: 98%; } - + /* Required fields asterisk styling */ .uniForm .inlineLabels label em, .uniForm .inlineLabels .label em{ display: block; float: none; margin: 0; position: absolute; right: 0; } @@ -124,22 +124,22 @@ /* Generals */ .uniForm legend{ color: inherit; } - + .uniForm .secondaryAction{ float: left; } - + /* .inlineLabel is used for inputs within labels - checkboxes and radio buttons */ .uniForm .inlineLabel input, .uniForm .inlineLabels .inlineLabel input, .uniForm .blockLabels .inlineLabel input, /* class .inlineLabel is depreciated */ .uniForm label input{ float: none; display: inline; margin: 0; padding: 0; border: none; } - + .uniForm .buttonHolder .inlineLabel, .uniForm .buttonHolder label{ float: left; margin: .5em 0 0 0; width: auto; max-width: 60%; text-align: left; } - + /* When you don't want to use a label */ .uniForm .inlineLabels .noLabel ul{ margin-left: 34%; /* Match to width of label + gap to field */ } - + /* Classes for control of the widths of the fields */ .uniForm .small { width: 30% !important; } .uniForm .medium{ width: 45% !important; } diff --git a/couchpotato/static/style/uniform.generic.css b/couchpotato/static/style/uniform.generic.css index a532dbf1..e70a9158 100644 --- a/couchpotato/static/style/uniform.generic.css +++ b/couchpotato/static/style/uniform.generic.css @@ -1,11 +1,11 @@ /* ------------------------------------------------------------------------------ - + UNI-FORM DEFAULT by DRAGAN BABIC (v2) | Wed, 31 Mar 10 - + ------------------------------------------------------------------------------ - + Copyright (c) 2010, Dragan Babic - + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without @@ -14,10 +14,10 @@ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND @@ -26,18 +26,18 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - + ------------------------------------------------------------------------------ */ .uniForm{} - + .uniForm legend{ font-weight: bold; font-size: 100%; margin: 0; padding: 1.5em 0; } - + .uniForm .ctrlHolder{ padding: 1em; border-bottom: 1px solid #efefef; } .uniForm .ctrlHolder.focused{ background: #fffcdf; } - + .uniForm .inlineLabels .noLabel{} - + .uniForm .buttonHolder{ background: #efefef; text-align: right; margin: 1.5em 0 0 0; padding: 1.5em; /* CSS3 */ border-radius: 4px; @@ -51,21 +51,21 @@ .uniForm .buttonHolder .primaryAction:active{ position: relative; top: 1px; } .uniForm .secondaryAction { text-align: left; } .uniForm button.secondaryAction { background: transparent; border: none; color: #777; margin: 1.25em 0 0 0; padding: 0; } - + .uniForm .inlineLabels label em, .uniForm .inlineLabels .label em{ font-style: normal; font-weight: bold; } .uniForm label small{ font-size: .75em; color: #777; } - + .uniForm .textInput, .uniForm textarea { padding: 4px 2px; border: 1px solid #aaa; background: #fff; } .uniForm textarea { height: 12em; } .uniForm select {} .uniForm .fileUpload {} - + .uniForm ul{} .uniForm li{} .uniForm ul li label{ font-size: .85em; } - + .uniForm .small {} .uniForm .medium{} .uniForm .large {} /* Large is default and should match the value you set for .textInput, textarea or select */ @@ -73,15 +73,15 @@ .uniForm .small, .uniForm .medium, .uniForm .auto{} - + /* Get rid of the 'glow' effect in WebKit, optional */ .uniForm .ctrlHolder .textInput:focus, .uniForm .ctrlHolder textarea:focus{ outline: none; } - + .uniForm .formHint { font-size: .85em; color: #777; } .uniForm .inlineLabels .formHint { padding-top: .5em; } .uniForm .ctrlHolder.focused .formHint{ color: #333; } - + /* ----------------------------------------------------------------------------- */ /* ############################### Messages #################################### */ /* ----------------------------------------------------------------------------- */ @@ -105,7 +105,7 @@ -o-border-radius: 4px; -khtml-border-radius: 4px; } - + .uniForm .ctrlHolder.error, .uniForm .ctrlHolder.focused.error{ background: #ffdfdf; border: 1px solid #f3afb5; /* CSS3 */ @@ -118,7 +118,7 @@ .uniForm .ctrlHolder.error input.error, .uniForm .ctrlHolder.error select.error, .uniForm .ctrlHolder.error textarea.error{ color: #af4c4c; margin: 0 0 6px 0; padding: 4px; } - + /* Success messages at the top of the form */ .uniForm #okMsg{ background: #c8ffbf; border: 1px solid #a2ef95; margin: 0 0 1.5em 0; padding: 0 1.5em; text-align: center; /* CSS3 */ From e69f25e8f319c95a4e98af5a1c1e2956bb4a9ae0 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 7 Apr 2012 20:27:54 +0200 Subject: [PATCH 98/99] Loading mask when adding movies --- .../core/plugins/movie/static/search.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js index a94b3438..53a8d399 100644 --- a/couchpotato/core/plugins/movie/static/search.js +++ b/couchpotato/core/plugins/movie/static/search.js @@ -285,6 +285,8 @@ Block.Search.Item = new Class({ var self = this; (e).preventDefault(); + self.loadingMask(); + Api.request('movie.add', { 'data': { 'identifier': self.info.imdb, @@ -358,6 +360,25 @@ Block.Search.Item = new Class({ }, + loadingMask: function(){ + var self = this; + + var s = self.options.getSize(); + + self.mask = new Element('span.mask', { + 'styles': { + 'width': s.x, + 'height': s.y + } + }).inject(self.options).fade('hide').position({ + 'relativeTo': self.options + }) + + createSpinner(self.mask) + self.mask.fade('in') + + }, + toElement: function(){ return this.el } From 787cee2a28527fe0c364bb9fff761addce16183d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 7 Apr 2012 20:39:51 +0200 Subject: [PATCH 99/99] Properly display quality in release list --- couchpotato/core/plugins/movie/static/movie.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index ee26ab95..dac9bf98 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -286,7 +286,7 @@ var ReleaseAction = new Class({ 'class': 'item ' + status.identifier }).adopt( new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}), - new Element('span.quality', {'text': quality.label}), + new Element('span.quality', {'text': quality.get('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')}),