Merge branch 'refs/heads/develop'

This commit is contained in:
Ruud
2012-05-02 21:40:01 +02:00
29 changed files with 214 additions and 78 deletions

View File

@@ -15,6 +15,7 @@ from sqlalchemy.orm import scoped_session
from sqlalchemy.orm.session import sessionmaker
from werkzeug.utils import redirect
import os
import time
log = CPLog(__name__)
@@ -73,5 +74,10 @@ def getApiKey():
def page_not_found(error):
index_url = url_for('web.index')
url = getattr(request, 'path')[len(index_url):]
return redirect(index_url + '#' + url)
if url[:3] != 'api':
return redirect(index_url + '#' + url)
else:
time.sleep(0.1)
return 'Wrong API key used', 404

View File

@@ -1,6 +1,5 @@
from flask.blueprints import Blueprint
from flask.helpers import url_for
from flask.templating import render_template
from werkzeug.utils import redirect
api = Blueprint('api', __name__)

View File

@@ -165,7 +165,7 @@ class Core(Plugin):
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else '')
def createApiUrl(self):
return '%s/%s' % (self.createBaseUrl(), Env.setting('api_key'))
return '%s/api/%s' % (self.createBaseUrl(), Env.setting('api_key'))
def version(self):
ver = fireEvent('updater.info', single = True)

View File

@@ -59,7 +59,7 @@ def getParam(attr, default = None):
try:
return toUnicode(unquote_plus(getattr(flask.request, 'args').get(attr, default))).encode('utf-8')
except:
return None
return default
def padded_jsonify(callback, *args, **kwargs):
content = str(callback) + '(' + json.dumps(dict(*args, **kwargs)) + ')'

View File

@@ -58,6 +58,7 @@ class Loader(object):
pass
# todo:: this needs to be more descriptive.
log.error('Import error, remove the empty folder: %s' % plugin.get('module'))
log.debug('Can\'t import %s: %s' % (module_name, traceback.format_exc()))
except:
log.error('Can\'t import %s: %s' % (module_name, traceback.format_exc()))

View File

@@ -30,7 +30,7 @@ class CoreNotifier(Notification):
addApiView('notification.markread', self.markAsRead, docs = {
'desc': 'Mark notifications as read',
'params': {
'id': {'desc': 'Notification id you want to mark as read.', 'type': 'int (comma separated)'},
'ids': {'desc': 'Notification id you want to mark as read.', 'type': 'int (comma separated)'},
},
})

View File

@@ -22,6 +22,21 @@ config = [{
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
{
'name': 'hostname',
'description': 'Notify growl over network. Needs restart.',
'advanced': True,
},
{
'name': 'port',
'type': 'int',
'advanced': True,
},
{
'name': 'password',
'type': 'password',
'advanced': True,
},
],
}
],

View File

@@ -23,11 +23,19 @@ class Growl(Notification):
def register(self):
if self.registered: return
try:
hostname = self.conf('hostname')
password = self.conf('password')
port = self.conf('port')
self.growl = notifier.GrowlNotifier(
applicationName = 'CouchPotato',
notifications = ["Updates"],
defaultNotifications = ["Updates"],
applicationIcon = '%s/static/images/couch.png' % fireEvent('app.api_url', single = True),
hostname = hostname if hostname else 'localhost',
password = password if password else None,
port = port if port else 23053
)
self.growl.register()
self.registered = True

View File

@@ -55,7 +55,7 @@ class Plugin(object):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__)
class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
path = '%s/static/%s/' % (Env.setting('api_key'), class_name)
path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name)
addView(path + '<path:filename>', self.showStatic, static = True)
if add_to_head:

View File

