Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop

Conflicts:
	couchpotato/core/_base/_core/__init__.py
This commit is contained in:
Ruud
2011-09-19 15:04:23 +02:00
93 changed files with 5831 additions and 2338 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ def start():
new_environ[key] = value.encode('iso-8859-1') new_environ[key] = value.encode('iso-8859-1')
subprocess.call(args, env = new_environ) subprocess.call(args, env = new_environ)
return os.path.isfile(os.path.join(options.data_dir, 'restart')) return os.path.isfile(os.path.join(base_path, 'restart'))
except Exception, e: except Exception, e:
log.critical(e) log.critical(e)
return 0 return 0
+1
View File
@@ -18,3 +18,4 @@ def index():
return jsonified({'routes': routes}) return jsonified({'routes': routes})
addApiView('', index) addApiView('', index)
addApiView('default', index)
+4 -4
View File
@@ -46,7 +46,7 @@ config = [{
{ {
'tab': 'general', 'tab': 'general',
'name': 'advanced', 'name': 'advanced',
'description': "For those who know what the're doing", 'description': "For those who know what they're doing",
'advanced': True, 'advanced': True,
'options': [ 'options': [
{ {
@@ -67,7 +67,7 @@ config = [{
'name': 'data_dir', 'name': 'data_dir',
'label': 'Data dir', 'label': 'Data dir',
'type': 'directory', 'type': 'directory',
'description': 'Where cache/logs/etc are stored.', 'description': 'Where cache/logs/etc are stored. Keep empty for <strong>./_data</strong>.',
}, },
{ {
'name': 'url_base', 'name': 'url_base',
@@ -79,13 +79,13 @@ config = [{
'name': 'permission_folder', 'name': 'permission_folder',
'default': 0755, 'default': 0755,
'label': 'Folder CHMOD', 'label': 'Folder CHMOD',
'description': 'Permission for creating/copying folders', 'description': 'Permission (octal) for creating/copying folders.',
}, },
{ {
'name': 'permission_file', 'name': 'permission_file',
'default': 0755, 'default': 0755,
'label': 'File CHMOD', 'label': 'File CHMOD',
'description': 'Permission for creating/copying files', 'description': 'Permission (octal) for creating/copying files',
}, },
], ],
}, },
+5 -6
View File
@@ -1,5 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -43,14 +43,13 @@ class Core(Plugin):
time.sleep(1) time.sleep(1)
if restart: if restart:
self.createFile(self.restartFilePath(), 'This is the most suckiest way to register if CP is restarted. Ever...') self.createFile(self.restartFilePath(), 'This is the most suckiest way to register if CP is restarted. Ever...')
func = request.environ.get('werkzeug.server.shutdown') try:
if func is None: request.environ.get('werkzeug.server.shutdown')()
except:
log.error('Failed shutting down the server') log.error('Failed shutting down the server')
func()
def removeRestartFile(self): def removeRestartFile(self):
try: try:
@@ -59,4 +58,4 @@ class Core(Plugin):
pass pass
def restartFilePath(self): def restartFilePath(self):
return os.path.join(Env.get('data_dir'), 'restart') return os.path.join(Env.get('app_dir'), 'restart')
+5 -1
View File
@@ -1,6 +1,7 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
log = CPLog(__name__) log = CPLog(__name__)
@@ -16,7 +17,10 @@ class Downloader(Plugin):
pass pass
def cpTag(self, movie): def cpTag(self, movie):
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else '' if Env.setting('enabled', 'renamer'):
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
return ''
def isDisabled(self): def isDisabled(self):
return not self.isEnabled() return not self.isEnabled()
@@ -27,18 +27,16 @@ class Blackhole(Downloader):
try: try:
if not os.path.isfile(fullPath): if not os.path.isfile(fullPath):
log.info('Downloading %s to %s.' % (data.get('type'), fullPath)) log.info('Downloading %s to %s.' % (data.get('type'), fullPath))
if isfunction(data.get('download')):
file = data.get('download')()
else:
file = self.urlopen(data.get('url'))
if not file or file == '': try:
file = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
with open(fullPath, 'wb') as f:
f.write(file)
except:
log.debug('Failed download file: %s' % data.get('name')) log.debug('Failed download file: %s' % data.get('name'))
return False return False
with open(fullPath, 'wb') as f:
f.write(file)
return True return True
else: else:
log.info('File %s already exists.' % fullPath) log.info('File %s already exists.' % fullPath)
+8 -11
View File
@@ -6,6 +6,7 @@ from urllib import urlencode
import base64 import base64
import os import os
import re import re
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -37,15 +38,11 @@ class Sabnzbd(Downloader):
params = { params = {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
'cat': self.conf('category'), 'cat': self.conf('category'),
'mode': 'addurl', 'mode': 'addfile',
'name': data.get('url'),
'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)), 'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)),
} }
# sabNzbd complains about "invalid archive file" for newzbin urls nzb_file = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
# added using addurl, works fine with addid
if data.get('addbyid'):
params['mode'] = 'addid'
if pp: if pp:
params['script'] = pp_script_fn params['script'] = pp_script_fn
@@ -53,9 +50,9 @@ class Sabnzbd(Downloader):
url = cleanHost(self.conf('host')) + "api?" + urlencode(params) url = cleanHost(self.conf('host')) + "api?" + urlencode(params)
try: try:
data = self.urlopen(url) data = self.urlopen(url, params = {"nzbfile": (params['nzbname'] + ".nzb", nzb_file)}, multipart = True)
except Exception, e: except Exception:
log.error("Unable to connect to SAB: %s" % e) log.error("Unable to connect to SAB: %s" % traceback.format_exc())
return False return False
result = data.strip() result = data.strip()
@@ -63,7 +60,7 @@ class Sabnzbd(Downloader):
log.error("SABnzbd didn't return anything.") log.error("SABnzbd didn't return anything.")
return False return False
log.debug("Result text from SAB: " + result) log.debug("Result text from SAB: " + result[:40])
if result == "ok": if result == "ok":
log.info("NZB sent to SAB successfully.") log.info("NZB sent to SAB successfully.")
return True return True
@@ -71,7 +68,7 @@ class Sabnzbd(Downloader):
log.error("Incorrect username/password.") log.error("Incorrect username/password.")
return False return False
else: else:
log.error("Unknown error: " + result) log.error("Unknown error: " + result[:40])
return False return False
def buildPp(self, imdb_id): def buildPp(self, imdb_id):
+2 -2
View File
@@ -8,7 +8,7 @@ log = CPLog(__name__)
events = {} events = {}
def addEvent(name, handler): def addEvent(name, handler, priority = 0):
if events.get(name): if events.get(name):
e = events[name] e = events[name]
@@ -27,7 +27,7 @@ def addEvent(name, handler):
return h return h
e += createHandle e.handle(createHandle, priority = priority)
def removeEvent(name, handler): def removeEvent(name, handler):
e = events[name] e = events[name]
+2 -2
View File
@@ -1,3 +1,4 @@
from couchpotato.core.helpers.variable import natcmp
from flask.globals import current_app from flask.globals import current_app
from flask.helpers import json from flask.helpers import json
from libs.werkzeug.urls import url_decode from libs.werkzeug.urls import url_decode
@@ -42,7 +43,7 @@ def dictToList(params):
new = {} new = {}
for x, value in params.iteritems(): for x, value in params.iteritems():
try: try:
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys())] new_value = [dictToList(value[k]) for k in sorted(value.iterkeys(), cmp = natcmp)]
except: except:
new_value = value new_value = value
@@ -70,4 +71,3 @@ def jsonified(*args, **kwargs):
return padded_jsonify(callback, *args, **kwargs) return padded_jsonify(callback, *args, **kwargs)
else: else:
return jsonify('text/javascript' if Env.doDebug() else 'application/json', *args, **kwargs) return jsonify('text/javascript' if Env.doDebug() else 'application/json', *args, **kwargs)
+12 -2
View File
@@ -1,10 +1,10 @@
import hashlib import hashlib
import os.path import os.path
import re
def isDict(object): def isDict(object):
return isinstance(object, dict) return isinstance(object, dict)
def mergeDicts(a, b): def mergeDicts(a, b):
assert isDict(a), isDict(b) assert isDict(a), isDict(b)
dst = a.copy() dst = a.copy()
@@ -16,7 +16,7 @@ def mergeDicts(a, b):
if key not in current_dst: if key not in current_dst:
current_dst[key] = current_src[key] current_dst[key] = current_src[key]
else: else:
if isDict(current_src[key]) and isDict(current_dst[key]) : if isDict(current_src[key]) and isDict(current_dst[key]):
stack.append((current_dst[key], current_src[key])) stack.append((current_dst[key], current_src[key]))
else: else:
current_dst[key] = current_src[key] current_dst[key] = current_src[key]
@@ -42,3 +42,13 @@ def cleanHost(host):
host += '/' host += '/'
return host return host
def tryInt(s):
try: return int(s)
except: return s
def natsortKey(s):
return map(tryInt, re.findall(r'(\d+|\D+)', s))
def natcmp(a, b):
return cmp(natsortKey(a), natsortKey(b))
+1 -1
View File
@@ -4,7 +4,7 @@ import re
class CPLog(): class CPLog():
context = '' context = ''
replace_private = ['api', 'apikey', 'api_key', 'password', 'username'] replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h']
def __init__(self, context = ''): def __init__(self, context = ''):
self.context = context self.context = context
+1 -1
View File
@@ -39,7 +39,7 @@ class Notification(Plugin):
data = {} data = {}
) )
#return jsonified({'success': success}) return jsonified({'success': success})
def testNotifyName(self): def testNotifyName(self):
return 'notify.%s.test' % self.getName().lower() return 'notify.%s.test' % self.getName().lower()
@@ -1,9 +1,7 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import History as Hist from couchpotato.core.settings.model import History as Hist
from couchpotato.environment import Env
import time import time
log = CPLog(__name__) log = CPLog(__name__)
@@ -13,12 +11,6 @@ class History(Notification):
listen_to = ['movie.downloaded', 'movie.snatched', 'renamer.canceled'] listen_to = ['movie.downloaded', 'movie.snatched', 'renamer.canceled']
def __init__(self):
super(Notification, self).__init__()
if Env.doDebug():
addEvent('app.load', self.test)
def notify(self, message = '', data = {}): def notify(self, message = '', data = {}):
db = get_session() db = get_session()
@@ -12,6 +12,7 @@ class Synoindex(Notification):
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, group = {}): def addToLibrary(self, group = {}):
if self.isDisabled(): return
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')] command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
log.info(u'Executing synoindex command: %s ' % command) log.info(u'Executing synoindex command: %s ' % command)
-74
View File
@@ -1,75 +1 @@
from uuid import uuid4
def start():
pass
config = [{
'name': 'core',
'groups': [
{
'tab': 'general',
'name': 'basics',
'description': 'Needs restart before changes take effect.',
'options': [
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'host',
'advanced': True,
'default': '0.0.0.0',
'label': 'IP',
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
},
{
'name': 'port',
'default': 5000,
'type': 'int',
'description': 'The port I should listen to.',
},
{
'name': 'launch_browser',
'default': 1,
'type': 'bool',
'label': 'Launch Browser',
'description': 'Launch the browser when I start.',
},
],
},
{
'tab': 'general',
'name': 'advanced',
'description': "For those who know what the're doing",
'advanced': True,
'options': [
{
'name': 'api_key',
'default': uuid4().hex,
'readonly': 1,
'label': 'Api Key',
'description': "This is top-secret! Don't share this!",
},
{
'name': 'debug',
'default': 0,
'type': 'bool',
'label': 'Debug',
'description': 'Enable debugging.',
},
{
'name': 'url_base',
'default': '',
'label': 'Url Base',
'description': 'When using mod_proxy use this to append the url with this.',
},
],
},
],
}]
+21 -7
View File
@@ -4,7 +4,9 @@ from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.helpers import send_from_directory from flask.helpers import send_from_directory
from libs.multipartpost import MultipartPostHandler
from urlparse import urlparse from urlparse import urlparse
import cookielib
import glob import glob
import math import math
import os.path import os.path
@@ -73,11 +75,14 @@ class Plugin(object):
try: try:
if not os.path.isdir(path): if not os.path.isdir(path):
os.makedirs(path, Env.getPermission('folder')) os.makedirs(path, Env.getPermission('folder'))
return True
except Exception, e: except Exception, e:
log.error('Unable to create folder "%s": %s' % (path, e)) log.error('Unable to create folder "%s": %s' % (path, e))
return False
# http request # http request
def urlopen(self, url, timeout = 10, params = {}, headers = {}): def urlopen(self, url, timeout = 10, params = {}, headers = {}, multipart = False):
socket.setdefaulttimeout(timeout) socket.setdefaulttimeout(timeout)
@@ -85,15 +90,24 @@ class Plugin(object):
self.wait(host) self.wait(host)
try: try:
log.info('Opening url: %s, params: %s' % (url, params))
data = urllib.urlencode(params) if len(params) > 0 else None if multipart:
request = urllib2.Request(url, data, headers) log.info('Opening multipart url: %s, params: %s' % (url, params.iterkeys()))
request = urllib2.Request(url, params, headers)
data = urllib2.urlopen(request).read() cookies = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
data = opener.open(request).read()
else:
log.info('Opening url: %s, params: %s' % (url, params))
data = urllib.urlencode(params) if len(params) > 0 else None
request = urllib2.Request(url, data, headers)
data = urllib2.urlopen(request).read()
except IOError, e: except IOError, e:
log.error('Failed opening url, %s: %s' % (url, e)) log.error('Failed opening url, %s: %s' % (url, e))
data = '' raise
self.http_last_use[host] = time.time() self.http_last_use[host] = time.time()
@@ -111,7 +125,7 @@ class Plugin(object):
time.sleep(last_use - now + self.http_time_between_calls) time.sleep(last_use - now + self.http_time_between_calls)
def beforeCall(self, handler): def beforeCall(self, handler):
log.debug('Calling %s.%s' % (self.getName(), handler.__name__)) #log.debug('Calling %s.%s' % (self.getName(), handler.__name__))
self.isRunning('%s.%s' % (self.getName(), handler.__name__)) self.isRunning('%s.%s' % (self.getName(), handler.__name__))
def afterCall(self, handler): def afterCall(self, handler):
+17 -1
View File
@@ -1,6 +1,7 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.request import getParam, jsonified from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import ctypes
import os import os
import string import string
@@ -23,7 +24,7 @@ class FileBrowser(Plugin):
dirs = [] dirs = []
for f in os.listdir(path): for f in os.listdir(path):
p = os.path.join(path, f) p = os.path.join(path, f)
if(os.path.isdir(p)): if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)):
dirs.append(p + '/') dirs.append(p + '/')
return dirs return dirs
@@ -48,6 +49,21 @@ class FileBrowser(Plugin):
dirs = [] dirs = []
return jsonified({ return jsonified({
'is_root': getParam('path', '/') == '/',
'empty': len(dirs) == 0, 'empty': len(dirs) == 0,
'dirs': dirs, 'dirs': dirs,
}) })
def is_hidden(self, filepath):
name = os.path.basename(os.path.abspath(filepath))
return name.startswith('.') or self.has_hidden_attribute(filepath)
def has_hidden_attribute(self, filepath):
try:
attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(filepath))
assert attrs != -1
result = bool(attrs & 2)
except (AttributeError, AssertionError):
result = False
return result
+2 -8
View File
@@ -42,17 +42,11 @@ class FileManager(Plugin):
if not dest: # to Cache if not dest: # to Cache
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url))) dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
if overwrite or not os.path.exists(dest): if overwrite or not os.path.isfile(dest):
log.debug('Writing file to: %s' % dest) self.createFile(dest, file)
output = open(dest, 'wb')
output.write(file)
output.close()
else:
log.debug('File already exists: %s' % dest)
return dest return dest
def add(self, path = '', part = 1, type = (), available = 1, properties = {}): def add(self, path = '', part = 1, type = (), available = 1, properties = {}):
db = get_session() db = get_session()
+21 -5
View File
@@ -2,14 +2,15 @@ from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, LibraryTitle, File from couchpotato.core.settings.model import Library, LibraryTitle, File, \
LibraryGenre
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
class LibraryPlugin(Plugin): class LibraryPlugin(Plugin):
default_dict = {'titles': {}, 'files':{}, 'info':{}} default_dict = {'titles': {}, 'files':{}, 'info':{}, 'genres':{}}
def __init__(self): def __init__(self):
addEvent('library.add', self.add) addEvent('library.add', self.add)
@@ -51,7 +52,9 @@ class LibraryPlugin(Plugin):
library = db.query(Library).filter_by(identifier = identifier).first() library = db.query(Library).filter_by(identifier = identifier).first()
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
library_dict = library.to_dict(self.default_dict) if library:
library_dict = library.to_dict(self.default_dict)
do_update = True do_update = True
if library.status_id == done_status.get('id') and not force: if library.status_id == done_status.get('id') and not force:
@@ -60,7 +63,7 @@ class LibraryPlugin(Plugin):
info = fireEvent('provider.movie.info', merge = True, identifier = identifier) info = fireEvent('provider.movie.info', merge = True, identifier = identifier)
if not info or len(info) == 0: if not info or len(info) == 0:
log.error('Could not update, no movie info to work with: %s' % identifier) log.error('Could not update, no movie info to work with: %s' % identifier)
do_update = False return False
# Main info # Main info
if do_update: if do_update:
@@ -75,7 +78,6 @@ class LibraryPlugin(Plugin):
db.commit() db.commit()
titles = info.get('titles', []) titles = info.get('titles', [])
log.debug('Adding titles: %s' % titles) log.debug('Adding titles: %s' % titles)
for title in titles: for title in titles:
t = LibraryTitle( t = LibraryTitle(
@@ -86,6 +88,20 @@ class LibraryPlugin(Plugin):
db.commit() db.commit()
# Genres
[db.delete(genre) for genre in library.genres]
db.commit()
genres = info.get('genres', [])
log.debug('Adding genres: %s' % genres)
for genre in genres:
g = LibraryGenre(
name = genre
)
library.genres.append(g)
db.commit()
# Files # Files
images = info.get('images', []) images = info.get('images', [])
for type in images: for type in images:
+1 -3
View File
@@ -8,9 +8,7 @@ log = CPLog(__name__)
class MetaData(Plugin): class MetaData(Plugin):
def __init__(self): def __init__(self):
addEvent('renaming.after', self.add) addEvent('renamer.after', self.add)
addEvent('app.load', self.add)
def add(self, data = {}): def add(self, data = {}):
log.info('Getting meta data') log.info('Getting meta data')
+22 -19
View File
@@ -5,11 +5,20 @@ from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie from couchpotato.core.settings.model import Movie
from couchpotato.environment import Env from couchpotato.environment import Env
from sqlalchemy.sql.expression import or_
from urllib import urlencode from urllib import urlencode
class MoviePlugin(Plugin): class MoviePlugin(Plugin):
default_dict = {
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
'library': {'titles': {}, 'files':{}},
'files': {},
'status': {}
}
def __init__(self): def __init__(self):
addApiView('movie.search', self.search) addApiView('movie.search', self.search)
addApiView('movie.list', self.list) addApiView('movie.list', self.list)
@@ -24,18 +33,16 @@ class MoviePlugin(Plugin):
params = getParams() params = getParams()
db = get_session() db = get_session()
results = db.query(Movie).filter( # Make a list from string
Movie.status.has(identifier = params.get('status', 'active')) status = params.get('status', ['active'])
).all() if not isinstance(status, (list, tuple)):
status = [status]
results = db.query(Movie).filter(or_(*[Movie.status.has(identifier = s) for s in status])).all()
movies = [] movies = []
for movie in results: for movie in results:
temp = movie.to_dict(deep = { temp = movie.to_dict(self.default_dict)
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
})
movies.append(temp) movies.append(temp)
return jsonified({ return jsonified({
@@ -59,12 +66,7 @@ class MoviePlugin(Plugin):
if movie: if movie:
#addEvent('library.update.after', ) #addEvent('library.update.after', )
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True) fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
fireEventAsync('searcher.single', movie.to_dict(deep = { fireEventAsync('searcher.single', movie.to_dict(self.default_dict))
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
}))
return jsonified({ return jsonified({
'success': True, 'success': True,
@@ -116,13 +118,14 @@ class MoviePlugin(Plugin):
if release.status_id == status_snatched.get('id'): if release.status_id == status_snatched.get('id'):
release.delete() release.delete()
m.profile_id = params.get('profile_id')
m.status_id = status_active.get('id') m.status_id = status_active.get('id')
db.commit() db.commit()
movie_dict = m.to_dict(deep = { movie_dict = m.to_dict(self.default_dict)
'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}},
'library': {'titles': {}} fireEventAsync('searcher.single', movie_dict)
})
return jsonified({ return jsonified({
'success': True, 'success': True,
@@ -27,12 +27,17 @@ var MovieList = new Class({
self.createNavigation(); self.createNavigation();
Object.each(self.movies, function(info){ Object.each(self.movies, function(info){
// Attach proper actions
var a = self.options.actions
var actions = a[info.status.identifier.capitalize()] || a.Wanted || {};
var m = new Movie(self, { var m = new Movie(self, {
'actions': self.options.actions 'actions': actions
}, info); }, info);
$(m).inject(self.el); $(m).inject(self.el);
m.fireEvent('injected'); m.fireEvent('injected');
if(self.options.navigation){ if(self.options.navigation){
var first_char = m.getTitle().substr(0, 1); var first_char = m.getTitle().substr(0, 1);
self.activateLetter(first_char); self.activateLetter(first_char);
@@ -71,7 +76,7 @@ var MovieList = new Class({
}); });
}, },
activateLetter: function(letter){ activateLetter: function(letter){
this.letters[letter].addClass('active'); this.letters[letter].addClass('active');
}, },
@@ -1,4 +1,8 @@
/* @override http://localhost:5000/static/movie_plugin/movie.css */ /* @override
http://localhost:5000/static/movie_plugin/movie.css
http://192.168.1.20:5000/static/movie_plugin/movie.css
http://127.0.0.1:5000/static/movie_plugin/movie.css
*/
.movies { .movies {
padding: 20px 0; padding: 20px 0;
@@ -79,13 +83,21 @@
float: left; float: left;
width: 5%; width: 5%;
padding: 0 0 0 3%; padding: 0 0 0 3%;
background: url('../images/rating.png') no-repeat left center;
} }
.movies .info .description { .movies .info .description {
clear: both; clear: both;
width: 95%; width: 95%;
} }
.movies .data .quality span {
padding: 5px;
font-weight: bold;
}
.movies .data .quality .available { color: orange; }
.movies .data .quality .snatched { color: lightgreen; }
.movies .data .actions { .movies .data .actions {
position: absolute; position: absolute;
right: 15px; right: 15px;
@@ -96,17 +108,14 @@
.movies .data:hover .action:hover { opacity: 1; } .movies .data:hover .action:hover { opacity: 1; }
.movies .data .action { .movies .data .action {
background: no-repeat center; background-repeat: no-repeat;
background-position: center;
display: inline-block; display: inline-block;
width: 20px; width: 20px;
height: 20px; height: 20px;
padding: 3px; padding: 3px;
opacity: 0; opacity: 0;
} }
.movies .data .action.refresh { background-image: url('../images/reload.png'); }
.movies .data .action.delete { background-image: url('../images/delete.png'); }
.movies .data .action.edit { background-image: url('../images/edit.png'); }
.movies .data .action.imdb { background-image: url('../images/imdb.png'); }
.movies .delete_container { .movies .delete_container {
clear: both; clear: both;
@@ -142,6 +151,65 @@
padding: 2%; padding: 2%;
} }
.movies .options .releases {
height: 157px;
overflow: auto;
margin: -20px -20px -20px 110px;
padding: 15px 0 5px;
}
.movies .options .releases .item {
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.movies .options .releases .item:last-child { border: 0; }
.movies .options .releases .item:nth-child(even) {
background: rgba(255,255,255,0.05);
}
.movies .options .releases .item:not(.head):hover {
background: rgba(255,255,255,0.03);
}
.movies .options .releases .item > * {
display: inline-block;
padding: 0 5px;
width: 50px;
min-height: 24px;
white-space: nowrap;
text-overflow: ellipsis;
-moz-text-overflow: ellipsis;
text-align: center;
vertical-align: top;
border-left: 1px solid rgba(255, 255, 255, 0.1);
}
.movies .options .releases .item > *:first-child {
border: 0;
}
.movies .options .releases .provider {
width: 120px;
}
.movies .options .releases .name {
width: 360px;
overflow: hidden;
text-align: left;
padding: 0 10px;
}
.movies .options .releases a {
width: 16px !important;
height: 16px;
opacity: 0.8;
}
.movies .options .releases a:hover {
opacity: 1;
}
.movies .options .releases .head > * {
font-weight: bold;
font-size: 14px;
padding-top: 4px;
padding-bottom: 4px;
height: auto;
}
.movies .alph_nav ul { .movies .alph_nav ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
+79 -10
View File
@@ -1,7 +1,7 @@
var Movie = new Class({ var Movie = new Class({
Extends: BlockBase, Extends: BlockBase,
action: {}, action: {},
initialize: function(self, options, data){ initialize: function(self, options, data){
@@ -32,7 +32,7 @@ var Movie = new Class({
self.year = new Element('div.year', { self.year = new Element('div.year', {
'text': self.data.library.year || 'Unknown' 'text': self.data.library.year || 'Unknown'
}), }),
self.rating = new Element('div.rating', { self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating 'text': self.data.library.rating
}), }),
self.description = new Element('div.description', { self.description = new Element('div.description', {
@@ -45,13 +45,20 @@ var Movie = new Class({
self.actions = new Element('div.actions') self.actions = new Element('div.actions')
) )
); );
self.profile.get('types').each(function(type){ self.profile.get('types').each(function(type){
// Check if quality is snatched
var is_snatched = self.data.releases.filter(function(release){
return release.quality_id == type.quality_id && release.status.identifier == 'snatched'
}).pick();
var q = Quality.getQuality(type.quality_id); var q = Quality.getQuality(type.quality_id);
new Element('span', { new Element('span', {
'text': ' '+q.label 'text': q.label,
'class': is_snatched ? 'snatched' : ''
}).inject(self.quality); }).inject(self.quality);
}) });
Object.each(self.options.actions, function(action, key){ Object.each(self.options.actions, function(action, key){
self.actions.adopt( self.actions.adopt(
@@ -127,7 +134,7 @@ var Movie = new Class({
var MovieAction = new Class({ var MovieAction = new Class({
class_name: 'action', class_name: 'action icon',
initialize: function(movie){ initialize: function(movie){
var self = this; var self = this;
@@ -193,7 +200,7 @@ var ReleaseAction = new Class({
self.id = self.movie.get('identifier'); self.id = self.movie.get('identifier');
self.el = new Element('a.releases', { self.el = new Element('a.releases.icon.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(), 'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': { 'events': {
'click': self.show.bind(self) 'click': self.show.bind(self)
@@ -211,16 +218,78 @@ var ReleaseAction = new Class({
$(self.movie.thumbnail).clone(), $(self.movie.thumbnail).clone(),
self.release_container = new Element('div.releases') self.release_container = new Element('div.releases')
).inject(self.movie, 'top'); ).inject(self.movie, 'top');
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'Release name'}),
new Element('span.quality', {'text': 'Quality'}),
new Element('span.size', {'text': 'Size (MB)'}),
new Element('span.age', {'text': 'Age'}),
new Element('span.score', {'text': 'Score'}),
new Element('span.provider', {'text': 'Provider'})
).inject(self.release_container)
Array.each(self.movie.data.releases, function(release){ Array.each(self.movie.data.releases, function(release){
p(release);
new Element('div', { new Element('div', {
'text': release.title 'class': 'item ' + release.status.identifier
}).inject(self.release_container) }).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
new Element('span.quality', {'text': release.quality.label}),
new Element('span.size', {'text': (self.get(release, 'size') || 'unknown')}),
new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', {'text': self.get(release, 'provider')}),
new Element('a.download.icon', {
'events': {
'click': function(e){
(e).stop();
self.download(release);
}
}
}),
new Element('a.delete.icon', {
'events': {
'click': function(e){
(e).stop();
self.del(release);
this.getParent('.item').destroy();
}
}
})
).inject(self.release_container)
}); });
} }
self.movie.slide('in'); self.movie.slide('in');
}, },
get: function(release, type){
var self = this;
return (release.info.filter(function(info){
return type == info.identifier
}).pick() || {}).value
},
download: function(release){
var self = this;
Api.request('release.download', {
'data': {
'id': release.id
}
});
},
del: function(release){
var self = this;
Api.request('release.delete', {
'data': {
'id': release.id
}
})
}
}); });
@@ -1,4 +1,7 @@
/* @override http://localhost:5000/static/movie_plugin/search.css */ /* @override
http://localhost:5000/static/movie_plugin/search.css
http://192.168.1.20:5000/static/movie_plugin/search.css
*/
.search_form { .search_form {
display: inline-block; display: inline-block;
@@ -86,7 +89,7 @@
margin-right: 10px; margin-right: 10px;
} }
.search_form .results .movie .options select[name=title] { width: 180px; } .search_form .results .movie .options select[name=title] { width: 180px; }
.search_form .results .movie .options select[name=quality] { width: 90px; } .search_form .results .movie .options select[name=profile] { width: 90px; }
.search_form .results .movie .options .button { .search_form .results .movie .options .button {
vertical-align: middle; vertical-align: middle;
@@ -10,6 +10,7 @@ Block.Search = new Class({
self.el = new Element('div.search_form').adopt( self.el = new Element('div.search_form').adopt(
new Element('div.input').adopt( new Element('div.input').adopt(
self.input = new Element('input.inlay', { self.input = new Element('input.inlay', {
'placeholder': 'Search for new movies',
'events': { 'events': {
'keyup': self.keyup.bind(self), 'keyup': self.keyup.bind(self),
'focus': self.hideResults.bind(self, false) 'focus': self.hideResults.bind(self, false)
@@ -28,7 +29,7 @@ Block.Search = new Class({
}).adopt( }).adopt(
new Element('div.pointer'), new Element('div.pointer'),
self.results = new Element('div.results') self.results = new Element('div.results')
).fade('hide') ).hide()
); );
self.spinner = new Spinner(self.result_container); self.spinner = new Spinner(self.result_container);
@@ -51,7 +52,7 @@ Block.Search = new Class({
if(self.hidden == bool) return; if(self.hidden == bool) return;
self.result_container.fade(bool ? 0 : 1) self.result_container[bool ? 'hide' : 'show']();
if(bool){ if(bool){
History.removeEvent('change', self.hideResults.bind(self, !bool)); History.removeEvent('change', self.hideResults.bind(self, !bool));
@@ -302,7 +303,7 @@ Block.Search.Item = new Class({
}).inject(self.title_select) }).inject(self.title_select)
}) })
Object.each(Quality.profiles, function(profile){ Object.each(Quality.getActiveProfiles(), function(profile){
new Element('option', { new Element('option', {
'value': profile.id ? profile.id : profile.data.id, 'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label 'text': profile.label ? profile.label : profile.data.label
+65 -1
View File
@@ -1,6 +1,7 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParams, getParam from couchpotato.core.helpers.request import jsonified, getParams, getParam
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -15,8 +16,11 @@ class ProfilePlugin(Plugin):
addEvent('profile.all', self.all) addEvent('profile.all', self.all)
addApiView('profile.save', self.save) addApiView('profile.save', self.save)
addApiView('profile.save_order', self.saveOrder)
addApiView('profile.delete', self.delete) addApiView('profile.delete', self.delete)
addEvent('app.initialize', self.fill, priority = 90)
def all(self): def all(self):
db = get_session() db = get_session()
@@ -50,7 +54,7 @@ class ProfilePlugin(Plugin):
for type in params.get('types', []): for type in params.get('types', []):
t = ProfileType( t = ProfileType(
order = order, order = order,
finish = type.get('finish'), finish = type.get('finish') if order > 0 else 1,
wait_for = params.get('wait_for'), wait_for = params.get('wait_for'),
quality_id = type.get('quality_id') quality_id = type.get('quality_id')
) )
@@ -67,6 +71,25 @@ class ProfilePlugin(Plugin):
'profile': profile_dict 'profile': profile_dict
}) })
def saveOrder(self):
params = getParams()
db = get_session()
order = 0
for profile in params.get('ids', []):
p = db.query(Profile).filter_by(id = profile).first()
p.hide = params.get('hidden')[order]
p.order = order
order += 1
db.commit()
return jsonified({
'success': True
})
def delete(self): def delete(self):
id = getParam('id') id = getParam('id')
@@ -90,3 +113,44 @@ class ProfilePlugin(Plugin):
'success': success, 'success': success,
'message': message 'message': message
}) })
def fill(self):
db = get_session();
profiles = [{
'label': 'Best',
'qualities': ['720p', '1080p', 'brrip', 'dvdrip']
}, {
'label': 'HD',
'qualities': ['720p', '1080p']
}]
# Create default quality profile
order = -2
for profile in profiles:
log.info('Creating default profile: %s' % profile.get('label'))
p = Profile(
label = toUnicode(profile.get('label')),
order = order
)
db.add(p)
quality_order = 0
for quality in profile.get('qualities'):
quality = fireEvent('quality.single', identifier = quality, single = True)
profile_type = ProfileType(
quality_id = quality.get('id'),
profile = p,
finish = True,
wait_for = 0,
order = quality_order
)
p.types.append(profile_type)
db.commit()
quality_order += 1
order += 1
return True
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

@@ -1,18 +1,134 @@
.profile > .delete { /* @override http://192.168.1.20:5000/static/profile_plugin/profile.css */
background-position: center;
height: 20px; .add_new_profile {
width: 20px; padding: 20px;
display: block;
text-align: center;
font-size: 20px;
border-bottom: 1px solid rgba(255,255,255,0.2);
} }
.profile .types .type .handle { .profile { border-bottom: 1px solid rgba(255,255,255,0.2) }
background: url('../../images/handle.png') center;
display: inline-block; .profile > .delete {
height: 20px; height: 20px;
width: 20px; width: 20px;
position: absolute;
margin-left: 690px;
padding: 14px;
background-position: center;
}
.profile .qualities {
min-height: 80px;
}
.profile .formHint {
width: 250px !important;
}
.profile .wait_for {
position: absolute;
margin: -45px 0 0 437px;
}
.profile .wait_for input {
margin: 0 5px !important;
}
.profile .types {
padding: 0;
margin: 0 20px 0 -4px;
display: inline-block;
}
.profile .types li {
padding: 3px 5px;
border-bottom: 1px solid rgba(255,255,255,0.2);
list-style: none;
}
.profile .types li:last-child { border: 0; }
.profile .types li > * {
display: inline-block;
vertical-align: middle;
line-height: 0;
margin-right: 10px;
}
.profile .quality_type select {
width: 186px;
margin-left: -1px;
}
.profile .types li.is_empty .check, .profile .types li.is_empty .delete, .profile .types li.is_empty .handle {
visibility: hidden;
}
.profile .types .type .handle {
background: url('./handle.png') center;
display: inline-block;
height: 20px;
width: 20px;
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
margin: 0;
}
.profile .types .type .delete {
background-position: left center;
height: 20px;
width: 20px;
visibility: hidden;
cursor: pointer;
}
.profile .types .type:hover:not(.is_empty) .delete {
visibility: visible;
}
#profile_ordering {
} }
.profile .types .type .delete { #profile_ordering ul {
background-position: center; float: left;
height: 20px; margin: 0;
width: 20px; width: 275px;
} padding: 0;
}
#profile_ordering li {
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
border-bottom: 1px solid rgba(255,255,255,0.2);
padding: 0 5px;
}
#profile_ordering li:last-child { border: 0; }
#profile_ordering li .check {
margin: 2px 10px 0 0;
vertical-align: top;
}
#profile_ordering li > span {
display: inline-block;
height: 20px;
vertical-align: top;
line-height: 20px;
}
#profile_ordering li .handle {
background: url('./handle.png') center;
width: 20px;
float: right;
}
#profile_ordering .formHint {
clear: none;
float: right;
width: 250px;
margin: 0;
}
@@ -24,47 +24,32 @@ var Profile = new Class({
var data = self.data; var data = self.data;
self.el = new Element('div.profile').adopt( self.el = new Element('div.profile').adopt(
self.header = new Element('h4', {'text': data.label}),
new Element('span.delete.icon', { new Element('span.delete.icon', {
'events': { 'events': {
'click': self.del.bind(self) 'click': self.del.bind(self)
} }
}), }),
new Element('div', { new Element('.quality_label.ctrlHolder').adopt(
'class': 'ctrlHolder'
}).adopt(
new Element('label', {'text':'Name'}), new Element('label', {'text':'Name'}),
new Element('input.label.textInput.large', { new Element('input.inlay', {
'type':'text', 'type':'text',
'value': data.label, 'value': data.label,
'events': { 'placeholder': 'Profile name'
'keyup': function(){
self.header.set('text', this.get('value'))
}
}
}) })
), ),
new Element('div.ctrlHolder').adopt( new Element('div.wait_for.ctrlHolder').adopt(
new Element('label', {'text':'Wait'}), new Element('span', {'text':'Wait'}),
new Element('input.wait_for.textInput.xsmall', { new Element('input.inlay.xsmall', {
'type':'text', 'type':'text',
'value': data.types && data.types.length > 0 ? data.types[0].wait_for : 0 'value': data.types && data.types.length > 0 ? data.types[0].wait_for : 0
}), }),
new Element('span', {'text':' day(s) for better quality.'}) new Element('span', {'text':'day(s) for a better quality.'})
), ),
new Element('div.ctrlHolder').adopt( new Element('div.qualities.ctrlHolder').adopt(
new Element('label', {'text': 'Qualities'}), new Element('label', {'text': 'Search for'}),
new Element('div.head').adopt(
new Element('span.quality_type', {'text': 'Search for'}),
new Element('span.finish', {'html': '<acronym title="Won\'t download anything else if it has found this quality.">Finish</acronym>'})
),
self.type_container = new Element('ol.types'), self.type_container = new Element('ol.types'),
new Element('a.addType', { new Element('div.formHint', {
'text': 'Add another quality to search for.', 'html': "Search these qualities (2 minimum), from top to bottom. Use the checkbox, to stop searching after it found this quality."
'href': '#',
'events': {
'click': self.addType.bind(self)
}
}) })
) )
); );
@@ -73,6 +58,8 @@ var Profile = new Class({
if(data.types) if(data.types)
Object.each(data.types, self.addType.bind(self)) Object.each(data.types, self.addType.bind(self))
self.addType();
}, },
save: function(delay){ save: function(delay){
@@ -81,6 +68,8 @@ var Profile = new Class({
if(self.save_timer) clearTimeout(self.save_timer); if(self.save_timer) clearTimeout(self.save_timer);
self.save_timer = (function(){ self.save_timer = (function(){
self.addType();
var data = self.getData(); var data = self.getData();
if(data.types.length < 2) return; if(data.types.length < 2) return;
@@ -96,6 +85,7 @@ var Profile = new Class({
} }
} }
}); });
}).delay(delay, self) }).delay(delay, self)
}, },
@@ -105,8 +95,8 @@ var Profile = new Class({
var data = { var data = {
'id' : self.data.id, 'id' : self.data.id,
'label' : self.el.getElement('.label').get('value'), 'label' : self.el.getElement('.quality_label input').get('value'),
'wait_for' : self.el.getElement('.wait_for').get('value'), 'wait_for' : self.el.getElement('.wait_for input').get('value'),
'types': [] 'types': []
} }
@@ -124,8 +114,19 @@ var Profile = new Class({
addType: function(data){ addType: function(data){
var self = this; var self = this;
var t = new Profile.Type(data); var has_empty = false;
self.types.each(function(type){
if($(type).hasClass('is_empty'))
has_empty = true;
});
if(has_empty) return;
var t = new Profile.Type(data, {
'onChange': self.save.bind(self, 0)
});
$(t).inject(self.type_container); $(t).inject(self.type_container);
self.sortable.addItems($(t)); self.sortable.addItems($(t));
self.types.include(t); self.types.include(t);
@@ -135,23 +136,35 @@ var Profile = new Class({
del: function(){ del: function(){
var self = this; var self = this;
if(!confirm('Are you sure you want to delete this profile?')) return var label = self.el.getElement('.quality_label input').get('value');
new Question('Are you sure you want to delete <strong>"'+label+'"</strong>?', 'Items using this profile, will be set to the default quality.', [{
Api.request('profile.delete', { 'text': 'Delete "'+label+'"',
'data': { 'class': 'delete',
'id': self.data.id 'events': {
}, 'click': function(e){
'useSpinner': true, (e).stop();
'spinnerOptions': { Api.request('profile.delete', {
'target': self.el 'data': {
}, 'id': self.data.id
'onComplete': function(json){ },
if(json.success) 'useSpinner': true,
self.el.destroy(); 'spinnerOptions': {
else 'target': self.el
alert(json.message) },
'onComplete': function(json){
if(json.success)
self.el.destroy();
else
alert(json.message)
}
});
}
} }
}); }, {
'text': 'Cancel',
'cancel': true
}]);
}, },
makeSortable: function(){ makeSortable: function(){
@@ -180,16 +193,24 @@ var Profile = new Class({
}); });
Profile.Type = Class({ Profile.Type = new Class({
Implements: [Events, Options],
deleted: false, deleted: false,
initialize: function(data){ initialize: function(data, options){
var self = this; var self = this;
self.setOptions(options);
self.data = data; self.data = data || {};
self.create(); self.create();
self.addEvent('change', function(){
self.el[self.qualities.get('value') == '-1' ? 'addClass' : 'removeClass']('is_empty');
self.deleted = self.qualities.get('value') == '-1';
});
}, },
create: function(){ create: function(){
@@ -201,10 +222,11 @@ Profile.Type = Class({
self.fillQualities() self.fillQualities()
), ),
new Element('span.finish').adopt( new Element('span.finish').adopt(
self.finish = new Element('input', { self.finish = new Element('input.inlay.finish[type=checkbox]', {
'type':'checkbox', 'checked': data.finish,
'class':'finish', 'events': {
'checked': data.finish 'change': self.fireEvent.bind(self, 'change')
}
}) })
), ),
new Element('span.delete.icon', { new Element('span.delete.icon', {
@@ -213,14 +235,27 @@ Profile.Type = Class({
} }
}), }),
new Element('span.handle') new Element('span.handle')
) );
self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
new Form.Check(self.finish);
}, },
fillQualities: function(){ fillQualities: function(){
var self = this; var self = this;
self.qualities = new Element('select'); self.qualities = new Element('select', {
'events': {
'change': self.fireEvent.bind(self, 'change')
}
}).adopt(
new Element('option', {
'text': '+ Add another quality',
'value': -1
})
);
Object.each(Quality.qualities, function(q){ Object.each(Quality.qualities, function(q){
new Element('option', { new Element('option', {
@@ -250,6 +285,8 @@ Profile.Type = Class({
self.el.addClass('deleted'); self.el.addClass('deleted');
self.el.hide(); self.el.hide();
self.deleted = true; self.deleted = true;
self.fireEvent('change');
}, },
toElement: function(){ toElement: function(){
+38 -33
View File
@@ -13,13 +13,13 @@ log = CPLog(__name__)
class QualityPlugin(Plugin): class QualityPlugin(Plugin):
qualities = [ qualities = [
{'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['x264', 'h264', 'bluray']}, {'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate']},
{'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']}, {'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
{'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']}, {'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
{'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']}, {'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['dvdscr'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['dvdscr', 'ppvrip'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
@@ -31,7 +31,8 @@ class QualityPlugin(Plugin):
addEvent('quality.all', self.all) addEvent('quality.all', self.all)
addEvent('quality.single', self.single) addEvent('quality.single', self.single)
addEvent('quality.guess', self.guess) addEvent('quality.guess', self.guess)
addEvent('app.load', self.fill)
addEvent('app.initialize', self.fill, priority = 10)
def all(self): def all(self):
@@ -112,40 +113,44 @@ class QualityPlugin(Plugin):
return True return True
def guess(self, files, extra = {}): def guess(self, files, extra = {}, loose = False):
found = False
for file in files: for file in files:
size = (os.path.getsize(file) / 1024 / 1024) size = (os.path.getsize(file) / 1024 / 1024)
words = re.split('\W+', file.lower()) words = re.split('\W+', file.lower())
for quality in self.all():
correctSize = False
if size >= quality['size_min'] and size <= quality['size_max']: for quality in self.all():
correctSize = True
# Check tags # Check tags
if type in words: if quality['identifier'] in words:
found = True log.debug('Found via identifier "%s" in %s' % (quality['identifier'], file))
for alt in quality.get('alternative'):
if alt in words:
found = True
for tag in quality.get('tags', []):
if tag in words:
found = True
# Check extension + filesize
for ext in quality.get('ext'):
if ext in words and correctSize:
found = True
# Last check on resolution only
if quality.get('width', 480) == extra.get('resolution_width', 0):
found = True
if found:
return quality return quality
return '' if list(set(quality.get('alternative', [])) & set(words)):
log.debug('Found %s via alt %s in %s' % (quality['identifier'], quality.get('alternative'), file))
return quality
if list(set(quality.get('tags', [])) & set(words)):
log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), file))
return quality
# Check on unreliable stuff
if loose:
# Check extension + filesize
if list(set(quality.get('ext', [])) & set(words)) and size >= quality['size_min'] and size <= quality['size_max']:
log.debug('Found %s via ext %s in %s' % (quality['identifier'], quality.get('ext'), words))
return quality
# Last check on resolution only
if quality.get('width', 480) == extra.get('resolution_width', 0):
log.debug('Found %s via resolution_width: %s == %s' % (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0)))
return quality
# Try again with loose testing
quality = self.guess(files, extra = extra, loose = True)
if quality:
return quality
log.error('Could not identify quality for: %s' % files)
return {}
@@ -19,6 +19,13 @@ var QualityBase = new Class({
return this.profiles[id] return this.profiles[id]
}, },
// Hide items when getting profiles
getActiveProfiles: function(){
return Object.filter(this.profiles, function(profile){
return !profile.data.hide
});
},
getQuality: function(id){ getQuality: function(id){
return this.qualities.filter(function(q){ return this.qualities.filter(function(q){
return q.id == id; return q.id == id;
@@ -31,7 +38,7 @@ var QualityBase = new Class({
self.settings = App.getPage('Settings') self.settings = App.getPage('Settings')
self.settings.addEvent('create', function(){ self.settings.addEvent('create', function(){
var tab = self.settings.createTab('profile', { var tab = self.settings.createTab('profile', {
'label': 'Profile', 'label': 'Quality',
'name': 'profile' 'name': 'profile'
}); });
@@ -39,6 +46,7 @@ var QualityBase = new Class({
self.content = tab.content; self.content = tab.content;
self.createProfiles(); self.createProfiles();
self.createProfileOrdering();
self.createSizes(); self.createSizes();
}) })
@@ -50,42 +58,104 @@ var QualityBase = new Class({
*/ */
createProfiles: function(){ createProfiles: function(){
var self = this; var self = this;
var non_core_profiles = Object.filter(self.profiles, function(profile){ return !profile.isCore() });
var count = Object.getLength(non_core_profiles);
self.settings.createGroup({ self.settings.createGroup({
'label': 'Custom', 'label': 'Quality Profiles',
'description': 'Discriptions' 'description': 'Create your own profiles with multiple qualities.'
}).inject(self.content).adopt( }).inject(self.content).adopt(
new Element('a.add_new', { self.profile_container = new Element('div.container'),
'text': 'Create a new quality profile', new Element('a.add_new_profile', {
'text': count > 0 ? 'Create another quality profile' : 'Click here to create a quality profile.',
'events': { 'events': {
'click': function(){ 'click': function(){
var profile = self.createProfilesClass(); var profile = self.createProfilesClass();
$(profile).inject(self.profile_container, 'top') $(profile).inject(self.profile_container)
} }
} }
}), })
self.profile_container = new Element('div.container') );
)
Object.each(self.profiles, function(profile){ // Add profiles, that aren't part of the core (for editing)
if(!profile.isCore()) Object.each(non_core_profiles, function(profile){
$(profile).inject(self.profile_container, 'top') $(profile).inject(self.profile_container)
}) });
}, },
createProfilesClass: function(data){ createProfilesClass: function(data){
var self = this; var self = this;
if(data){ var data = data || {'id': randomString()}
return self.profiles[data.id] = new Profile(data);
} return self.profiles[data.id] = new Profile(data);
else { },
var data = {
'id': randomString() createProfileOrdering: function(){
var self = this;
var profile_list;
var group = self.settings.createGroup({
'label': 'Profile Defaults'
}).adopt(
new Element('.ctrlHolder#profile_ordering').adopt(
new Element('label[text=Order]'),
profile_list = new Element('ul'),
new Element('p.formHint', {
'html': 'Change the order the profiles are in the dropdown list. Uncheck to hide it completely.<br />First one will be default.'
})
)
).inject(self.content)
Object.each(self.profiles, function(profile){
var check;
new Element('li', {'data-id': profile.data.id}).adopt(
check = new Element('input.inlay[type=checkbox]', {
'checked': !profile.data.hide,
'events': {
'change': self.saveProfileOrdering.bind(self)
}
}),
new Element('span.profile_label', {
'text': profile.data.label
}),
new Element('span.handle')
).inject(profile_list);
new Form.Check(check);
});
// Sortable
self.profile_sortable = new Sortables(profile_list, {
'revert': true,
'handle': '',
'opacity': 0.5,
'onComplete': self.saveProfileOrdering.bind(self)
});
},
saveProfileOrdering: function(){
var self = this;
var ids = [];
var hidden = [];
self.profile_sortable.list.getElements('li').each(function(el, nr){
ids.include(el.get('data-id'));
hidden[nr] = +!el.getElement('input[type=checkbox]').get('checked');
});
Api.request('profile.save_order', {
'data': {
'ids': ids,
'hidden': hidden
} }
return self.profiles[data.id] = new Profile(data); });
}
}, },
/** /**
@@ -96,24 +166,25 @@ var QualityBase = new Class({
var group = self.settings.createGroup({ var group = self.settings.createGroup({
'label': 'Sizes', 'label': 'Sizes',
'description': 'Discriptions', 'description': 'Edit the minimal and maximum sizes (in MB) for each quality.',
'advanced': true 'advanced': true
}).inject(self.content) }).inject(self.content)
new Element('div.item.header').adopt(
new Element('div.item.head').adopt(
new Element('span.label', {'text': 'Quality'}), new Element('span.label', {'text': 'Quality'}),
new Element('span.min', {'text': 'Min'}), new Element('span.min', {'text': 'Min'}),
new Element('span.max', {'text': 'Max'}) new Element('span.max', {'text': 'Max'})
).inject(group) ).inject(group)
Object.each(self.qualities, function(quality){ Object.each(self.qualities, function(quality){
new Element('div.item').adopt( new Element('div.ctrlHolder.item').adopt(
new Element('span.label', {'text': quality.label}), new Element('span.label', {'text': quality.label}),
new Element('input.min', {'value': quality.size_min}), new Element('input.min', {'value': quality.size_min}),
new Element('input.max', {'value': quality.size_max}) new Element('input.max', {'value': quality.size_max})
).inject(group) ).inject(group)
}); });
} }
}); });
+60 -10
View File
@@ -1,8 +1,10 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File, Release, Movie from couchpotato.core.settings.model import File, Release as Relea, Movie
from sqlalchemy.sql.expression import and_, or_ from sqlalchemy.sql.expression import and_, or_
log = CPLog(__name__) log = CPLog(__name__)
@@ -13,6 +15,9 @@ class Release(Plugin):
def __init__(self): def __init__(self):
addEvent('release.add', self.add) addEvent('release.add', self.add)
addApiView('release.download', self.download)
addApiView('release.delete', self.delete)
def add(self, group): def add(self, group):
db = get_session() db = get_session()
@@ -30,22 +35,22 @@ class Release(Plugin):
db.add(movie) db.add(movie)
db.commit() db.commit()
# Add release # Add Release
snatched_status = fireEvent('status.get', 'snatched', single = True) snatched_status = fireEvent('status.get', 'snatched', single = True)
release = db.query(Release).filter( rel = db.query(Relea).filter(
or_( or_(
Release.identifier == identifier, Relea.identifier == identifier,
and_(Release.identifier.startswith(group['library']['identifier'], Release.status_id == snatched_status.get('id'))) and_(Relea.identifier.startswith(group['library']['identifier'], Relea.status_id == snatched_status.get('id')))
) )
).first() ).first()
if not release: if not rel:
release = Release( rel = Relea(
identifier = identifier, identifier = identifier,
movie = movie, movie = movie,
quality_id = group['meta_data']['quality'].get('id'), quality_id = group['meta_data']['quality'].get('id'),
status_id = done_status.get('id') status_id = done_status.get('id')
) )
db.add(release) db.add(rel)
db.commit() db.commit()
# Add each file type # Add each file type
@@ -54,10 +59,10 @@ class Release(Plugin):
added_file = self.saveFile(file, type = type, include_media_info = type is 'movie') added_file = self.saveFile(file, type = type, include_media_info = type is 'movie')
try: try:
added_file = db.query(File).filter_by(id = added_file.get('id')).one() added_file = db.query(File).filter_by(id = added_file.get('id')).one()
release.files.append(added_file) Relea.files.append(added_file)
db.commit() db.commit()
except Exception, e: except Exception, e:
log.debug('Failed to attach "%s" to release: %s' % (file, e)) log.debug('Failed to attach "%s" to Relea: %s' % (file, e))
db.remove() db.remove()
@@ -73,3 +78,48 @@ class Release(Plugin):
# Check database and update/insert if necessary # Check database and update/insert if necessary
return fireEvent('file.add', path = file, part = self.getPartNumber(file), type = self.file_types[type], properties = properties, single = True) return fireEvent('file.add', path = file, part = self.getPartNumber(file), type = self.file_types[type], properties = properties, single = True)
def delete(self):
db = get_session()
id = getParam('id')
rel = db.query(Relea).filter_by(id = id).first()
if rel:
rel.delete()
db.commit()
return jsonified({
'success': True
})
def download(self):
db = get_session()
id = getParam('id')
rel = db.query(Relea).filter_by(id = id).first()
if rel:
item = {}
for info in rel.info:
item[info.identifier] = info.value
# Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], single = True)
item['download'] = provider.download
fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
}))
return jsonified({
'success': True
})
else:
log.error('Couldn\'t find release with id: %s' % id)
return jsonified({
'success': False
})
+10 -8
View File
@@ -61,20 +61,22 @@ config = [{
'advanced': True, 'advanced': True,
'options': [ 'options': [
{ {
'name': 'trailer_name', 'name': 'rename_nfo',
'label': 'Trailer naming', 'label': 'Rename .NFO',
'default': '<filename>-trailer.<ext>', 'description': 'Rename original .nfo file',
'type': 'bool',
'default': True,
}, },
{ {
'name': 'nfo_name', 'name': 'nfo_name',
'label': 'NFO naming', 'label': 'NFO naming',
'default': '<filename>.<ext>', 'default': '<filename>.<ext>-orig',
}, },
{ {
'name': 'backdrop_name', 'name': 'trailer_name',
'label': 'Backdrop naming', 'label': 'Trailer naming',
'default': '<filename>-backdrop.<ext>', 'default': '<filename>-trailer.<ext>',
} },
], ],
}, },
], ],
+66 -29
View File
@@ -1,10 +1,10 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import getExt from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library from couchpotato.core.settings.model import Library, Movie
import os.path import os.path
import re import re
import shutil import shutil
@@ -40,7 +40,7 @@ class Renamer(Plugin):
group = groups[group_identifier] group = groups[group_identifier]
rename_files = {} rename_files = {}
# Add _UNKNOWN_ if no library is connected # Add _UNKNOWN_ if no library item is connected
if not group['library']: if not group['library']:
if group['dirname']: if group['dirname']:
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname']) rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname'])
@@ -53,6 +53,10 @@ class Renamer(Plugin):
# Rename the files using the library data # Rename the files using the library data
else: else:
group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True) group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True)
if not group['library']:
log.error('Could not rename, no library item to work with: %s' % group_identifier)
continue
library = group['library'] library = group['library']
# Find subtitle for renaming # Find subtitle for renaming
@@ -85,12 +89,9 @@ class Renamer(Plugin):
for file_type in group['files']: for file_type in group['files']:
# Move DVD files (no renaming)
if group['is_dvd'] and file_type is 'movie':
continue
# Move nfo depending on settings # Move nfo depending on settings
if file_type is 'nfo' and not self.conf('rename_nfo'): if file_type is 'nfo' and not self.conf('rename_nfo'):
log.debug('Skipping, renaming of %s disabled' % file_type)
continue continue
# Subtitle extra # Subtitle extra
@@ -98,7 +99,7 @@ class Renamer(Plugin):
continue continue
# Move other files # Move other files
multiple = len(group['files']['movie']) > 1 multiple = len(group['files']['movie']) > 1 and not group['is_dvd']
cd = 1 if multiple else 0 cd = 1 if multiple else 0
for file in sorted(list(group['files'][file_type])): for file in sorted(list(group['files'][file_type])):
@@ -118,21 +119,35 @@ class Renamer(Plugin):
final_folder_name = self.doReplace(folder_name, replacements) final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements) final_file_name = self.doReplace(file_name, replacements)
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)] replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
group['filename'] = replacements['filename']
# Meta naming # Meta naming
if file_type is 'trailer': if file_type is 'trailer':
final_file_name = self.doReplace(trailer_name, replacements) final_file_name = self.doReplace(trailer_name, replacements)
elif file_type is 'nfo': elif file_type is 'nfo':
final_file_name = self.doReplace(nfo_name, replacements) + '-orig' final_file_name = self.doReplace(nfo_name, replacements)
elif file_type is 'backdrop':
final_file_name = self.doReplace(backdrop_name, replacements)
# Seperator replace # Seperator replace
if separator: if separator:
final_file_name = final_file_name.replace(' ', separator) final_file_name = final_file_name.replace(' ', separator)
# Main file # Move DVD files (no structure renaming)
rename_files[file] = os.path.join(destination, final_folder_name, final_file_name) if group['is_dvd'] and file_type is 'movie':
found = False
for top_dir in ['video_ts', 'audio_ts', 'bdmv', 'certificate']:
has_string = file.lower().find(os.path.sep + top_dir + os.path.sep)
if has_string >= 0:
structure_dir = file[has_string:].lstrip(os.path.sep)
rename_files[file] = os.path.join(destination, final_folder_name, structure_dir)
found = True
break
if not found:
log.error('Could not determin dvd structure for: %s' % file)
# Do rename others
else:
rename_files[file] = os.path.join(destination, final_folder_name, final_file_name)
# Check for extra subtitle files # Check for extra subtitle files
if file_type is 'subtitle': if file_type is 'subtitle':
@@ -154,21 +169,43 @@ class Renamer(Plugin):
if multiple: if multiple:
cd += 1 cd += 1
# Notify on download
download_message = 'Download of %s (%s) successful.' % (group['library']['titles'][0]['title'], replacements['quality'])
fireEvent('movie.downloaded', message = download_message, data = group)
# Before renaming, remove the lower quality files # Before renaming, remove the lower quality files
db = get_session() db = get_session()
library = db.query(Library).filter_by(identifier = group['library']['identifier']).first() library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
active_status = fireEvent('status.get', 'active', single = True)
for movie in library.movies: for movie in library.movies:
# Mark movie "done" onces it found the quality with the finish check
try:
if movie.status_id == active_status.get('id'):
for type in movie.profile.types:
if type.quality_id == group['meta_data']['quality']['id'] and type.finish:
movie.status_id = done_status.get('id')
db.commit()
except Exception, e:
log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc()))
# Go over current movie releases
for release in movie.releases: for release in movie.releases:
if release.quality.order < group['meta_data']['quality']['order']:
# This is where CP removes older, lesser quality releases
if release.quality.order > group['meta_data']['quality']['order']:
log.info('Removing older release for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label)) log.info('Removing older release for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
for file in release.files:
log.info('Removing (not really) "%s"' % file.path)
# When a release already exists
elif release.status_id is done_status.get('id'): elif release.status_id is done_status.get('id'):
# Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc
if release.quality.order is group['meta_data']['quality']['order']: if release.quality.order is group['meta_data']['quality']['order']:
log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label)) log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label))
# Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan
else: else:
log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label)) log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
@@ -188,10 +225,7 @@ class Renamer(Plugin):
break break
for file in release.files: # Rename all files marked
log.info('Removing (not really) "%s"' % file.path)
# Rename
for src in rename_files: for src in rename_files:
if rename_files[src]: if rename_files[src]:
@@ -200,21 +234,24 @@ class Renamer(Plugin):
log.info('Renaming "%s" to "%s"' % (src, dst)) log.info('Renaming "%s" to "%s"' % (src, dst))
path = os.path.dirname(dst) path = os.path.dirname(dst)
try:
if not os.path.isdir(path): os.makedirs(path) # Create dir
except: self.makeDir(path)
log.error('Failed creating dir %s: %s' % (path, traceback.format_exc()))
continue
try: try:
shutil.move(src, dst) pass
#shutil.move(src, dst)
except: except:
log.error('Failed moving the file "%s" : %s' % (os.path.basename(src), traceback.format_exc())) log.error('Failed moving the file "%s" : %s' % (os.path.basename(src), traceback.format_exc()))
#print rename_me, rename_files[rename_me] #print rename_me, rename_files[rename_me]
# Search for trailers etc # Search for trailers etc
fireEvent('renamer.after', group) fireEventAsync('renamer.after', group)
# Notify on download
download_message = 'Download of %s (%s) successful.' % (group['library']['titles'][0]['title'], replacements['quality'])
fireEventAsync('movie.downloaded', message = download_message, data = group)
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
+35 -18
View File
@@ -4,13 +4,13 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import getExt from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File, Release, Movie from couchpotato.core.settings.model import File
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.helpers import json from flask.helpers import json
from sqlalchemy.sql.expression import and_, or_
import os import os
import re import re
import subprocess import subprocess
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -23,11 +23,11 @@ class Scanner(Plugin):
'trailer': 1048576, # 1MB 'trailer': 1048576, # 1MB
} }
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads'] ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
extensions = { extensions = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'], 'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'],
'dvd': ['vts_*', 'vob'], 'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'], 'nfo': ['nfo', 'nfo-orig', 'txt', 'tag'],
'subtitle': ['sub', 'srt', 'ssa', 'ass'], 'subtitle': ['sub', 'srt', 'ssa', 'ass'],
'subtitle_extra': ['idx'], 'subtitle_extra': ['idx'],
'trailer': ['mov', 'mp4', 'flv'] 'trailer': ['mov', 'mp4', 'flv']
@@ -172,9 +172,21 @@ class Scanner(Plugin):
# Determine file types # Determine file types
delete_identifier = []
for identifier in movie_files: for identifier in movie_files:
group = movie_files[identifier] group = movie_files[identifier]
# Check if movie is fresh and maybe still unpacking, ignore files new then 1 minute
file_too_new = False
for file in group['unsorted_files']:
if os.path.getmtime(file) > time.time() - 60:
file_too_new = True
if file_too_new:
log.info('Files seem to be still unpacking or just unpacked, ignoring for now: %s' % identifier)
delete_identifier.append(identifier)
continue
# Group extra (and easy) files first # Group extra (and easy) files first
images = self.getImages(group['unsorted_files']) images = self.getImages(group['unsorted_files'])
group['files'] = { group['files'] = {
@@ -182,7 +194,7 @@ class Scanner(Plugin):
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']), 'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
'nfo': self.getNfo(group['unsorted_files']), 'nfo': self.getNfo(group['unsorted_files']),
'trailer': self.getTrailers(group['unsorted_files']), 'trailer': self.getTrailers(group['unsorted_files']),
'backdrop': images['backdrop'], #'backdrop': images['backdrop'],
'leftover': set(group['unsorted_files']), 'leftover': set(group['unsorted_files']),
} }
@@ -198,12 +210,13 @@ class Scanner(Plugin):
group['parentdir'] = os.path.dirname(movie_file) group['parentdir'] = os.path.dirname(movie_file)
group['dirname'] = None group['dirname'] = None
folders = group['parentdir'].replace(folder, '').split(os.path.sep) folder_names = group['parentdir'].replace(folder, '').split(os.path.sep)
folder_names.reverse()
# Try and get a proper dirname, so no "A", "Movie", "Download" # Try and get a proper dirname, so no "A", "Movie", "Download" etc
for folder in folders: for folder_name in folder_names:
if folder.lower() in self.ignore_names or len(folder) < 2: if folder_name.lower() not in self.ignore_names and len(folder_name) > 2:
group['dirname'] = folder group['dirname'] = folder_name
break break
break break
@@ -220,12 +233,16 @@ class Scanner(Plugin):
if not group['library']: if not group['library']:
log.error('Unable to determin movie: %s' % group['identifiers']) log.error('Unable to determin movie: %s' % group['identifiers'])
# Delete still (asuming) unpacking files
for identifier in delete_identifier:
del movie_files[identifier]
return movie_files return movie_files
def getMetaData(self, group): def getMetaData(self, group):
data = {} data = {}
files = group['files']['movie'] files = list(group['files']['movie'])
for file in files: for file in files:
if os.path.getsize(file) < self.minimal_filesize['media']: continue # Ignore smaller files if os.path.getsize(file) < self.minimal_filesize['media']: continue # Ignore smaller files
@@ -246,10 +263,11 @@ class Scanner(Plugin):
if not data['quality']: if not data['quality']:
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True) data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 720 else 'SD' data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 else 'SD'
data['group'] = self.getGroup(file[0]) file = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
data['source'] = self.getSourceMedia(file[0]) data['group'] = self.getGroup(file)
data['source'] = self.getSourceMedia(file)
return data return data
@@ -365,7 +383,6 @@ class Scanner(Plugin):
return set(filter(test, files)) return set(filter(test, files))
def getDVDFiles(self, files): def getDVDFiles(self, files):
def test(s): def test(s):
return self.isDVDFile(s) return self.isDVDFile(s)
@@ -409,7 +426,7 @@ class Scanner(Plugin):
if list(set(file.lower().split(os.path.sep)) & set(['video_ts', 'audio_ts'])): if list(set(file.lower().split(os.path.sep)) & set(['video_ts', 'audio_ts'])):
return True return True
for needle in ['vts_', 'video_ts', 'audio_ts']: for needle in ['vts_', 'video_ts', 'audio_ts', 'bdmv', 'certificate']:
if needle in file.lower(): if needle in file.lower():
return True return True
@@ -510,8 +527,8 @@ class Scanner(Plugin):
def getGroup(self, file): def getGroup(self, file):
try: try:
group = re.search('-(?P<group>[A-Z0-9]+)$', file, re.I) match = re.search('-(?P<group>[A-Z0-9]+).', file, re.I)
return group.group('group') or '' return match.group('group') or ''
except: except:
return '' return ''
@@ -23,7 +23,7 @@ config = [{
'name': 'required_words', 'name': 'required_words',
'label': 'Required words', 'label': 'Required words',
'default': '', 'default': '',
'description': 'Ignore releases that doesn\'t contain one of these words.' 'description': 'Ignore releases that don\'t contain at least one of these words.'
}, },
{ {
'name': 'ignored_words', 'name': 'ignored_words',
+38 -32
View File
@@ -6,7 +6,9 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env from couchpotato.environment import Env
from sqlalchemy.exc import InterfaceError
import re import re
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -17,6 +19,7 @@ class Searcher(Plugin):
addEvent('searcher.all', self.all) addEvent('searcher.all', self.all)
addEvent('searcher.single', self.single) addEvent('searcher.single', self.single)
addEvent('searcher.correct_movie', self.correctMovie) addEvent('searcher.correct_movie', self.correctMovie)
addEvent('searcher.download', self.download)
# Schedule cronjob # Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.all, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) fireEvent('schedule.cron', 'searcher.all', self.all, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
@@ -34,7 +37,7 @@ class Searcher(Plugin):
for movie in movies: for movie in movies:
self.single(movie.to_dict(deep = { self.single(movie.to_dict({
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}}, 'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
@@ -47,11 +50,8 @@ class Searcher(Plugin):
def single(self, movie): def single(self, movie):
downloaded_status = fireEvent('status.get', 'downloaded', single = True)
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
snatched_status = fireEvent('status.get', 'snatched', single = True)
successful = False
for type in movie['profile']['types']: for type in movie['profile']['types']:
has_better_quality = 0 has_better_quality = 0
@@ -85,37 +85,22 @@ class Searcher(Plugin):
db.commit() db.commit()
for info in nzb: for info in nzb:
rls_info = ReleaseInfo( try:
identifier = info, if not isinstance(nzb[info], (str, unicode, int, long)):
value = nzb[info] continue
)
rls.info.append(rls_info) rls_info = ReleaseInfo(
db.commit() identifier = info,
value = nzb[info]
)
rls.info.append(rls_info)
db.commit()
except InterfaceError:
log.debug('Couldn\'t add %s to ReleaseInfo: %s' % (info, traceback.format_exc()))
for nzb in sorted_results: for nzb in sorted_results:
successful = fireEvent('download', data = nzb, movie = movie, single = True) return self.download(data = nzb, movie = movie)
if successful:
# Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first()
rls.status_id = snatched_status.get('id')
db.commit()
# Mark movie snatched if quality is finish-checked
if type['finish']:
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = snatched_status.get('id')
db.commit()
log.info('Downloading of %s successful.' % nzb.get('name'))
fireEvent('movie.snatched', message = 'Downloading of %s successful.' % nzb.get('name'), data = rls.to_dict())
return True
return False
else: else:
log.info('Better quality (%s) already available or snatched for %s' % (type['quality']['label'], default_title)) log.info('Better quality (%s) already available or snatched for %s' % (type['quality']['label'], default_title))
break break
@@ -126,6 +111,26 @@ class Searcher(Plugin):
return False return False
def download(self, data, movie):
snatched_status = fireEvent('status.get', 'snatched', single = True)
successful = fireEvent('download', data = data, movie = movie, single = True)
if successful:
# Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
rls.status_id = snatched_status.get('id')
db.commit()
log.info('Downloading of %s successful.' % data.get('name'))
fireEvent('movie.snatched', message = 'Downloading of %s successful.' % data.get('name'), data = rls.to_dict())
return True
return False
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs): def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
@@ -192,6 +197,7 @@ class Searcher(Plugin):
if len(movie_words) == 2 and self.correctYear([nzb['name']], movie['library']['year'], 0): if len(movie_words) == 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
return True return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie['library']['titles'][0]['title'], movie['library']['year']))
return False return False
def containsOtherQuality(self, name, preferred_quality = {}, single_category = False): def containsOtherQuality(self, name, preferred_quality = {}, single_category = False):
+28 -11
View File
@@ -1,4 +1,6 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -10,14 +12,13 @@ log = CPLog(__name__)
class Updater(Plugin): class Updater(Plugin):
git = 'git://github.com/CouchPotato/CouchPotato.git' repo_name = 'RuudBurger/CouchPotatoServer'
running = False running = False
version = None version = None
updateFailed = False update_failed = False
updateAvailable = False update_version = None
updateVersion = None last_check = 0
lastCheck = 0
def __init__(self): def __init__(self):
@@ -27,6 +28,18 @@ class Updater(Plugin):
addEvent('app.load', self.check) addEvent('app.load', self.check)
addApiView('updater.info', self.getInfo)
addApiView('updater.update', self.doUpdateView)
def getInfo(self):
return jsonified({
'repo_name': self.repo_name,
'last_check': self.last_check,
'update_version': self.update_version,
'version': self.getVersion(),
})
def getVersion(self): def getVersion(self):
if not self.version: if not self.version:
@@ -42,7 +55,7 @@ class Updater(Plugin):
def check(self): def check(self):
if self.updateAvailable or self.isDisabled(): if self.update_version or self.isDisabled():
return return
current_branch = self.repo.getCurrentBranch().name current_branch = self.repo.getCurrentBranch().name
@@ -54,13 +67,17 @@ class Updater(Plugin):
remote = branch.getHead() remote = branch.getHead()
if local.getDate() < remote.getDate(): if local.getDate() < remote.getDate():
if self.conf('automatic') and not self.updateFailed: if self.conf('automatic') and not self.update_failed:
self.doUpdate() self.doUpdate()
else: else:
self.updateAvailable = True self.update_version = remote.hash
self.updateVersion = remote.hash
self.lastCheck = time.time() self.last_check = time.time()
def doUpdateView(self):
return jsonified({
'success': self.doUpdate()
})
def doUpdate(self): def doUpdate(self):
try: try:
@@ -70,7 +87,7 @@ class Updater(Plugin):
except Exception, e: except Exception, e:
log.error('Failed updating via GIT: %s' % e) log.error('Failed updating via GIT: %s' % e)
self.updateFailed = True self.update_failed = True
return False return False
@@ -0,0 +1,89 @@
var UpdaterBase = new Class({
initialize: function(){
var self = this;
App.addEvent('load', self.info.bind(self, 1000))
},
info: function(timeout){
var self = this;
if(self.timer) clearTimeout(self.timer);
self.timer = setTimeout(function(){
Api.request('updater.info', {
'onComplete': function(json){
if(json.update_version){
self.createMessage(json);
}
else {
if(self.message)
self.message.destroy();
}
}
})
}, (timeout || 0))
},
createMessage: function(data){
var self = this;
self.message = new Element('div.message.update').adopt(
new Element('span', {
'text': 'A new version is available'
}),
new Element('a', {
'href': 'https://github.com/'+data.repo_name+'/compare/'+data.version.substr(0, 7)+'...'+data.update_version.substr(0, 7),
'text': 'see what has changed',
'target': '_blank'
}),
new Element('span[text=or]'),
new Element('a', {
'text': 'just update, gogogo!',
'events': {
'click': self.doUpdate.bind(self)
}
})
).inject($(document.body).getElement('.header'))
},
doUpdate: function(){
var self = this;
Api.request('updater.update', {
'onComplete': function(json){
if(json.success){
App.restart();
$(document.body).set('spin', {
'message': 'Updating'
});
$(document.body).spin();
var checks = 0;
var interval = 0;
interval = setInterval(function(){
Api.request('', {
'onSuccess': function(){
if(checks > 2){
clearInterval(interval);
$(document.body).unspin();
self.info();
}
}
});
checks++;
}, 500)
}
}
});
}
});
var Updater = new UpdaterBase();
+1 -1
View File
@@ -4,7 +4,7 @@ def start():
return Wizard() return Wizard()
config = [{ config = [{
'name': 'global', 'name': 'core',
'groups': [ 'groups': [
{ {
'tab': 'general', 'tab': 'general',
+23 -5
View File
@@ -2,7 +2,6 @@ from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from urllib2 import URLError
from urlparse import urlparse from urlparse import urlparse
import re import re
import time import time
@@ -60,22 +59,36 @@ class YarrProvider(Provider):
sizeMb = ['mb', 'mib'] sizeMb = ['mb', 'mib']
sizeKb = ['kb', 'kib'] sizeKb = ['kb', 'kib']
def __init__(self):
addEvent('provider.belongs_to', self.belongsTo)
def belongsTo(self, url, host = None):
try:
hostname = urlparse(url).hostname
download_url = host if host else self.urls['download']
if hostname in download_url:
return self
except:
log.debug('Url % s doesn\'t belong to %s' % (url, self.getName()))
return
def parseSize(self, size): def parseSize(self, size):
sizeRaw = size.lower() sizeRaw = size.lower()
size = re.sub(r'[^0-9.]', '', size).strip() size = float(re.sub(r'[^0-9.]', '', size).strip())
for s in self.sizeGb: for s in self.sizeGb:
if s in sizeRaw: if s in sizeRaw:
return float(size) * 1024 return int(size) * 1024
for s in self.sizeMb: for s in self.sizeMb:
if s in sizeRaw: if s in sizeRaw:
return float(size) return int(size)
for s in self.sizeKb: for s in self.sizeKb:
if s in sizeRaw: if s in sizeRaw:
return float(size) / 1024 return int(size) / 1024
return 0 return 0
@@ -96,11 +109,16 @@ class NZBProvider(YarrProvider):
type = 'nzb' type = 'nzb'
def __init__(self): def __init__(self):
super(NZBProvider, self).__init__()
addEvent('provider.nzb.search', self.search) addEvent('provider.nzb.search', self.search)
addEvent('provider.yarr.search', self.search) addEvent('provider.yarr.search', self.search)
addEvent('provider.nzb.feed', self.feed) addEvent('provider.nzb.feed', self.feed)
def download(self, url = '', nzb_id = ''):
return self.urlopen(url)
def feed(self): def feed(self):
return [] return []
+2 -2
View File
@@ -1,4 +1,4 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -17,7 +17,7 @@ class MetaDataBase(Plugin):
log.info('Creating %s metadata.' % self.getName()) log.info('Creating %s metadata.' % self.getName())
root = self.getRootName() root = self.getRootName(release)
for type in ['nfo', 'thumbnail', 'fanart']: for type in ['nfo', 'thumbnail', 'fanart']:
try: try:
@@ -8,7 +8,7 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'renamer', 'tab': 'renamer',
'name': 'metadata', 'name': 'mediabrowser_metadata',
'label': 'MediaBrowser', 'label': 'MediaBrowser',
'description': 'Enable metadata MediaBrowser can understand', 'description': 'Enable metadata MediaBrowser can understand',
'options': [ 'options': [
@@ -8,7 +8,7 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'renamer', 'tab': 'renamer',
'name': 'metadata', 'name': 'sonyps3_metadata',
'label': 'Sony PS3', 'label': 'Sony PS3',
'description': 'Enable metadata your Playstation 3 can understand', 'description': 'Enable metadata your Playstation 3 can understand',
'options': [ 'options': [
@@ -8,7 +8,7 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'renamer', 'tab': 'renamer',
'name': 'metadata', 'name': 'wdtv_metadata',
'label': 'WDTV', 'label': 'WDTV',
'description': 'Enable metadata WDTV can understand', 'description': 'Enable metadata WDTV can understand',
'options': [ 'options': [
@@ -8,7 +8,7 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'renamer', 'tab': 'renamer',
'name': 'metadata', 'name': 'xbmc_metadata',
'label': 'XBMC', 'label': 'XBMC',
'description': 'Enable metadata XBMC can understand', 'description': 'Enable metadata XBMC can understand',
'options': [ 'options': [
+33 -142
View File
@@ -1,15 +1,14 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.providers.metadata.base import MetaDataBase from couchpotato.core.providers.metadata.base import MetaDataBase
from xml.etree.ElementTree import Element, SubElement, tostring from xml.etree.ElementTree import Element, SubElement, tostring
import os
import re import re
import xml.dom.minidom import xml.dom.minidom
class XBMC(MetaDataBase): class XBMC(MetaDataBase):
def getRootName(self, data = {}): def getRootName(self, data = {}):
return os.path.join(data['destination_dir'], data['filename'])
return '/Users/ruud/Downloads/Test/Transformers'
def getFanartName(self, root): def getFanartName(self, root):
return '%s-fanart.jpg' % root return '%s-fanart.jpg' % root
@@ -23,8 +22,30 @@ class XBMC(MetaDataBase):
def getNfo(self, data): def getNfo(self, data):
nfoxml = Element('movie') nfoxml = Element('movie')
types = ['title', 'rating', 'year', 'votes', 'rating', 'mpaa', 'originaltitle:original_title', 'outline:overview', 'premiered:released', 'id:imdb_id'] types = ['rating', 'year', 'votes', 'rating', 'mpaa', 'originaltitle:original_title', 'outline:plot', 'premiered:released']
# Title
try:
el = SubElement(nfoxml, 'title')
el.text = toUnicode(data['library']['titles'][0]['title'])
except:
pass
# IMDB id
try:
el = SubElement(nfoxml, 'id')
el.text = toUnicode(data['library']['identifier'])
except:
pass
# Runtime
try:
runtime = SubElement(nfoxml, 'runtime')
runtime.text = '%s min' % data['library']['runtime']
except:
pass
# Other values
for type in types: for type in types:
if ':' in type: if ':' in type:
@@ -33,20 +54,17 @@ class XBMC(MetaDataBase):
name = type name = type
try: try:
el = SubElement(nfoxml, name) if data['library'].get(type):
el.text = data.get(type, '') el = SubElement(nfoxml, name)
el.text = toUnicode(data['library'].get(type, ''))
except: except:
pass pass
#for genre in self.get('genres'): # Genre
# genres = SubElement(nfoxml, 'genre') for genre in data['library'].get('genres', []):
# genres.text = genre genres = SubElement(nfoxml, 'genre')
genres.text = genre.get('name')
try:
runtime = SubElement(nfoxml, 'runtime')
runtime.text = data.get('runtime') + " min"
except:
pass
# Clean up the xml and return it # Clean up the xml and return it
nfoxml = xml.dom.minidom.parseString(tostring(nfoxml)) nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
@@ -55,130 +73,3 @@ class XBMC(MetaDataBase):
xml_string = text_re.sub('>\g<1></', xml_string) xml_string = text_re.sub('>\g<1></', xml_string)
return xml_string.encode('utf-8') return xml_string.encode('utf-8')
"""
def _get_fanart(self, min_height, min_width):
''' Fetches the fanart for the specified imdb_id and saves it to dir.
Arguments
min_height/width: Sets lowest acceptable resolution fanart. 0 means
disregard. If no fanart available at specified resolution or greater, then
we disregard.
'''
images = [image['image'] for image in self.tmdb_data['backdrops'] if image['image'].get('size') == 'original']
if len(images) == 0:
return
return self._get_image(images, min_height, min_width)
def get_fanart_url(self, min_height, min_width):
return self._get_fanart(min_height, min_width)['url']
def write_fanart(self, filename_root, path, min_height, min_width):
fanart_url = self.get_fanart_url(min_height, min_width)
#fetch and write to disk
dest = os.path.join(path, filename_root)
try:
f = open(dest, 'wb')
except:
raise IOError("Can't open for writing: %s" % dest)
response = urllib2.urlopen(fanart_url)
f.write(response.read())
f.close()
return True
def _get_poster(self, min_height, min_width):
''' Fetches the poster for the specified imdb_id and saves it to dir.
Arguments
min_height/width: Sets lowest acceptable resolution poster. 0 means
disregard. If no poster available at specified resolution or greater, then
we disregard.
'''
images = [image['image'] for image in self.tmdb_data['posters'] if image['image'].get('size') == 'original']
if len(images) == 0:
return
return self._get_image(images, min_height, min_width)
def get_poster_url(self, min_height, min_width):
return self._get_poster(min_height, min_width)['url']
def write_poster(self, filename_root, path, min_height, min_width):
poster_url = self.get_poster_url(min_height, min_width)
dest = os.path.join(path, filename_root)
try:
f = open(dest, 'wb')
except:
raise IOError("Can't open for writing: %s" % dest)
response = urllib2.urlopen(poster_url)
f.write(response.read())
f.close()
return True
def _get_tmdb_imdb(self):
url = "http://api.themoviedb.org/2.1/Movie.imdbLookup/en/json/%s/%s" % (__tmdb_apikey__, self.imdbid)
count = 0
while 1:
count += 1
response = urllib2.urlopen(url)
json_string = response.read()
try:
tmdb_data = json.loads(json_string)[0]
return tmdb_data
except ValueError, e:
if count < 3:
continue
else:
raise ApiError("Invalid JSON: %s: %s" % (e, json_string))
except:
ApiError("JSON error with: %s" % json_string)
def _get_image(self, image_list, min_height, min_width):
#Select image
images = []
for image in image_list:
if not min_height or min_width:
images.append(image)
break
elif min_height and not min_width:
if image['height'] >= min_height:
images.append(image)
break
elif min_width and not min_height:
if image['width'] >= min_width:
images.append(image)
break
elif min_width and min_height:
if image['width'] >= min_width and image['height'] >= min_height:
images.append(image)
break
#No image meets our resolution requirements, so disregard those requirements
if len(images) == 0 and min_height or min_width:
images.append(image_list[0])
return images[0]
if __name__ == "__main__":
import sys
try:
id = sys.argv[1]
except:
id = 'tt0111161'
x = MetaGen(id)
x.write_nfo("movie.nfo")
try:
x.write_fanart("fanart.jpg", ".", 0, 0)
except: pass
try:
x.write_poster("movie.tbn", ".", 0, 0)
except: pass
"""
@@ -10,7 +10,7 @@ class IMDB(MovieProvider):
def __init__(self): def __init__(self):
addEvent('provider.movie.search', self.search) #addEvent('provider.movie.search', self.search)
self.p = IMDb('http') self.p = IMDb('http')
@@ -92,7 +92,7 @@ class TheMovieDb(MovieProvider):
def getInfo(self, identifier = None): def getInfo(self, identifier = None):
cache_key = 'tmdb.cache.%s' % identifier cache_key = 'tmdb.cache.%s' % identifier
result = self.getCache(cache_key) result = None #self.getCache(cache_key)
if not result: if not result:
result = {} result = {}
@@ -112,27 +112,33 @@ class TheMovieDb(MovieProvider):
def parseMovie(self, movie): def parseMovie(self, movie):
year = str(movie.get('released', 'none'))[:4]
# Poster url # Poster url
poster = self.getImage(movie, type = 'poster') poster = self.getImage(movie, type = 'poster')
backdrop = self.getImage(movie, type = 'backdrop') backdrop = self.getImage(movie, type = 'backdrop')
# Genres
genres = self.getCategory(movie, 'genre')
# 1900 is the same as None # 1900 is the same as None
year = str(movie.get('released', 'none'))[:4]
if year == '1900' or year.lower() == 'none': if year == '1900' or year.lower() == 'none':
year = None year = None
movie_data = { movie_data = {
'id': int(movie.get('id', 0)), 'id': int(movie.get('id', 0)),
'titles': [toUnicode(movie.get('name'))], 'titles': [toUnicode(movie.get('name'))],
'original_title': movie.get('original_name'),
'images': { 'images': {
'posters': [poster], 'posters': [poster],
'backdrops': [backdrop], 'backdrops': [backdrop],
}, },
'imdb': movie.get('imdb_id'), 'imdb': movie.get('imdb_id'),
'runtime': movie.get('runtime'),
'released': movie.get('released'),
'year': year, 'year': year,
'plot': movie.get('overview', ''), 'plot': movie.get('overview', ''),
'tagline': '', 'tagline': '',
'genres': genres,
} }
# Add alternative names # Add alternative names
@@ -153,6 +159,19 @@ class TheMovieDb(MovieProvider):
return image return image
def getCategory(self, movie, type = 'genre'):
cats = movie.get('categories', {}).get(type)
categories = []
for category in cats:
try:
categories.append(category)
except:
pass
return categories
def isDisabled(self): def isDisabled(self):
if self.conf('api_key') == '': if self.conf('api_key') == '':
log.error('No API key provided.') log.error('No API key provided.')
@@ -13,10 +13,9 @@ log = CPLog(__name__)
class Newzbin(NZBProvider, RSS): class Newzbin(NZBProvider, RSS):
urls = { urls = {
'search': 'https://www.newzbin.com/search/',
'download': 'http://www.newzbin.com/api/dnzb/', 'download': 'http://www.newzbin.com/api/dnzb/',
'search': 'https://www.newzbin.com/search/',
} }
searchUrl = 'https://www.newzbin.com/search/'
format_ids = { format_ids = {
2: ['scr'], 2: ['scr'],
@@ -36,7 +35,7 @@ class Newzbin(NZBProvider, RSS):
def search(self, movie, quality): def search(self, movie, quality):
results = [] results = []
if self.isDisabled() or not self.isAvailable(self.searchUrl): if self.isDisabled() or not self.isAvailable(self.urls['search']):
return results return results
format_id = self.getFormatId(type) format_id = self.getFormatId(type)
@@ -97,11 +96,12 @@ class Newzbin(NZBProvider, RSS):
new = { new = {
'id': id, 'id': id,
'type': 'nzb', 'type': 'nzb',
'provider': self.getName(),
'name': title, 'name': title,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': self.parseSize(size), 'size': self.parseSize(size),
'url': str(self.getTextElement(nzb, '{%s}nzb' % REPORT_NS)), 'url': str(self.getTextElement(nzb, '{%s}nzb' % REPORT_NS)),
'download': lambda: self.download(id), 'download': self.download,
'detail_url': str(self.getTextElement(nzb, 'link')), 'detail_url': str(self.getTextElement(nzb, 'link')),
'description': self.getTextElement(nzb, "description"), 'description': self.getTextElement(nzb, "description"),
'check_nzb': False, 'check_nzb': False,
@@ -121,7 +121,7 @@ class Newzbin(NZBProvider, RSS):
return results return results
def download(self, nzb_id): def download(self, url = '', nzb_id = ''):
try: try:
log.info('Download nzb from newzbin, report id: %s ' % nzb_id) log.info('Download nzb from newzbin, report id: %s ' % nzb_id)
@@ -9,7 +9,7 @@ config = [{
{ {
'tab': 'providers', 'tab': 'providers',
'name': 'newznab', 'name': 'newznab',
'description': 'Enable multiple NewzNab providers', 'description': 'Enable multiple NewzNab providers such as <a href="http://nzb.su" target="_blank">NZB.su</a>',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@@ -21,8 +21,8 @@ config = [{
}, },
{ {
'name': 'host', 'name': 'host',
'default': 'http://nzb.su', 'default': 'nzb.su',
'description': 'The hostname of your newznab provider, like http://nzb.su' 'description': 'The hostname of your newznab provider'
}, },
{ {
'name': 'api_key', 'name': 'api_key',
@@ -5,6 +5,7 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import NZBProvider from couchpotato.core.providers.base import NZBProvider
from dateutil.parser import parse from dateutil.parser import parse
from urllib import urlencode from urllib import urlencode
from urlparse import urlparse
import time import time
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
@@ -130,11 +131,13 @@ class Newznab(NZBProvider, RSS):
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop() id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
new = { new = {
'id': id, 'id': id,
'provider': self.getName(),
'type': 'nzb', 'type': 'nzb',
'name': self.getTextElement(nzb, "title"), 'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(size) / 1024 / 1024, 'size': int(size) / 1024 / 1024,
'url': (self.getUrl(host['host'], self.urls['download']) % id) + self.getApiExt(host), 'url': (self.getUrl(host['host'], self.urls['download']) % id) + self.getApiExt(host),
'download': self.download,
'detail_url': (self.getUrl(host['host'], self.urls['detail']) % id) + self.getApiExt(host), 'detail_url': (self.getUrl(host['host'], self.urls['detail']) % id) + self.getApiExt(host),
'content': self.getTextElement(nzb, "description"), 'content': self.getTextElement(nzb, "description"),
} }
@@ -173,6 +176,17 @@ class Newznab(NZBProvider, RSS):
return list return list
def belongsTo(self, url):
hosts = self.getHosts()
for host in hosts:
result = super(Newznab, self).belongsTo(url, host = host['host'])
if result:
return result
return
def getUrl(self, host, type): def getUrl(self, host, type):
return cleanHost(host) + 'api?t=' + type return cleanHost(host) + 'api?t=' + type
@@ -31,28 +31,51 @@ var MultipleNewznab = new Class({
}); });
self.inputs[name].getParent().hide() self.inputs[name].getParent().hide()
self.inputs[name].addEvent('change', self.addEmpty.bind(self))
}); });
self.values.each(function(item, nr){ self.values.each(function(item, nr){
self.createItem(item.use, item.host, item.api_key); self.createItem(item.use, item.host, item.api_key);
}); });
new Element('a.nice_button', { new Element('div.head').adopt(
'text': 'Add new NewzNab provider', new Element('abbr.host', {
'events': { 'text': 'Host',
'click': function(e){ 'title': self.inputs['host'].getNext().get('text')
(e).stop(); }),
new Element('abbr.api_key', {
self.createItem(1, '', ''); 'text': 'Api Key',
} 'title': self.inputs['api_key'].getNext().get('text')
} })
}).inject(self.fieldset.getElement('h2'), 'after'); ).inject(self.fieldset.getElement('h2'), 'after');
self.addEmpty();
}) })
}, },
add_empty_timeout: 0,
addEmpty: function(){
var self = this;
if(self.add_empty_timeout) clearTimeout(self.add_empty_timeout);
var has_empty = false;
self.items.each(function(ctrl_holder){
if(ctrl_holder.getElement('.host').get('value') == '' && ctrl_holder.getElement('.api_key').get('value') == ''){
has_empty = true;
}
ctrl_holder[has_empty ? 'addClass' : 'removeClass']('is_empty');
});
if(has_empty) return;
self.add_empty_timeout = setTimeout(function(){
self.createItem(false, null, null);
}, 10);
},
createItem: function(use, host, api){ createItem: function(use, host, api){
var self = this; var self = this;
@@ -83,12 +106,12 @@ var MultipleNewznab = new Class({
} }
}), }),
new Element('a.icon.delete', { new Element('a.icon.delete', {
'text': 'delete',
'events': { 'events': {
'click': self.deleteItem.bind(self) 'click': self.deleteItem.bind(self)
} }
}) })
).inject(self.fieldset); ).inject(self.fieldset);
item[!host ? 'addClass' : 'removeClass']('is_empty');
new Form.Check(checkbox, { new Form.Check(checkbox, {
'onChange': checkbox.fireEvent.bind(checkbox, 'change') 'onChange': checkbox.fireEvent.bind(checkbox, 'change')
@@ -105,6 +128,7 @@ var MultipleNewznab = new Class({
self.items.each(function(item, nr){ self.items.each(function(item, nr){
self.input_types.each(function(type){ self.input_types.each(function(type){
var input = item.getElement('input.'+type); var input = item.getElement('input.'+type);
if(input.getParent('.ctrlHolder').hasClass('is_empty')) return;
if(!temp[type]) temp[type] = []; if(!temp[type]) temp[type] = [];
temp[type][nr] = input.get('type') == 'checkbox' ? +input.get('checked') : input.get('value').trim(); temp[type][nr] = input.get('type') == 'checkbox' ? +input.get('checked') : input.get('value').trim();
@@ -125,7 +149,7 @@ var MultipleNewznab = new Class({
(e).stop(); (e).stop();
var item = e.target.getParent(); var item = e.target.getParent();
self.items.erase(item); self.items.erase(item);
item.destroy(); item.destroy();
@@ -14,7 +14,7 @@ log = CPLog(__name__)
class NzbIndex(NZBProvider, RSS): class NzbIndex(NZBProvider, RSS):
urls = { urls = {
'download': 'http://www.nzbindex.nl/download/%s/%s', 'download': 'http://www.nzbindex.nl/download/',
'api': 'http://www.nzbindex.nl/rss/', #http://www.nzbindex.nl/rss/?q=due+date+720p&age=1000&sort=agedesc&minsize=3500&maxsize=10000 'api': 'http://www.nzbindex.nl/rss/', #http://www.nzbindex.nl/rss/?q=due+date+720p&age=1000&sort=agedesc&minsize=3500&maxsize=10000
} }
@@ -63,10 +63,12 @@ class NzbIndex(NZBProvider, RSS):
new = { new = {
'id': id, 'id': id,
'type': 'nzb', 'type': 'nzb',
'provider': self.getName(),
'name': self.getTextElement(nzb, "title"), 'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': enclosure['length'], 'size': enclosure['length'],
'url': enclosure['url'], 'url': enclosure['url'],
'download': self.download,
'detail_url': enclosure['url'].replace('/download/', '/release/'), 'detail_url': enclosure['url'].replace('/download/', '/release/'),
'description': self.getTextElement(nzb, "description"), 'description': self.getTextElement(nzb, "description"),
'check_nzb': True, 'check_nzb': True,
@@ -81,10 +81,12 @@ class NZBMatrix(NZBProvider, RSS):
new = { new = {
'id': id, 'id': id,
'type': 'nzb', 'type': 'nzb',
'provider': self.getName(),
'name': title, 'name': title,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': self.parseSize(size), 'size': self.parseSize(size),
'url': self.urls['download'] % id + self.getApiExt(), 'url': self.urls['download'] % id + self.getApiExt(),
'download': self.download,
'detail_url': self.urls['detail'] % id, 'detail_url': self.urls['detail'] % id,
'description': self.getTextElement(nzb, "description"), 'description': self.getTextElement(nzb, "description"),
'check_nzb': True, 'check_nzb': True,
@@ -9,6 +9,7 @@ config = [{
{ {
'tab': 'providers', 'tab': 'providers',
'name': 'nzbs', 'name': 'nzbs',
'description': 'Id and Key can be found <a href="http://nzbs.org/index.php?action=rss" target="_blank">on your nzbs.org RSS page</a>.',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@@ -17,12 +18,12 @@ config = [{
{ {
'name': 'id', 'name': 'id',
'label': 'Id', 'label': 'Id',
'description': 'Can be found <a href="http://nzbs.org/index.php?action=rss" target="_blank">here</a>, the number after "&amp;i="', 'description': 'The number after "&amp;i="',
}, },
{ {
'name': 'api_key', 'name': 'api_key',
'label': 'Api Key', 'label': 'Api Key',
'description': 'Can be found <a href="http://nzbs.org/index.php?action=rss" target="_blank">here</a>, the string after "&amp;h="' 'description': 'The string after "&amp;h="'
}, },
], ],
}, },
@@ -71,10 +71,12 @@ class Nzbs(NZBProvider, RSS):
new = { new = {
'id': id, 'id': id,
'type': 'nzb', 'type': 'nzb',
'provider': self.getName(),
'name': self.getTextElement(nzb, "title"), 'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': self.parseSize(self.getTextElement(nzb, "description").split('</a><br />')[1].split('">')[1]), 'size': self.parseSize(self.getTextElement(nzb, "description").split('</a><br />')[1].split('">')[1]),
'url': self.urls['download'] % (id, self.getApiExt()), 'url': self.urls['download'] % (id, self.getApiExt()),
'download': self.download,
'detail_url': self.urls['detail'] % id, 'detail_url': self.urls['detail'] % id,
'description': self.getTextElement(nzb, "description"), 'description': self.getTextElement(nzb, "description"),
'check_nzb': True, 'check_nzb': True,
@@ -3,22 +3,4 @@ from .main import ThePirateBay
def start(): def start():
return ThePirateBay() return ThePirateBay()
config = [{ config = []
'name': 'themoviedb',
'groups': [
{
'tab': 'providers',
'name': 'tmdb',
'label': 'TheMovieDB',
'advanced': True,
'description': 'Used for all calls to TheMovieDB.',
'options': [
{
'name': 'api_key',
'default': '9b939aee0aaafc12a65bf448e4af9543',
'label': 'Api Key',
},
],
},
],
}]
+7 -1
View File
@@ -3,6 +3,7 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import isInt from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.helpers.request import getParams, jsonified from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import mergeDicts
import ConfigParser import ConfigParser
import os.path import os.path
import time import time
@@ -93,7 +94,12 @@ class Settings():
self.p.set(section, option, value) self.p.set(section, option, value)
def addOptions(self, section_name, options): def addOptions(self, section_name, options):
self.options[section_name] = options
if not self.options.get(section_name):
self.options[section_name] = options
else:
options['groups'] = self.options[section_name].get('groups') + options.get('groups')
self.options[section_name] = mergeDicts(self.options[section_name], options)
def getOptions(self): def getOptions(self):
return self.options return self.options
+9
View File
@@ -44,6 +44,7 @@ class Library(Entity):
status = ManyToOne('Status') status = ManyToOne('Status')
movies = OneToMany('Movie') movies = OneToMany('Movie')
titles = OneToMany('LibraryTitle') titles = OneToMany('LibraryTitle')
genres = ManyToMany('LibraryGenre')
files = ManyToMany('File') files = ManyToMany('File')
info = OneToMany('LibraryInfo') info = OneToMany('LibraryInfo')
@@ -68,6 +69,14 @@ class LibraryTitle(Entity):
libraries = ManyToOne('Library') libraries = ManyToOne('Library')
class LibraryGenre(Entity):
""""""
name = Field(Unicode)
libraries = ManyToMany('Library')
class Language(Entity): class Language(Entity):
"""""" """"""
+11 -4
View File
@@ -115,8 +115,10 @@ def runCouchPotato(options, base_path, args):
latest_db_version = version(repo) latest_db_version = version(repo)
initialize = True
try: try:
current_db_version = db_version(db, repo) current_db_version = db_version(db, repo)
initialize = False
except: except:
version_control(db, repo, version = latest_db_version) version_control(db, repo, version = latest_db_version)
current_db_version = db_version(db, repo) current_db_version = db_version(db, repo)
@@ -131,6 +133,9 @@ def runCouchPotato(options, base_path, args):
fireEventAsync('app.load') fireEventAsync('app.load')
if initialize:
fireEventAsync('app.initialize')
# Create app # Create app
from couchpotato import app from couchpotato import app
api_key = Env.setting('api_key') api_key = Env.setting('api_key')
@@ -138,10 +143,12 @@ def runCouchPotato(options, base_path, args):
reloader = debug and not options.daemonize reloader = debug and not options.daemonize
# Basic config # Basic config
app.host = Env.setting('host', default = '0.0.0.0')
app.port = Env.setting('port', default = 5000)
app.debug = debug
app.secret_key = api_key app.secret_key = api_key
config = {
'use_reloader': reloader,
'host': Env.setting('host', default = '0.0.0.0'),
'port': Env.setting('port', default = 5000)
}
# Static path # Static path
web.add_url_rule(url_base + '/static/<path:filename>', web.add_url_rule(url_base + '/static/<path:filename>',
@@ -153,4 +160,4 @@ def runCouchPotato(options, base_path, args):
app.register_blueprint(api, url_prefix = '%s/%s/' % (url_base, api_key)) app.register_blueprint(api, url_prefix = '%s/%s/' % (url_base, api_key))
# Go go go! # Go go go!
app.run(use_reloader = reloader) app.run(**config)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Before

Width:  |  Height:  |  Size: 159 B

After

Width:  |  Height:  |  Size: 159 B

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

+9 -1
View File
@@ -29,7 +29,7 @@ var CouchPotato = new Class({
else else
self.openPage(window.location.pathname); self.openPage(window.location.pathname);
self.c.addEvent('click:relay(a)', self.pushState.bind(self)); self.c.addEvent('click:relay(a:not([target=_blank]))', self.pushState.bind(self));
}, },
pushState: function(e){ pushState: function(e){
@@ -100,6 +100,14 @@ var CouchPotato = new Class({
getPage: function(name){ getPage: function(name){
return this.pages[name] return this.pages[name]
},
shutdown: function(){
Api.request('app.shutdown');
},
restart: function(){
Api.request('app.restart');
} }
}); });
@@ -96,14 +96,14 @@ Form.Check = new Class({
this.fireEvent('removeHighlight', this); this.fireEvent('removeHighlight', this);
}, },
keyToggle: function(e) { keyToggle: function(e) {
var evt = new Event(e); var evt = (e);
if (evt.key === 'space') { this.toggle(e); } if (evt.key === 'space') { this.toggle(e); }
}, },
toggle: function(e) { toggle: function(e) {
var evt; var evt;
if (this.disabled) { return this; } if (this.disabled) { return this; }
if (e) { if (e) {
evt = new Event(e).stopPropagation(); evt = (e).stopPropagation();
if (evt.target.tagName.toLowerCase() !== 'a') { if (evt.target.tagName.toLowerCase() !== 'a') {
evt.stop(); evt.stop();
} }
@@ -114,6 +114,7 @@ Form.Check = new Class({
this.check(); this.check();
} }
this.fireEvent('change', this); this.fireEvent('change', this);
this.input.fireEvent('change', this);
return this; return this;
}, },
uncheck: function() { uncheck: function() {
@@ -117,7 +117,7 @@ Form.Dropdown = new Class({
}, },
expand: function(e) { expand: function(e) {
clearTimeout(this.collapseInterval); clearTimeout(this.collapseInterval);
var evt = e ? new Event(e).stop() : null; var evt = e ? (e).stop() : null;
this.open = true; this.open = true;
this.input.focus(); this.input.focus();
this.element.addClass('active').addClass('dropdown-active'); this.element.addClass('active').addClass('dropdown-active');
@@ -22,7 +22,7 @@ Form.Radio = new Class({
toggle: function(e) { toggle: function(e) {
if (this.element.hasClass('checked') || this.disabled) { return; } if (this.element.hasClass('checked') || this.disabled) { return; }
var evt; var evt;
if (e) { evt = new Event(e).stop(); } if (e) { evt = (e).stop(); }
if (this.checked) { if (this.checked) {
this.uncheck(); this.uncheck();
} else { } else {
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
// MooTools: the javascript framework. // MooTools: the javascript framework.
// Load this file's selection again by visiting: http://mootools.net/more/1e3edb90c5e02d9b9013b54e6ab001ea // Load this file's selection again by visiting: http://mootools.net/more/13115b95c0560a5c35a61ccf237f3ed9
// Or build this file again with packager using: packager build More/Element.Forms More/Element.Delegation More/Element.Shortcuts More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical More/Spinner // Or build this file again with packager using: packager build More/Element.Forms More/Element.Shortcuts More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical More/Spinner
/* /*
--- ---
@@ -20,6 +20,7 @@ authors:
- Tim Wienk - Tim Wienk
- Christoph Pojer - Christoph Pojer
- Aaron Newton - Aaron Newton
- Jacob Thornton
requires: requires:
- Core/MooTools - Core/MooTools
@@ -30,8 +31,8 @@ provides: [MooTools.More]
*/ */
MooTools.More = { MooTools.More = {
'version': '1.3.1.1', 'version': '1.4.0.1',
'build': '0292a3af1eea242b817fecf9daa127417d10d4ce' 'build': 'a4244edf2aa97ac8a196fc96082dd35af1abab87'
}; };
@@ -182,7 +183,7 @@ String.implement({
}); });
}).call(this); })();
/* /*
@@ -327,368 +328,6 @@ Element.implement({
}); });
/*
---
name: Events.Pseudos
description: Adds the functionality to add pseudo events
license: MIT-style license
authors:
- Arian Stolwijk
requires: [Core/Class.Extras, Core/Slick.Parser, More/MooTools.More]
provides: [Events.Pseudos]
...
*/
Events.Pseudos = function(pseudos, addEvent, removeEvent){
var storeKey = 'monitorEvents:';
var storageOf = function(object){
return {
store: object.store ? function(key, value){
object.store(storeKey + key, value);
} : function(key, value){
(object.$monitorEvents || (object.$monitorEvents = {}))[key] = value;
},
retrieve: object.retrieve ? function(key, dflt){
return object.retrieve(storeKey + key, dflt);
} : function(key, dflt){
if (!object.$monitorEvents) return dflt;
return object.$monitorEvents[key] || dflt;
}
};
};
var splitType = function(type){
if (type.indexOf(':') == -1 || !pseudos) return null;
var parsed = Slick.parse(type).expressions[0][0],
parsedPseudos = parsed.pseudos,
l = parsedPseudos.length,
splits = [];
while (l--) if (pseudos[parsedPseudos[l].key]){
splits.push({
event: parsed.tag,
value: parsedPseudos[l].value,
pseudo: parsedPseudos[l].key,
original: type
});
}
return splits.length ? splits : null;
};
var mergePseudoOptions = function(split){
return Object.merge.apply(this, split.map(function(item){
return pseudos[item.pseudo].options || {};
}));
};
return {
addEvent: function(type, fn, internal){
var split = splitType(type);
if (!split) return addEvent.call(this, type, fn, internal);
var storage = storageOf(this),
events = storage.retrieve(type, []),
eventType = split[0].event,
options = mergePseudoOptions(split),
stack = fn,
eventOptions = options[eventType] || {},
args = Array.slice(arguments, 2),
self = this,
monitor;
if (eventOptions.args) args.append(Array.from(eventOptions.args));
if (eventOptions.base) eventType = eventOptions.base;
if (eventOptions.onAdd) eventOptions.onAdd(this);
split.each(function(item){
var stackFn = stack;
stack = function(){
(eventOptions.listener || pseudos[item.pseudo].listener).call(self, item, stackFn, arguments, monitor, options);
};
});
monitor = stack.bind(this);
events.include({event: fn, monitor: monitor});
storage.store(type, events);
addEvent.apply(this, [type, fn].concat(args));
return addEvent.apply(this, [eventType, monitor].concat(args));
},
removeEvent: function(type, fn){
var split = splitType(type);
if (!split) return removeEvent.call(this, type, fn);
var storage = storageOf(this),
events = storage.retrieve(type);
if (!events) return this;
var eventType = split[0].event,
options = mergePseudoOptions(split),
eventOptions = options[eventType] || {},
args = Array.slice(arguments, 2);
if (eventOptions.args) args.append(Array.from(eventOptions.args));
if (eventOptions.base) eventType = eventOptions.base;
if (eventOptions.onRemove) eventOptions.onRemove(this);
removeEvent.apply(this, [type, fn].concat(args));
events.each(function(monitor, i){
if (!fn || monitor.event == fn) removeEvent.apply(this, [eventType, monitor.monitor].concat(args));
delete events[i];
}, this);
storage.store(type, events);
return this;
}
};
};
(function(){
var pseudos = {
once: {
listener: function(split, fn, args, monitor){
fn.apply(this, args);
this.removeEvent(split.event, monitor)
.removeEvent(split.original, fn);
}
},
throttle: {
listener: function(split, fn, args){
if (!fn._throttled){
fn.apply(this, args);
fn._throttled = setTimeout(function(){
fn._throttled = false;
}, split.value || 250);
}
}
},
pause: {
listener: function(split, fn, args){
clearTimeout(fn._pause);
fn._pause = fn.delay(split.value || 250, this, args);
}
}
};
Events.definePseudo = function(key, listener){
pseudos[key] = Type.isFunction(listener) ? {listener: listener} : listener;
return this;
};
Events.lookupPseudo = function(key){
return pseudos[key];
};
var proto = Events.prototype;
Events.implement(Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent));
['Request', 'Fx'].each(function(klass){
if (this[klass]) this[klass].implement(Events.prototype);
});
}).call(this);
/*
---
name: Element.Event.Pseudos
description: Adds the functionality to add pseudo events for Elements
license: MIT-style license
authors:
- Arian Stolwijk
requires: [Core/Element.Event, Events.Pseudos]
provides: [Element.Event.Pseudos]
...
*/
(function(){
var pseudos = {},
copyFromEvents = ['once', 'throttle', 'pause'],
count = copyFromEvents.length;
while (count--) pseudos[copyFromEvents[count]] = Events.lookupPseudo(copyFromEvents[count]);
Event.definePseudo = function(key, listener){
pseudos[key] = Type.isFunction(listener) ? {listener: listener} : listener;
return this;
};
var proto = Element.prototype;
[Element, Window, Document].invoke('implement', Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent));
}).call(this);
/*
---
script: Element.Delegation.js
name: Element.Delegation
description: Extends the Element native object to include the delegate method for more efficient event management.
credits:
- "Event checking based on the work of Daniel Steigerwald. License: MIT-style license. Copyright: Copyright (c) 2008 Daniel Steigerwald, daniel.steigerwald.cz"
license: MIT-style license
authors:
- Aaron Newton
- Daniel Steigerwald
requires: [/MooTools.More, Element.Event.Pseudos]
provides: [Element.Delegation]
...
*/
(function(){
var eventListenerSupport = !(window.attachEvent && !window.addEventListener),
nativeEvents = Element.NativeEvents;
nativeEvents.focusin = 2;
nativeEvents.focusout = 2;
var check = function(split, target, event){
var elementEvent = Element.Events[split.event], condition;
if (elementEvent) condition = elementEvent.condition;
return Slick.match(target, split.value) && (!condition || condition.call(target, event));
};
var formObserver = function(eventName){
var $delegationKey = '$delegation:';
return {
base: 'focusin',
onRemove: function(element){
element.retrieve($delegationKey + 'forms', []).each(function(el){
el.retrieve($delegationKey + 'listeners', []).each(function(listener){
el.removeEvent(eventName, listener);
});
el.eliminate($delegationKey + eventName + 'listeners')
.eliminate($delegationKey + eventName + 'originalFn');
});
},
listener: function(split, fn, args, monitor, options){
var event = args[0],
forms = this.retrieve($delegationKey + 'forms', []),
target = event.target,
form = (target.get('tag') == 'form') ? target : event.target.getParent('form'),
formEvents = form.retrieve($delegationKey + 'originalFn', []),
formListeners = form.retrieve($delegationKey + 'listeners', []);
forms.include(form);
this.store($delegationKey + 'forms', forms);
if (!formEvents.contains(fn)){
var formListener = function(event){
if (check(split, this, event)) fn.call(this, event);
};
form.addEvent(eventName, formListener);
formEvents.push(fn);
formListeners.push(formListener);
form.store($delegationKey + eventName + 'originalFn', formEvents)
.store($delegationKey + eventName + 'listeners', formListeners);
}
}
};
};
var inputObserver = function(eventName){
return {
base: 'focusin',
listener: function(split, fn, args){
var events = {blur: function(){
this.removeEvents(events);
}};
events[eventName] = function(event){
if (check(split, this, event)) fn.call(this, event);
};
args[0].target.addEvents(events);
}
};
};
var eventOptions = {
mouseenter: {
base: 'mouseover'
},
mouseleave: {
base: 'mouseout'
},
focus: {
base: 'focus' + (eventListenerSupport ? '' : 'in'),
args: [true]
},
blur: {
base: eventListenerSupport ? 'blur' : 'focusout',
args: [true]
}
};
if (!eventListenerSupport) Object.append(eventOptions, {
submit: formObserver('submit'),
reset: formObserver('reset'),
change: inputObserver('change'),
select: inputObserver('select')
});
Event.definePseudo('relay', {
listener: function(split, fn, args, monitor, options){
var event = args[0];
for (var target = event.target; target && target != this; target = target.parentNode){
var finalTarget = document.id(target);
if (check(split, finalTarget, event)){
if (finalTarget) fn.call(finalTarget, event, finalTarget);
return;
}
}
},
options: eventOptions
});
}).call(this);
/* /*
--- ---
@@ -826,7 +465,7 @@ Fx.Slide = new Class({
this.addEvent('complete', function(){ this.addEvent('complete', function(){
this.open = (wrapper['offset' + this.layout.capitalize()] != 0); this.open = (wrapper['offset' + this.layout.capitalize()] != 0);
if (this.open && options.resetHeight) wrapper.setStyle('height', ''); if (this.open && this.options.resetHeight) wrapper.setStyle('height', '');
}, true); }, true);
}, },
@@ -1050,12 +689,6 @@ var Drag = new Class({
var limit = options.limit; var limit = options.limit;
this.limit = {x: [], y: []}; this.limit = {x: [], y: []};
var styles = this.element.getStyles('left', 'right', 'top', 'bottom');
this._invert = {
x: options.modifiers.x == 'left' && styles.left == 'auto' && !isNaN(styles.right.toInt()) && (options.modifiers.x = 'right'),
y: options.modifiers.y == 'top' && styles.top == 'auto' && !isNaN(styles.bottom.toInt()) && (options.modifiers.y = 'bottom')
};
var z, coordinates; var z, coordinates;
for (z in options.modifiers){ for (z in options.modifiers){
if (!options.modifiers[z]) continue; if (!options.modifiers[z]) continue;
@@ -1072,7 +705,6 @@ var Drag = new Class({
else this.value.now[z] = this.element[options.modifiers[z]]; else this.value.now[z] = this.element[options.modifiers[z]];
if (options.invert) this.value.now[z] *= -1; if (options.invert) this.value.now[z] *= -1;
if (this._invert[z]) this.value.now[z] *= -1;
this.mouse.pos[z] = event.page[z] - this.value.now[z]; this.mouse.pos[z] = event.page[z] - this.value.now[z];
@@ -1122,7 +754,6 @@ var Drag = new Class({
this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z]; this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z];
if (options.invert) this.value.now[z] *= -1; if (options.invert) this.value.now[z] *= -1;
if (this._invert[z]) this.value.now[z] *= -1;
if (options.limit && this.limit[z]){ if (options.limit && this.limit[z]){
if ((this.limit[z][1] || this.limit[z][1] === 0) && (this.value.now[z] > this.limit[z][1])){ if ((this.limit[z][1] || this.limit[z][1] === 0) && (this.value.now[z] > this.limit[z][1])){
@@ -1236,10 +867,9 @@ Drag.Move = new Class({
this.container = document.id(this.container.getDocument().body); this.container = document.id(this.container.getDocument().body);
if (this.options.style){ if (this.options.style){
if (this.options.modifiers.x == "left" && this.options.modifiers.y == "top"){ if (this.options.modifiers.x == 'left' && this.options.modifiers.y == 'top'){
var parentStyles, var parent = element.getOffsetParent(),
parent = element.getOffsetParent(); styles = element.getStyles('left', 'top');
var styles = element.getStyles('left', 'top');
if (parent && (styles.left == 'auto' || styles.top == 'auto')){ if (parent && (styles.left == 'auto' || styles.top == 'auto')){
element.setPosition(element.getPosition(parent)); element.setPosition(element.getPosition(parent));
} }
@@ -1529,7 +1159,7 @@ var Sortables = new Class({
if ( if (
!this.idle || !this.idle ||
event.rightClick || event.rightClick ||
['button', 'input', 'a'].contains(event.target.get('tag')) ['button', 'input', 'a', 'textarea'].contains(event.target.get('tag'))
) return; ) return;
this.idle = false; this.idle = false;
@@ -1640,8 +1270,7 @@ Request.JSONP = new Class({
Implements: [Chain, Events, Options], Implements: [Chain, Events, Options],
options: { options: {/*
/*
onRequest: function(src, scriptElement){}, onRequest: function(src, scriptElement){},
onComplete: function(data){}, onComplete: function(data){},
onSuccess: function(data){}, onSuccess: function(data){},
@@ -1708,7 +1337,8 @@ Request.JSONP = new Class({
}, },
getScript: function(src){ getScript: function(src){
if (!this.script) this.script = new Element('script[type=text/javascript]', { if (!this.script) this.script = new Element('script', {
type: 'text/javascript',
async: true, async: true,
src: src src: src
}); });
@@ -1716,7 +1346,7 @@ Request.JSONP = new Class({
}, },
success: function(args, index){ success: function(args, index){
if (!this.running) return false; if (!this.running) return;
this.clear() this.clear()
.fireEvent('complete', args).fireEvent('success', args) .fireEvent('complete', args).fireEvent('success', args)
.callChain(); .callChain();
@@ -1835,10 +1465,10 @@ Class.refactor = function(original, refactors){
Object.each(refactors, function(item, name){ Object.each(refactors, function(item, name){
var origin = original.prototype[name]; var origin = original.prototype[name];
if (origin && origin.$origin) origin = origin.$origin; origin = (origin && origin.$origin) || origin || function(){};
original.implement(name, (typeof item == 'function') ? function(){ original.implement(name, (typeof item == 'function') ? function(){
var old = this.previous; var old = this.previous;
this.previous = origin || function(){}; this.previous = origin;
var value = item.apply(this, arguments); var value = item.apply(this, arguments);
this.previous = old; this.previous = old;
return value; return value;
@@ -1875,7 +1505,7 @@ provides: [Class.Binds]
Class.Mutators.Binds = function(binds){ Class.Mutators.Binds = function(binds){
if (!this.prototype.initialize) this.implement('initialize', function(){}); if (!this.prototype.initialize) this.implement('initialize', function(){});
return binds; return Array.from(binds).concat(this.prototype.Binds || []);
}; };
Class.Mutators.initialize = function(initialize){ Class.Mutators.initialize = function(initialize){
@@ -2055,7 +1685,7 @@ Element.implement({
}); });
}).call(this); })();
/* /*
@@ -2071,219 +1701,228 @@ license: MIT-style license
authors: authors:
- Aaron Newton - Aaron Newton
- Jacob Thornton
requires: requires:
- Core/Options
- Core/Element.Dimensions - Core/Element.Dimensions
- /Element.Measure - Element.Measure
provides: [Element.Position] provides: [Element.Position]
... ...
*/ */
(function(){ (function(original){
var original = Element.prototype.position; var local = Element.Position = {
options: {/*
edge: false,
returnPos: false,
minimum: {x: 0, y: 0},
maximum: {x: 0, y: 0},
relFixedPosition: false,
ignoreMargins: false,
ignoreScroll: false,
allowNegative: false,*/
relativeTo: document.body,
position: {
x: 'center', //left, center, right
y: 'center' //top, center, bottom
},
offset: {x: 0, y: 0}
},
getOptions: function(element, options){
options = Object.merge({}, local.options, options);
local.setPositionOption(options);
local.setEdgeOption(options);
local.setOffsetOption(element, options);
local.setDimensionsOption(element, options);
return options;
},
setPositionOption: function(options){
options.position = local.getCoordinateFromValue(options.position);
},
setEdgeOption: function(options){
var edgeOption = local.getCoordinateFromValue(options.edge);
options.edge = edgeOption ? edgeOption :
(options.position.x == 'center' && options.position.y == 'center') ? {x: 'center', y: 'center'} :
{x: 'left', y: 'top'};
},
setOffsetOption: function(element, options){
var parentOffset = {x: 0, y: 0},
offsetParent = element.measure(function(){
return document.id(this.getOffsetParent());
}),
parentScroll = offsetParent.getScroll();
if (!offsetParent || offsetParent == element.getDocument().body) return;
parentOffset = offsetParent.measure(function(){
var position = this.getPosition();
if (this.getStyle('position') == 'fixed'){
var scroll = window.getScroll();
position.x += scroll.x;
position.y += scroll.y;
}
return position;
});
options.offset = {
parentPositioned: offsetParent != document.id(options.relativeTo),
x: options.offset.x - parentOffset.x + parentScroll.x,
y: options.offset.y - parentOffset.y + parentScroll.y
};
},
setDimensionsOption: function(element, options){
options.dimensions = element.getDimensions({
computeSize: true,
styles: ['padding', 'border', 'margin']
});
},
getPosition: function(element, options){
var position = {};
options = local.getOptions(element, options);
var relativeTo = document.id(options.relativeTo) || document.body;
local.setPositionCoordinates(options, position, relativeTo);
if (options.edge) local.toEdge(position, options);
var offset = options.offset;
position.left = ((position.x >= 0 || offset.parentPositioned || options.allowNegative) ? position.x : 0).toInt();
position.top = ((position.y >= 0 || offset.parentPositioned || options.allowNegative) ? position.y : 0).toInt();
local.toMinMax(position, options);
if (options.relFixedPosition || relativeTo.getStyle('position') == 'fixed') local.toRelFixedPosition(relativeTo, position);
if (options.ignoreScroll) local.toIgnoreScroll(relativeTo, position);
if (options.ignoreMargins) local.toIgnoreMargins(position, options);
position.left = Math.ceil(position.left);
position.top = Math.ceil(position.top);
delete position.x;
delete position.y;
return position;
},
setPositionCoordinates: function(options, position, relativeTo){
var offsetY = options.offset.y,
offsetX = options.offset.x,
calc = (relativeTo == document.body) ? window.getScroll() : relativeTo.getPosition(),
top = calc.y,
left = calc.x,
winSize = window.getSize();
switch(options.position.x){
case 'left': position.x = left + offsetX; break;
case 'right': position.x = left + offsetX + relativeTo.offsetWidth; break;
default: position.x = left + ((relativeTo == document.body ? winSize.x : relativeTo.offsetWidth) / 2) + offsetX; break;
}
switch(options.position.y){
case 'top': position.y = top + offsetY; break;
case 'bottom': position.y = top + offsetY + relativeTo.offsetHeight; break;
default: position.y = top + ((relativeTo == document.body ? winSize.y : relativeTo.offsetHeight) / 2) + offsetY; break;
}
},
toMinMax: function(position, options){
var xy = {left: 'x', top: 'y'}, value;
['minimum', 'maximum'].each(function(minmax){
['left', 'top'].each(function(lr){
value = options[minmax] ? options[minmax][xy[lr]] : null;
if (value != null && ((minmax == 'minimum') ? position[lr] < value : position[lr] > value)) position[lr] = value;
});
});
},
toRelFixedPosition: function(relativeTo, position){
var winScroll = window.getScroll();
position.top += winScroll.y;
position.left += winScroll.x;
},
toIgnoreScroll: function(relativeTo, position){
var relScroll = relativeTo.getScroll();
position.top -= relScroll.y;
position.left -= relScroll.x;
},
toIgnoreMargins: function(position, options){
position.left += options.edge.x == 'right'
? options.dimensions['margin-right']
: (options.edge.x != 'center'
? -options.dimensions['margin-left']
: -options.dimensions['margin-left'] + ((options.dimensions['margin-right'] + options.dimensions['margin-left']) / 2));
position.top += options.edge.y == 'bottom'
? options.dimensions['margin-bottom']
: (options.edge.y != 'center'
? -options.dimensions['margin-top']
: -options.dimensions['margin-top'] + ((options.dimensions['margin-bottom'] + options.dimensions['margin-top']) / 2));
},
toEdge: function(position, options){
var edgeOffset = {},
dimensions = options.dimensions,
edge = options.edge;
switch(edge.x){
case 'left': edgeOffset.x = 0; break;
case 'right': edgeOffset.x = -dimensions.x - dimensions.computedRight - dimensions.computedLeft; break;
// center
default: edgeOffset.x = -(Math.round(dimensions.totalWidth / 2)); break;
}
switch(edge.y){
case 'top': edgeOffset.y = 0; break;
case 'bottom': edgeOffset.y = -dimensions.y - dimensions.computedTop - dimensions.computedBottom; break;
// center
default: edgeOffset.y = -(Math.round(dimensions.totalHeight / 2)); break;
}
position.x += edgeOffset.x;
position.y += edgeOffset.y;
},
getCoordinateFromValue: function(option){
if (typeOf(option) != 'string') return option;
option = option.toLowerCase();
return {
x: option.test('left') ? 'left'
: (option.test('right') ? 'right' : 'center'),
y: option.test(/upper|top/) ? 'top'
: (option.test('bottom') ? 'bottom' : 'center')
};
}
};
Element.implement({ Element.implement({
position: function(options){ position: function(options){
//call original position if the options are x/y values
if (options && (options.x != null || options.y != null)){ if (options && (options.x != null || options.y != null)){
return original ? original.apply(this, arguments) : this; return (original ? original.apply(this, arguments) : this);
} }
var position = this.setStyle('position', 'absolute').calculatePosition(options);
return (options && options.returnPos) ? position : this.setStyles(position);
},
Object.each(options || {}, function(v, k){ calculatePosition: function(options){
if (v == null) delete options[k]; return local.getPosition(this, options);
});
options = Object.merge({
// minimum: { x: 0, y: 0 },
// maximum: { x: 0, y: 0},
relativeTo: document.body,
position: {
x: 'center', //left, center, right
y: 'center' //top, center, bottom
},
offset: {x: 0, y: 0}/*,
edge: false,
returnPos: false,
relFixedPosition: false,
ignoreMargins: false,
ignoreScroll: false,
allowNegative: false*/
}, options);
//compute the offset of the parent positioned element if this element is in one
var parentOffset = {x: 0, y: 0},
parentPositioned = false;
/* dollar around getOffsetParent should not be necessary, but as it does not return
* a mootools extended element in IE, an error occurs on the call to expose. See:
* http://mootools.lighthouseapp.com/projects/2706/tickets/333-element-getoffsetparent-inconsistency-between-ie-and-other-browsers */
var offsetParent = this.measure(function(){
return document.id(this.getOffsetParent());
});
if (offsetParent && offsetParent != this.getDocument().body){
parentOffset = offsetParent.measure(function(){
return this.getPosition();
});
parentPositioned = offsetParent != document.id(options.relativeTo);
options.offset.x = options.offset.x - parentOffset.x;
options.offset.y = options.offset.y - parentOffset.y;
}
//upperRight, bottomRight, centerRight, upperLeft, bottomLeft, centerLeft
//topRight, topLeft, centerTop, centerBottom, center
var fixValue = function(option){
if (typeOf(option) != 'string') return option;
option = option.toLowerCase();
var val = {};
if (option.test('left')){
val.x = 'left';
} else if (option.test('right')){
val.x = 'right';
} else {
val.x = 'center';
}
if (option.test('upper') || option.test('top')){
val.y = 'top';
} else if (option.test('bottom')){
val.y = 'bottom';
} else {
val.y = 'center';
}
return val;
};
options.edge = fixValue(options.edge);
options.position = fixValue(options.position);
if (!options.edge){
if (options.position.x == 'center' && options.position.y == 'center') options.edge = {x:'center', y:'center'};
else options.edge = {x:'left', y:'top'};
}
this.setStyle('position', 'absolute');
var rel = document.id(options.relativeTo) || document.body,
calc = rel == document.body ? window.getScroll() : rel.getPosition(),
top = calc.y, left = calc.x;
var dim = this.getDimensions({
computeSize: true,
styles:['padding', 'border','margin']
});
var pos = {},
prefY = options.offset.y,
prefX = options.offset.x,
winSize = window.getSize();
switch (options.position.x){
case 'left':
pos.x = left + prefX;
break;
case 'right':
pos.x = left + prefX + rel.offsetWidth;
break;
default: //center
pos.x = left + ((rel == document.body ? winSize.x : rel.offsetWidth)/2) + prefX;
break;
}
switch (options.position.y){
case 'top':
pos.y = top + prefY;
break;
case 'bottom':
pos.y = top + prefY + rel.offsetHeight;
break;
default: //center
pos.y = top + ((rel == document.body ? winSize.y : rel.offsetHeight)/2) + prefY;
break;
}
if (options.edge){
var edgeOffset = {};
switch (options.edge.x){
case 'left':
edgeOffset.x = 0;
break;
case 'right':
edgeOffset.x = -dim.x-dim.computedRight-dim.computedLeft;
break;
default: //center
edgeOffset.x = -(dim.totalWidth/2);
break;
}
switch (options.edge.y){
case 'top':
edgeOffset.y = 0;
break;
case 'bottom':
edgeOffset.y = -dim.y-dim.computedTop-dim.computedBottom;
break;
default: //center
edgeOffset.y = -(dim.totalHeight/2);
break;
}
pos.x += edgeOffset.x;
pos.y += edgeOffset.y;
}
pos = {
left: ((pos.x >= 0 || parentPositioned || options.allowNegative) ? pos.x : 0).toInt(),
top: ((pos.y >= 0 || parentPositioned || options.allowNegative) ? pos.y : 0).toInt()
};
var xy = {left: 'x', top: 'y'};
['minimum', 'maximum'].each(function(minmax){
['left', 'top'].each(function(lr){
var val = options[minmax] ? options[minmax][xy[lr]] : null;
if (val != null && ((minmax == 'minimum') ? pos[lr] < val : pos[lr] > val)) pos[lr] = val;
});
});
if (rel.getStyle('position') == 'fixed' || options.relFixedPosition){
var winScroll = window.getScroll();
pos.top+= winScroll.y;
pos.left+= winScroll.x;
}
if (options.ignoreScroll){
var relScroll = rel.getScroll();
pos.top -= relScroll.y;
pos.left -= relScroll.x;
}
if (options.ignoreMargins){
pos.left += (
options.edge.x == 'right' ? dim['margin-right'] :
options.edge.x == 'center' ? -dim['margin-left'] + ((dim['margin-right'] + dim['margin-left'])/2) :
- dim['margin-left']
);
pos.top += (
options.edge.y == 'bottom' ? dim['margin-bottom'] :
options.edge.y == 'center' ? -dim['margin-top'] + ((dim['margin-bottom'] + dim['margin-top'])/2) :
- dim['margin-top']
);
}
pos.left = Math.ceil(pos.left);
pos.top = Math.ceil(pos.top);
if (options.returnPos) return pos;
else this.setStyles(pos);
return this;
} }
}); });
}).call(this); })(Element.prototype.position);
/* /*
@@ -0,0 +1,81 @@
var Question = new Class( {
initialize : function(question, hint, answers) {
var self = this
self.question = question
self.hint = hint
self.answers = answers
self.createQuestion()
self.answers.each(function(answer) {
self.createAnswer(answer)
})
self.createMask()
},
createMask : function() {
var self = this
$(document.body).mask( {
'hideOnClick' : true,
'destroyOnHide' : true,
'onHide' : function() {
self.container.destroy();
}
}).show();
},
createQuestion : function() {
this.container = new Element('div', {
'class' : 'question'
}).adopt(
new Element('h3', {
'html': this.question
}),
new Element('div.hint', {
'html': this.hint
})
).inject(document.body)
this.container.position( {
'position' : 'center'
});
},
createAnswer : function(options) {
var self = this
var answer = new Element('a', Object.merge(options, {
'class' : 'answer button '+(options['class'] || '')+(options['cancel'] ? ' cancel' : '')
})).inject(this.container)
if (options.cancel) {
answer.addEvent('click', self.close.bind(self))
}
else if (options.request) {
answer.addEvent('click', function(e){
e.stop();
new Request(Object.merge(options, {
'url': options.href,
'onComplete': function() {
(options.onComplete || function(){})()
self.close();
}
})).send();
});
}
},
close : function() {
$(document.body).get('mask').destroy();
},
toElement : function() {
return this.container
}
})
+87 -55
View File
@@ -82,7 +82,9 @@ Page.Settings = new Class({
var self = this; var self = this;
var c = self.advanced_toggle.checked ? 'addClass' : 'removeClass'; var c = self.advanced_toggle.checked ? 'addClass' : 'removeClass';
self.el[c]('show_advanced') self.el[c]('show_advanced');
Cookie.write('advanced_toggle_checked', +self.advanced_toggle.checked, {'duration': 365});
}, },
create: function(json){ create: function(json){
@@ -96,6 +98,7 @@ Page.Settings = new Class({
'text': 'Show advanced settings' 'text': 'Show advanced settings'
}), }),
self.advanced_toggle = new Element('input[type=checkbox].inlay', { self.advanced_toggle = new Element('input[type=checkbox].inlay', {
'checked': +Cookie.read('advanced_toggle_checked'),
'events': { 'events': {
'change': self.showAdvanced.bind(self) 'change': self.showAdvanced.bind(self)
} }
@@ -103,10 +106,9 @@ Page.Settings = new Class({
) )
) )
); );
self.showAdvanced();
new Form.Check(self.advanced_toggle, {
'onChange': self.showAdvanced.bind(self) new Form.Check(self.advanced_toggle)
})
// Create tabs // Create tabs
Object.each(self.tabs, function(tab, tab_name){ Object.each(self.tabs, function(tab, tab_name){
@@ -132,7 +134,7 @@ Page.Settings = new Class({
var class_name = (option.type || 'string').capitalize(); var class_name = (option.type || 'string').capitalize();
var input = new Option[class_name](self, section_name, option.name, option); var input = new Option[class_name](self, section_name, option.name, option);
input.inject(self.tabs[group.tab].groups[group.name]); input.inject(self.tabs[group.tab].groups[group.name]);
input.fireEvent('injected') input.fireEvent('injected');
}); });
}); });
@@ -182,7 +184,7 @@ Page.Settings = new Class({
'text': (group.label || group.name).capitalize() 'text': (group.label || group.name).capitalize()
}).adopt( }).adopt(
new Element('span.hint', { new Element('span.hint', {
'html': group.description 'html': group.description || ''
}) })
) )
) )
@@ -397,9 +399,7 @@ Option.Checkbox = new Class({
}) })
); );
new Form.Check(self.input, { new Form.Check(self.input);
'onChange': self.changed.bind(self)
});
}, },
@@ -435,16 +435,11 @@ Option.Enabler = new Class({
self.input = new Element('input.inlay', { self.input = new Element('input.inlay', {
'type': 'checkbox', 'type': 'checkbox',
'checked': self.getSettingValue(), 'checked': self.getSettingValue(),
'id': 'r-'+randomString(), 'id': 'r-'+randomString()
'events': {
'change': self.checkState.bind(self)
}
}) })
); );
new Form.Check(self.input, { new Form.Check(self.input);
'onChange': self.changed.bind(self)
});
}, },
changed: function(){ changed: function(){
@@ -479,63 +474,86 @@ Option.Directory = new Class({
type: 'span', type: 'span',
browser: '', browser: '',
save_on_change: false, save_on_change: false,
use_cache: false,
create: function(){ create: function(){
var self = this; var self = this;
self.el.adopt( self.el.adopt(
self.createLabel(), self.createLabel(),
self.input = new Element('span.directory', { new Element('span.directory.inlay', {
'text': self.getSettingValue(),
'events': { 'events': {
'click': self.showBrowser.bind(self) 'click': self.showBrowser.bind(self)
} }
}) }).adopt(
self.input = new Element('span', {
'text': self.getSettingValue()
})
)
); );
self.cached = {}; self.cached = {};
}, },
selectDirectory: function(e, el){ selectDirectory: function(dir){
var self = this; var self = this;
self.input.set('text', el.get('data-value')); self.input.set('text', dir);
self.getDirs() self.getDirs()
self.fireEvent('change')
}, },
previousDirectory: function(e){ previousDirectory: function(e){
var self = this; var self = this;
self.selectDirectory(null, self.back_button) self.selectDirectory(self.getParentDir())
}, },
showBrowser: function(){ showBrowser: function(){
var self = this; var self = this;
if(!self.browser) if(!self.browser){
self.browser = new Element('div.directory_list').adopt( self.browser = new Element('div.directory_list').adopt(
new Element('div.pointer'),
new Element('div.actions').adopt( new Element('div.actions').adopt(
self.back_button = new Element('a.button.back', { self.back_button = new Element('a.back', {
'text': '', 'html': '',
'events': { 'events': {
'click': self.previousDirectory.bind(self) 'click': self.previousDirectory.bind(self)
} }
}), }),
new Element('label', { new Element('label', {
'text': 'Show hidden files' 'text': 'Hidden folders'
}).adopt( }).adopt(
self.show_hidden = new Element('input[type=checkbox].inlay') self.show_hidden = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.getDirs.bind(self)
}
})
) )
), ),
self.dir_list = new Element('ul', { self.dir_list = new Element('ul', {
'events': { 'events': {
'click:relay(li)': self.selectDirectory.bind(self) 'click:relay(li)': function(e, el){
(e).stop();
self.selectDirectory(el.get('data-value'))
},
'mousewheel': function(e){
(e).stopPropagation();
}
} }
}), }),
new Element('div.actions').adopt( new Element('div.actions').adopt(
new Element('a.button.cancel', { new Element('a.clear.button', {
'text': 'Clear',
'events': {
'click': function(e){
self.input.set('text', '');
self.hideBrowser(e, true);
}
}
}),
new Element('a.cancel', {
'text': 'Cancel', 'text': 'Cancel',
'events': { 'events': {
'click': self.hideBrowser.bind(self) 'click': self.hideBrowser.bind(self)
@@ -547,21 +565,32 @@ Option.Directory = new Class({
self.save_button = new Element('a.button.save', { self.save_button = new Element('a.button.save', {
'text': 'Save', 'text': 'Save',
'events': { 'events': {
'click': self.hideBrowser.bind(self, true) 'click': function(e){
self.hideBrowser(e, true)
}
} }
}) })
) )
).inject(self.input, 'after') ).inject(self.el)
new Form.Check(self.show_hidden);
}
self.initial_directory = self.input.get('text');
self.getDirs() self.getDirs()
self.browser.show() self.browser.show()
self.el.addEvent('outerClick', self.hideBrowser.bind(self)) self.el.addEvent('outerClick', self.hideBrowser.bind(self))
}, },
hideBrowser: function(save){ hideBrowser: function(e, save){
var self = this; var self = this;
(e).stop();
if(save) self.save() if(save)
self.save()
else
self.input.set('text', self.initial_directory);
self.browser.hide() self.browser.hide()
self.el.removeEvent('outerClick', self.hideBrowser.bind(self)) self.el.removeEvent('outerClick', self.hideBrowser.bind(self))
@@ -571,41 +600,43 @@ Option.Directory = new Class({
fillBrowser: function(json){ fillBrowser: function(json){
var self = this; var self = this;
var c = self.getParentDir();
var v = self.input.get('text'); var v = self.input.get('text');
var previous_dir = self.getParentDir(c.substring(0, c.length-1)); var previous_dir = self.getParentDir();
if(previous_dir){ if(previous_dir != v){
self.back_button.set('data-value', previous_dir) self.back_button.set('data-value', previous_dir)
self.back_button.set('text', self.getCurrentDirname(previous_dir)) self.back_button.set('html', '&laquo; '+self.getCurrentDirname(previous_dir))
self.back_button.show() self.back_button.show()
} }
else { else {
self.back_button.hide() self.back_button.hide()
} }
if(!json) if(self.use_cache)
json = self.cached[c]; if(!json)
else json = self.cached[v];
self.cached[c] = json; else
self.cached[v] = json;
self.dir_list.empty(); setTimeout(function(){
json.dirs.each(function(dir){ self.dir_list.empty();
if(dir.indexOf(v) != -1){ json.dirs.each(function(dir){
new Element('li', { if(dir.indexOf(v) != -1){
'data-value': dir, new Element('li', {
'text': self.getCurrentDirname(dir) 'data-value': dir,
}).inject(self.dir_list) 'text': self.getCurrentDirname(dir)
} }).inject(self.dir_list)
}) }
});
}, 50);
}, },
getDirs: function(){ getDirs: function(){
var self = this; var self = this;
var c = self.getParentDir(); var c = self.input.get('text');
if(self.cached[c]){ if(self.cached[c] && self.use_cache){
self.fillBrowser() self.fillBrowser()
} }
else { else {
@@ -625,7 +656,8 @@ Option.Directory = new Class({
var v = dir || self.input.get('text'); var v = dir || self.input.get('text');
var sep = Api.getOption('path_sep'); var sep = Api.getOption('path_sep');
var dirs = v.split(sep); var dirs = v.split(sep);
dirs.pop(); if(dirs.pop() == '')
dirs.pop();
return dirs.join(sep) + sep return dirs.join(sep) + sep
}, },
+43 -43
View File
@@ -8,12 +8,12 @@ Page.Wanted = new Class({
indexAction: function(param){ indexAction: function(param){
var self = this; var self = this;
if(!self.list){ if(!self.wanted){
// Wanted movies // Wanted movies
self.wanted = new MovieList({ self.wanted = new MovieList({
'status': 'active', 'status': 'active',
'actions': WantedActions 'actions': MovieActions
}); });
$(self.wanted).inject(self.el); $(self.wanted).inject(self.el);
App.addEvent('library.update', self.wanted.update.bind(self.wanted)); App.addEvent('library.update', self.wanted.update.bind(self.wanted));
@@ -23,30 +23,32 @@ Page.Wanted = new Class({
}); });
var WantedActions = { var MovieActions = {};
MovieActions.Wanted = {
'IMBD': IMDBAction 'IMBD': IMDBAction
//,'releases': ReleaseAction ,'releases': ReleaseAction
,'Edit': new Class({ ,'Edit': new Class({
Extends: MovieAction, Extends: MovieAction,
create: function(){ create: function(){
var self = this; var self = this;
self.el = new Element('a.edit', { self.el = new Element('a.edit', {
'title': 'Refresh the movie info and do a forced search', 'title': 'Refresh the movie info and do a forced search',
'events': { 'events': {
'click': self.editMovie.bind(self) 'click': self.editMovie.bind(self)
} }
}); });
}, },
editMovie: function(e){ editMovie: function(e){
var self = this; var self = this;
(e).stop(); (e).stop();
if(!self.options_container){ if(!self.options_container){
self.options_container = new Element('div.options').adopt( self.options_container = new Element('div.options').adopt(
$(self.movie.thumbnail).clone(), $(self.movie.thumbnail).clone(),
@@ -69,29 +71,29 @@ var WantedActions = {
}) })
) )
).inject(self.movie, 'top'); ).inject(self.movie, 'top');
Array.each(self.movie.data.library.titles, function(alt){ Array.each(self.movie.data.library.titles, function(alt){
new Element('option', { new Element('option', {
'text': alt.title 'text': alt.title
}).inject(self.title_select); }).inject(self.title_select);
}); });
Object.each(Quality.profiles, function(profile){ Object.each(Quality.getActiveProfiles(), function(profile){
new Element('option', { new Element('option', {
'value': profile.id ? profile.id : profile.data.id, 'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label 'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select); }).inject(self.profile_select);
self.profile_select.set('value', self.movie.profile.get('id')); self.profile_select.set('value', self.movie.profile.get('id'));
}); });
} }
self.movie.slide('in'); self.movie.slide('in');
}, },
save: function(e){ save: function(e){
(e).stop(); (e).stop();
var self = this; var self = this;
Api.request('movie.edit', { Api.request('movie.edit', {
'data': { 'data': {
'id': self.movie.get('id'), 'id': self.movie.get('id'),
@@ -105,63 +107,63 @@ var WantedActions = {
self.movie.title.set('text', self.title_select.getSelected()[0].get('text')); self.movie.title.set('text', self.title_select.getSelected()[0].get('text'));
} }
}); });
self.movie.slide('out'); self.movie.slide('out');
} }
}) })
,'Refresh': new Class({ ,'Refresh': new Class({
Extends: MovieAction, Extends: MovieAction,
create: function(){ create: function(){
var self = this; var self = this;
self.el = new Element('a.refresh', { self.el = new Element('a.refresh', {
'title': 'Refresh the movie info and do a forced search', 'title': 'Refresh the movie info and do a forced search',
'events': { 'events': {
'click': self.doSearch.bind(self) 'click': self.doSearch.bind(self)
} }
}); });
}, },
doSearch: function(e){ doSearch: function(e){
var self = this; var self = this;
(e).stop(); (e).stop();
Api.request('movie.refresh', { Api.request('movie.refresh', {
'data': { 'data': {
'id': self.movie.get('id') 'id': self.movie.get('id')
} }
}); });
} }
}) })
,'Delete': new Class({ ,'Delete': new Class({
Extends: MovieAction, Extends: MovieAction,
Implements: [Chain], Implements: [Chain],
create: function(){ create: function(){
var self = this; var self = this;
self.el = new Element('a.delete', { self.el = new Element('a.delete', {
'title': 'Remove the movie from your wanted list', 'title': 'Remove the movie from your wanted list',
'events': { 'events': {
'click': self.showConfirm.bind(self) 'click': self.showConfirm.bind(self)
} }
}); });
}, },
showConfirm: function(e){ showConfirm: function(e){
var self = this; var self = this;
(e).stop(); (e).stop();
if(!self.delete_container){ if(!self.delete_container){
self.delete_container = new Element('div.delete_container', { self.delete_container = new Element('div.delete_container', {
'styles': { 'styles': {
@@ -185,27 +187,26 @@ var WantedActions = {
}) })
).inject(self.movie, 'top'); ).inject(self.movie, 'top');
} }
self.movie.slide('in'); self.movie.slide('in');
}, },
hideConfirm: function(e){ hideConfirm: function(e){
var self = this; var self = this;
(e).stop(); (e).stop();
self.movie.slide('out'); self.movie.slide('out');
}, },
del: function(e){ del: function(e){
(e).stop(); (e).stop();
var self = this; var self = this;
var movie = $(self.movie); var movie = $(self.movie);
self.chain( self.chain(
function(){ function(){
$(movie).mask().addClass('loading');
self.callChain(); self.callChain();
}, },
function(){ function(){
@@ -224,16 +225,15 @@ var WantedActions = {
}); });
} }
); );
self.callChain(); self.callChain();
} }
}) })
}; };
var SnatchedActions = { MovieActions.Snatched = {
'IMBD': IMDBAction 'IMBD': IMDBAction
,'Releases': ReleaseAction ,'Delete': MovieActions.Wanted.Delete
,'Delete': WantedActions.Delete
}; };
+102 -4
View File
@@ -1,4 +1,8 @@
/* @override http://localhost:5000/static/style/main.css */ /* @override
http://localhost:5000/static/style/main.css
http://192.168.1.20:5000/static/style/main.css
http://127.0.0.1:5000/static/style/main.css
*/
html { html {
color: #fff; color: #fff;
@@ -35,15 +39,24 @@ input, textarea {
font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
} }
input:-moz-placeholder, textarea:-moz-placeholder {
color: rgba(255, 255, 255, 0.6);
}
::-webkit-input-placeholder, ::-webkit-textarea-placeholder {
color: rgba(255, 255, 255, 0.6);
}
a img { a img {
border:none; border:none;
} }
a { a {
text-decoration:none; text-decoration:none;
color: #fff; color: #ebfcbc;
outline: 0; outline: 0;
cursor: pointer; cursor: pointer;
font-weight: bold;
} }
a:hover { color: #f3f3f3; } a:hover { color: #f3f3f3; }
@@ -125,10 +138,18 @@ form {
} }
/*** Icons ***/ /*** Icons ***/
.icon.delete { .icon {
background: url('../images/delete.png') no-repeat;
display: inline-block; display: inline-block;
background: center no-repeat;
} }
.icon.delete { background-image: url('../images/icon.delete.png'); }
.icon.download { background-image: url('../images/icon.download.png'); }
.icon.edit { background-image: url('../images/icon.edit.png'); }
.icon.check { background-image: url('../images/icon.check.png'); }
.icon.folder { background-image: url('../images/icon.folder.png'); }
.icon.imdb { background-image: url('../images/icon.imdb.png'); }
.icon.refresh { background-image: url('../images/icon.refresh.png'); }
.icon.rating { background-image: url('../images/icon.rating.png'); }
/*** Navigation ***/ /*** Navigation ***/
.header { .header {
@@ -191,6 +212,27 @@ form {
.header .navigation li a:hover, .header .navigation li a:active { .header .navigation li a:hover, .header .navigation li a:active {
color: #b1d8dc; color: #b1d8dc;
} }
.header .message.update {
text-align: center;
position: relative;
top: -70px;
padding: 15px 0 20px;
background: #ff6134;
font-size: 26px;
border-radius: 0 0 5px 5px;
-moz-border-radius: 0 0 5px 5px;
-webkit-border-radius: 0 0 5px 5px;
box-shadow: 0 2px 1px rgba(0,0,0, 0.3);
-moz-box-shadow: 0 2px 1px rgba(0,0,0, 0.3);
-webkit-box-shadow: 0 2px 1px rgba(0,0,0, 0.3);
}
.header .message a {
padding: 0 10px;
}
/*** Global Styles ***/ /*** Global Styles ***/
.check { .check {
@@ -333,3 +375,59 @@ form {
rgb(73,83,98) 100% rgb(73,83,98) 100%
); );
} }
.mask {
background: rgba(0,0,0, 0.7);
z-index: 100;
}
.question {
display: block;
width: 600px;
padding: 20px;
background: #f5f5f5;
position:fixed;
z-index:101;
text-align: center;
background: #5c697b;
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
box-shadow: 0 0 50px rgba(0,0,0,0.55);
-moz-box-shadow: 0 0 50px rgba(0,0,0,0.55);
-webkit-box-shadow: 0 0 50px rgba(0,0,0,0.55);
}
.question h3 {
font-size: 25px;
padding: 0;
margin: 0 0 20px;
}
.question .hint {
font-size: 14px;
color: #ccc;
text-shadow: none;
}
.question .answer {
font-size: 17px;
display: inline-block;
padding: 10px;
margin: 5px 1%;
cursor: pointer;
width: auto;
}
.question .answer:hover {
background: #f1f1f1;
}
.question .answer.delete {
background-color: #a82f12;
}
.question .answer.cancel {
margin-top: 20px;
background-color: #4c5766;
}
+161 -30
View File
@@ -1,7 +1,15 @@
/* @override http://localhost:5000/static/style/page/settings.css */ /* @override
http://localhost:5000/static/style/page/settings.css
http://192.168.1.20:5000/static/style/page/settings.css
*/
.page.settings { .page.settings:after {
overflow: hidden; content: ".";
display: block;
clear: both;
visibility: hidden;
line-height: 0;
height: 0;
} }
.page.settings .tabs { .page.settings .tabs {
@@ -30,6 +38,7 @@
.page.settings .tabs a { .page.settings .tabs a {
display: block; display: block;
padding: 11px 15px; padding: 11px 15px;
color: #fff;
} }
.page.settings .tabs .active a { .page.settings .tabs .active a {
background: #4e5969; background: #4e5969;
@@ -84,6 +93,7 @@
position: relative; position: relative;
margin-bottom: -25px; margin-bottom: -25px;
border: none; border: none;
width: 20px;
} }
.page.settings .ctrlHolder { .page.settings .ctrlHolder {
@@ -92,9 +102,13 @@
font-size: 14px; font-size: 14px;
border: 0; border: 0;
} }
.page.settings .ctrlHolder.save_success:not(:first-child) {
background: url('../../images/icon.check.png') no-repeat 7px center;
}
.page.settings .ctrlHolder:last-child { border: none; } .page.settings .ctrlHolder:last-child { border: none; }
.page.settings .ctrlHolder:hover { background: rgba(255,255,255,0.05); } .page.settings .ctrlHolder:hover { background-color: rgba(255,255,255,0.05); }
.page.settings .ctrlHolder.focused { background: rgba(255,255,255,0.2); } .page.settings .ctrlHolder.focused { background-color: rgba(255,255,255,0.2); }
.page.settings .ctrlHolder.focused:first-child, .page.settings .ctrlHolder:first-child{ background-color: transparent; }
.page.settings .ctrlHolder .formHint { .page.settings .ctrlHolder .formHint {
float: right; float: right;
@@ -135,6 +149,8 @@
margin: 0; margin: 0;
padding: 6px 0 0; padding: 6px 0 0;
} }
.page.settings .xsmall { width: 20px !important; text-align: center; }
.page.settings input[type=text], .page.settings input[type=password] { .page.settings input[type=text], .page.settings input[type=password] {
padding: 5px 3px; padding: 5px 3px;
@@ -165,39 +181,154 @@
.page.settings .directory { .page.settings .directory {
display: inline-block; display: inline-block;
padding: 0 4px; padding: 0 4% 0 4px;
font-size: 13px; font-size: 13px;
width: 29.7%; width: 26.3%;
} background-image: url('../../images/icon.folder.gif');
.page.settings .directory_list { background-repeat: no-repeat;
position: absolute; background-position: 97% center;
width: 300px; overflow: hidden;
margin: 0 0 0 16.5%; vertical-align: top;
background: #282d34;
border: 1px solid #1f242b;
position: absolute;
box-shadow: 0 1px 2px rgba(0,0,0,0.4);
-moz-box-shadow: 0 1px 2px rgba(0,0,0,0.4);
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.4);
border-radius:3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
} }
.page.settings .directory > span {
height: 25px;
display: inline-block;
float: right;
text-align: right;
white-space: nowrap;
cursor: pointer;
}
.page.settings .directory_list {
z-index: 2;
position: absolute;
width: 360px;
margin: -2px 0 20px 60px;
background: #5c697b;
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
box-shadow: 0 0 50px rgba(0,0,0,0.55);
-moz-box-shadow: 0 0 50px rgba(0,0,0,0.55);
-webkit-box-shadow: 0 0 50px rgba(0,0,0,0.55);
}
.page.settings .directory_list .pointer {
border-right: 10px solid transparent;
border-left: 10px solid transparent;
border-bottom: 10px solid #5c697b;
display: block;
position: absolute;
width: 0px;
margin: -9px 0 0 48.5%;
}
.page.settings .directory_list ul { .page.settings .directory_list ul {
width: 100%; width: 92%;
max-height: 200px; height: 300px;
overflow: auto; overflow: auto;
margin: 0 4%;
} }
.page.settings .directory_list li { .page.settings .directory_list li {
padding: 2px 10px; padding: 0 10px;
} cursor: pointer;
margin: 0;
border-top: 1px solid rgba(255,255,255,0.1);
background: url('../../images/right.arrow.png') no-repeat 98% center;
}
.page.settings .directory_list li:last-child {
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.page.settings .directory_list li:hover {
background-color: #515c68;
}
.page.settings .directory_list .actions { .page.settings .directory_list .actions {
clear: both; clear: both;
padding: 10px; padding: 4% 4% 2%;
background: #414953; min-height: 25px;
} }
.page.settings .directory_list .actions:first-child { border-bottom: 1px solid #1f242b; }
.page.settings .directory_list .actions:last-child { border-top: 1px solid #1f242b; } .page.settings .directory_list .actions label {
float: right;
width: auto;
padding: 0;
}
.page.settings .directory_list .actions .inlay {
margin: -2px 0 0 7px;
}
.page.settings .directory_list .actions .back {
font-weight: bold;
width: 160px;
display: inline-block;
padding: 0;
line-height: 120%;
vertical-align: top;
}
.page.settings .directory_list .actions:last-child {
float: right;
padding: 4%;
}
.page.settings .directory_list .actions:last-child > span {
padding: 0 5px;
text-shadow: none;
}
.page.settings .directory_list .actions:last-child > .clear {
left: -90%;
position: relative;
background-color: #af3128;
}
.page.settings .directory_list .actions:last-child > .cancel {
font-weight: bold;
color: #ddd;
}
.page.settings .directory_list .actions:last-child > .save {
background: #9dc156;
}
.page.settings .section_newznab {
}
.page.settings .section_newznab .head {
margin: 0 0 0 60px;
}
.page.settings .section_newznab .head abbr {
display: inline-block;
font-weight: bold;
border-bottom: 1px dotted #fff;
line-height: 140%;
cursor: help;
}
.page.settings .section_newznab .head abbr.host {
margin-right: 197px;
}
.page.settings .section_newznab .ctrlHolder {
padding-top: 2px;
padding-bottom: 3px;
}
.page.settings .section_newznab .ctrlHolder > * {
margin: 0 10px 0 0;
}
.page.settings .section_newznab .ctrlHolder .delete {
display: inline-block;
width: 22px;
height: 22px;
vertical-align: middle;
background-position: left center;
}
.page.settings .section_newznab .ctrlHolder.is_empty .delete, .page.settings .section_newznab .ctrlHolder.is_empty .use {
visibility: hidden;
}
+1
View File
@@ -16,6 +16,7 @@
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_radio.js') }}"></script> <script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_radio.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_dropdown.js') }}"></script> <script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_dropdown.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_selectoption.js') }}"></script> <script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_selectoption.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/question.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/couchpotato.js') }}"></script> <script type="text/javascript" src="{{ url_for('.static', filename='scripts/couchpotato.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/history.js') }}"></script> <script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/history.js') }}"></script>
-4
View File
@@ -1,4 +0,0 @@
Dependencies
===========
Holds all dependencies that are required by CouchPotato.
+7 -4
View File
@@ -11,7 +11,10 @@
# Source: http://pypi.python.org/pypi/axel # Source: http://pypi.python.org/pypi/axel
# Docs: http://packages.python.org/axel # Docs: http://packages.python.org/axel
import sys, threading, Queue from couchpotato.core.helpers.variable import natcmp
import Queue
import sys
import threading
class Event(object): class Event(object):
""" """
@@ -100,7 +103,7 @@ class Event(object):
self.handlers = {} self.handlers = {}
self.memoize = {} self.memoize = {}
def handle(self, handler): def handle(self, handler, priority = 0):
""" Registers a handler. The handler can be transmitted together """ Registers a handler. The handler can be transmitted together
with two arguments as a list or dictionary. The arguments are: with two arguments as a list or dictionary. The arguments are:
@@ -118,7 +121,7 @@ class Event(object):
event += {'handler':handler, 'memoize':True, 'timeout':1.5} event += {'handler':handler, 'memoize':True, 'timeout':1.5}
""" """
handler_, memoize, timeout = self._extract(handler) handler_, memoize, timeout = self._extract(handler)
self.handlers[hash(handler_)] = (handler_, memoize, timeout) self.handlers['%s.%s' % (priority, hash(handler_))] = (handler_, memoize, timeout)
return self return self
def unhandle(self, handler): def unhandle(self, handler):
@@ -144,7 +147,7 @@ class Event(object):
t.daemon = True t.daemon = True
t.start() t.start()
for handler in self.handlers: for handler in sorted(self.handlers.iterkeys(), cmp = natcmp):
self.queue.put(handler) self.queue.put(handler)
if self.asynchronous: if self.asynchronous:
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/python
####
# 06/2010 Nic Wolfe <nic@wolfeden.ca>
# 02/2006 Will Holcomb <wholcomb@gmail.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
import urllib
import urllib2
import mimetools, mimetypes
import os, sys
# Controls how sequences are uncoded. If true, elements may be given multiple values by
# assigning a sequence.
doseq = 1
class MultipartPostHandler(urllib2.BaseHandler):
handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first
def http_request(self, request):
data = request.get_data()
if data is not None and type(data) != str:
v_files = []
v_vars = []
try:
for(key, value) in data.items():
if type(value) in (file, list, tuple):
v_files.append((key, value))
else:
v_vars.append((key, value))
except TypeError:
systype, value, traceback = sys.exc_info()
raise TypeError, "not a valid non-string sequence or mapping object", traceback
if len(v_files) == 0:
data = urllib.urlencode(v_vars, doseq)
else:
boundary, data = MultipartPostHandler.multipart_encode(v_vars, v_files)
contenttype = 'multipart/form-data; boundary=%s' % boundary
if(request.has_header('Content-Type')
and request.get_header('Content-Type').find('multipart/form-data') != 0):
print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data')
request.add_unredirected_header('Content-Type', contenttype)
request.add_data(data)
return request
@staticmethod
def multipart_encode(vars, files, boundary = None, buffer = None):
if boundary is None:
boundary = mimetools.choose_boundary()
if buffer is None:
buffer = ''
for(key, value) in vars:
buffer += '--%s\r\n' % boundary
buffer += 'Content-Disposition: form-data; name="%s"' % key
buffer += '\r\n\r\n' + value + '\r\n'
for(key, fd) in files:
# allow them to pass in a file or a tuple with name & data
if type(fd) == file:
name_in = fd.name
fd.seek(0)
data_in = fd.read()
elif type(fd) in (tuple, list):
name_in, data_in = fd
filename = os.path.basename(name_in)
contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
buffer += '--%s\r\n' % boundary
buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename)
buffer += 'Content-Type: %s\r\n' % contenttype
# buffer += 'Content-Length: %s\r\n' % file_size
buffer += '\r\n' + data_in + '\r\n'
buffer += '--%s--\r\n\r\n' % boundary
return boundary, buffer
https_request = http_request
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -350,7 +350,7 @@ class MovieDb:
etree = XmlHandler(url).getEt() etree = XmlHandler(url).getEt()
lookup_results = SearchResults() lookup_results = SearchResults()
for cur_lookup in etree.find("movies").findall("movie"): for cur_lookup in etree.find("movies").findall("movie"):
cur_movie = self._parseSearchResults(cur_lookup) cur_movie = self._parseMovie(cur_lookup)
lookup_results.append(cur_movie) lookup_results.append(cur_movie)
return lookup_results return lookup_results
View File
-206
View File
@@ -1,206 +0,0 @@
import json
import os
import urllib2
__author__ = 'Therms'
__tmdb_apikey__ = '6d96a9efb4752ed0d126d94e12e52036'
class XmgException(Exception):
pass
class ApiError(XmgException):
pass
class IdError(XmgException):
pass
class NfoError(XmgException):
pass
class MetaGen():
def __init__(self, imdbid, imdbpy = None):
''' metagen is used to download metadata for a movie or tv show and then create
the necessary files for the media to be imported into XBMC.
Arguments
===========
fanart/poster_height/width_min: Sets lowest acceptable image resolution. 0 means
disregard. If no fanart available at specified resolution or greater, then
we disregard this setting, and download highest resolution that is available.
name*: In the case of a movie, ideally this should be the full movie name
followed by the year of the movie in parentheses. e.g. "The Matrix (1999)".
If this is specific enough to generate only one search result then we'll
continue. Otherwise, we'll raise IdError.
Because of the imprecise nature of this method of id, only use it if you
don't have the imdb_id or tmdb_id
imdb_id: Use this argument if you know the imdb id of the show/movie. If
this is used, the tmdb_id argument is ignored.
tmdb_id*: Use this argument if you know the tmdb id of the movie. If this
is used, the imdb_id argument is ignored.
imdbpy: When xmg is used as a library, imdbpy may not be installed
system-wide, but included with your application. If this is the case, pass
your instance of imdb.IMDb() to metagen, so we can use it.
* These arguments are not yet supported.
'''
if imdbid[:2].lower() == 'tt':
self.imdbid = imdbid[2:]
else:
self.imdbid = imdbid
self.nfo_string = 'http://www.imdb.com/title/' + imdbid + '/'
self.tmdb_data = self._get_tmdb_imdb()
self._validate_tmdb_json()
#TODO: Search by movie name
#TODO: Search by tmdb_id
#TODO: Search by movie hash
def _validate_tmdb_json(self):
try:
_ = self._get_fanart(0,0)
except:
try:
_ = self._get_poster(0,0)
except:
raise ApiError("Unknown TMDB data format: %s" % self.tmdb_data)
def write_nfo(self, path):
try:
f = open(path, 'w')
f.write(self.nfo_string)
f.close()
except:
raise NfoError("Couldn't write nfo")
def _get_fanart(self, min_height, min_width):
''' Fetches the fanart for the specified imdb_id and saves it to dir.
Arguments
min_height/width: Sets lowest acceptable resolution fanart. 0 means
disregard. If no fanart available at specified resolution or greater, then
we disregard.
'''
images = [image['image'] for image in self.tmdb_data['backdrops'] if image['image'].get('size') == 'original']
if len(images) == 0:
raise ApiError("No fanart")
return self._get_image(images, min_height, min_width)
def get_fanart_url(self, min_height, min_width):
return self._get_fanart(min_height, min_width)['url']
def write_fanart(self, filename_root, path, min_height, min_width):
fanart_url = self.get_fanart_url(min_height, min_width)
#fetch and write to disk
dest = os.path.join(path, filename_root)
try:
f = open(dest, 'wb')
except:
raise IOError("Can't open for writing: %s" % dest)
response = urllib2.urlopen(fanart_url)
f.write(response.read())
f.close()
return True
def _get_poster(self, min_height, min_width):
''' Fetches the poster for the specified imdb_id and saves it to dir.
Arguments
min_height/width: Sets lowest acceptable resolution poster. 0 means
disregard. If no poster available at specified resolution or greater, then
we disregard.
'''
images = [image['image'] for image in self.tmdb_data['posters'] if image['image'].get('size') == 'original']
if len(images) == 0:
raise ApiError("No posters")
return self._get_image(images, min_height, min_width)
def get_poster_url(self, min_height, min_width):
return self._get_poster(min_height, min_width)['url']
def write_poster(self, filename_root, path, min_height, min_width):
poster_url = self.get_poster_url(min_height, min_width)
dest = os.path.join(path, filename_root)
try:
f = open(dest, 'wb')
except:
raise IOError("Can't open for writing: %s" % dest)
response = urllib2.urlopen(poster_url)
f.write(response.read())
f.close()
return True
def _get_tmdb_imdb(self):
url = "http://api.themoviedb.org/2.1/Movie.imdbLookup/en/json/%s/%s" % (__tmdb_apikey__, "tt" + self.imdbid)
count = 0
while 1:
count += 1
response = urllib2.urlopen(url)
json_string = response.read()
try:
tmdb_data = json.loads(json_string)[0]
return tmdb_data
except ValueError, e:
if count < 3:
continue
else:
raise ApiError("Invalid JSON: %s: %s" % (e, json_string))
except:
ApiError("JSON error with: %s" % json_string)
def _get_image(self, image_list, min_height, min_width):
#Select image
images = []
for image in image_list:
if not min_height or min_width:
images.append(image)
break
elif min_height and not min_width:
if image['height'] >= min_height:
images.append(image)
break
elif min_width and not min_height:
if image['width'] >= min_width:
images.append(image)
break
elif min_width and min_height:
if image['width'] >= min_width and image['height'] >= min_height:
images.append(image)
break
#No image meets our resolution requirements, so disregard those requirements
if len(images) == 0 and min_height or min_width:
images.append(image_list[0])
return images[0]
if __name__ == "__main__":
import sys
try:
id = sys.argv[1]
except:
id = 'tt0111161'
x = MetaGen(id)
x.write_nfo(".\movie.nfo")
x.write_fanart("fanart", ".", 0, 0)
x.write_poster("movie", ".", 0, 0)