Browse Source

Switched event system (again)

pull/1/merge
Ruud 14 years ago
parent
commit
f9523444f4
  1. 45
      couchpotato/api/__init__.py
  2. 13
      couchpotato/cli.py
  3. 3
      couchpotato/core/__init__.py
  4. 2
      couchpotato/core/auth.py
  5. 37
      couchpotato/core/event.py
  6. 37
      couchpotato/core/helpers.py
  7. 86
      couchpotato/core/loader.py
  8. 4
      couchpotato/core/plugins/base.py
  9. 6
      couchpotato/core/plugins/file_browser/__init__.py
  10. 18
      couchpotato/core/plugins/file_browser/main.py
  11. 6
      couchpotato/core/plugins/movie_add/__init__.py
  12. 30
      couchpotato/core/plugins/movie_add/main.py
  13. 3
      couchpotato/core/plugins/renamer/__init__.py
  14. 57
      couchpotato/core/providers/base.py
  15. 4
      couchpotato/core/providers/imdb/__init__.py
  16. 83
      couchpotato/core/providers/imdb/main.py
  17. 5
      couchpotato/core/providers/tmdb/__init__.py
  18. 92
      couchpotato/core/providers/tmdb/main.py
  19. 56
      couchpotato/core/settings/__init__.py
  20. 79
      couchpotato/core/settings/loader.py
  21. 12
      couchpotato/environment.py
  22. 10
      couchpotato/static/scripts/block.js
  23. 32
      couchpotato/static/scripts/block/search.js
  24. 2
      couchpotato/static/scripts/couchpotato.js

45
couchpotato/api/__init__.py

@ -1,42 +1,20 @@
from couchpotato.api.file_browser import FileBrowser
from couchpotato.core.settings.loader import settings_loader
from couchpotato.core.settings.model import Resource
from couchpotato.environment import Env
from flask import Module
from flask.helpers import jsonify
import flask
api = Module(__name__)
def addApiView(route, func):
api.add_url_rule(route + '/', route, func)
@api.route('')
def index():
return jsonify({'test': 'bla'})
@api.route('settings/')
def settings_view():
return jsonify({
'sections': settings_loader.sections,
'values': Env.get('settings').getValues()
})
@api.route('setting.save/')
def setting_save_view():
a = flask.request.args
section = a.get('section')
option = a.get('name')
value = a.get('value')
Env.setting(option, section, value).save()
return jsonify({
'success': True,
});
@api.route('movie/')
def movie():
return jsonify({
'success': True,
'movies': [
@ -50,18 +28,3 @@ def movie():
}
]
})
@api.route('directory.list/')
def director_list():
a = flask.request.args
try:
fb = FileBrowser(a.get('path', '/'))
dirs = fb.getDirectories()
except:
dirs = []
return jsonify({
'empty': len(dirs) == 0,
'dirs': dirs,
})

13
couchpotato/cli.py

@ -2,7 +2,6 @@ from argparse import ArgumentParser
from couchpotato import get_engine, web
from couchpotato.api import api
from couchpotato.core.settings.model import *
from couchpotato.environment import Env
from libs.daemon import createDaemon
from logging import handlers
import logging
@ -44,10 +43,12 @@ def cmd_couchpotato(base_path, args):
# Register environment settings
from couchpotato.environment import Env
Env.get('settings').setFile(os.path.join(options.data_dir, 'settings.conf'))
Env.set('app_dir', base_path)
Env.set('data_dir', options.data_dir)
Env.set('db_path', 'sqlite:///' + os.path.join(options.data_dir, 'couchpotato.db'))
Env.set('cache_dir', os.path.join(options.data_dir, 'cache'))
Env.set('quiet', options.quiet)
Env.set('daemonize', options.daemonize)
Env.set('args', args)
@ -84,11 +85,11 @@ def cmd_couchpotato(base_path, args):
log.debug('Started with options %s' % options)
# Load configs
from couchpotato.core.settings.loader import settings_loader
settings_loader.load(root = base_path)
settings_loader.addConfig('couchpotato', 'core')
settings_loader.run()
# Load configs & plugins
loader = Env.get('loader')
loader.preload(root = base_path)
loader.addModule('core', 'couchpotato.core', 'core')
loader.run()
# Load migrations

