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

After

Width:  |  Height:  |  Size: 160 B

@@ -1,18 +1,134 @@
.profile > .delete {
background-position: center;
height: 20px;
width: 20px;
/* @override http://192.168.1.20:5000/static/profile_plugin/profile.css */
.add_new_profile {
padding: 20px;
display: block;
text-align: center;
font-size: 20px;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
.profile .types .type .handle {
background: url('../../images/handle.png') center;
display: inline-block;
height: 20px;
width: 20px;
.profile { border-bottom: 1px solid rgba(255,255,255,0.2) }
.profile > .delete {
height: 20px;
width: 20px;
position: absolute;
margin-left: 690px;
padding: 14px;
background-position: center;
}
.profile .qualities {
min-height: 80px;
}
.profile .formHint {
width: 250px !important;
}
.profile .wait_for {
position: absolute;
margin: -45px 0 0 437px;
}
.profile .wait_for input {
margin: 0 5px !important;
}
.profile .types {
padding: 0;
margin: 0 20px 0 -4px;
display: inline-block;
}
.profile .types li {
padding: 3px 5px;
border-bottom: 1px solid rgba(255,255,255,0.2);
list-style: none;
}
.profile .types li:last-child { border: 0; }
.profile .types li > * {
display: inline-block;
vertical-align: middle;
line-height: 0;
margin-right: 10px;
}
.profile .quality_type select {
width: 186px;
margin-left: -1px;
}
.profile .types li.is_empty .check, .profile .types li.is_empty .delete, .profile .types li.is_empty .handle {
visibility: hidden;
}
.profile .types .type .handle {
background: url('./handle.png') center;
display: inline-block;
height: 20px;
width: 20px;
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
margin: 0;
}
.profile .types .type .delete {
background-position: left center;
height: 20px;
width: 20px;
visibility: hidden;
cursor: pointer;
}
.profile .types .type:hover:not(.is_empty) .delete {
visibility: visible;
}
#profile_ordering {
}
.profile .types .type .delete {
background-position: center;
height: 20px;
width: 20px;
}
#profile_ordering ul {
float: left;
margin: 0;
width: 275px;
padding: 0;
}
#profile_ordering li {
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
border-bottom: 1px solid rgba(255,255,255,0.2);
padding: 0 5px;
}
#profile_ordering li:last-child { border: 0; }
#profile_ordering li .check {
margin: 2px 10px 0 0;
vertical-align: top;
}
#profile_ordering li > span {
display: inline-block;
height: 20px;
vertical-align: top;
line-height: 20px;
}
#profile_ordering li .handle {
background: url('./handle.png') center;
width: 20px;
float: right;
}
#profile_ordering .formHint {
clear: none;
float: right;
width: 250px;
margin: 0;
}
@@ -24,47 +24,32 @@ var Profile = new Class({
var data = self.data;
self.el = new Element('div.profile').adopt(
self.header = new Element('h4', {'text': data.label}),
new Element('span.delete.icon', {
'events': {
'click': self.del.bind(self)
}
}),
new Element('div', {
'class': 'ctrlHolder'
}).adopt(
new Element('.quality_label.ctrlHolder').adopt(
new Element('label', {'text':'Name'}),
new Element('input.label.textInput.large', {
new Element('input.inlay', {
'type':'text',
'value': data.label,
'events': {
'keyup': function(){
self.header.set('text', this.get('value'))
}
}
'placeholder': 'Profile name'
})
),
new Element('div.ctrlHolder').adopt(
new Element('label', {'text':'Wait'}),
new Element('input.wait_for.textInput.xsmall', {
new Element('div.wait_for.ctrlHolder').adopt(
new Element('span', {'text':'Wait'}),
new Element('input.inlay.xsmall', {
'type':'text',
'value': data.types && data.types.length > 0 ? data.types[0].wait_for : 0
}),
new Element('span', {'text':' day(s) for better quality.'})
new Element('span', {'text':'day(s) for a better quality.'})
),
new Element('div.ctrlHolder').adopt(
new Element('label', {'text': 'Qualities'}),
new Element('div.head').adopt(
new Element('span.quality_type', {'text': 'Search for'}),
new Element('span.finish', {'html': '<acronym title="Won\'t download anything else if it has found this quality.">Finish</acronym>'})
),
new Element('div.qualities.ctrlHolder').adopt(
new Element('label', {'text': 'Search for'}),
self.type_container = new Element('ol.types'),
new Element('a.addType', {
'text': 'Add another quality to search for.',
'href': '#',
'events': {
'click': self.addType.bind(self)
}
new Element('div.formHint', {
'html': "Search these qualities (2 minimum), from top to bottom. Use the checkbox, to stop searching after it found this quality."
})
)
);
@@ -73,6 +58,8 @@ var Profile = new Class({
if(data.types)
Object.each(data.types, self.addType.bind(self))
self.addType();
},
save: function(delay){
@@ -81,6 +68,8 @@ var Profile = new Class({
if(self.save_timer) clearTimeout(self.save_timer);
self.save_timer = (function(){
self.addType();
var data = self.getData();
if(data.types.length < 2) return;
@@ -96,6 +85,7 @@ var Profile = new Class({
}
}
});
}).delay(delay, self)
},
@@ -105,8 +95,8 @@ var Profile = new Class({
var data = {
'id' : self.data.id,
'label' : self.el.getElement('.label').get('value'),
'wait_for' : self.el.getElement('.wait_for').get('value'),
'label' : self.el.getElement('.quality_label input').get('value'),
'wait_for' : self.el.getElement('.wait_for input').get('value'),
'types': []
}
@@ -124,8 +114,19 @@ var Profile = new Class({
addType: function(data){
var self = this;
var t = new Profile.Type(data);
var has_empty = false;
self.types.each(function(type){
if($(type).hasClass('is_empty'))
has_empty = true;
});
if(has_empty) return;
var t = new Profile.Type(data, {
'onChange': self.save.bind(self, 0)
});
$(t).inject(self.type_container);
self.sortable.addItems($(t));
self.types.include(t);
@@ -135,23 +136,35 @@ var Profile = new Class({
del: function(){
var self = this;
if(!confirm('Are you sure you want to delete this profile?')) return
Api.request('profile.delete', {
'data': {
'id': self.data.id
},
'useSpinner': true,
'spinnerOptions': {
'target': self.el
},
'onComplete': function(json){
if(json.success)
self.el.destroy();
else
alert(json.message)
var label = self.el.getElement('.quality_label input').get('value');
new Question('Are you sure you want to delete <strong>"'+label+'"</strong>?', 'Items using this profile, will be set to the default quality.', [{
'text': 'Delete "'+label+'"',
'class': 'delete',
'events': {
'click': function(e){
(e).stop();
Api.request('profile.delete', {
'data': {
'id': self.data.id
},
'useSpinner': true,
'spinnerOptions': {
'target': self.el
},
'onComplete': function(json){
if(json.success)
self.el.destroy();
else
alert(json.message)
}
});
}
}
});
}, {
'text': 'Cancel',
'cancel': true
}]);
},
makeSortable: function(){
@@ -180,16 +193,24 @@ var Profile = new Class({
});
Profile.Type = Class({
Profile.Type = new Class({
Implements: [Events, Options],
deleted: false,
initialize: function(data){
initialize: function(data, options){
var self = this;
self.setOptions(options);
self.data = data;
self.data = data || {};
self.create();
self.addEvent('change', function(){
self.el[self.qualities.get('value') == '-1' ? 'addClass' : 'removeClass']('is_empty');
self.deleted = self.qualities.get('value') == '-1';
});
},
create: function(){
@@ -201,10 +222,11 @@ Profile.Type = Class({
self.fillQualities()
),
new Element('span.finish').adopt(
self.finish = new Element('input', {
'type':'checkbox',
'class':'finish',
'checked': data.finish
self.finish = new Element('input.inlay.finish[type=checkbox]', {
'checked': data.finish,
'events': {
'change': self.fireEvent.bind(self, 'change')
}
})
),
new Element('span.delete.icon', {
@@ -213,14 +235,27 @@ Profile.Type = Class({
}
}),
new Element('span.handle')
)
);
self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
new Form.Check(self.finish);
},
fillQualities: function(){
var self = this;
self.qualities = new Element('select');
self.qualities = new Element('select', {
'events': {
'change': self.fireEvent.bind(self, 'change')
}
}).adopt(
new Element('option', {
'text': '+ Add another quality',
'value': -1
})
);
Object.each(Quality.qualities, function(q){
new Element('option', {
@@ -250,6 +285,8 @@ Profile.Type = Class({
self.el.addClass('deleted');
self.el.hide();
self.deleted = true;
self.fireEvent('change');
},
toElement: function(){
+38 -33
View File
@@ -13,13 +13,13 @@ log = CPLog(__name__)
class QualityPlugin(Plugin):
qualities = [
{'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['x264', 'h264', 'bluray']},
{'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate']},
{'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
{'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
{'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['dvdscr'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['dvdscr', 'ppvrip'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
@@ -31,7 +31,8 @@ class QualityPlugin(Plugin):
addEvent('quality.all', self.all)
addEvent('quality.single', self.single)
addEvent('quality.guess', self.guess)
addEvent('app.load', self.fill)
addEvent('app.initialize', self.fill, priority = 10)
def all(self):
@@ -112,40 +113,44 @@ class QualityPlugin(Plugin):
return True
def guess(self, files, extra = {}):
found = False
def guess(self, files, extra = {}, loose = False):
for file in files:
size = (os.path.getsize(file) / 1024 / 1024)
words = re.split('\W+', file.lower())
for quality in self.all():
correctSize = False
if size >= quality['size_min'] and size <= quality['size_max']:
correctSize = True
for quality in self.all():
# Check tags
if type in words:
found = True
for alt in quality.get('alternative'):
if alt in words:
found = True
for tag in quality.get('tags', []):
if tag in words:
found = True
# Check extension + filesize
for ext in quality.get('ext'):
if ext in words and correctSize:
found = True
# Last check on resolution only
if quality.get('width', 480) == extra.get('resolution_width', 0):
found = True
if found:
if quality['identifier'] in words:
log.debug('Found via identifier "%s" in %s' % (quality['identifier'], file))
return quality
return ''
if list(set(quality.get('alternative', [])) & set(words)):
log.debug('Found %s via alt %s in %s' % (quality['identifier'], quality.get('alternative'), file))
return quality
if list(set(quality.get('tags', [])) & set(words)):
log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), file))
return quality
# Check on unreliable stuff
if loose:
# Check extension + filesize
if list(set(quality.get('ext', [])) & set(words)) and size >= quality['size_min'] and size <= quality['size_max']:
log.debug('Found %s via ext %s in %s' % (quality['identifier'], quality.get('ext'), words))
return quality
# Last check on resolution only
if quality.get('width', 480) == extra.get('resolution_width', 0):
log.debug('Found %s via resolution_width: %s == %s' % (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0)))
return quality
# Try again with loose testing
quality = self.guess(files, extra = extra, loose = True)
if quality:
return quality
log.error('Could not identify quality for: %s' % files)
return {}
@@ -19,6 +19,13 @@ var QualityBase = new Class({
return this.profiles[id]
},
// Hide items when getting profiles
getActiveProfiles: function(){
return Object.filter(this.profiles, function(profile){
return !profile.data.hide
});
},
getQuality: function(id){
return this.qualities.filter(function(q){
return q.id == id;
@@ -31,7 +38,7 @@ var QualityBase = new Class({
self.settings = App.getPage('Settings')
self.settings.addEvent('create', function(){
var tab = self.settings.createTab('profile', {
'label': 'Profile',
'label': 'Quality',
'name': 'profile'
});
@@ -39,6 +46,7 @@ var QualityBase = new Class({
self.content = tab.content;
self.createProfiles();
self.createProfileOrdering();
self.createSizes();
})
@@ -50,42 +58,104 @@ var QualityBase = new Class({
*/
createProfiles: function(){
var self = this;
var non_core_profiles = Object.filter(self.profiles, function(profile){ return !profile.isCore() });
var count = Object.getLength(non_core_profiles);
self.settings.createGroup({
'label': 'Custom',
'description': 'Discriptions'
'label': 'Quality Profiles',
'description': 'Create your own profiles with multiple qualities.'
}).inject(self.content).adopt(
new Element('a.add_new', {
'text': 'Create a new quality profile',
self.profile_container = new Element('div.container'),
new Element('a.add_new_profile', {
'text': count > 0 ? 'Create another quality profile' : 'Click here to create a quality profile.',
'events': {
'click': function(){
var profile = self.createProfilesClass();
$(profile).inject(self.profile_container, 'top')
$(profile).inject(self.profile_container)
}
}
}),
self.profile_container = new Element('div.container')
)
})
);
Object.each(self.profiles, function(profile){
if(!profile.isCore())
$(profile).inject(self.profile_container, 'top')
})
// Add profiles, that aren't part of the core (for editing)
Object.each(non_core_profiles, function(profile){
$(profile).inject(self.profile_container)
});
},
createProfilesClass: function(data){
var self = this;
if(data){
return self.profiles[data.id] = new Profile(data);
}
else {
var data = {
'id': randomString()
var data = data || {'id': randomString()}
return self.profiles[data.id] = new Profile(data);
},
createProfileOrdering: function(){
var self = this;
var profile_list;
var group = self.settings.createGroup({
'label': 'Profile Defaults'
}).adopt(
new Element('.ctrlHolder#profile_ordering').adopt(
new Element('label[text=Order]'),
profile_list = new Element('ul'),
new Element('p.formHint', {
'html': 'Change the order the profiles are in the dropdown list. Uncheck to hide it completely.<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({
'label': 'Sizes',
'description': 'Discriptions',
'description': 'Edit the minimal and maximum sizes (in MB) for each quality.',
'advanced': true
}).inject(self.content)
new Element('div.item.header').adopt(
new Element('div.item.head').adopt(
new Element('span.label', {'text': 'Quality'}),
new Element('span.min', {'text': 'Min'}),
new Element('span.max', {'text': 'Max'})
).inject(group)
Object.each(self.qualities, function(quality){
new Element('div.item').adopt(
new Element('div.ctrlHolder.item').adopt(
new Element('span.label', {'text': quality.label}),
new Element('input.min', {'value': quality.size_min}),
new Element('input.max', {'value': quality.size_max})
).inject(group)
});
}
});
+60 -10
View File
@@ -1,8 +1,10 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File, Release, Movie
from couchpotato.core.settings.model import File, Release as Relea, Movie
from sqlalchemy.sql.expression import and_, or_
log = CPLog(__name__)
@@ -13,6 +15,9 @@ class Release(Plugin):
def __init__(self):
addEvent('release.add', self.add)
addApiView('release.download', self.download)
addApiView('release.delete', self.delete)
def add(self, group):
db = get_session()
@@ -30,22 +35,22 @@ class Release(Plugin):
db.add(movie)
db.commit()
# Add release
# Add Release
snatched_status = fireEvent('status.get', 'snatched', single = True)
release = db.query(Release).filter(
rel = db.query(Relea).filter(
or_(
Release.identifier == identifier,
and_(Release.identifier.startswith(group['library']['identifier'], Release.status_id == snatched_status.get('id')))
Relea.identifier == identifier,
and_(Relea.identifier.startswith(group['library']['identifier'], Relea.status_id == snatched_status.get('id')))
)
).first()
if not release:
release = Release(
if not rel:
rel = Relea(
identifier = identifier,
movie = movie,
quality_id = group['meta_data']['quality'].get('id'),
status_id = done_status.get('id')
)
db.add(release)
db.add(rel)
db.commit()
# Add each file type
@@ -54,10 +59,10 @@ class Release(Plugin):
added_file = self.saveFile(file, type = type, include_media_info = type is 'movie')
try:
added_file = db.query(File).filter_by(id = added_file.get('id')).one()
release.files.append(added_file)
Relea.files.append(added_file)
db.commit()
except Exception, e:
log.debug('Failed to attach "%s" to release: %s' % (file, e))
log.debug('Failed to attach "%s" to Relea: %s' % (file, e))
db.remove()
@@ -73,3 +78,48 @@ class Release(Plugin):
# Check database and update/insert if necessary
return fireEvent('file.add', path = file, part = self.getPartNumber(file), type = self.file_types[type], properties = properties, single = True)
def delete(self):
db = get_session()
id = getParam('id')
rel = db.query(Relea).filter_by(id = id).first()
if rel:
rel.delete()
db.commit()
return jsonified({
'success': True
})
def download(self):
db = get_session()
id = getParam('id')
rel = db.query(Relea).filter_by(id = id).first()
if rel:
item = {}
for info in rel.info:
item[info.identifier] = info.value
# Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], single = True)
item['download'] = provider.download
fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
}))
return jsonified({
'success': True
})
else:
log.error('Couldn\'t find release with id: %s' % id)
return jsonified({
'success': False
})
+10 -8
View File
@@ -61,20 +61,22 @@ config = [{
'advanced': True,
'options': [
{
'name': 'trailer_name',
'label': 'Trailer naming',
'default': '<filename>-trailer.<ext>',
'name': 'rename_nfo',
'label': 'Rename .NFO',
'description': 'Rename original .nfo file',
'type': 'bool',
'default': True,
},
{
'name': 'nfo_name',
'label': 'NFO naming',
'default': '<filename>.<ext>',
'default': '<filename>.<ext>-orig',
},
{
'name': 'backdrop_name',
'label': 'Backdrop naming',
'default': '<filename>-backdrop.<ext>',
}
'name': 'trailer_name',
'label': 'Trailer naming',
'default': '<filename>-trailer.<ext>',
},
],
},
],
+66 -29
View File
@@ -1,10 +1,10 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library
from couchpotato.core.settings.model import Library, Movie
import os.path
import re
import shutil
@@ -40,7 +40,7 @@ class Renamer(Plugin):
group = groups[group_identifier]
rename_files = {}
# Add _UNKNOWN_ if no library is connected
# Add _UNKNOWN_ if no library item is connected
if not group['library']:
if group['dirname']:
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname'])
@@ -53,6 +53,10 @@ class Renamer(Plugin):
# Rename the files using the library data
else:
group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True)
if not group['library']:
log.error('Could not rename, no library item to work with: %s' % group_identifier)
continue
library = group['library']
# Find subtitle for renaming
@@ -85,12 +89,9 @@ class Renamer(Plugin):
for file_type in group['files']:
# Move DVD files (no renaming)
if group['is_dvd'] and file_type is 'movie':
continue
# Move nfo depending on settings
if file_type is 'nfo' and not self.conf('rename_nfo'):
log.debug('Skipping, renaming of %s disabled' % file_type)
continue
# Subtitle extra
@@ -98,7 +99,7 @@ class Renamer(Plugin):
continue
# Move other files
multiple = len(group['files']['movie']) > 1
multiple = len(group['files']['movie']) > 1 and not group['is_dvd']
cd = 1 if multiple else 0
for file in sorted(list(group['files'][file_type])):
@@ -118,21 +119,35 @@ class Renamer(Plugin):
final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements)
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
group['filename'] = replacements['filename']
# Meta naming
if file_type is 'trailer':
final_file_name = self.doReplace(trailer_name, replacements)
elif file_type is 'nfo':
final_file_name = self.doReplace(nfo_name, replacements) + '-orig'
elif file_type is 'backdrop':
final_file_name = self.doReplace(backdrop_name, replacements)
final_file_name = self.doReplace(nfo_name, replacements)
# Seperator replace
if separator:
final_file_name = final_file_name.replace(' ', separator)
# Main file
rename_files[file] = os.path.join(destination, final_folder_name, final_file_name)
# Move DVD files (no structure renaming)
if group['is_dvd'] and file_type is 'movie':
found = False
for top_dir in ['video_ts', 'audio_ts', 'bdmv', 'certificate']:
has_string = file.lower().find(os.path.sep + top_dir + os.path.sep)
if has_string >= 0:
structure_dir = file[has_string:].lstrip(os.path.sep)
rename_files[file] = os.path.join(destination, final_folder_name, structure_dir)
found = True
break
if not found:
log.error('Could not determin dvd structure for: %s' % file)
# Do rename others
else:
rename_files[file] = os.path.join(destination, final_folder_name, final_file_name)
# Check for extra subtitle files
if file_type is 'subtitle':
@@ -154,21 +169,43 @@ class Renamer(Plugin):
if multiple:
cd += 1
# Notify on download
download_message = 'Download of %s (%s) successful.' % (group['library']['titles'][0]['title'], replacements['quality'])
fireEvent('movie.downloaded', message = download_message, data = group)
# Before renaming, remove the lower quality files
db = get_session()
library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
done_status = fireEvent('status.get', 'done', single = True)
active_status = fireEvent('status.get', 'active', single = True)
for movie in library.movies:
# Mark movie "done" onces it found the quality with the finish check
try:
if movie.status_id == active_status.get('id'):
for type in movie.profile.types:
if type.quality_id == group['meta_data']['quality']['id'] and type.finish:
movie.status_id = done_status.get('id')
db.commit()
except Exception, e:
log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc()))
# Go over current movie releases
for release in movie.releases:
if release.quality.order < group['meta_data']['quality']['order']:
# This is where CP removes older, lesser quality releases
if release.quality.order > group['meta_data']['quality']['order']:
log.info('Removing older release for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
for file in release.files:
log.info('Removing (not really) "%s"' % file.path)
# When a release already exists
elif release.status_id is done_status.get('id'):
# Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc
if release.quality.order is group['meta_data']['quality']['order']:
log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label))
# Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan
else:
log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
@@ -188,10 +225,7 @@ class Renamer(Plugin):
break
for file in release.files:
log.info('Removing (not really) "%s"' % file.path)
# Rename
# Rename all files marked
for src in rename_files:
if rename_files[src]:
@@ -200,21 +234,24 @@ class Renamer(Plugin):
log.info('Renaming "%s" to "%s"' % (src, dst))
path = os.path.dirname(dst)
try:
if not os.path.isdir(path): os.makedirs(path)
except:
log.error('Failed creating dir %s: %s' % (path, traceback.format_exc()))
continue
# Create dir
self.makeDir(path)
try:
shutil.move(src, dst)
pass
#shutil.move(src, dst)
except:
log.error('Failed moving the file "%s" : %s' % (os.path.basename(src), traceback.format_exc()))
#print rename_me, rename_files[rename_me]
# Search for trailers etc
fireEvent('renamer.after', group)
fireEventAsync('renamer.after', group)
# Notify on download
download_message = 'Download of %s (%s) successful.' % (group['library']['titles'][0]['title'], replacements['quality'])
fireEventAsync('movie.downloaded', message = download_message, data = group)
# Break if CP wants to shut down
if self.shuttingDown():
+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.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File, Release, Movie
from couchpotato.core.settings.model import File
from couchpotato.environment import Env
from flask.helpers import json
from sqlalchemy.sql.expression import and_, or_
import os
import re
import subprocess
import time
import traceback
log = CPLog(__name__)
@@ -23,11 +23,11 @@ class Scanner(Plugin):
'trailer': 1048576, # 1MB
}
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads']
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
extensions = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'],
'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'],
'nfo': ['nfo', 'nfo-orig', 'txt', 'tag'],
'subtitle': ['sub', 'srt', 'ssa', 'ass'],
'subtitle_extra': ['idx'],
'trailer': ['mov', 'mp4', 'flv']
@@ -172,9 +172,21 @@ class Scanner(Plugin):
# Determine file types
delete_identifier = []
for identifier in movie_files:
group = movie_files[identifier]
# Check if movie is fresh and maybe still unpacking, ignore files new then 1 minute
file_too_new = False
for file in group['unsorted_files']:
if os.path.getmtime(file) > time.time() - 60:
file_too_new = True
if file_too_new:
log.info('Files seem to be still unpacking or just unpacked, ignoring for now: %s' % identifier)
delete_identifier.append(identifier)
continue
# Group extra (and easy) files first
images = self.getImages(group['unsorted_files'])
group['files'] = {
@@ -182,7 +194,7 @@ class Scanner(Plugin):
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
'nfo': self.getNfo(group['unsorted_files']),
'trailer': self.getTrailers(group['unsorted_files']),
'backdrop': images['backdrop'],
#'backdrop': images['backdrop'],
'leftover': set(group['unsorted_files']),
}
@@ -198,12 +210,13 @@ class Scanner(Plugin):
group['parentdir'] = os.path.dirname(movie_file)
group['dirname'] = None
folders = group['parentdir'].replace(folder, '').split(os.path.sep)
folder_names = group['parentdir'].replace(folder, '').split(os.path.sep)
folder_names.reverse()
# Try and get a proper dirname, so no "A", "Movie", "Download"
for folder in folders:
if folder.lower() in self.ignore_names or len(folder) < 2:
group['dirname'] = folder
# Try and get a proper dirname, so no "A", "Movie", "Download" etc
for folder_name in folder_names:
if folder_name.lower() not in self.ignore_names and len(folder_name) > 2:
group['dirname'] = folder_name
break
break
@@ -220,12 +233,16 @@ class Scanner(Plugin):
if not group['library']:
log.error('Unable to determin movie: %s' % group['identifiers'])
# Delete still (asuming) unpacking files
for identifier in delete_identifier:
del movie_files[identifier]
return movie_files
def getMetaData(self, group):
data = {}
files = group['files']['movie']
files = list(group['files']['movie'])
for file in files:
if os.path.getsize(file) < self.minimal_filesize['media']: continue # Ignore smaller files
@@ -246,10 +263,11 @@ class Scanner(Plugin):
if not data['quality']:
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 720 else 'SD'
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 else 'SD'
data['group'] = self.getGroup(file[0])
data['source'] = self.getSourceMedia(file[0])
file = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
data['group'] = self.getGroup(file)
data['source'] = self.getSourceMedia(file)
return data
@@ -365,7 +383,6 @@ class Scanner(Plugin):
return set(filter(test, files))
def getDVDFiles(self, files):
def test(s):
return self.isDVDFile(s)
@@ -409,7 +426,7 @@ class Scanner(Plugin):
if list(set(file.lower().split(os.path.sep)) & set(['video_ts', 'audio_ts'])):
return True
for needle in ['vts_', 'video_ts', 'audio_ts']:
for needle in ['vts_', 'video_ts', 'audio_ts', 'bdmv', 'certificate']:
if needle in file.lower():
return True
@@ -510,8 +527,8 @@ class Scanner(Plugin):
def getGroup(self, file):
try:
group = re.search('-(?P<group>[A-Z0-9]+)$', file, re.I)
return group.group('group') or ''
match = re.search('-(?P<group>[A-Z0-9]+).', file, re.I)
return match.group('group') or ''
except:
return ''
@@ -23,7 +23,7 @@ config = [{
'name': 'required_words',
'label': 'Required words',
'default': '',
'description': 'Ignore releases that doesn\'t contain one of these words.'
'description': 'Ignore releases that don\'t contain at least one of these words.'
},
{
'name': 'ignored_words',
+38 -32
View File
@@ -6,7 +6,9 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env
from sqlalchemy.exc import InterfaceError
import re
import traceback
log = CPLog(__name__)
@@ -17,6 +19,7 @@ class Searcher(Plugin):
addEvent('searcher.all', self.all)
addEvent('searcher.single', self.single)
addEvent('searcher.correct_movie', self.correctMovie)
addEvent('searcher.download', self.download)
# Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.all, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
@@ -34,7 +37,7 @@ class Searcher(Plugin):
for movie in movies:
self.single(movie.to_dict(deep = {
self.single(movie.to_dict({
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
@@ -47,11 +50,8 @@ class Searcher(Plugin):
def single(self, movie):
downloaded_status = fireEvent('status.get', 'downloaded', single = True)
available_status = fireEvent('status.get', 'available', single = True)
snatched_status = fireEvent('status.get', 'snatched', single = True)
successful = False
for type in movie['profile']['types']:
has_better_quality = 0
@@ -85,37 +85,22 @@ class Searcher(Plugin):
db.commit()
for info in nzb:
rls_info = ReleaseInfo(
identifier = info,
value = nzb[info]
)
rls.info.append(rls_info)
db.commit()
try:
if not isinstance(nzb[info], (str, unicode, int, long)):
continue
rls_info = ReleaseInfo(
identifier = info,
value = nzb[info]
)
rls.info.append(rls_info)
db.commit()
except InterfaceError:
log.debug('Couldn\'t add %s to ReleaseInfo: %s' % (info, traceback.format_exc()))
for nzb in sorted_results:
successful = fireEvent('download', data = nzb, movie = movie, single = True)
if successful:
# Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first()
rls.status_id = snatched_status.get('id')
db.commit()
# Mark movie snatched if quality is finish-checked
if type['finish']:
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = snatched_status.get('id')
db.commit()
log.info('Downloading of %s successful.' % nzb.get('name'))
fireEvent('movie.snatched', message = 'Downloading of %s successful.' % nzb.get('name'), data = rls.to_dict())
return True
return False
return self.download(data = nzb, movie = movie)
else:
log.info('Better quality (%s) already available or snatched for %s' % (type['quality']['label'], default_title))
break
@@ -126,6 +111,26 @@ class Searcher(Plugin):
return False
def download(self, data, movie):
snatched_status = fireEvent('status.get', 'snatched', single = True)
successful = fireEvent('download', data = data, movie = movie, single = True)
if successful:
# Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
rls.status_id = snatched_status.get('id')
db.commit()
log.info('Downloading of %s successful.' % data.get('name'))
fireEvent('movie.snatched', message = 'Downloading of %s successful.' % data.get('name'), data = rls.to_dict())
return True
return False
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
@@ -192,6 +197,7 @@ class Searcher(Plugin):
if len(movie_words) == 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie['library']['titles'][0]['title'], movie['library']['year']))
return False
def containsOtherQuality(self, name, preferred_quality = {}, single_category = False):
+28 -11
View File
@@ -1,4 +1,6 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
@@ -10,14 +12,13 @@ log = CPLog(__name__)
class Updater(Plugin):
git = 'git://github.com/CouchPotato/CouchPotato.git'
repo_name = 'RuudBurger/CouchPotatoServer'
running = False
version = None
updateFailed = False
updateAvailable = False
updateVersion = None
lastCheck = 0
update_failed = False
update_version = None
last_check = 0
def __init__(self):
@@ -27,6 +28,18 @@ class Updater(Plugin):
addEvent('app.load', self.check)
addApiView('updater.info', self.getInfo)
addApiView('updater.update', self.doUpdateView)
def getInfo(self):
return jsonified({
'repo_name': self.repo_name,
'last_check': self.last_check,
'update_version': self.update_version,
'version': self.getVersion(),
})
def getVersion(self):
if not self.version:
@@ -42,7 +55,7 @@ class Updater(Plugin):
def check(self):
if self.updateAvailable or self.isDisabled():
if self.update_version or self.isDisabled():
return
current_branch = self.repo.getCurrentBranch().name
@@ -54,13 +67,17 @@ class Updater(Plugin):
remote = branch.getHead()
if local.getDate() < remote.getDate():
if self.conf('automatic') and not self.updateFailed:
if self.conf('automatic') and not self.update_failed:
self.doUpdate()
else:
self.updateAvailable = True
self.updateVersion = remote.hash
self.update_version = remote.hash
self.lastCheck = time.time()
self.last_check = time.time()
def doUpdateView(self):
return jsonified({
'success': self.doUpdate()
})
def doUpdate(self):
try:
@@ -70,7 +87,7 @@ class Updater(Plugin):
except Exception, e:
log.error('Failed updating via GIT: %s' % e)
self.updateFailed = True
self.update_failed = True
return False
@@ -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()
config = [{
'name': 'global',
'name': 'core',
'groups': [
{
'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.plugins.base import Plugin
from couchpotato.environment import Env
from urllib2 import URLError
from urlparse import urlparse
import re
import time
@@ -60,22 +59,36 @@ class YarrProvider(Provider):
sizeMb = ['mb', 'mib']
sizeKb = ['kb', 'kib']
def __init__(self):
addEvent('provider.belongs_to', self.belongsTo)
def belongsTo(self, url, host = None):
try:
hostname = urlparse(url).hostname
download_url = host if host else self.urls['download']
if hostname in download_url:
return self
except:
log.debug('Url % s doesn\'t belong to %s' % (url, self.getName()))
return
def parseSize(self, size):
sizeRaw = size.lower()
size = re.sub(r'[^0-9.]', '', size).strip()
size = float(re.sub(r'[^0-9.]', '', size).strip())
for s in self.sizeGb:
if s in sizeRaw:
return float(size) * 1024
return int(size) * 1024
for s in self.sizeMb:
if s in sizeRaw:
return float(size)
return int(size)
for s in self.sizeKb:
if s in sizeRaw:
return float(size) / 1024
return int(size) / 1024
return 0
@@ -96,11 +109,16 @@ class NZBProvider(YarrProvider):
type = 'nzb'
def __init__(self):
super(NZBProvider, self).__init__()
addEvent('provider.nzb.search', self.search)
addEvent('provider.yarr.search', self.search)
addEvent('provider.nzb.feed', self.feed)
def download(self, url = '', nzb_id = ''):
return self.urlopen(url)
def feed(self):
return []
+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.plugins.base import Plugin
@@ -17,7 +17,7 @@ class MetaDataBase(Plugin):
log.info('Creating %s metadata.' % self.getName())
root = self.getRootName()
root = self.getRootName(release)
for type in ['nfo', 'thumbnail', 'fanart']:
try:
@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'renamer',
'name': 'metadata',
'name': 'mediabrowser_metadata',
'label': 'MediaBrowser',
'description': 'Enable metadata MediaBrowser can understand',
'options': [
@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'renamer',
'name': 'metadata',
'name': 'sonyps3_metadata',
'label': 'Sony PS3',
'description': 'Enable metadata your Playstation 3 can understand',
'options': [
@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'renamer',
'name': 'metadata',
'name': 'wdtv_metadata',
'label': 'WDTV',
'description': 'Enable metadata WDTV can understand',
'options': [
@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'renamer',
'name': 'metadata',
'name': 'xbmc_metadata',
'label': 'XBMC',
'description': 'Enable metadata XBMC can understand',
'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 xml.etree.ElementTree import Element, SubElement, tostring
import os
import re
import xml.dom.minidom
class XBMC(MetaDataBase):
def getRootName(self, data = {}):
return '/Users/ruud/Downloads/Test/Transformers'
return os.path.join(data['destination_dir'], data['filename'])
def getFanartName(self, root):
return '%s-fanart.jpg' % root
@@ -23,8 +22,30 @@ class XBMC(MetaDataBase):
def getNfo(self, data):
nfoxml = Element('movie')
types = ['title', 'rating', 'year', 'votes', 'rating', 'mpaa', 'originaltitle:original_title', 'outline:overview', 'premiered:released', 'id:imdb_id']
types = ['rating', 'year', 'votes', 'rating', 'mpaa', 'originaltitle:original_title', 'outline:plot', 'premiered:released']
# Title
try:
el = SubElement(nfoxml, 'title')
el.text = toUnicode(data['library']['titles'][0]['title'])
except:
pass
# IMDB id
try:
el = SubElement(nfoxml, 'id')
el.text = toUnicode(data['library']['identifier'])
except:
pass
# Runtime
try:
runtime = SubElement(nfoxml, 'runtime')
runtime.text = '%s min' % data['library']['runtime']
except:
pass
# Other values
for type in types:
if ':' in type:
@@ -33,20 +54,17 @@ class XBMC(MetaDataBase):
name = type
try:
el = SubElement(nfoxml, name)
el.text = data.get(type, '')
if data['library'].get(type):
el = SubElement(nfoxml, name)
el.text = toUnicode(data['library'].get(type, ''))
except:
pass
#for genre in self.get('genres'):
# genres = SubElement(nfoxml, 'genre')
# genres.text = genre
# Genre
for genre in data['library'].get('genres', []):
genres = SubElement(nfoxml, 'genre')
genres.text = genre.get('name')
try:
runtime = SubElement(nfoxml, 'runtime')
runtime.text = data.get('runtime') + " min"
except:
pass
# Clean up the xml and return it
nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
@@ -55,130 +73,3 @@ class XBMC(MetaDataBase):
xml_string = text_re.sub('>\g<1></', xml_string)
return xml_string.encode('utf-8')
"""
def _get_fanart(self, min_height, min_width):
''' Fetches the fanart for the specified imdb_id and saves it to dir.
Arguments
min_height/width: Sets lowest acceptable resolution fanart. 0 means
disregard. If no fanart available at specified resolution or greater, then
we disregard.
'''
images = [image['image'] for image in self.tmdb_data['backdrops'] if image['image'].get('size') == 'original']
if len(images) == 0:
return
return self._get_image(images, min_height, min_width)
def get_fanart_url(self, min_height, min_width):
return self._get_fanart(min_height, min_width)['url']
def write_fanart(self, filename_root, path, min_height, min_width):
fanart_url = self.get_fanart_url(min_height, min_width)
#fetch and write to disk
dest = os.path.join(path, filename_root)
try:
f = open(dest, 'wb')
except:
raise IOError("Can't open for writing: %s" % dest)
response = urllib2.urlopen(fanart_url)
f.write(response.read())
f.close()
return True
def _get_poster(self, min_height, min_width):
''' Fetches the poster for the specified imdb_id and saves it to dir.
Arguments
min_height/width: Sets lowest acceptable resolution poster. 0 means
disregard. If no poster available at specified resolution or greater, then
we disregard.
'''
images = [image['image'] for image in self.tmdb_data['posters'] if image['image'].get('size') == 'original']
if len(images) == 0:
return
return self._get_image(images, min_height, min_width)
def get_poster_url(self, min_height, min_width):
return self._get_poster(min_height, min_width)['url']
def write_poster(self, filename_root, path, min_height, min_width):
poster_url = self.get_poster_url(min_height, min_width)
dest = os.path.join(path, filename_root)
try:
f = open(dest, 'wb')
except:
raise IOError("Can't open for writing: %s" % dest)
response = urllib2.urlopen(poster_url)
f.write(response.read())
f.close()
return True
def _get_tmdb_imdb(self):
url = "http://api.themoviedb.org/2.1/Movie.imdbLookup/en/json/%s/%s" % (__tmdb_apikey__, self.imdbid)
count = 0
while 1:
count += 1
response = urllib2.urlopen(url)
json_string = response.read()
try:
tmdb_data = json.loads(json_string)[0]
return tmdb_data
except ValueError, e:
if count < 3:
continue
else:
raise ApiError("Invalid JSON: %s: %s" % (e, json_string))
except:
ApiError("JSON error with: %s" % json_string)
def _get_image(self, image_list, min_height, min_width):
#Select image
images = []
for image in image_list:
if not min_height or min_width:
images.append(image)
break
elif min_height and not min_width:
if image['height'] >= min_height:
images.append(image)
break
elif min_width and not min_height:
if image['width'] >= min_width:
images.append(image)
break
elif min_width and min_height:
if image['width'] >= min_width and image['height'] >= min_height:
images.append(image)
break
#No image meets our resolution requirements, so disregard those requirements
if len(images) == 0 and min_height or min_width:
images.append(image_list[0])
return images[0]
if __name__ == "__main__":
import sys
try:
id = sys.argv[1]
except:
id = 'tt0111161'
x = MetaGen(id)
x.write_nfo("movie.nfo")
try:
x.write_fanart("fanart.jpg", ".", 0, 0)
except: pass
try:
x.write_poster("movie.tbn", ".", 0, 0)
except: pass
"""
@@ -10,7 +10,7 @@ class IMDB(MovieProvider):
def __init__(self):
addEvent('provider.movie.search', self.search)
#addEvent('provider.movie.search', self.search)
self.p = IMDb('http')
@@ -92,7 +92,7 @@ class TheMovieDb(MovieProvider):
def getInfo(self, identifier = None):
cache_key = 'tmdb.cache.%s' % identifier
result = self.getCache(cache_key)
result = None #self.getCache(cache_key)
if not result:
result = {}
@@ -112,27 +112,33 @@ class TheMovieDb(MovieProvider):
def parseMovie(self, movie):
year = str(movie.get('released', 'none'))[:4]
# Poster url
poster = self.getImage(movie, type = 'poster')
backdrop = self.getImage(movie, type = 'backdrop')
# Genres
genres = self.getCategory(movie, 'genre')
# 1900 is the same as None
year = str(movie.get('released', 'none'))[:4]
if year == '1900' or year.lower() == 'none':
year = None
movie_data = {
'id': int(movie.get('id', 0)),
'titles': [toUnicode(movie.get('name'))],
'original_title': movie.get('original_name'),
'images': {
'posters': [poster],
'backdrops': [backdrop],
},
'imdb': movie.get('imdb_id'),
'runtime': movie.get('runtime'),
'released': movie.get('released'),
'year': year,
'plot': movie.get('overview', ''),
'tagline': '',
'genres': genres,
}
# Add alternative names
@@ -153,6 +159,19 @@ class TheMovieDb(MovieProvider):
return image
def getCategory(self, movie, type = 'genre'):
cats = movie.get('categories', {}).get(type)
categories = []
for category in cats:
try:
categories.append(category)
except:
pass
return categories
def isDisabled(self):
if self.conf('api_key') == '':
log.error('No API key provided.')
@@ -13,10 +13,9 @@ log = CPLog(__name__)
class Newzbin(NZBProvider, RSS):
urls = {
'search': 'https://www.newzbin.com/search/',
'download': 'http://www.newzbin.com/api/dnzb/',
'search': 'https://www.newzbin.com/search/',
}
searchUrl = 'https://www.newzbin.com/search/'
format_ids = {
2: ['scr'],
@@ -36,7 +35,7 @@ class Newzbin(NZBProvider, RSS):
def search(self, movie, quality):
results = []
if self.isDisabled() or not self.isAvailable(self.searchUrl):
if self.isDisabled() or not self.isAvailable(self.urls['search']):
return results
format_id = self.getFormatId(type)
@@ -97,11 +96,12 @@ class Newzbin(NZBProvider, RSS):
new = {
'id': id,
'type': 'nzb',
'provider': self.getName(),
'name': title,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': self.parseSize(size),
'url': str(self.getTextElement(nzb, '{%s}nzb' % REPORT_NS)),
'download': lambda: self.download(id),
'download': self.download,
'detail_url': str(self.getTextElement(nzb, 'link')),
'description': self.getTextElement(nzb, "description"),
'check_nzb': False,
@@ -121,7 +121,7 @@ class Newzbin(NZBProvider, RSS):
return results
def download(self, nzb_id):
def download(self, url = '', nzb_id = ''):
try:
log.info('Download nzb from newzbin, report id: %s ' % nzb_id)
@@ -9,7 +9,7 @@ config = [{
{
'tab': 'providers',
'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': [
{
'name': 'enabled',
@@ -21,8 +21,8 @@ config = [{
},
{
'name': 'host',
'default': 'http://nzb.su',
'description': 'The hostname of your newznab provider, like http://nzb.su'
'default': 'nzb.su',
'description': 'The hostname of your newznab provider'
},
{
'name': 'api_key',
@@ -5,6 +5,7 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import NZBProvider
from dateutil.parser import parse
from urllib import urlencode
from urlparse import urlparse
import time
import xml.etree.ElementTree as XMLTree
@@ -130,11 +131,13 @@ class Newznab(NZBProvider, RSS):
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
new = {
'id': id,
'provider': self.getName(),
'type': 'nzb',
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(size) / 1024 / 1024,
'url': (self.getUrl(host['host'], self.urls['download']) % id) + self.getApiExt(host),
'download': self.download,
'detail_url': (self.getUrl(host['host'], self.urls['detail']) % id) + self.getApiExt(host),
'content': self.getTextElement(nzb, "description"),
}
@@ -173,6 +176,17 @@ class Newznab(NZBProvider, RSS):
return list
def belongsTo(self, url):
hosts = self.getHosts()
for host in hosts:
result = super(Newznab, self).belongsTo(url, host = host['host'])
if result:
return result
return
def getUrl(self, host, type):
return cleanHost(host) + 'api?t=' + type
@@ -31,28 +31,51 @@ var MultipleNewznab = new Class({
});
self.inputs[name].getParent().hide()
self.inputs[name].addEvent('change', self.addEmpty.bind(self))
});
self.values.each(function(item, nr){
self.createItem(item.use, item.host, item.api_key);
});
new Element('a.nice_button', {
'text': 'Add new NewzNab provider',
'events': {
'click': function(e){
(e).stop();
self.createItem(1, '', '');
}
}
}).inject(self.fieldset.getElement('h2'), 'after');
new Element('div.head').adopt(
new Element('abbr.host', {
'text': 'Host',
'title': self.inputs['host'].getNext().get('text')
}),
new Element('abbr.api_key', {
'text': 'Api Key',
'title': self.inputs['api_key'].getNext().get('text')
})
).inject(self.fieldset.getElement('h2'), 'after');
self.addEmpty();
})
},
add_empty_timeout: 0,
addEmpty: function(){
var self = this;
if(self.add_empty_timeout) clearTimeout(self.add_empty_timeout);
var has_empty = false;
self.items.each(function(ctrl_holder){
if(ctrl_holder.getElement('.host').get('value') == '' && ctrl_holder.getElement('.api_key').get('value') == ''){
has_empty = true;
}
ctrl_holder[has_empty ? 'addClass' : 'removeClass']('is_empty');
});
if(has_empty) return;
self.add_empty_timeout = setTimeout(function(){
self.createItem(false, null, null);
}, 10);
},
createItem: function(use, host, api){
var self = this;
@@ -83,12 +106,12 @@ var MultipleNewznab = new Class({
}
}),
new Element('a.icon.delete', {
'text': 'delete',
'events': {
'click': self.deleteItem.bind(self)
}
})
).inject(self.fieldset);
item[!host ? 'addClass' : 'removeClass']('is_empty');
new Form.Check(checkbox, {
'onChange': checkbox.fireEvent.bind(checkbox, 'change')
@@ -105,6 +128,7 @@ var MultipleNewznab = new Class({
self.items.each(function(item, nr){
self.input_types.each(function(type){
var input = item.getElement('input.'+type);
if(input.getParent('.ctrlHolder').hasClass('is_empty')) return;
if(!temp[type]) temp[type] = [];
temp[type][nr] = input.get('type') == 'checkbox' ? +input.get('checked') : input.get('value').trim();
@@ -125,7 +149,7 @@ var MultipleNewznab = new Class({
(e).stop();
var item = e.target.getParent();
self.items.erase(item);
item.destroy();
@@ -14,7 +14,7 @@ log = CPLog(__name__)
class NzbIndex(NZBProvider, RSS):
urls = {
'download': 'http://www.nzbindex.nl/download/%s/%s',
'download': 'http://www.nzbindex.nl/download/',
'api': 'http://www.nzbindex.nl/rss/', #http://www.nzbindex.nl/rss/?q=due+date+720p&age=1000&sort=agedesc&minsize=3500&maxsize=10000
}
@@ -63,10 +63,12 @@ class NzbIndex(NZBProvider, RSS):
new = {
'id': id,
'type': 'nzb',
'provider': self.getName(),
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': enclosure['length'],
'url': enclosure['url'],
'download': self.download,
'detail_url': enclosure['url'].replace('/download/', '/release/'),
'description': self.getTextElement(nzb, "description"),
'check_nzb': True,
@@ -81,10 +81,12 @@ class NZBMatrix(NZBProvider, RSS):
new = {
'id': id,
'type': 'nzb',
'provider': self.getName(),
'name': title,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': self.parseSize(size),
'url': self.urls['download'] % id + self.getApiExt(),
'download': self.download,
'detail_url': self.urls['detail'] % id,
'description': self.getTextElement(nzb, "description"),
'check_nzb': True,
@@ -9,6 +9,7 @@ config = [{
{
'tab': 'providers',
'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': [
{
'name': 'enabled',
@@ -17,12 +18,12 @@ config = [{
{
'name': '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',
'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 = {
'id': id,
'type': 'nzb',
'provider': self.getName(),
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': self.parseSize(self.getTextElement(nzb, "description").split('</a><br />')[1].split('">')[1]),
'url': self.urls['download'] % (id, self.getApiExt()),
'download': self.download,
'detail_url': self.urls['detail'] % id,
'description': self.getTextElement(nzb, "description"),
'check_nzb': True,
@@ -3,22 +3,4 @@ from .main import ThePirateBay
def start():
return ThePirateBay()
config = [{
'name': 'themoviedb',
'groups': [
{
'tab': 'providers',
'name': 'tmdb',
'label': 'TheMovieDB',
'advanced': True,
'description': 'Used for all calls to TheMovieDB.',
'options': [
{
'name': 'api_key',
'default': '9b939aee0aaafc12a65bf448e4af9543',
'label': 'Api Key',
},
],
},
],
}]
config = []
+7 -1
View File
@@ -3,6 +3,7 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import mergeDicts
import ConfigParser
import os.path
import time
@@ -93,7 +94,12 @@ class Settings():
self.p.set(section, option, value)
def addOptions(self, section_name, options):
self.options[section_name] = options
if not self.options.get(section_name):
self.options[section_name] = options
else:
options['groups'] = self.options[section_name].get('groups') + options.get('groups')
self.options[section_name] = mergeDicts(self.options[section_name], options)
def getOptions(self):
return self.options
+9
View File
@@ -44,6 +44,7 @@ class Library(Entity):
status = ManyToOne('Status')
movies = OneToMany('Movie')
titles = OneToMany('LibraryTitle')
genres = ManyToMany('LibraryGenre')
files = ManyToMany('File')
info = OneToMany('LibraryInfo')
@@ -68,6 +69,14 @@ class LibraryTitle(Entity):
libraries = ManyToOne('Library')
class LibraryGenre(Entity):
""""""
name = Field(Unicode)
libraries = ManyToMany('Library')
class Language(Entity):
""""""
+11 -4
View File
@@ -115,8 +115,10 @@ def runCouchPotato(options, base_path, args):
latest_db_version = version(repo)
initialize = True
try:
current_db_version = db_version(db, repo)
initialize = False
except:
version_control(db, repo, version = latest_db_version)
current_db_version = db_version(db, repo)
@@ -131,6 +133,9 @@ def runCouchPotato(options, base_path, args):
fireEventAsync('app.load')
if initialize:
fireEventAsync('app.initialize')
# Create app
from couchpotato import app
api_key = Env.setting('api_key')
@@ -138,10 +143,12 @@ def runCouchPotato(options, base_path, args):
reloader = debug and not options.daemonize
# Basic config
app.host = Env.setting('host', default = '0.0.0.0')
app.port = Env.setting('port', default = 5000)
app.debug = debug
app.secret_key = api_key
config = {
'use_reloader': reloader,
'host': Env.setting('host', default = '0.0.0.0'),
'port': Env.setting('port', default = 5000)
}
# Static path
web.add_url_rule(url_base + '/static/<path:filename>',
@@ -153,4 +160,4 @@ def runCouchPotato(options, base_path, args):
app.register_blueprint(api, url_prefix = '%s/%s/' % (url_base, api_key))
# Go go go!
app.run(use_reloader = reloader)
app.run(**config)
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
self.openPage(window.location.pathname);
self.c.addEvent('click:relay(a)', self.pushState.bind(self));
self.c.addEvent('click:relay(a:not([target=_blank]))', self.pushState.bind(self));
},
pushState: function(e){
@@ -100,6 +100,14 @@ var CouchPotato = new Class({
getPage: function(name){
return this.pages[name]
},
shutdown: function(){
Api.request('app.shutdown');
},
restart: function(){
Api.request('app.restart');
}
});
@@ -96,14 +96,14 @@ Form.Check = new Class({
this.fireEvent('removeHighlight', this);
},
keyToggle: function(e) {
var evt = new Event(e);
var evt = (e);
if (evt.key === 'space') { this.toggle(e); }
},
toggle: function(e) {
var evt;
if (this.disabled) { return this; }
if (e) {
evt = new Event(e).stopPropagation();
evt = (e).stopPropagation();
if (evt.target.tagName.toLowerCase() !== 'a') {
evt.stop();
}
@@ -114,6 +114,7 @@ Form.Check = new Class({
this.check();
}
this.fireEvent('change', this);
this.input.fireEvent('change', this);
return this;
},
uncheck: function() {
@@ -117,7 +117,7 @@ Form.Dropdown = new Class({
},
expand: function(e) {
clearTimeout(this.collapseInterval);
var evt = e ? new Event(e).stop() : null;
var evt = e ? (e).stop() : null;
this.open = true;
this.input.focus();
this.element.addClass('active').addClass('dropdown-active');
@@ -22,7 +22,7 @@ Form.Radio = new Class({
toggle: function(e) {
if (this.element.hasClass('checked') || this.disabled) { return; }
var evt;
if (e) { evt = new Event(e).stop(); }
if (e) { evt = (e).stop(); }
if (this.checked) {
this.uncheck();
} else {
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
// MooTools: the javascript framework.
// Load this file's selection again by visiting: http://mootools.net/more/1e3edb90c5e02d9b9013b54e6ab001ea
// Or build this file again with packager using: packager build More/Element.Forms More/Element.Delegation More/Element.Shortcuts More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical More/Spinner
// Load this file's selection again by visiting: http://mootools.net/more/13115b95c0560a5c35a61ccf237f3ed9
// Or build this file again with packager using: packager build More/Element.Forms More/Element.Shortcuts More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical More/Spinner
/*
---
@@ -20,6 +20,7 @@ authors:
- Tim Wienk
- Christoph Pojer
- Aaron Newton
- Jacob Thornton
requires:
- Core/MooTools
@@ -30,8 +31,8 @@ provides: [MooTools.More]
*/
MooTools.More = {
'version': '1.3.1.1',
'build': '0292a3af1eea242b817fecf9daa127417d10d4ce'
'version': '1.4.0.1',
'build': 'a4244edf2aa97ac8a196fc96082dd35af1abab87'
};
@@ -182,7 +183,7 @@ String.implement({
});
}).call(this);
})();
/*
@@ -327,368 +328,6 @@ Element.implement({
});
/*
---
name: Events.Pseudos
description: Adds the functionality to add pseudo events
license: MIT-style license
authors:
- Arian Stolwijk
requires: [Core/Class.Extras, Core/Slick.Parser, More/MooTools.More]
provides: [Events.Pseudos]
...
*/
Events.Pseudos = function(pseudos, addEvent, removeEvent){
var storeKey = 'monitorEvents:';
var storageOf = function(object){
return {
store: object.store ? function(key, value){
object.store(storeKey + key, value);
} : function(key, value){
(object.$monitorEvents || (object.$monitorEvents = {}))[key] = value;
},
retrieve: object.retrieve ? function(key, dflt){
return object.retrieve(storeKey + key, dflt);
} : function(key, dflt){
if (!object.$monitorEvents) return dflt;
return object.$monitorEvents[key] || dflt;
}
};
};
var splitType = function(type){
if (type.indexOf(':') == -1 || !pseudos) return null;
var parsed = Slick.parse(type).expressions[0][0],
parsedPseudos = parsed.pseudos,
l = parsedPseudos.length,
splits = [];
while (l--) if (pseudos[parsedPseudos[l].key]){
splits.push({
event: parsed.tag,
value: parsedPseudos[l].value,
pseudo: parsedPseudos[l].key,
original: type
});
}
return splits.length ? splits : null;
};
var mergePseudoOptions = function(split){
return Object.merge.apply(this, split.map(function(item){
return pseudos[item.pseudo].options || {};
}));
};
return {
addEvent: function(type, fn, internal){
var split = splitType(type);
if (!split) return addEvent.call(this, type, fn, internal);
var storage = storageOf(this),
events = storage.retrieve(type, []),
eventType = split[0].event,
options = mergePseudoOptions(split),
stack = fn,
eventOptions = options[eventType] || {},
args = Array.slice(arguments, 2),
self = this,
monitor;
if (eventOptions.args) args.append(Array.from(eventOptions.args));
if (eventOptions.base) eventType = eventOptions.base;
if (eventOptions.onAdd) eventOptions.onAdd(this);
split.each(function(item){
var stackFn = stack;
stack = function(){
(eventOptions.listener || pseudos[item.pseudo].listener).call(self, item, stackFn, arguments, monitor, options);
};
});
monitor = stack.bind(this);
events.include({event: fn, monitor: monitor});
storage.store(type, events);
addEvent.apply(this, [type, fn].concat(args));
return addEvent.apply(this, [eventType, monitor].concat(args));
},
removeEvent: function(type, fn){
var split = splitType(type);
if (!split) return removeEvent.call(this, type, fn);
var storage = storageOf(this),
events = storage.retrieve(type);
if (!events) return this;
var eventType = split[0].event,
options = mergePseudoOptions(split),
eventOptions = options[eventType] || {},
args = Array.slice(arguments, 2);
if (eventOptions.args) args.append(Array.from(eventOptions.args));
if (eventOptions.base) eventType = eventOptions.base;
if (eventOptions.onRemove) eventOptions.onRemove(this);
removeEvent.apply(this, [type, fn].concat(args));
events.each(function(monitor, i){
if (!fn || monitor.event == fn) removeEvent.apply(this, [eventType, monitor.monitor].concat(args));
delete events[i];
}, this);
storage.store(type, events);
return this;
}
};
};
(function(){
var pseudos = {
once: {
listener: function(split, fn, args, monitor){
fn.apply(this, args);
this.removeEvent(split.event, monitor)
.removeEvent(split.original, fn);
}
},
throttle: {
listener: function(split, fn, args){
if (!fn._throttled){
fn.apply(this, args);
fn._throttled = setTimeout(function(){
fn._throttled = false;
}, split.value || 250);
}
}
},
pause: {
listener: function(split, fn, args){
clearTimeout(fn._pause);
fn._pause = fn.delay(split.value || 250, this, args);
}
}
};
Events.definePseudo = function(key, listener){
pseudos[key] = Type.isFunction(listener) ? {listener: listener} : listener;
return this;
};
Events.lookupPseudo = function(key){
return pseudos[key];
};
var proto = Events.prototype;
Events.implement(Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent));
['Request', 'Fx'].each(function(klass){
if (this[klass]) this[klass].implement(Events.prototype);
});
}).call(this);
/*
---
name: Element.Event.Pseudos
description: Adds the functionality to add pseudo events for Elements
license: MIT-style license
authors:
- Arian Stolwijk
requires: [Core/Element.Event, Events.Pseudos]
provides: [Element.Event.Pseudos]
...
*/
(function(){
var pseudos = {},
copyFromEvents = ['once', 'throttle', 'pause'],
count = copyFromEvents.length;
while (count--) pseudos[copyFromEvents[count]] = Events.lookupPseudo(copyFromEvents[count]);
Event.definePseudo = function(key, listener){
pseudos[key] = Type.isFunction(listener) ? {listener: listener} : listener;
return this;
};
var proto = Element.prototype;
[Element, Window, Document].invoke('implement', Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent));
}).call(this);
/*
---
script: Element.Delegation.js
name: Element.Delegation
description: Extends the Element native object to include the delegate method for more efficient event management.
credits:
- "Event checking based on the work of Daniel Steigerwald. License: MIT-style license. Copyright: Copyright (c) 2008 Daniel Steigerwald, daniel.steigerwald.cz"
license: MIT-style license
authors:
- Aaron Newton
- Daniel Steigerwald
requires: [/MooTools.More, Element.Event.Pseudos]
provides: [Element.Delegation]
...
*/
(function(){
var eventListenerSupport = !(window.attachEvent && !window.addEventListener),
nativeEvents = Element.NativeEvents;
nativeEvents.focusin = 2;
nativeEvents.focusout = 2;
var check = function(split, target, event){
var elementEvent = Element.Events[split.event], condition;
if (elementEvent) condition = elementEvent.condition;
return Slick.match(target, split.value) && (!condition || condition.call(target, event));
};
var formObserver = function(eventName){
var $delegationKey = '$delegation:';
return {
base: 'focusin',
onRemove: function(element){
element.retrieve($delegationKey + 'forms', []).each(function(el){
el.retrieve($delegationKey + 'listeners', []).each(function(listener){
el.removeEvent(eventName, listener);
});
el.eliminate($delegationKey + eventName + 'listeners')
.eliminate($delegationKey + eventName + 'originalFn');
});
},
listener: function(split, fn, args, monitor, options){
var event = args[0],
forms = this.retrieve($delegationKey + 'forms', []),
target = event.target,
form = (target.get('tag') == 'form') ? target : event.target.getParent('form'),
formEvents = form.retrieve($delegationKey + 'originalFn', []),
formListeners = form.retrieve($delegationKey + 'listeners', []);
forms.include(form);
this.store($delegationKey + 'forms', forms);
if (!formEvents.contains(fn)){
var formListener = function(event){
if (check(split, this, event)) fn.call(this, event);
};
form.addEvent(eventName, formListener);
formEvents.push(fn);
formListeners.push(formListener);
form.store($delegationKey + eventName + 'originalFn', formEvents)
.store($delegationKey + eventName + 'listeners', formListeners);
}
}
};
};
var inputObserver = function(eventName){
return {
base: 'focusin',
listener: function(split, fn, args){
var events = {blur: function(){
this.removeEvents(events);
}};
events[eventName] = function(event){
if (check(split, this, event)) fn.call(this, event);
};
args[0].target.addEvents(events);
}
};
};
var eventOptions = {
mouseenter: {
base: 'mouseover'
},
mouseleave: {
base: 'mouseout'
},
focus: {
base: 'focus' + (eventListenerSupport ? '' : 'in'),
args: [true]
},
blur: {
base: eventListenerSupport ? 'blur' : 'focusout',
args: [true]
}
};
if (!eventListenerSupport) Object.append(eventOptions, {
submit: formObserver('submit'),
reset: formObserver('reset'),
change: inputObserver('change'),
select: inputObserver('select')
});
Event.definePseudo('relay', {
listener: function(split, fn, args, monitor, options){
var event = args[0];
for (var target = event.target; target && target != this; target = target.parentNode){
var finalTarget = document.id(target);
if (check(split, finalTarget, event)){
if (finalTarget) fn.call(finalTarget, event, finalTarget);
return;
}
}
},
options: eventOptions
});
}).call(this);
/*
---
@@ -826,7 +465,7 @@ Fx.Slide = new Class({
this.addEvent('complete', function(){
this.open = (wrapper['offset' + this.layout.capitalize()] != 0);
if (this.open && options.resetHeight) wrapper.setStyle('height', '');
if (this.open && this.options.resetHeight) wrapper.setStyle('height', '');
}, true);
},
@@ -1050,12 +689,6 @@ var Drag = new Class({
var limit = options.limit;
this.limit = {x: [], y: []};
var styles = this.element.getStyles('left', 'right', 'top', 'bottom');
this._invert = {
x: options.modifiers.x == 'left' && styles.left == 'auto' && !isNaN(styles.right.toInt()) && (options.modifiers.x = 'right'),
y: options.modifiers.y == 'top' && styles.top == 'auto' && !isNaN(styles.bottom.toInt()) && (options.modifiers.y = 'bottom')
};
var z, coordinates;
for (z in options.modifiers){
if (!options.modifiers[z]) continue;
@@ -1072,7 +705,6 @@ var Drag = new Class({
else this.value.now[z] = this.element[options.modifiers[z]];
if (options.invert) this.value.now[z] *= -1;
if (this._invert[z]) this.value.now[z] *= -1;
this.mouse.pos[z] = event.page[z] - this.value.now[z];
@@ -1122,7 +754,6 @@ var Drag = new Class({
this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z];
if (options.invert) this.value.now[z] *= -1;
if (this._invert[z]) this.value.now[z] *= -1;
if (options.limit && this.limit[z]){
if ((this.limit[z][1] || this.limit[z][1] === 0) && (this.value.now[z] > this.limit[z][1])){
@@ -1236,10 +867,9 @@ Drag.Move = new Class({
this.container = document.id(this.container.getDocument().body);
if (this.options.style){
if (this.options.modifiers.x == "left" && this.options.modifiers.y == "top"){
var parentStyles,
parent = element.getOffsetParent();
var styles = element.getStyles('left', 'top');
if (this.options.modifiers.x == 'left' && this.options.modifiers.y == 'top'){
var parent = element.getOffsetParent(),
styles = element.getStyles('left', 'top');
if (parent && (styles.left == 'auto' || styles.top == 'auto')){
element.setPosition(element.getPosition(parent));
}
@@ -1529,7 +1159,7 @@ var Sortables = new Class({
if (
!this.idle ||
event.rightClick ||
['button', 'input', 'a'].contains(event.target.get('tag'))
['button', 'input', 'a', 'textarea'].contains(event.target.get('tag'))
) return;
this.idle = false;
@@ -1640,8 +1270,7 @@ Request.JSONP = new Class({
Implements: [Chain, Events, Options],
options: {
/*
options: {/*
onRequest: function(src, scriptElement){},
onComplete: function(data){},
onSuccess: function(data){},
@@ -1708,7 +1337,8 @@ Request.JSONP = new Class({
},
getScript: function(src){
if (!this.script) this.script = new Element('script[type=text/javascript]', {
if (!this.script) this.script = new Element('script', {
type: 'text/javascript',
async: true,
src: src
});
@@ -1716,7 +1346,7 @@ Request.JSONP = new Class({
},
success: function(args, index){
if (!this.running) return false;
if (!this.running) return;
this.clear()
.fireEvent('complete', args).fireEvent('success', args)
.callChain();
@@ -1835,10 +1465,10 @@ Class.refactor = function(original, refactors){
Object.each(refactors, function(item, name){
var origin = original.prototype[name];
if (origin && origin.$origin) origin = origin.$origin;
origin = (origin && origin.$origin) || origin || function(){};
original.implement(name, (typeof item == 'function') ? function(){
var old = this.previous;
this.previous = origin || function(){};
this.previous = origin;
var value = item.apply(this, arguments);
this.previous = old;
return value;
@@ -1875,7 +1505,7 @@ provides: [Class.Binds]
Class.Mutators.Binds = function(binds){
if (!this.prototype.initialize) this.implement('initialize', function(){});
return binds;
return Array.from(binds).concat(this.prototype.Binds || []);
};
Class.Mutators.initialize = function(initialize){
@@ -2055,7 +1685,7 @@ Element.implement({
});
}).call(this);
})();
/*
@@ -2071,219 +1701,228 @@ license: MIT-style license
authors:
- Aaron Newton
- Jacob Thornton
requires:
- Core/Options
- Core/Element.Dimensions
- /Element.Measure
- Element.Measure
provides: [Element.Position]
...
*/
(function(){
(function(original){
var original = Element.prototype.position;
var local = Element.Position = {
options: {/*
edge: false,
returnPos: false,
minimum: {x: 0, y: 0},
maximum: {x: 0, y: 0},
relFixedPosition: false,
ignoreMargins: false,
ignoreScroll: false,
allowNegative: false,*/
relativeTo: document.body,
position: {
x: 'center', //left, center, right
y: 'center' //top, center, bottom
},
offset: {x: 0, y: 0}
},
getOptions: function(element, options){
options = Object.merge({}, local.options, options);
local.setPositionOption(options);
local.setEdgeOption(options);
local.setOffsetOption(element, options);
local.setDimensionsOption(element, options);
return options;
},
setPositionOption: function(options){
options.position = local.getCoordinateFromValue(options.position);
},
setEdgeOption: function(options){
var edgeOption = local.getCoordinateFromValue(options.edge);
options.edge = edgeOption ? edgeOption :
(options.position.x == 'center' && options.position.y == 'center') ? {x: 'center', y: 'center'} :
{x: 'left', y: 'top'};
},
setOffsetOption: function(element, options){
var parentOffset = {x: 0, y: 0},
offsetParent = element.measure(function(){
return document.id(this.getOffsetParent());
}),
parentScroll = offsetParent.getScroll();
if (!offsetParent || offsetParent == element.getDocument().body) return;
parentOffset = offsetParent.measure(function(){
var position = this.getPosition();
if (this.getStyle('position') == 'fixed'){
var scroll = window.getScroll();
position.x += scroll.x;
position.y += scroll.y;
}
return position;
});
options.offset = {
parentPositioned: offsetParent != document.id(options.relativeTo),
x: options.offset.x - parentOffset.x + parentScroll.x,
y: options.offset.y - parentOffset.y + parentScroll.y
};
},
setDimensionsOption: function(element, options){
options.dimensions = element.getDimensions({
computeSize: true,
styles: ['padding', 'border', 'margin']
});
},
getPosition: function(element, options){
var position = {};
options = local.getOptions(element, options);
var relativeTo = document.id(options.relativeTo) || document.body;
local.setPositionCoordinates(options, position, relativeTo);
if (options.edge) local.toEdge(position, options);
var offset = options.offset;
position.left = ((position.x >= 0 || offset.parentPositioned || options.allowNegative) ? position.x : 0).toInt();
position.top = ((position.y >= 0 || offset.parentPositioned || options.allowNegative) ? position.y : 0).toInt();
local.toMinMax(position, options);
if (options.relFixedPosition || relativeTo.getStyle('position') == 'fixed') local.toRelFixedPosition(relativeTo, position);
if (options.ignoreScroll) local.toIgnoreScroll(relativeTo, position);
if (options.ignoreMargins) local.toIgnoreMargins(position, options);
position.left = Math.ceil(position.left);
position.top = Math.ceil(position.top);
delete position.x;
delete position.y;
return position;
},
setPositionCoordinates: function(options, position, relativeTo){
var offsetY = options.offset.y,
offsetX = options.offset.x,
calc = (relativeTo == document.body) ? window.getScroll() : relativeTo.getPosition(),
top = calc.y,
left = calc.x,
winSize = window.getSize();
switch(options.position.x){
case 'left': position.x = left + offsetX; break;
case 'right': position.x = left + offsetX + relativeTo.offsetWidth; break;
default: position.x = left + ((relativeTo == document.body ? winSize.x : relativeTo.offsetWidth) / 2) + offsetX; break;
}
switch(options.position.y){
case 'top': position.y = top + offsetY; break;
case 'bottom': position.y = top + offsetY + relativeTo.offsetHeight; break;
default: position.y = top + ((relativeTo == document.body ? winSize.y : relativeTo.offsetHeight) / 2) + offsetY; break;
}
},
toMinMax: function(position, options){
var xy = {left: 'x', top: 'y'}, value;
['minimum', 'maximum'].each(function(minmax){
['left', 'top'].each(function(lr){
value = options[minmax] ? options[minmax][xy[lr]] : null;
if (value != null && ((minmax == 'minimum') ? position[lr] < value : position[lr] > value)) position[lr] = value;
});
});
},
toRelFixedPosition: function(relativeTo, position){
var winScroll = window.getScroll();
position.top += winScroll.y;
position.left += winScroll.x;
},
toIgnoreScroll: function(relativeTo, position){
var relScroll = relativeTo.getScroll();
position.top -= relScroll.y;
position.left -= relScroll.x;
},
toIgnoreMargins: function(position, options){
position.left += options.edge.x == 'right'
? options.dimensions['margin-right']
: (options.edge.x != 'center'
? -options.dimensions['margin-left']
: -options.dimensions['margin-left'] + ((options.dimensions['margin-right'] + options.dimensions['margin-left']) / 2));
position.top += options.edge.y == 'bottom'
? options.dimensions['margin-bottom']
: (options.edge.y != 'center'
? -options.dimensions['margin-top']
: -options.dimensions['margin-top'] + ((options.dimensions['margin-bottom'] + options.dimensions['margin-top']) / 2));
},
toEdge: function(position, options){
var edgeOffset = {},
dimensions = options.dimensions,
edge = options.edge;
switch(edge.x){
case 'left': edgeOffset.x = 0; break;
case 'right': edgeOffset.x = -dimensions.x - dimensions.computedRight - dimensions.computedLeft; break;
// center
default: edgeOffset.x = -(Math.round(dimensions.totalWidth / 2)); break;
}
switch(edge.y){
case 'top': edgeOffset.y = 0; break;
case 'bottom': edgeOffset.y = -dimensions.y - dimensions.computedTop - dimensions.computedBottom; break;
// center
default: edgeOffset.y = -(Math.round(dimensions.totalHeight / 2)); break;
}
position.x += edgeOffset.x;
position.y += edgeOffset.y;
},
getCoordinateFromValue: function(option){
if (typeOf(option) != 'string') return option;
option = option.toLowerCase();
return {
x: option.test('left') ? 'left'
: (option.test('right') ? 'right' : 'center'),
y: option.test(/upper|top/) ? 'top'
: (option.test('bottom') ? 'bottom' : 'center')
};
}
};
Element.implement({
position: function(options){
//call original position if the options are x/y values
if (options && (options.x != null || options.y != null)){
return original ? original.apply(this, arguments) : this;
return (original ? original.apply(this, arguments) : this);
}
var position = this.setStyle('position', 'absolute').calculatePosition(options);
return (options && options.returnPos) ? position : this.setStyles(position);
},
Object.each(options || {}, function(v, k){
if (v == null) delete options[k];
});
options = Object.merge({
// minimum: { x: 0, y: 0 },
// maximum: { x: 0, y: 0},
relativeTo: document.body,
position: {
x: 'center', //left, center, right
y: 'center' //top, center, bottom
},
offset: {x: 0, y: 0}/*,
edge: false,
returnPos: false,
relFixedPosition: false,
ignoreMargins: false,
ignoreScroll: false,
allowNegative: false*/
}, options);
//compute the offset of the parent positioned element if this element is in one
var parentOffset = {x: 0, y: 0},
parentPositioned = false;
/* dollar around getOffsetParent should not be necessary, but as it does not return
* a mootools extended element in IE, an error occurs on the call to expose. See:
* http://mootools.lighthouseapp.com/projects/2706/tickets/333-element-getoffsetparent-inconsistency-between-ie-and-other-browsers */
var offsetParent = this.measure(function(){
return document.id(this.getOffsetParent());
});
if (offsetParent && offsetParent != this.getDocument().body){
parentOffset = offsetParent.measure(function(){
return this.getPosition();
});
parentPositioned = offsetParent != document.id(options.relativeTo);
options.offset.x = options.offset.x - parentOffset.x;
options.offset.y = options.offset.y - parentOffset.y;
}
//upperRight, bottomRight, centerRight, upperLeft, bottomLeft, centerLeft
//topRight, topLeft, centerTop, centerBottom, center
var fixValue = function(option){
if (typeOf(option) != 'string') return option;
option = option.toLowerCase();
var val = {};
if (option.test('left')){
val.x = 'left';
} else if (option.test('right')){
val.x = 'right';
} else {
val.x = 'center';
}
if (option.test('upper') || option.test('top')){
val.y = 'top';
} else if (option.test('bottom')){
val.y = 'bottom';
} else {
val.y = 'center';
}
return val;
};
options.edge = fixValue(options.edge);
options.position = fixValue(options.position);
if (!options.edge){
if (options.position.x == 'center' && options.position.y == 'center') options.edge = {x:'center', y:'center'};
else options.edge = {x:'left', y:'top'};
}
this.setStyle('position', 'absolute');
var rel = document.id(options.relativeTo) || document.body,
calc = rel == document.body ? window.getScroll() : rel.getPosition(),
top = calc.y, left = calc.x;
var dim = this.getDimensions({
computeSize: true,
styles:['padding', 'border','margin']
});
var pos = {},
prefY = options.offset.y,
prefX = options.offset.x,
winSize = window.getSize();
switch (options.position.x){
case 'left':
pos.x = left + prefX;
break;
case 'right':
pos.x = left + prefX + rel.offsetWidth;
break;
default: //center
pos.x = left + ((rel == document.body ? winSize.x : rel.offsetWidth)/2) + prefX;
break;
}
switch (options.position.y){
case 'top':
pos.y = top + prefY;
break;
case 'bottom':
pos.y = top + prefY + rel.offsetHeight;
break;
default: //center
pos.y = top + ((rel == document.body ? winSize.y : rel.offsetHeight)/2) + prefY;
break;
}
if (options.edge){
var edgeOffset = {};
switch (options.edge.x){
case 'left':
edgeOffset.x = 0;
break;
case 'right':
edgeOffset.x = -dim.x-dim.computedRight-dim.computedLeft;
break;
default: //center
edgeOffset.x = -(dim.totalWidth/2);
break;
}
switch (options.edge.y){
case 'top':
edgeOffset.y = 0;
break;
case 'bottom':
edgeOffset.y = -dim.y-dim.computedTop-dim.computedBottom;
break;
default: //center
edgeOffset.y = -(dim.totalHeight/2);
break;
}
pos.x += edgeOffset.x;
pos.y += edgeOffset.y;
}
pos = {
left: ((pos.x >= 0 || parentPositioned || options.allowNegative) ? pos.x : 0).toInt(),
top: ((pos.y >= 0 || parentPositioned || options.allowNegative) ? pos.y : 0).toInt()
};
var xy = {left: 'x', top: 'y'};
['minimum', 'maximum'].each(function(minmax){
['left', 'top'].each(function(lr){
var val = options[minmax] ? options[minmax][xy[lr]] : null;
if (val != null && ((minmax == 'minimum') ? pos[lr] < val : pos[lr] > val)) pos[lr] = val;
});
});
if (rel.getStyle('position') == 'fixed' || options.relFixedPosition){
var winScroll = window.getScroll();
pos.top+= winScroll.y;
pos.left+= winScroll.x;
}
if (options.ignoreScroll){
var relScroll = rel.getScroll();
pos.top -= relScroll.y;
pos.left -= relScroll.x;
}
if (options.ignoreMargins){
pos.left += (
options.edge.x == 'right' ? dim['margin-right'] :
options.edge.x == 'center' ? -dim['margin-left'] + ((dim['margin-right'] + dim['margin-left'])/2) :
- dim['margin-left']
);
pos.top += (
options.edge.y == 'bottom' ? dim['margin-bottom'] :
options.edge.y == 'center' ? -dim['margin-top'] + ((dim['margin-bottom'] + dim['margin-top'])/2) :
- dim['margin-top']
);
}
pos.left = Math.ceil(pos.left);
pos.top = Math.ceil(pos.top);
if (options.returnPos) return pos;
else this.setStyles(pos);
return this;
calculatePosition: function(options){
return local.getPosition(this, options);
}
});
}).call(this);
})(Element.prototype.position);
/*
@@ -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 c = self.advanced_toggle.checked ? 'addClass' : 'removeClass';
self.el[c]('show_advanced')
self.el[c]('show_advanced');
Cookie.write('advanced_toggle_checked', +self.advanced_toggle.checked, {'duration': 365});
},
create: function(json){
@@ -96,6 +98,7 @@ Page.Settings = new Class({
'text': 'Show advanced settings'
}),
self.advanced_toggle = new Element('input[type=checkbox].inlay', {
'checked': +Cookie.read('advanced_toggle_checked'),
'events': {
'change': self.showAdvanced.bind(self)
}
@@ -103,10 +106,9 @@ Page.Settings = new Class({
)
)
);
new Form.Check(self.advanced_toggle, {
'onChange': self.showAdvanced.bind(self)
})
self.showAdvanced();
new Form.Check(self.advanced_toggle)
// Create tabs
Object.each(self.tabs, function(tab, tab_name){
@@ -132,7 +134,7 @@ Page.Settings = new Class({
var class_name = (option.type || 'string').capitalize();
var input = new Option[class_name](self, section_name, option.name, option);
input.inject(self.tabs[group.tab].groups[group.name]);
input.fireEvent('injected')
input.fireEvent('injected');
});
});
@@ -182,7 +184,7 @@ Page.Settings = new Class({
'text': (group.label || group.name).capitalize()
}).adopt(
new Element('span.hint', {
'html': group.description
'html': group.description || ''
})
)
)
@@ -397,9 +399,7 @@ Option.Checkbox = new Class({
})
);
new Form.Check(self.input, {
'onChange': self.changed.bind(self)
});
new Form.Check(self.input);
},
@@ -435,16 +435,11 @@ Option.Enabler = new Class({
self.input = new Element('input.inlay', {
'type': 'checkbox',
'checked': self.getSettingValue(),
'id': 'r-'+randomString(),
'events': {
'change': self.checkState.bind(self)
}
'id': 'r-'+randomString()
})
);
new Form.Check(self.input, {
'onChange': self.changed.bind(self)
});
new Form.Check(self.input);
},
changed: function(){
@@ -479,63 +474,86 @@ Option.Directory = new Class({
type: 'span',
browser: '',
save_on_change: false,
use_cache: false,
create: function(){
var self = this;
self.el.adopt(
self.createLabel(),
self.input = new Element('span.directory', {
'text': self.getSettingValue(),
new Element('span.directory.inlay', {
'events': {
'click': self.showBrowser.bind(self)
}
})
}).adopt(
self.input = new Element('span', {
'text': self.getSettingValue()
})
)
);
self.cached = {};
},
selectDirectory: function(e, el){
selectDirectory: function(dir){
var self = this;
self.input.set('text', el.get('data-value'));
self.input.set('text', dir);
self.getDirs()
self.fireEvent('change')
},
previousDirectory: function(e){
var self = this;
self.selectDirectory(null, self.back_button)
self.selectDirectory(self.getParentDir())
},
showBrowser: function(){
var self = this;
if(!self.browser)
if(!self.browser){
self.browser = new Element('div.directory_list').adopt(
new Element('div.pointer'),
new Element('div.actions').adopt(
self.back_button = new Element('a.button.back', {
'text': '',
self.back_button = new Element('a.back', {
'html': '',
'events': {
'click': self.previousDirectory.bind(self)
}
}),
new Element('label', {
'text': 'Show hidden files'
'text': 'Hidden folders'
}).adopt(
self.show_hidden = new Element('input[type=checkbox].inlay')
self.show_hidden = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.getDirs.bind(self)
}
})
)
),
self.dir_list = new Element('ul', {
'events': {
'click:relay(li)': self.selectDirectory.bind(self)
'click:relay(li)': function(e, el){
(e).stop();
self.selectDirectory(el.get('data-value'))
},
'mousewheel': function(e){
(e).stopPropagation();
}
}
}),
new Element('div.actions').adopt(
new Element('a.button.cancel', {
new Element('a.clear.button', {
'text': 'Clear',
'events': {
'click': function(e){
self.input.set('text', '');
self.hideBrowser(e, true);
}
}
}),
new Element('a.cancel', {
'text': 'Cancel',
'events': {
'click': self.hideBrowser.bind(self)
@@ -547,21 +565,32 @@ Option.Directory = new Class({
self.save_button = new Element('a.button.save', {
'text': 'Save',
'events': {
'click': self.hideBrowser.bind(self, true)
'click': function(e){
self.hideBrowser(e, true)
}
}
})
)
).inject(self.input, 'after')
).inject(self.el)
new Form.Check(self.show_hidden);
}
self.initial_directory = self.input.get('text');
self.getDirs()
self.browser.show()
self.el.addEvent('outerClick', self.hideBrowser.bind(self))
},
hideBrowser: function(save){
hideBrowser: function(e, save){
var self = this;
(e).stop();
if(save) self.save()
if(save)
self.save()
else
self.input.set('text', self.initial_directory);
self.browser.hide()
self.el.removeEvent('outerClick', self.hideBrowser.bind(self))
@@ -571,41 +600,43 @@ Option.Directory = new Class({
fillBrowser: function(json){
var self = this;
var c = self.getParentDir();
var v = self.input.get('text');
var previous_dir = self.getParentDir(c.substring(0, c.length-1));
var previous_dir = self.getParentDir();
if(previous_dir){
if(previous_dir != v){
self.back_button.set('data-value', previous_dir)
self.back_button.set('text', self.getCurrentDirname(previous_dir))
self.back_button.set('html', '&laquo; '+self.getCurrentDirname(previous_dir))
self.back_button.show()
}
else {
self.back_button.hide()
}
if(!json)
json = self.cached[c];
else
self.cached[c] = json;
if(self.use_cache)
if(!json)
json = self.cached[v];
else
self.cached[v] = json;
self.dir_list.empty();
json.dirs.each(function(dir){
if(dir.indexOf(v) != -1){
new Element('li', {
'data-value': dir,
'text': self.getCurrentDirname(dir)
}).inject(self.dir_list)
}
})
setTimeout(function(){
self.dir_list.empty();
json.dirs.each(function(dir){
if(dir.indexOf(v) != -1){
new Element('li', {
'data-value': dir,
'text': self.getCurrentDirname(dir)
}).inject(self.dir_list)
}
});
}, 50);
},
getDirs: function(){
var self = this;
var c = self.getParentDir();
var c = self.input.get('text');
if(self.cached[c]){
if(self.cached[c] && self.use_cache){
self.fillBrowser()
}
else {
@@ -625,7 +656,8 @@ Option.Directory = new Class({
var v = dir || self.input.get('text');
var sep = Api.getOption('path_sep');
var dirs = v.split(sep);
dirs.pop();
if(dirs.pop() == '')
dirs.pop();
return dirs.join(sep) + sep
},
+43 -43
View File
@@ -8,12 +8,12 @@ Page.Wanted = new Class({
indexAction: function(param){
var self = this;
if(!self.list){
if(!self.wanted){
// Wanted movies
self.wanted = new MovieList({
'status': 'active',
'actions': WantedActions
'actions': MovieActions
});
$(self.wanted).inject(self.el);
App.addEvent('library.update', self.wanted.update.bind(self.wanted));
@@ -23,30 +23,32 @@ Page.Wanted = new Class({
});
var WantedActions = {
var MovieActions = {};
MovieActions.Wanted = {
'IMBD': IMDBAction
//,'releases': ReleaseAction
,'releases': ReleaseAction
,'Edit': new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.edit', {
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.editMovie.bind(self)
}
});
},
editMovie: function(e){
var self = this;
(e).stop();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
$(self.movie.thumbnail).clone(),
@@ -69,29 +71,29 @@ var WantedActions = {
})
)
).inject(self.movie, 'top');
Array.each(self.movie.data.library.titles, function(alt){
new Element('option', {
'text': alt.title
}).inject(self.title_select);
});
Object.each(Quality.profiles, function(profile){
Object.each(Quality.getActiveProfiles(), function(profile){
new Element('option', {
'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select);
self.profile_select.set('value', self.movie.profile.get('id'));
});
}
self.movie.slide('in');
},
save: function(e){
(e).stop();
var self = this;
Api.request('movie.edit', {
'data': {
'id': self.movie.get('id'),
@@ -105,63 +107,63 @@ var WantedActions = {
self.movie.title.set('text', self.title_select.getSelected()[0].get('text'));
}
});
self.movie.slide('out');
}
})
,'Refresh': new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.refresh', {
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.doSearch.bind(self)
}
});
},
doSearch: function(e){
var self = this;
(e).stop();
Api.request('movie.refresh', {
'data': {
'id': self.movie.get('id')
}
});
}
})
,'Delete': new Class({
Extends: MovieAction,
Implements: [Chain],
create: function(){
var self = this;
self.el = new Element('a.delete', {
'title': 'Remove the movie from your wanted list',
'events': {
'click': self.showConfirm.bind(self)
}
});
},
showConfirm: function(e){
var self = this;
(e).stop();
if(!self.delete_container){
self.delete_container = new Element('div.delete_container', {
'styles': {
@@ -185,27 +187,26 @@ var WantedActions = {
})
).inject(self.movie, 'top');
}
self.movie.slide('in');
},
hideConfirm: function(e){
var self = this;
(e).stop();
self.movie.slide('out');
},
del: function(e){
(e).stop();
var self = this;
var movie = $(self.movie);
self.chain(
function(){
$(movie).mask().addClass('loading');
self.callChain();
},
function(){
@@ -224,16 +225,15 @@ var WantedActions = {
});
}
);
self.callChain();
}
})
};
var SnatchedActions = {
MovieActions.Snatched = {
'IMBD': IMDBAction
,'Releases': ReleaseAction
,'Delete': WantedActions.Delete
,'Delete': MovieActions.Wanted.Delete
};
+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 {
color: #fff;
@@ -35,15 +39,24 @@ input, textarea {
font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
}
input:-moz-placeholder, textarea:-moz-placeholder {
color: rgba(255, 255, 255, 0.6);
}
::-webkit-input-placeholder, ::-webkit-textarea-placeholder {
color: rgba(255, 255, 255, 0.6);
}
a img {
border:none;
}
a {
text-decoration:none;
color: #fff;
color: #ebfcbc;
outline: 0;
cursor: pointer;
font-weight: bold;
}
a:hover { color: #f3f3f3; }
@@ -125,10 +138,18 @@ form {
}
/*** Icons ***/
.icon.delete {
background: url('../images/delete.png') no-repeat;
.icon {
display: inline-block;
background: center no-repeat;
}
.icon.delete { background-image: url('../images/icon.delete.png'); }
.icon.download { background-image: url('../images/icon.download.png'); }
.icon.edit { background-image: url('../images/icon.edit.png'); }
.icon.check { background-image: url('../images/icon.check.png'); }
.icon.folder { background-image: url('../images/icon.folder.png'); }
.icon.imdb { background-image: url('../images/icon.imdb.png'); }
.icon.refresh { background-image: url('../images/icon.refresh.png'); }
.icon.rating { background-image: url('../images/icon.rating.png'); }
/*** Navigation ***/
.header {
@@ -191,6 +212,27 @@ form {
.header .navigation li a:hover, .header .navigation li a:active {
color: #b1d8dc;
}
.header .message.update {
text-align: center;
position: relative;
top: -70px;
padding: 15px 0 20px;
background: #ff6134;
font-size: 26px;
border-radius: 0 0 5px 5px;
-moz-border-radius: 0 0 5px 5px;
-webkit-border-radius: 0 0 5px 5px;
box-shadow: 0 2px 1px rgba(0,0,0, 0.3);
-moz-box-shadow: 0 2px 1px rgba(0,0,0, 0.3);
-webkit-box-shadow: 0 2px 1px rgba(0,0,0, 0.3);
}
.header .message a {
padding: 0 10px;
}
/*** Global Styles ***/
.check {
@@ -333,3 +375,59 @@ form {
rgb(73,83,98) 100%
);
}
.mask {
background: rgba(0,0,0, 0.7);
z-index: 100;
}
.question {
display: block;
width: 600px;
padding: 20px;
background: #f5f5f5;
position:fixed;
z-index:101;
text-align: center;
background: #5c697b;
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
box-shadow: 0 0 50px rgba(0,0,0,0.55);
-moz-box-shadow: 0 0 50px rgba(0,0,0,0.55);
-webkit-box-shadow: 0 0 50px rgba(0,0,0,0.55);
}
.question h3 {
font-size: 25px;
padding: 0;
margin: 0 0 20px;
}
.question .hint {
font-size: 14px;
color: #ccc;
text-shadow: none;
}
.question .answer {
font-size: 17px;
display: inline-block;
padding: 10px;
margin: 5px 1%;
cursor: pointer;
width: auto;
}
.question .answer:hover {
background: #f1f1f1;
}
.question .answer.delete {
background-color: #a82f12;
}
.question .answer.cancel {
margin-top: 20px;
background-color: #4c5766;
}
+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 {
overflow: hidden;
.page.settings:after {
content: ".";
display: block;
clear: both;
visibility: hidden;
line-height: 0;
height: 0;
}
.page.settings .tabs {
@@ -30,6 +38,7 @@
.page.settings .tabs a {
display: block;
padding: 11px 15px;
color: #fff;
}
.page.settings .tabs .active a {
background: #4e5969;
@@ -84,6 +93,7 @@
position: relative;
margin-bottom: -25px;
border: none;
width: 20px;
}
.page.settings .ctrlHolder {
@@ -92,9 +102,13 @@
font-size: 14px;
border: 0;
}
.page.settings .ctrlHolder.save_success:not(:first-child) {
background: url('../../images/icon.check.png') no-repeat 7px center;
}
.page.settings .ctrlHolder:last-child { border: none; }
.page.settings .ctrlHolder:hover { background: rgba(255,255,255,0.05); }
.page.settings .ctrlHolder.focused { background: rgba(255,255,255,0.2); }
.page.settings .ctrlHolder:hover { background-color: rgba(255,255,255,0.05); }
.page.settings .ctrlHolder.focused { background-color: rgba(255,255,255,0.2); }
.page.settings .ctrlHolder.focused:first-child, .page.settings .ctrlHolder:first-child{ background-color: transparent; }
.page.settings .ctrlHolder .formHint {
float: right;
@@ -135,6 +149,8 @@
margin: 0;
padding: 6px 0 0;
}
.page.settings .xsmall { width: 20px !important; text-align: center; }
.page.settings input[type=text], .page.settings input[type=password] {
padding: 5px 3px;
@@ -165,39 +181,154 @@
.page.settings .directory {
display: inline-block;
padding: 0 4px;
padding: 0 4% 0 4px;
font-size: 13px;
width: 29.7%;
}
.page.settings .directory_list {
position: absolute;
width: 300px;
margin: 0 0 0 16.5%;
background: #282d34;
border: 1px solid #1f242b;
position: absolute;
box-shadow: 0 1px 2px rgba(0,0,0,0.4);
-moz-box-shadow: 0 1px 2px rgba(0,0,0,0.4);
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.4);
border-radius:3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
width: 26.3%;
background-image: url('../../images/icon.folder.gif');
background-repeat: no-repeat;
background-position: 97% center;
overflow: hidden;
vertical-align: top;
}
.page.settings .directory > span {
height: 25px;
display: inline-block;
float: right;
text-align: right;
white-space: nowrap;
cursor: pointer;
}
.page.settings .directory_list {
z-index: 2;
position: absolute;
width: 360px;
margin: -2px 0 20px 60px;
background: #5c697b;
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
box-shadow: 0 0 50px rgba(0,0,0,0.55);
-moz-box-shadow: 0 0 50px rgba(0,0,0,0.55);
-webkit-box-shadow: 0 0 50px rgba(0,0,0,0.55);
}
.page.settings .directory_list .pointer {
border-right: 10px solid transparent;
border-left: 10px solid transparent;
border-bottom: 10px solid #5c697b;
display: block;
position: absolute;
width: 0px;
margin: -9px 0 0 48.5%;
}
.page.settings .directory_list ul {
width: 100%;
max-height: 200px;
width: 92%;
height: 300px;
overflow: auto;
margin: 0 4%;
}
.page.settings .directory_list li {
padding: 2px 10px;
}
.page.settings .directory_list li {
padding: 0 10px;
cursor: pointer;
margin: 0;
border-top: 1px solid rgba(255,255,255,0.1);
background: url('../../images/right.arrow.png') no-repeat 98% center;
}
.page.settings .directory_list li:last-child {
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.page.settings .directory_list li:hover {
background-color: #515c68;
}
.page.settings .directory_list .actions {
clear: both;
padding: 10px;
background: #414953;
padding: 4% 4% 2%;
min-height: 25px;
}
.page.settings .directory_list .actions:first-child { border-bottom: 1px solid #1f242b; }
.page.settings .directory_list .actions:last-child { border-top: 1px solid #1f242b; }
.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_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/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/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
# Docs: http://packages.python.org/axel
import sys, threading, Queue
from couchpotato.core.helpers.variable import natcmp
import Queue
import sys
import threading
class Event(object):
"""
@@ -100,7 +103,7 @@ class Event(object):
self.handlers = {}
self.memoize = {}
def handle(self, handler):
def handle(self, handler, priority = 0):
""" Registers a handler. The handler can be transmitted together
with two arguments as a list or dictionary. The arguments are:
@@ -118,7 +121,7 @@ class Event(object):
event += {'handler':handler, 'memoize':True, 'timeout':1.5}
"""
handler_, memoize, timeout = self._extract(handler)
self.handlers[hash(handler_)] = (handler_, memoize, timeout)
self.handlers['%s.%s' % (priority, hash(handler_))] = (handler_, memoize, timeout)
return self
def unhandle(self, handler):
@@ -144,7 +147,7 @@ class Event(object):
t.daemon = True
t.start()
for handler in self.handlers:
for handler in sorted(self.handlers.iterkeys(), cmp = natcmp):
self.queue.put(handler)
if self.asynchronous:
+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()
lookup_results = SearchResults()
for cur_lookup in etree.find("movies").findall("movie"):
cur_movie = self._parseSearchResults(cur_lookup)
cur_movie = self._parseMovie(cur_lookup)
lookup_results.append(cur_movie)
return lookup_results
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)