@@ -21,13 +21,24 @@ class Logging(Plugin):
'success': True,
'log': string, //Log file
'total': int, //Total log files available
}"""}
})
addApiView('logging.partial', self.partial, docs = {
'desc': 'Get a partial log',
'params': {
'type': {'desc': 'Type of log', 'type': 'string: all(default), error, info, debug'},
'lines': {'desc': 'Number of lines. Last to first. Default 30'},
},
'return': {'type': 'object', 'example': """{
'success': True,
'log': string, //Log file
}"""}
})
addApiView('logging.clear', self.clear, docs = {
'desc': 'Remove all the log files'
})
addApiView('logging.log', self.log, docs = {
'desc': 'Get the full log file by number',
'desc': 'Log errors',
'params': {
'type': {'desc': 'Type of logging, default "error"'},
'**kwargs': {'type':'object', 'desc': 'All other params will be printed in the log string.'},
@@ -64,6 +75,46 @@ class Logging(Plugin):
'total': total,
})
def partial(self):
log_type = getParam('type', 'all')
total_lines = getParam('lines', 30)
log_lines = []
for x in range(0, 50):
path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '')
# Check see if the log exists
if not os.path.isfile(path):
break
reversed_lines = []
f = open(path, 'r')
reversed_lines = f.read().split('[0m\n')
reversed_lines.reverse()
brk = False
for line in reversed_lines:
#print '%s ' % log_type in line.lower()
if log_type == 'all' or '%s ' % log_type.upper() in line:
log_lines.append(line)
if len(log_lines) >= total_lines:
brk = True
break
if brk:
break
log_lines.reverse()
return jsonified({
'success': True,
'log': '[0m\n'.join(log_lines),
})
def clear(self):
for x in range(0, 50):

View File

@@ -1,8 +1,7 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
simplifyString
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.request import getParams, jsonified, getParam
from couchpotato.core.helpers.variable import getImdb
from couchpotato.core.logger import CPLog
@@ -79,6 +78,7 @@ class MoviePlugin(Plugin):
'desc': 'Delete a movie from the wanted list',
'params': {
'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'},
'delete_from': {'desc': 'Delete movie from this page', 'type': 'string: all (default), wanted, manage'},
}
})
@@ -358,20 +358,45 @@ class MoviePlugin(Plugin):
ids = [x.strip() for x in params.get('id').split(',')]
for movie_id in ids:
self.delete(movie_id)
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
return jsonified({
'success': True,
})
def delete(self, movie_id):
def delete(self, movie_id, delete_from = None):
db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first()
if movie:
db.delete(movie)
db.commit()
if delete_from == 'all':
db.delete(movie)
db.commit()
else:
done_status = fireEvent('status.get', 'done', single = True)
total_releases = len(movie.releases)
total_deleted = 0
new_movie_status = None
for release in movie.releases:
if delete_from == 'wanted' and release.status_id != done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'done'
elif delete_from == 'manage' and release.status_id == done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'active'
db.commit()
if total_releases == total_deleted:
db.delete(movie)
db.commit()
elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True)
movie.status_id = new_status.get('id')
db.commit()
return True

View File

@@ -253,7 +253,8 @@ var MovieList = new Class({
(e).preventDefault();
Api.request('movie.delete', {
'data': {
'id': ids.join(',')
'id': ids.join(','),
'delete_from': self.options.identifier
},
'onSuccess': function(){
qObj.close();
@@ -381,6 +382,8 @@ var MovieList = new Class({
update: function(){
var self = this;
self.reset();
self.movie_list.empty();
self.getMovies();
},

View File

@@ -4,11 +4,12 @@ var Movie = new Class({
action: {},
initialize: function(self, options, data){
initialize: function(list, options, data){
var self = this;
self.data = data;
self.view = options.view || 'thumbs';
self.list = list;
self.profile = Quality.getProfile(data.profile_id) || {};
self.parent(self, options);
@@ -35,10 +36,10 @@ var Movie = new Class({
}).adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle()
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'Unknown'
'text': self.data.library.year || 'n/a'
}),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
@@ -294,7 +295,7 @@ var ReleaseAction = new Class({
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
new Element('span.quality', {'text': quality.get('label')}),
new Element('span.size', {'text': (self.get(release, 'size') || 'unknown')}),
new Element('span.size', {'text': (self.get(release, 'size'))}),
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')}),
@@ -332,7 +333,7 @@ var ReleaseAction = new Class({
return (release.info.filter(function(info){
return type == info.identifier
}).pick() || {}).value
}).pick() || {}).value || 'n/a'
},
download: function(release){

View File

@@ -15,6 +15,8 @@ Block.Search = new Class({
'keyup': self.keyup.bind(self),
'focus': function(){
self.el.addClass('focused')
if(this.get('value'))
self.hideResults(false)
},
'blur': function(){
self.el.removeClass('focused')
@@ -55,6 +57,7 @@ Block.Search = new Class({
var self = this;
(e).preventDefault();
self.last_q = '';
self.input.set('value', '');
self.input.focus()
@@ -319,6 +322,13 @@ Block.Search.Item = new Class({
var self = this;
if(!self.options.hasClass('set')){
if(self.info.in_library){
var in_library = [];
self.info.in_library.releases.each(function(release){
in_library.include(release.quality.label)
});
}
self.options.adopt(
new Element('div').adopt(
@@ -326,9 +336,9 @@ Block.Search.Item = new Class({
'src': self.info.images.poster[0]
}) : null,
self.info.in_wanted ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.label
}) : (self.info.in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + self.info.in_library.label
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
}) : (in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + in_library.join(', ')
}) : null),
self.title_select = new Element('select', {
'name': 'title'

View File

@@ -1,7 +1,7 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode, toSafeString
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
from couchpotato.core.logger import CPLog
@@ -184,16 +184,17 @@ class QualityPlugin(Plugin):
# 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 self.setCache(hash, 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 self.setCache(hash, quality)
# 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 and filesize %s in %s' % (quality['identifier'], quality.get('ext'), words))
return self.setCache(hash, quality)
# Try again with loose testing
if not loose:
@@ -202,4 +203,4 @@ class QualityPlugin(Plugin):
return self.setCache(hash, quality)
log.debug('Could not identify quality for: %s' % files)
return self.setCache(hash, self.single('dvdrip'))
return None

View File

@@ -180,15 +180,18 @@ class Scanner(Plugin):
# Normal identifier
identifier = self.createStringIdentifier(file_path, folder, exclude_filename = is_dvd_file)
identifiers = [identifier]
# Identifier with quality
quality = fireEvent('quality.guess', [file_path], single = True) if not is_dvd_file else {'identifier':'dvdr'}
identifier_with_quality = '%s %s' % (identifier, quality.get('identifier', ''))
if quality:
identifier_with_quality = '%s %s' % (identifier, quality.get('identifier', ''))
identifiers = [identifier_with_quality, identifier]
if not movie_files.get(identifier):
movie_files[identifier] = {
'unsorted_files': [],
'identifiers': [identifier_with_quality, identifier],
'identifiers': identifiers,
'is_dvd': is_dvd_file,
}
@@ -495,7 +498,7 @@ class Scanner(Plugin):
'identifier': imdb_id
}, update_after = False, single = True)
log.error('No imdb_id found for %s.' % group['identifiers'])
log.error('No imdb_id found for %s. Add a NFO file with IMDB id or add the year to the filename.' % group['identifiers'])
return {}
def getCPImdb(self, string):

View File

@@ -65,6 +65,8 @@ class Searcher(Plugin):
def single(self, movie):
db = get_session()
pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
available_status = fireEvent('status.get', 'available', single = True)
@@ -94,7 +96,6 @@ class Searcher(Plugin):
# Add them to this movie releases list
for nzb in sorted_results:
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first()
if not rls:
@@ -106,20 +107,23 @@ class Searcher(Plugin):
)
db.add(rls)
db.commit()
else:
[db.delete(info) for info in rls.info]
db.commit()
for info in nzb:
try:
if not isinstance(nzb[info], (str, unicode, int, long)):
continue
for info in nzb:
try:
if not isinstance(nzb[info], (str, unicode, int, long)):
continue
rls_info = ReleaseInfo(
identifier = info,
value = toUnicode(nzb[info])
)
rls.info.append(rls_info)
db.commit()
except InterfaceError:
log.debug('Couldn\'t add %s to ReleaseInfo: %s' % (info, traceback.format_exc()))
rls_info = ReleaseInfo(
identifier = info,
value = toUnicode(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:
@@ -137,6 +141,7 @@ class Searcher(Plugin):
if self.shuttingDown():
break
db.remove()
return False
def download(self, data, movie, manual = False):
@@ -356,15 +361,15 @@ class Searcher(Plugin):
else:
if wanted_quality in pre_releases:
# Prerelease 1 week before theaters
if dates.get('theater') >= now - 604800 and wanted_quality in pre_releases:
if dates.get('theater') - 604800 < now:
return True
else:
# 6 weeks after theater release
if dates.get('theater') < now - 3628800:
if dates.get('theater') + 3628800 < now:
return True
# 6 weeks before dvd release
if dates.get('dvd') > now - 3628800:
if dates.get('dvd') - 3628800 < now:
return True
# Dvd should be released

View File

@@ -28,14 +28,16 @@ class MetaDataBase(Plugin):
except:
log.error('Failed to update movie, before creating metadata: %s' % traceback.format_exc())
root = self.getRootName(release)
root_name = self.getRootName(release)
meta_name = os.path.basename(root_name)
root = os.path.dirname(root_name)
movie_info = release['library'].get('info')
for file_type in ['nfo', 'thumbnail', 'fanart']:
try:
# Get file path
name = getattr(self, 'get' + file_type.capitalize() + 'Name')(root)
name = getattr(self, 'get' + file_type.capitalize() + 'Name')(meta_name, root)
if name and self.conf('meta_' + file_type):
@@ -54,13 +56,13 @@ class MetaDataBase(Plugin):
def getRootName(self, data):
return
def getFanartName(self, root):
def getFanartName(self, name, root):
return
def getThumbnailName(self, root):
def getThumbnailName(self, name, root):
return
def getNfoName(self, root):
def getNfoName(self, name, root):
return
def getNfo(self, movie_info = {}, data = {}):

View File

@@ -14,14 +14,17 @@ class XBMC(MetaDataBase):
def getRootName(self, data = {}):
return os.path.join(data['destination_dir'], data['filename'])
def getFanartName(self, root):
return self.conf('meta_fanart_name') % root
def getFanartName(self, name, root):
return self.createMetaName(self.conf('meta_fanart_name'), name, root)
def getThumbnailName(self, root):
return self.conf('meta_thumbnail_name') % root
def getThumbnailName(self, name, root):
return self.createMetaName(self.conf('meta_thumbnail_name'), name, root)
def getNfoName(self, root):
return self.conf('meta_nfo_name') % root
def getNfoName(self, name, root):
return self.createMetaName(self.conf('meta_nfo_name'), name, root)
def createMetaName(self, basename, name, root):
return os.path.join(root, basename.replace('%s', name))
def getNfo(self, movie_info = {}, data = {}):
nfoxml = Element('movie')

View File

@@ -55,11 +55,11 @@ class MovieResultModifier(Plugin):
for movie in l.movies:
if movie.status_id == active_status['id']:
temp['in_wanted'] = movie.profile.to_dict()
temp['in_wanted'] = fireEvent('movie.get', movie.id, single = True)
for release in movie.releases:
if release.status_id == done_status['id']:
temp['in_library'] = release.quality.to_dict()
temp['in_library'] = fireEvent('movie.get', movie.id, single = True)
except:
log.error('Tried getting more info on searched movies: %s' % traceback.format_exc())

View File

@@ -1,4 +1,3 @@
from couchpotato.core.event import addEvent
from couchpotato.core.providers.base import YarrProvider
import time

View File

@@ -133,7 +133,7 @@ class Newznab(NZBProvider, RSS):
'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),
'detail_url': '%sdetails/%s' % (cleanHost(host['host']), id),
'content': self.getTextElement(nzb, "description"),
}

View File

@@ -3,8 +3,8 @@ from elixir.entity import Entity
from elixir.fields import Field
from elixir.options import options_defaults, using_options
from elixir.relationships import ManyToMany, OneToMany, ManyToOne
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \
String, TypeDecorator
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, String, \
TypeDecorator
import json
import time
@@ -41,7 +41,7 @@ class Movie(Entity):
last_edit = Field(Integer, default = lambda: int(time.time()))
library = ManyToOne('Library')
library = ManyToOne('Library', cascade = 'delete, delete-orphan', single_parent = True)
status = ManyToOne('Status')
profile = ManyToOne('Profile')
releases = OneToMany('Release', cascade = 'all, delete-orphan')

View File

@@ -172,13 +172,13 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Static path
app.static_folder = os.path.join(base_path, 'couchpotato', 'static')
web.add_url_rule('%s/static/<path:filename>' % api_key,
web.add_url_rule('api/%s/static/<path:filename>' % api_key,
endpoint = 'static',
view_func = app.send_static_file)
# Register modules
app.register_blueprint(web, url_prefix = '%s/' % url_base)
app.register_blueprint(api, url_prefix = '%s/%s/' % (url_base, api_key))
app.register_blueprint(api, url_prefix = '%s/api/%s/' % (url_base, api_key))
# Some logging and fire load event
try: log.info('Starting server on port %(port)s' % config)

View File

@@ -117,15 +117,15 @@ var CouchPotato = new Class({
openPage: function(url) {
var self = this;
var current_url = url.replace(/^\/+|\/+$/g, '');
if(current_url == self.current_url)
return;
self.route.parse();
var page_name = self.route.getPage().capitalize();
var action = self.route.getAction();
var params = self.route.getParams();
var current_url = self.route.getCurrentUrl();
if(current_url == self.current_url)
return;
if(self.current_page)
self.current_page.hide()
@@ -287,8 +287,8 @@ var Route = new Class({
var self = this;
var path = History.getPath().replace(Api.getOption('url'), '/').replace(App.getOption('base_url'), '/')
var current = path.replace(/^\/+|\/+$/g, '')
var url = current.split('/')
self.current = path.replace(/^\/+|\/+$/g, '')
var url = self.current.split('/')
self.page = (url.length > 0) ? url.shift() : self.defaults.page
self.action = (url.length > 0) ? url.shift() : self.defaults.action
@@ -324,6 +324,10 @@ var Route = new Class({
return this.params
},
getCurrentUrl: function(){
return this.current
},
get: function(param){
return this.params[param]
}

View File

@@ -1114,7 +1114,7 @@ Option.Combined = new Class({
self.values[nr][name] = value.trim();
});
self.inputs[name].getParent('.ctrlHolder').hide();
self.inputs[name].getParent('.ctrlHolder').setStyle('display', 'none');
self.inputs[name].addEvent('change', self.addEmpty.bind(self))
});

View File

@@ -206,7 +206,8 @@ window.addEvent('domready', function(){
function(){
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id')
'id': self.movie.get('id'),
'delete_from': self.movie.list.options.identifier
},
'onComplete': function(){
movie.set('tween', {

View File

@@ -21,7 +21,7 @@
<br />
<br />
Get the API key:
<pre><a href="/getkey/?p=md5(password)&amp;u=md5(username)">/getkey/?p=md5(password)&amp;u=md5(username)</a></pre>
<pre><a href="{{ url_for('web.index') }}getkey/?p=md5(password)&amp;u=md5(username)">{{ url_for('web.index') }}getkey/?p=md5(password)&amp;u=md5(username)</a></pre>
Will return {"api_key": "XXXXXXXXXX", "success": true}. When username or password is empty you don't need to md5 it.
<br />
</div>

View File

@@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import ntpath
import os.path
import zipfile
@@ -46,7 +45,7 @@ def split_path(path):
"""
result = []
while True:
head, tail = ntpath.split(path)
head, tail = os.path.split(path)
# on Unix systems, the root folder is '/'
if head == '/' and tail == '':