3
couchpotato/core/__init__.py

@ -1,5 +1,8 @@
from uuid import uuid4
def start():
pass
config = [{
'name': 'global',
'tab': 'general',

2
couchpotato/core/auth.py

@ -15,7 +15,7 @@ def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
auth = getattr(request, 'authorization')
if Env.setting('username') and (not auth or not check_auth(auth.username, auth.password)):
return authenticate()

37
couchpotato/core/event.py

@ -0,0 +1,37 @@
from couchpotato.core.logger import CPLog
from axl.axel import Event
log = CPLog(__name__)
events = {}
def addEvent(name, handler):
if events.get(name):
e = events[name]
else:
e = events[name] = Event(threads = 20, exc_info = True, traceback = True)
e += handler
def removeEvent(name, handler):
e = events[name]
e -= handler
def fireEvent(name, *args, **kwargs):
try:
e = events[name]
e.asynchronous = False
return e(*args, **kwargs)
except Exception, e:
log.debug(e)
def fireEventAsync(name, *args, **kwargs):
try:
e = events[name]
e.asynchronous = True
return e(*args, **kwargs)
except Exception, e:
log.debug(e)
def getEvent(name):
return events[name]

37
couchpotato/core/helpers.py

@ -0,0 +1,37 @@
def latinToAscii(unicrap):
xlate = {0xc0:'A', 0xc1:'A', 0xc2:'A', 0xc3:'A', 0xc4:'A', 0xc5:'A',
0xc6:'Ae', 0xc7:'C',
0xc8:'E', 0xc9:'E', 0xca:'E', 0xcb:'E', 0x86:'e',
0xcc:'I', 0xcd:'I', 0xce:'I', 0xcf:'I',
0xd0:'Th', 0xd1:'N',
0xd2:'O', 0xd3:'O', 0xd4:'O', 0xd5:'O', 0xd6:'O', 0xd8:'O',
0xd9:'U', 0xda:'U', 0xdb:'U', 0xdc:'U',
0xdd:'Y', 0xde:'th', 0xdf:'ss',
0xe0:'a', 0xe1:'a', 0xe2:'a', 0xe3:'a', 0xe4:'a', 0xe5:'a',
0xe6:'ae', 0xe7:'c',
0xe8:'e', 0xe9:'e', 0xea:'e', 0xeb:'e',
0xec:'i', 0xed:'i', 0xee:'i', 0xef:'i',
0xf0:'th', 0xf1:'n',
0xf2:'o', 0xf3:'o', 0xf4:'o', 0xf5:'o', 0xf6:'o', 0xf8:'o',
0xf9:'u', 0xfa:'u', 0xfb:'u', 0xfc:'u',
0xfd:'y', 0xfe:'th', 0xff:'y',
0xa1:'!', 0xa2:'{cent}', 0xa3:'{pound}', 0xa4:'{currency}',
0xa5:'{yen}', 0xa6:'|', 0xa7:'{section}', 0xa8:'{umlaut}',
0xa9:'{C}', 0xaa:'{^a}', 0xab:'<<', 0xac:'{not}',
0xad:'-', 0xae:'{R}', 0xaf:'_', 0xb0:'{degrees}',
0xb1:'{+/-}', 0xb2:'{^2}', 0xb3:'{^3}', 0xb4:"'",
0xb5:'{micro}', 0xb6:'{paragraph}', 0xb7:'*', 0xb8:'{cedilla}',
0xb9:'{^1}', 0xba:'{^o}', 0xbb:'>>',
0xbc:'{1/4}', 0xbd:'{1/2}', 0xbe:'{3/4}', 0xbf:'?',
0xd7:'*', 0xf7:'/'
}
r = ''
for i in unicrap:
if xlate.has_key(ord(i)):
r += xlate[ord(i)]
elif ord(i) >= 0x80:
pass
else:
r += str(i)
return r

86
couchpotato/core/loader.py

@ -0,0 +1,86 @@
from couchpotato.core.event import fireEvent, fireEventAsync
from couchpotato.core.logger import CPLog
import glob
import os
log = CPLog(__name__)
class Loader:
plugins = {}
providers = {}
modules = {}
def preload(self, root = ''):
self.paths = {
'plugin' : ('couchpotato.core.plugins', os.path.join(root, 'couchpotato', 'core', 'plugins')),
'provider' : ('couchpotato.core.providers', os.path.join(root, 'couchpotato', 'core', 'providers')),
}
for type, tuple in self.paths.iteritems():
self.addFromDir(type, tuple[0], tuple[1])
def run(self):
did_save = 0
for module_name, plugin in sorted(self.modules.iteritems()):
# Load module
m = getattr(self.loadModule(module_name), plugin.get('name'))
log.info("Loading '%s'" % module_name)
# Save default settings for plugin/provider
did_save += self.loadSettings(m, module_name, save = False)
self.loadPlugins(m, plugin.get('name'))
if did_save:
fireEvent('settings.save')
def addFromDir(self, type, module, dir):
for file in glob.glob(os.path.join(dir, '*')):
name = os.path.basename(file)
if os.path.isdir(os.path.join(dir, name)):
module_name = '%s.%s' % (module, name)
self.addModule(type, module_name, name)
def loadSettings(self, module, name, save = True):
try:
for section in module.config:
fireEventAsync('settings.options', section['name'], section)
options = {}
for key, option in section['options'].iteritems():
options[key] = option['default']
fireEventAsync('settings.register', section_name = section['name'], options = options, save = save)
return True
except Exception, e:
log.debug("Failed loading settings for '%s': %s" % (name, e))
return False
def loadPlugins(self, module, name):
try:
module.start()
return True
except Exception, e:
log.debug("Failed loading plugin '%s': %s" % (name, e))
return False
def addModule(self, type, module, name):
self.modules[module] = {
'type': type,
'name': name,
}
def loadModule(self, name):
try:
m = __import__(name)
splitted = name.split('.')
for sub in splitted[1:-1]:
m = getattr(m, sub)
return m
except:
raise

4
couchpotato/core/plugins/base.py

@ -0,0 +1,4 @@
class Plugin():
def __init__(self):
pass

6
couchpotato/core/plugins/file_browser/__init__.py

@ -0,0 +1,6 @@
from couchpotato.core.plugins.file_browser.main import FileBrowser
def start():
return FileBrowser()
config = []

18
couchpotato/api/file_browser.py → couchpotato/core/plugins/file_browser/main.py

@ -1,3 +1,6 @@
from couchpotato.api import addApiView
from couchpotato.environment import Env
from flask.helpers import jsonify
import os
import string
@ -9,6 +12,8 @@ class FileBrowser():
def __init__(self, path = '/'):
self.path = path
addApiView('directory.list', self.view)
def getDirectories(self):
# Return driveletters or root if path is empty
@ -36,3 +41,16 @@ class FileBrowser():
driveletters.append(drive + ":")
return driveletters
def view(self):
try:
fb = FileBrowser(Env.getParam('path', '/'))
dirs = fb.getDirectories()
except:
dirs = []
return jsonify({
'empty': len(dirs) == 0,
'dirs': dirs,
})

6
couchpotato/core/plugins/movie_add/__init__.py

@ -0,0 +1,6 @@
from couchpotato.core.plugins.movie_add.main import MovieAdd
def start():
return MovieAdd()
config = []

30
couchpotato/core/plugins/movie_add/main.py

@ -0,0 +1,30 @@
from couchpotato.api import addApiView
from couchpotato.core.event import getEvent, fireEvent
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from flask.helpers import jsonify
class MovieAdd(Plugin):
def __init__(self):
addApiView('movie.add.search', self.search)
def search(self):
a = Env.getParams()
print fireEvent('provider.movie.search', q = a.get('q'))
movies = [
{'id': 1, 'name': 'test'}
]
return jsonify({
'success': True,
'empty': len(movies) == 0,
'movies': movies,
})
def select(self):
pass

3
couchpotato/core/plugins/renamer/__init__.py

@ -1,3 +1,6 @@
def start():
pass
config = [{
'name': 'Renamer',
'tab': 'renaming',

57
couchpotato/core/providers/base.py

@ -0,0 +1,57 @@
from couchpotato.core.helpers import latinToAscii
from couchpotato.core.logger import CPLog
from string import ascii_letters, digits
import re
import unicodedata
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
class Provider():
type = None # movie, nzb, torrent, subtitle, trailer
timeout = 10 # Default timeout for url requests
def __init__(self):
pass
def toSaveString(self, string):
string = latinToAscii(string)
string = ''.join((c for c in unicodedata.normalize('NFD', unicode(string)) if unicodedata.category(c) != 'Mn'))
safe_chars = ascii_letters + digits + '_ -.,\':!?'
r = ''.join([char if char in safe_chars else ' ' for char in string])
return re.sub('\s+' , ' ', r)
def toSearchString(self, string):
string = latinToAscii(string)
string = ''.join((c for c in unicodedata.normalize('NFD', unicode(string)) if unicodedata.category(c) != 'Mn'))
safe_chars = ascii_letters + digits + ' \''
r = ''.join([char if char in safe_chars else ' ' for char in string])
return re.sub('\s+' , ' ', r).replace('\'s', 's').replace('\'', ' ')
def gettextelements(self, xml, path):
''' Find elements and return tree'''
textelements = []
try:
elements = xml.findall(path)
except:
return
for element in elements:
textelements.append(element.text)
return textelements
def gettextelement(self, xml, path):
''' Find element and return text'''
try:
return xml.find(path).text
except:
return
def getItems(self, data, path = 'channel/item'):
try:
return XMLTree.parse(data).findall(path)
except Exception, e:
log.error('Error parsing RSS. %s' % e)
return []

4
couchpotato/core/providers/imdb/__init__.py

@ -0,0 +1,4 @@
def start():
pass
config = []

83
couchpotato/core/providers/imdb/main.py

@ -0,0 +1,83 @@
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
from imdb import IMDb
log = CPLog(__name__)
class IMDB(Provider):
type = 'movie'
def __init__(self):
addEvent('provider.movie.search', self.search)
self.p = IMDb('http')
def search(self):
print 'search'
def conf(self, option):
return self.config.get('IMDB', option)
def find(self, q, limit = 8, alternative = True):
''' Find movie by name '''
log.info('IMDB - Searching for movie: %s' % q)
r = self.p.search_movie(q)
return self.toResults(r, limit)
def toResults(self, r, limit = 8, one = False):
results = []
if one:
new = self.feedItem()
new.imdb = 'tt' + r.movieID
new.name = self.toSaveString(r['title'])
try:
new.year = r['year']
except:
new.year = ''
return new
else :
nr = 0
for movie in r:
results.append(self.toResults(movie, one = True))
nr += 1
if nr == limit:
break
return results
def findById(self, id):
''' Find movie by TheMovieDB ID '''
return []
def findByImdbId(self, id, details = False):
''' Find movie by IMDB ID '''
log.info('IMDB - Searching for movie: %s' % str(id))
r = self.p.get_movie(id.replace('tt', ''))
if not details:
return self.toResults(r, one = True)
else:
self.p.update(r)
self.p.update(r, info = 'release dates')
self.p.update(r, info = 'taglines')
return r
def get_IMDb_instance(self):
return IMDb('http')
def findReleaseDate(self, movie):
pass

5
couchpotato/core/providers/tmdb/__init__.py

@ -1,3 +1,8 @@
from couchpotato.core.providers.tmdb.main import TMDB
def start():
return TMDB()
config = [{
'name': 'TheMovieDB',
'tab': 'providers',

92
couchpotato/core/providers/tmdb/main.py

@ -0,0 +1,92 @@
from __future__ import with_statement
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env
from urllib import quote_plus
import urllib2
log = CPLog(__name__)
class TMDB(Provider):
"""Api for theMovieDb"""
type = 'movie'
apiUrl = 'http://api.themoviedb.org/2.1'
imageUrl = 'http://hwcdn.themoviedb.org'
def __init__(self):
addEvent('provider.movie.search', self.search)
def conf(self, attr):
return Env.setting(attr, 'TheMovieDB')
def search(self, q, limit = 8, alternative = True):
''' Find movie by name '''
if self.isDisabled():
return False
log.debug('TheMovieDB - Searching for movie: %s' % q)
url = "%s/%s/%s/xml/%s/%s" % (self.apiUrl, 'Movie.search', 'en', self.conf('api_key'), quote_plus(self.toSearchString(q)))
log.info('Searching: %s' % url)
data = urllib2.urlopen(url)
return self.parseXML(data, limit, alternative = alternative)
def parseXML(self, data, limit, alternative = True):
if data:
log.debug('TheMovieDB - Parsing RSS')
try:
xml = self.getItems(data, 'movies/movie')
results = []
nr = 0
for movie in xml:
id = int(self.gettextelement(movie, "id"))
name = self.gettextelement(movie, "name")
imdb = self.gettextelement(movie, "imdb_id")
year = str(self.gettextelement(movie, "released"))[:4]
# 1900 is the same as None
if year == '1900':
year = 'None'
results.append({
'id': id,
'name': self.toSaveString(name),
'imdb': imdb,
'year': year
})
alternativeName = self.gettextelement(movie, "alternative_name")
if alternativeName and alternative:
if alternativeName.lower() != name.lower() and alternativeName.lower() != 'none' and alternativeName != None:
results.append({
'id': id,
'name': self.toSaveString(alternativeName),
'imdb': imdb,
'year': year
})
nr += 1
if nr == limit:
break
#log.info('TheMovieDB - Found: %s' % results)
return results
except SyntaxError:
log.error('TheMovieDB - Failed to parse XML response from TheMovieDb')
return False
def isDisabled(self):
if self.conf('api_key') == '':
log.error('TheMovieDB - No API key provided for TheMovieDB')
True
else:
False

56
couchpotato/core/settings/__init__.py

@ -1,5 +1,7 @@
from __future__ import with_statement
from blinker import signal, Signal
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from flask.helpers import jsonify
import ConfigParser
import os.path
import time
@ -7,10 +9,13 @@ import time
class Settings():
on_save = Signal()
on_register = Signal()
bool = {'true':True, 'false':False}
options = {}
def __init__(self):
addApiView('settings', self.view)
addApiView('settings.save', self.saveView)
def setFile(self, file):
self.file = file
@ -21,7 +26,7 @@ class Settings():
from couchpotato.core.logger import CPLog
self.log = CPLog(__name__)
self.connectSignals()
self.connectEvents()
def parser(self):
return self.p
@ -29,18 +34,17 @@ class Settings():
def sections(self):
return self.p.sections()
def connectSignals(self):
signal('settings.register').connect(self.registerDefaults)
signal('settings.save').connect(self.save)
def connectEvents(self):
addEvent('settings.options', self.addOptions)
addEvent('settings.register', self.registerDefaults)
addEvent('settings.save', self.save)
def registerDefaults(self, section_name, options = {}, save = True):
self.addSection(section_name)
for option, value in options.iteritems():
self.setDefault(section_name, option, value)
self.log.debug('Defaults for "%s": %s' % (section_name, options))
self.on_register.send(self)
if save:
self.save(self)
@ -73,12 +77,11 @@ class Settings():
values[section][option_name] = option_value
return values
def save(self, caller = ''):
def save(self):
with open(self.file, 'wb') as configfile:
self.p.write(configfile)
self.log.debug('Saved settings')
self.on_save.send(self)
def addSection(self, section):
if not self.p.has_section(section):
@ -94,3 +97,32 @@ class Settings():
return True
except ValueError:
return False
def addOptions(self, section_name, options):
self.options[section_name] = options
def getOptions(self):
return self.options
def view(self):
return jsonify({
'options': self.getOptions(),
'values': self.getValues()
})
def saveView(self):
a = Env.getParams()
section = a.get('section')
option = a.get('name')
value = a.get('value')
self.set(option, section, value)
self.save()
return jsonify({
'success': True,
})

79
couchpotato/core/settings/loader.py

@ -1,79 +0,0 @@
from blinker import signal
from couchpotato.core.logger import CPLog
import glob
import os
log = CPLog(__name__)
class SettingsLoader:
configs = {}
sections = {}
def __init__(self):
self.settings_register = signal('settings.register')
self.settings_save = signal('settings.save')
def load(self, root = ''):
self.paths = {
'plugins' : ('couchpotato.core.plugins', os.path.join(root, 'couchpotato', 'core', 'plugins')),
'providers' : ('couchpotato.core.providers', os.path.join(root, 'couchpotato', 'core', 'providers')),
}
for type, tuple in self.paths.iteritems():
self.addFromDir(tuple[0], tuple[1])
def run(self):
did_save = 0
for module, plugin_name in self.configs.iteritems():
did_save += self.loadConfig(module, plugin_name, save = False)
if did_save:
self.settings_save.send()
def addFromDir(self, module, dir):
for file in glob.glob(os.path.join(dir, '*')):
name = os.path.basename(file)
if os.path.isdir(os.path.join(dir, name)):
self.addConfig(module, name)
def loadConfig(self, module, name, save = True):
module_name = '%s.%s' % (module, name)
try:
m = getattr(self.loadModule(module_name), name)
for section in m.config:
self.addSection(section['name'], section)
options = {}
for key, option in section['options'].iteritems():
options[key] = option['default']
self.settings_register.send(section['name'], options = options, save = save)
return True
except Exception, e:
log.error("Failed loading config for %s: %s" % (name, e))
return False
def addConfig(self, module, name):
self.configs[module] = name
def loadModule(self, name):
try:
m = __import__(name)
splitted = name.split('.')
for sub in splitted[1:-1]:
m = getattr(m, sub)
return m
except:
raise
def addSection(self, section_name, options):
self.sections[section_name] = options
def getSections(self):
return self.sections
settings_loader = SettingsLoader()

12
couchpotato/environment.py

@ -1,10 +1,13 @@
from couchpotato.core.loader import Loader
from couchpotato.core.settings import Settings
import flask
class Env:
''' Environment variables '''
_debug = False
_settings = Settings()
_loader = Loader()
_options = None
_args = None
_quiet = False
@ -13,6 +16,7 @@ class Env:
''' Data paths and directories '''
_app_dir = ""
_data_dir = ""
_cache_dir = ""
_db_path = ""
@staticmethod
@ -38,3 +42,11 @@ class Env:
s = Env.get('settings')
s.set(section, attr, value)
return s
@staticmethod
def getParams():
return getattr(flask.request, 'args')
@staticmethod
def getParam(attr, default = None):
return getattr(flask.request, 'args').get(attr, default)

10
couchpotato/static/scripts/block.js

@ -8,7 +8,7 @@ var BlockBase = new Class({
var self = this;
self.setOptions(options);
self.parent = parent;
self.page = parent;
self.create();
},
@ -17,6 +17,14 @@ var BlockBase = new Class({
this.el = new Element('div.block');
},
api: function(){
return this.getParent().getApi()
},
getParent: function(){
return this.page
},
toElement: function(){
return this.el
}

32
couchpotato/static/scripts/block/search.js

@ -5,7 +5,37 @@ Block.Search = new Class({
create: function(){
var self = this;
self.el = new Element('div.search_form');
self.el = new Element('div.search_form').adopt(
self.input = new Element('input', {
'events': {
'keyup': self.autocomplete.bind(self)
}
})
);
},
autocomplete: function(){
var self = this;
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
self.autocomplete_timer = self.list.delay(300, self)
},
list: function(){
var self = this;
if(self.api_request) self.api_request.cancel();
self.api_request = self.api().request('movie.add.search', {
'data': {
'q': self.input.get('value')
},
'onComplete': self.fill.bind(self)
})
},
fill: function(){
}
});

2
couchpotato/static/scripts/couchpotato.js

@ -109,7 +109,7 @@ var Api = new Class({
request: function(type, options){
var self = this;
new Request.JSON(Object.merge({
return new Request.JSON(Object.merge({
'method': 'get',
'url': self.createUrl(type),
}, options)).send()

Loading…
Cancel
Save