Merge branch 'refs/heads/develop'

This commit is contained in:
Ruud
2012-05-31 19:30:18 +02:00
38 changed files with 709 additions and 252 deletions

View File

@@ -1,10 +1,39 @@
from flask.blueprints import Blueprint
from flask.helpers import url_for
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, asynchronous
from werkzeug.utils import redirect
api = Blueprint('api', __name__)
api_docs = {}
api_docs_missing = []
api_nonblock = {}
class NonBlockHandler(RequestHandler):
stoppers = []
@asynchronous
def get(self, route):
cls = NonBlockHandler
start, stop = api_nonblock[route]
cls.stoppers.append(stop)
start(self.onNewMessage, last_id = self.get_argument("last_id", None))
def onNewMessage(self, response):
if self.request.connection.stream.closed():
return
self.finish(response)
def on_connection_close(self):
cls = NonBlockHandler
for stop in cls.stoppers:
stop(self.onNewMessage)
cls.stoppers = []
def addApiView(route, func, static = False, docs = None, **kwargs):
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
@@ -13,6 +42,14 @@ def addApiView(route, func, static = False, docs = None, **kwargs):
else:
api_docs_missing.append(route)
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
api_nonblock[route] = func_tuple
if docs:
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
else:
api_docs_missing.append(route)
""" Api view """
def index():
index_url = url_for('web.index')

View File

@@ -114,7 +114,6 @@ class Core(Plugin):
log.debug('Save to shutdown/restart')
try:
Env.get('httpserver').stop()
IOLoop.instance().stop()
except RuntimeError:
pass

View File

@@ -28,7 +28,7 @@ class Updater(Plugin):
else:
self.updater = SourceUpdater()
fireEvent('schedule.interval', 'updater.check', self.check, hours = 6)
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
addEvent('app.load', self.check)
addEvent('updater.info', self.info)
@@ -48,17 +48,20 @@ class Updater(Plugin):
'return': {'type': 'see updater.info'}
})
def autoUpdate(self):
if self.check() and self.conf('automatic') and not self.updater.update_failed:
self.updater.doUpdate()
def check(self):
if self.isDisabled():
return
if self.updater.check():
if self.conf('automatic') and not self.updater.update_failed:
if self.updater.doUpdate():
fireEventAsync('app.restart')
else:
if self.conf('notification'):
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
if self.conf('notification') and not self.conf('automatic'):
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
return True
return False
def info(self):
return self.updater.info()
@@ -67,12 +70,22 @@ class Updater(Plugin):
return jsonified(self.updater.info())
def checkView(self):
self.check()
return self.updater.getInfo()
return jsonified({
'update_available': self.check(),
'info': self.updater.info()
})
def doUpdateView(self):
self.check()
if not self.update_version:
log.error('Trying to update when no update is available.')
success = False
else:
success = self.updater.doUpdate()
return jsonified({
'success': self.updater.doUpdate()
'success': success
})
@@ -137,6 +150,7 @@ class GitUpdater(BaseUpdater):
self.repo = LocalRepository(Env.get('app_dir'), command = git_command)
def doUpdate(self):
try:
log.debug('Stashing local changes')
self.repo.saveStash()
@@ -152,6 +166,8 @@ class GitUpdater(BaseUpdater):
version_date = datetime.fromtimestamp(info['update_version']['date'])
fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info)
fireEventAsync('app.restart')
return True
except:
log.error('Failed updating via GIT: %s' % traceback.format_exc())
@@ -243,6 +259,8 @@ class SourceUpdater(BaseUpdater):
# Write update version to file
self.createFile(self.version_file, json.dumps(self.update_version))
fireEventAsync('app.restart')
return True
except:
log.error('Failed updating: %s' % traceback.format_exc())

View File

@@ -16,7 +16,15 @@ var UpdaterBase = new Class({
var self = this;
Api.request('updater.check', {
'onComplete': onComplete || Function.from()
'onComplete': function(json){
if(onComplete)
onComplete(json);
if(json.update_available)
self.doUpdate();
else
App.unBlockPage()
}
})
},
@@ -52,7 +60,7 @@ var UpdaterBase = new Class({
createMessage: function(data){
var self = this;
var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.update_version.hash;
var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.branch;
if(data.update_version.changelog)
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash
@@ -81,13 +89,19 @@ var UpdaterBase = new Class({
Api.request('updater.update', {
'onComplete': function(json){
if(json.success){
App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
App.checkAvailable.delay(500, App);
if(self.message)
self.message.destroy();
self.updating();
}
}
});
},
updating: function(){
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
App.checkAvailable.delay(500, App, [1000, function(){
window.location.reload();
}]);
if(self.message)
self.message.destroy();
}
});

View File

@@ -53,6 +53,13 @@ def fireEvent(name, *args, **kwargs):
is_after_event = True
except: pass
# onComplete event
on_complete = False
try:
on_complete = kwargs['on_complete']
del kwargs['on_complete']
except: pass
# Return single handler
single = False
try:
@@ -129,24 +136,23 @@ def fireEvent(name, *args, **kwargs):
if not is_after_event:
fireEvent('%s.after' % name, is_after_event = True)
if on_complete:
on_complete()
return results
except KeyError, e:
pass
except Exception:
log.error('%s: %s' % (name, traceback.format_exc()))
def fireEventAsync(name, *args, **kwargs):
#log.debug('Async "%s": %s, %s' % (name, args, kwargs))
def fireEventAsync(*args, **kwargs):
try:
e = events[name]
e.lock.acquire()
e.asynchronous = True
e.error_handler = errorHandler
e(*args, **kwargs)
e.lock.release()
my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
my_thread.setDaemon(True)
my_thread.start()
return True
except Exception, e:
log.error('%s: %s' % (name, e))
log.error('%s: %s' % (args[0], e))
def errorHandler(error):
etype, value, tb = error

View File

@@ -1,7 +1,7 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natcmp
from flask.globals import current_app
from flask.helpers import json
from flask.helpers import json, make_response
from libs.werkzeug.urls import url_decode
from urllib import unquote
import flask
@@ -70,9 +70,13 @@ def jsonify(mimetype, *args, **kwargs):
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
def jsonified(*args, **kwargs):
from couchpotato.environment import Env
callback = getParam('callback_func', None)
if callback:
return padded_jsonify(callback, *args, **kwargs)
content = padded_jsonify(callback, *args, **kwargs)
else:
return jsonify('text/javascript' if Env.doDebug() else 'application/json', *args, **kwargs)
content = jsonify('application/json', *args, **kwargs)
response = make_response(content)
response.cache_control.no_cache = True
return response

View File

@@ -1,6 +1,6 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt
@@ -8,14 +8,19 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif
from sqlalchemy.sql.expression import or_
import threading
import time
import uuid
log = CPLog(__name__)
class CoreNotifier(Notification):
m_lock = threading.Lock()
messages = []
listeners = []
listen_to = [
'movie.downloaded', 'movie.snatched',
'updater.available', 'updater.updated',
@@ -46,14 +51,9 @@ class CoreNotifier(Notification):
}"""}
})
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
addApiView('notification.listener', self.listener)
self.registerEvents()
def registerEvents(self):
# Library update, frontend refresh
addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data))
def markAsRead(self):
ids = [x.strip() for x in getParam('ids').split(',')]
@@ -114,25 +114,85 @@ class CoreNotifier(Notification):
ndict = n.to_dict()
ndict['type'] = 'notification'
ndict['time'] = time.time()
self.messages.append(ndict)
self.frontend(type = listener, data = data)
#db.close()
return True
def frontend(self, type = 'notification', data = {}):
self.messages.append({
self.m_lock.acquire()
message = {
'message_id': str(uuid.uuid4()),
'time': time.time(),
'type': type,
'data': data,
})
}
self.messages.append(message)
while len(self.listeners) > 0 and not self.shuttingDown():
try:
listener, last_id = self.listeners.pop()
listener({
'success': True,
'result': [message],
})
except:
break
self.m_lock.release()
self.cleanMessages()
def addListener(self, callback, last_id = None):
if last_id:
messages = self.getMessages(last_id)
if len(messages) > 0:
return callback({
'success': True,
'result': messages,
})
self.listeners.append((callback, last_id))
def removeListener(self, callback):
for list_tuple in self.listeners:
try:
listener, last_id = list_tuple
if listener == callback:
self.listeners.remove(list_tuple)
except:
pass
def cleanMessages(self):
self.m_lock.acquire()
for message in self.messages:
if message['time'] < (time.time() - 15):
self.messages.remove(message)
self.m_lock.release()
def getMessages(self, last_id):
self.m_lock.acquire()
recent = []
index = 0
for i in xrange(len(self.messages)):
index = len(self.messages) - i - 1
if self.messages[index]["message_id"] == last_id: break
recent = self.messages[index:]
self.m_lock.release()
return recent or []
def listener(self):
messages = []
for message in self.messages:
#delete message older then 15s
if message['time'] > (time.time() - 15):
messages.append(message)
# Get unread
if getParam('init'):
@@ -146,9 +206,6 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification'
messages.append(ndict)
#db.close()
self.messages = []
return jsonified({
'success': True,
'result': messages,

View File

@@ -8,8 +8,8 @@ var NotificationBase = new Class({
self.setOptions(options);
// Listener
App.addEvent('load', self.startInterval.bind(self));
App.addEvent('unload', self.stopTimer.bind(self));
App.addEvent('unload', self.stopPoll.bind(self));
App.addEvent('reload', self.startInterval.bind(self, [true]));
App.addEvent('notification', self.notify.bind(self));
// Add test buttons to settings page
@@ -30,7 +30,11 @@ var NotificationBase = new Class({
'href': App.createUrl('notifications'),
'text': 'Show older notifications'
})); */
})
});
window.addEvent('load', function(){
self.startInterval()
});
},
@@ -83,37 +87,61 @@ var NotificationBase = new Class({
},
startInterval: function(){
startInterval: function(force){
var self = this;
if(self.stopped && !force){
self.stopped = false;
return;
}
self.request = Api.request('notification.listener', {
'initialDelay': 100,
'delay': 3000,
Api.request('notification.listener', {
'data': {'init':true},
'onSuccess': self.processData.bind(self)
})
self.request.startTimer()
}).send()
},
startTimer: function(){
if(this.request)
this.request.startTimer()
startPoll: function(){
var self = this;
if(self.stopped || (self.request && self.request.isRunning()))
return;
self.request = Api.request('nonblock/notification.listener', {
'onSuccess': self.processData.bind(self),
'data': {
'last_id': self.last_id
},
'onFailure': function(){
self.startPoll.delay(2000, self)
}
}).send()
},
stopTimer: function(){
stopPoll: function(){
if(this.request)
this.request.stopTimer()
this.request.cancel()
this.stopped = true;
},
processData: function(json){
var self = this;
self.request.options.data = {}
Array.each(json.result, function(result){
App.fireEvent(result.type, result)
})
// Process data
if(json){
Array.each(json.result, function(result){
App.fireEvent(result.type, result)
})
if(json.result.length > 0)
self.last_id = json.result.getLast().message_id
}
// Restart poll
self.startPoll()
},
addTestButtons: function(){

View File

@@ -1,9 +1,8 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.environment import Env
from gntp import notifier
import logging
import traceback
log = CPLog(__name__)
@@ -17,7 +16,7 @@ class Growl(Notification):
super(Growl, self).__init__()
if self.isEnabled():
self.register()
addEvent('app.load', self.register)
def register(self):
if self.registered: return

View File

@@ -62,13 +62,15 @@ class FileBrowser(Plugin):
def view(self):
path = getParam('path', '/')
try:
dirs = self.getDirectories(path = getParam('path', '/'), show_hidden = getParam('show_hidden', True))
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
except:
dirs = []
return jsonified({
'is_root': getParam('path', '/') == '/',
'is_root': path == '/' or not path,
'empty': len(dirs) == 0,
'dirs': dirs,
})

View File

@@ -127,9 +127,6 @@ class LibraryPlugin(Plugin):
library_dict = library.to_dict(self.default_dict)
fireEvent('library.update_finish', data = library_dict)
#db.close()
return library_dict
def updateReleaseDate(self, identifier):
@@ -138,8 +135,8 @@ class LibraryPlugin(Plugin):
library = db.query(Library).filter_by(identifier = identifier).first()
if not library.info:
self.update(identifier)
dates = library.get('info', {}).get('release_dates')
library_dict = self.update(identifier)
dates = library_dict.get('info', {}).get('release_dates')
else:
dates = library.info.get('release_date')

View File

@@ -239,16 +239,18 @@ class MoviePlugin(Plugin):
db = get_session()
for id in getParam('id').split(','):
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
movie = db.query(Movie).filter_by(id = id).first()
# Get current selected title
default_title = ''
for title in movie.library.titles:
if title.default: default_title = title.title
if movie:
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
fireEventAsync('searcher.single', movie.to_dict(self.default_dict))
# Get current selected title
default_title = ''
for title in movie.library.titles:
if title.default: default_title = title.title
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
#db.close()
return jsonified({
@@ -287,6 +289,7 @@ class MoviePlugin(Plugin):
db = get_session()
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
added = True
do_search = False
if not m:
m = Movie(
@@ -295,8 +298,14 @@ class MoviePlugin(Plugin):
status_id = status_active.get('id'),
)
db.add(m)
fireEvent('library.update', params.get('identifier'), default_title = params.get('title', ''))
do_search = True
db.commit()
onComplete = None
if search_after:
onComplete = self.createOnComplete(m.id)
fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
search_after = False
elif force_readd:
# Clean snatched history
for release in m.releases:
@@ -306,9 +315,11 @@ class MoviePlugin(Plugin):
m.profile_id = params.get('profile_id', default_profile.get('id'))
else:
log.debug('Movie already exists, not updating: %s' % params)
added = False
if force_readd:
m.status_id = status_active.get('id')
do_search = True
db.commit()
@@ -321,8 +332,12 @@ class MoviePlugin(Plugin):
movie_dict = m.to_dict(self.default_dict)
if (force_readd or do_search) and search_after:
fireEventAsync('searcher.single', movie_dict)
if do_search and search_after:
onComplete = self.createOnComplete(m.id)
onComplete()
if added:
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict)
#db.close()
return movie_dict
@@ -369,7 +384,7 @@ class MoviePlugin(Plugin):
fireEvent('movie.restatus', m.id)
movie_dict = m.to_dict(self.default_dict)
fireEventAsync('searcher.single', movie_dict)
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
#db.close()
return jsonified({
@@ -458,3 +473,22 @@ class MoviePlugin(Plugin):
#db.close()
return True
def createOnComplete(self, movie_id):
def onComplete():
db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first()
fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
return onComplete
def createNotifyFront(self, movie_id):
def notifyFront():
db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first()
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
return notifyFront

View File

@@ -5,10 +5,12 @@ var MovieList = new Class({
options: {
navigation: true,
limit: 50,
menu: []
menu: [],
add_new: false
},
movies: [],
movies_added: {},
letters: {},
filter: {
'startswith': null,
@@ -30,6 +32,17 @@ var MovieList = new Class({
})
);
self.getMovies();
if(options.add_new)
App.addEvent('movie.added', self.movieAdded.bind(self))
},
movieAdded: function(notification){
var self = this;
window.scroll(0,0);
if(!self.movies_added[notification.data.id])
self.createMovie(notification.data, 'top');
},
create: function(){
@@ -71,26 +84,31 @@ var MovieList = new Class({
}
Object.each(movies, function(movie){
// Attach proper actions
var a = self.options.actions,
status = Status.get(movie.status_id);
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
var m = new Movie(self, {
'actions': actions,
'view': self.current_view,
'onSelect': self.calculateSelected.bind(self)
}, movie);
$(m).inject(self.movie_list);
m.fireEvent('injected');
self.movies.include(m)
self.createMovie(movie);
});
},
createMovie: function(movie, inject_at){
var self = this;
// Attach proper actions
var a = self.options.actions,
status = Status.get(movie.status_id);
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
var m = new Movie(self, {
'actions': actions,
'view': self.current_view,
'onSelect': self.calculateSelected.bind(self)
}, movie);
$(m).inject(self.movie_list, inject_at || 'bottom');
m.fireEvent('injected');
self.movies.include(m)
self.movies_added[movie.id] = true;
},
createNavigation: function(){
var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
@@ -260,6 +278,7 @@ var MovieList = new Class({
'events': {
'click': function(e){
(e).preventDefault();
this.set('text', 'Deleting..')
Api.request('movie.delete', {
'data': {
'id': ids.join(','),
@@ -268,14 +287,19 @@ var MovieList = new Class({
'onSuccess': function(){
qObj.close();
var erase_movies = [];
self.movies.each(function(movie){
if (movie.isSelected()){
$(movie).destroy()
self.movies.erase(movie)
erase_movies.include(movie)
}
});
self.calculateSelected()
erase_movies.each(function(movie){
self.movies.erase(movie);
});
self.calculateSelected();
}
});

View File

@@ -89,7 +89,7 @@
font-size: 16px;
font-weight: normal;
text-overflow: ellipsis;
width: 64%;
width: auto;
}
.movies .info .year {
@@ -152,7 +152,6 @@
.movies .list_view .data .quality, .movies .mass_edit_view .data .quality {
text-align: right;
float: right;
width: 30%;
}
.movies .data .quality .available, .movies .data .quality .snatched {
@@ -200,6 +199,7 @@
.movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions {
margin: -34px 2px 0 0;
background: #4e5969;
position: relative;
}
.movies .delete_container {

View File

@@ -11,53 +11,121 @@ var Movie = new Class({
self.view = options.view || 'thumbs';
self.list = list;
self.el = new Element('div.movie.inlay');
self.profile = Quality.getProfile(data.profile_id) || {};
self.parent(self, options);
App.addEvent('movie.update.'+data.id, self.update.bind(self));
App.addEvent('movie.busy.'+data.id, function(notification){
if(notification.data)
self.busy(true)
});
},
busy: function(set_busy){
var self = this;
if(!set_busy){
if(self.spinner){
self.mask.fade('out');
setTimeout(function(){
if(self.mask)
self.mask.destroy();
if(self.spinner)
self.spinner.el.destroy();
self.spinner = null;
self.mask = null;
}, 400);
}
}
else if(!self.spinner) {
self.createMask();
self.spinner = createSpinner(self.mask);
self.positionMask();
self.mask.fade('in');
}
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.el, 'top').fade('hide');
self.positionMask();
},
positionMask: function(){
var self = this,
s = self.el.getSize()
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': self.el
})
},
update: function(notification){
var self = this;
self.data = notification.data;
self.container.destroy();
self.profile = Quality.getProfile(self.data.profile_id) || {};
self.create();
self.busy(false);
},
create: function(){
var self = this;
self.el = new Element('div.movie.inlay').adopt(
self.select_checkbox = new Element('input[type=checkbox].inlay', {
'events': {
'change': function(){
self.fireEvent('select')
}
}
}),
self.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light', {
'tween': {
duration: 400,
transition: 'quint:in:out',
onComplete: self.fireEvent.bind(self, 'slideEnd')
}
}).adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
}),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', {
'text': self.data.library.plot
}),
self.quality = new Element('div.quality', {
'events': {
'click': function(e){
var releases = self.el.getElement('.actions .releases');
if(releases)
releases.fireEvent('click', [e])
}
self.el.adopt(
self.container = new Element('div.movie_container').adopt(
self.select_checkbox = new Element('input[type=checkbox].inlay', {
'events': {
'change': function(){
self.fireEvent('select')
}
})
),
self.actions = new Element('div.actions')
}
}),
self.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light', {
'tween': {
duration: 400,
transition: 'quint:in:out',
onComplete: self.fireEvent.bind(self, 'slideEnd')
}
}).adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
}),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', {
'text': self.data.library.plot
}),
self.quality = new Element('div.quality', {
'events': {
'click': function(e){
var releases = self.el.getElement('.actions .releases');
if(releases)
releases.fireEvent('click', [e])
}
}
})
),
self.actions = new Element('div.actions')
)
)
);
@@ -150,7 +218,7 @@ var Movie = new Class({
self.el.removeEvents('outerClick')
self.addEvent('slideEnd:once', function(){
self.el.getElements('> :not(.data):not(.poster)').hide();
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
});
self.data_container.tween('right', -840, 0);

View File

@@ -221,7 +221,9 @@ Block.Search.Item = new Class({
}
}).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0]
'src': info.images.poster[0],
'height': null,
'width': null
}) : null,
new Element('div.info').adopt(
self.title = new Element('h2', {
@@ -332,8 +334,10 @@ Block.Search.Item = new Class({
self.options.adopt(
new Element('div').adopt(
self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': self.info.images.poster[0]
self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': self.info.images.poster[0],
'height': null,
'width': null
}) : null,
self.info.in_wanted ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label

View File

@@ -17,9 +17,11 @@ rename_options = {
'audio': 'Audio (DTS)',
'group': 'Releasegroup name',
'source': 'Source media (Bluray)',
'filename': 'Original filename',
'original': 'Original filename',
'original_folder': 'Original foldername',
'imdb_id': 'IMDB id (tt0123456)',
'cd': 'CD number (cd1)',
'cd_nr': 'Just the cd nr. (1)',
},
}

View File

@@ -211,7 +211,7 @@ class Renamer(Plugin):
if file_type is 'subtitle':
# rename subtitles with or without language
#rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
sub_langs = group['subtitle_language'].get(current_file, [])
rename_extras = self.getRenameExtras(
@@ -314,7 +314,6 @@ class Renamer(Plugin):
break
elif release.status_id is snatched_status.get('id'):
print release.quality.label, group['meta_data']['quality']['label']
if release.quality.id is group['meta_data']['quality']['id']:
log.debug('Marking release as downloaded')
release.status_id = downloaded_status.get('id')

View File

@@ -4,7 +4,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import getExt, getImdb, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File
from couchpotato.core.settings.model import File, Movie
from couchpotato.environment import Env
from enzyme.exceptions import NoParserError, ParseError
from guessit import guess_movie_info
@@ -27,7 +27,7 @@ class Scanner(Plugin):
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', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
extensions = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts'],
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
'movie_extra': ['mds'],
'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'],
@@ -161,6 +161,8 @@ class Scanner(Plugin):
except:
log.error('Failed getting files from %s: %s' % (folder, traceback.format_exc()))
db = get_session()
for file_path in files:
if not os.path.exists(file_path):
@@ -237,19 +239,51 @@ class Scanner(Plugin):
# Group the files based on the identifier
for identifier, group in movie_files.iteritems():
delete_identifiers = []
for identifier, found_files in self.path_identifiers.iteritems():
log.debug('Grouping files on identifier: %s' % identifier)
found_files = set(self.path_identifiers.get(identifier, []))
group['unsorted_files'].extend(found_files)
group = movie_files.get(identifier)
if group:
group['unsorted_files'].extend(found_files)
delete_identifiers.append(identifier)
# Remove the found files from the leftover stack
leftovers = leftovers - found_files
# Remove the found files from the leftover stack
leftovers = leftovers - set(found_files)
# Break if CP wants to shut down
if self.shuttingDown():
break
# Cleaning up used
for identifier in delete_identifiers:
del self.path_identifiers[identifier]
del delete_identifiers
# Group based on folder
delete_identifiers = []
for identifier, found_files in self.path_identifiers.iteritems():
log.debug('Grouping files on foldername: %s' % identifier)
for ff in found_files:
new_identifier = self.createStringIdentifier(os.path.dirname(ff), folder)
group = movie_files.get(new_identifier)
if group:
group['unsorted_files'].extend([ff])
delete_identifiers.append(identifier)
# Remove the found files from the leftover stack
leftovers = leftovers - set([ff])
# Break if CP wants to shut down
if self.shuttingDown():
break
# Cleaning up used
for identifier in delete_identifiers:
del self.path_identifiers[identifier]
del delete_identifiers
# Determine file types
processed_movies = {}
@@ -327,6 +361,10 @@ class Scanner(Plugin):
group['library'] = self.determineMovie(group)
if not group['library']:
log.error('Unable to determine movie: %s' % group['identifiers'])
else:
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
group['movie_id'] = None if not movie else movie.id
processed_movies[identifier] = group
@@ -647,7 +685,7 @@ class Scanner(Plugin):
identifier = self.removeCPTag(identifier)
# groups, release tags, scenename cleaner, regex isn't correct
identifier = re.sub(self.clean, '::', simplifyString(identifier))
identifier = re.sub(self.clean, '::', simplifyString(identifier)).strip(':')
# Year
year = self.findYear(identifier)

View File

@@ -29,7 +29,7 @@ config = [{
{
'name': 'ignored_words',
'label': 'Ignored words',
'default': 'german, dutch, french, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
},
],
}, {

View File

@@ -83,6 +83,9 @@ class Searcher(Plugin):
if not default_title:
return
fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True)
ret = False
for quality_type in movie['profile']['types']:
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
log.info('To early to search for %s, %s' % (quality_type['quality']['identifier'], default_title))
@@ -107,7 +110,7 @@ class Searcher(Plugin):
# Check if movie isn't deleted while searching
if not db.query(Movie).filter_by(id = movie.get('id')).first():
return
break
# Add them to this movie releases list
for nzb in sorted_results:
@@ -144,7 +147,8 @@ class Searcher(Plugin):
for nzb in sorted_results:
downloaded = self.download(data = nzb, movie = movie)
if downloaded is True:
return True
ret = True
break
elif downloaded != 'try_next':
break
else:
@@ -153,11 +157,13 @@ class Searcher(Plugin):
break
# Break if CP wants to shut down
if self.shuttingDown():
if self.shuttingDown() or ret:
break
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
#db.close()
return False
return ret
def download(self, data, movie, manual = False):

View File

@@ -1,5 +1,5 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.helpers.variable import getExt, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
import os
@@ -17,6 +17,9 @@ class Trailer(Plugin):
if self.isDisabled() or len(group['files']['trailer']) > 0: return
trailers = fireEvent('trailer.search', group = group, merge = True)
if not trailers or trailers == []:
log.info('No trailers found for: %s' % getTitle(group['library']))
return
for trailer in trailers.get(self.conf('quality'), []):
destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer))

View File

@@ -10,7 +10,7 @@ config = [{
'tab': 'automation',
'name': 'imdb_automation',
'label': 'IMDB',
'description': 'From any <strong>public</strong> IMDB watchlists',
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.',
'options': [
{
'name': 'automation_enabled',

View File

@@ -1,17 +1,17 @@
from couchpotato.core.helpers.variable import md5
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5, getImdb
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
from couchpotato.environment import Env
from dateutil.parser import parse
import StringIO
import csv
import time
import traceback
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
class IMDB(Automation):
class IMDB(Automation, RSS):
interval = 1800
@@ -21,34 +21,41 @@ class IMDB(Automation):
return
movies = []
headers = {}
for csv_url in self.conf('automation_urls').split(','):
prop_name = 'automation.imdb.last_update.%s' % md5(csv_url)
enablers = self.conf('automation_urls_use').split(',')
index = -1
for rss_url in self.conf('automation_urls').split(','):
index += 1
if not enablers[index]:
continue
elif 'rss.imdb' not in rss_url:
log.error('This isn\'t the correct url.: %s' % rss_url)
continue
prop_name = 'automation.imdb.last_update.%s' % md5(rss_url)
last_update = float(Env.prop(prop_name, default = 0))
try:
cache_key = 'imdb_csv.%s' % md5(csv_url)
csv_data = self.getCache(cache_key, csv_url)
csv_reader = csv.reader(StringIO.StringIO(csv_data))
if not headers:
nr = 0
for column in csv_reader.next():
headers[column] = nr
nr += 1
cache_key = 'imdb.rss.%s' % md5(rss_url)
for row in csv_reader:
created = int(time.mktime(parse(row[headers['created']]).timetuple()))
if created < last_update:
rss_data = self.getCache(cache_key, rss_url)
data = XMLTree.fromstring(rss_data)
rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies:
created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
imdb = getImdb(self.getTextElement(movie, "link"))
if not imdb or created < last_update:
continue
imdb = row[headers['const']]
if imdb:
movies.append(imdb)
movies.append(imdb)
except:
log.error('Failed loading IMDB watchlist: %s %s' % (csv_url, traceback.format_exc()))
log.error('Failed loading IMDB watchlist: %s %s' % (rss_url, traceback.format_exc()))
Env.prop(prop_name, time.time())
return movies

View File

@@ -151,7 +151,7 @@ class TheMovieDb(MovieProvider):
movie_data = {
'via_tmdb': True,
'id': int(movie.get('id', 0)),
'tmdb_id': int(movie.get('id', 0)),
'titles': [toUnicode(movie.get('name'))],
'original_title': movie.get('original_name'),
'images': {

View File

@@ -81,6 +81,7 @@ class Mysterbin(NZBProvider):
'size': size,
'url': self.urls['download'] % myster_id,
'description': description,
'download': self.download,
'check_nzb': False,
}

View File

@@ -61,7 +61,6 @@ class Newzbin(NZBProvider, RSS):
url = "%s?%s" % (self.urls['search'], arguments)
cache_key = str('newzbin.%s.%s.%s' % (movie['library']['identifier'], str(format_id), str(cat_id)))
single_cat = True
data = self.getCache(cache_key)
if not data:
@@ -118,7 +117,7 @@ class Newzbin(NZBProvider, RSS):
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = single_cat, single = True)
imdb_results = True, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)

View File

@@ -71,6 +71,7 @@ class NzbIndex(NZBProvider, RSS):
'id': nzbindex_id,
'type': 'nzb',
'provider': self.getName(),
'download': self.download,
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024,

View File

@@ -23,7 +23,6 @@ class Env(object):
_deamonize = False
_desktop = None
_session = None
_httpserver = None
''' Data paths and directories '''
_app_dir = ""

View File

@@ -1,13 +1,13 @@
from argparse import ArgumentParser
from couchpotato import web
from couchpotato.api import api
from couchpotato.api import api, NonBlockHandler
from couchpotato.core.event import fireEventAsync, fireEvent
from couchpotato.core.helpers.variable import getDataDir, tryInt
from logging import handlers
from tornado import autoreload
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler
from tornado.web import RequestHandler, Application, FallbackHandler
from tornado.wsgi import WSGIContainer
from werkzeug.contrib.cache import FileSystemCache
import locale
@@ -227,20 +227,22 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Go go go!
web_container = WSGIContainer(app)
web_container._log = _log
http_server = HTTPServer(web_container, no_keep_alive = True)
Env.set('httpserver', http_server)
loop = IOLoop.instance()
application = Application([
(r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler),
(r'.*', FallbackHandler, dict(fallback = web_container)),
],
log_function = lambda x : None,
debug = config['use_reloader']
)
try_restart = True
restart_tries = 5
while try_restart:
try:
http_server.listen(config['port'], config['host'])
if config['use_reloader']:
autoreload.start(loop)
application.listen(config['port'], config['host'], no_keep_alive = True)
loop.start()
except Exception, e:
try:

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -24,8 +24,8 @@ var CouchPotato = new Class({
if(window.location.hash)
History.handleInitialState();
else
self.openPage(window.location.pathname);
self.openPage(window.location.pathname);
History.addEvent('change', self.openPage.bind(self));
self.c.addEvent('click:relay(a[href^=/]:not([target]))', self.pushState.bind(self));
@@ -211,27 +211,30 @@ var CouchPotato = new Class({
}]);
},
checkForUpdate: function(func){
checkForUpdate: function(onComplete){
var self = this;
Updater.check(func)
Updater.check(onComplete)
self.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates');
self.checkAvailable(3000);
},
checkAvailable: function(delay){
checkAvailable: function(delay, onAvailable){
var self = this;
(function(){
Api.request('app.available', {
'onFailure': function(){
self.checkAvailable.delay(1000, self);
self.checkAvailable.delay(1000, self, [delay, onAvailable]);
self.fireEvent('unload');
},
'onSuccess': function(){
if(onAvailable)
onAvailable()
self.unBlockPage();
self.fireEvent('reload');
}
});
@@ -241,6 +244,8 @@ var CouchPotato = new Class({
blockPage: function(message, title){
var self = this;
self.unBlockPage();
var body = $(document.body);
self.mask = new Element('div.mask').adopt(
new Element('div').adopt(
@@ -256,20 +261,14 @@ var CouchPotato = new Class({
unBlockPage: function(){
var self = this;
self.mask.get('tween').start('opacity', 0).chain(function(){
this.element.destroy()
});
if(self.mask)
self.mask.get('tween').start('opacity', 0).chain(function(){
this.element.destroy()
});
},
createUrl: function(action, params){
return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '')
},
notify: function(options){
return this.growl.notify({
title: "this scrolls away",
text: "test - hello there. mouseover to pause away action"
});
}
});

View File

@@ -48,7 +48,7 @@ var AboutSettingTab = new Class({
'text': 'Getting version...',
'events': {
'click': App.checkForUpdate.bind(App, function(json){
self.fillVersion(json)
self.fillVersion(json.info)
}),
'mouseenter': function(){
this.set('text', 'Check for updates')

View File

@@ -702,7 +702,7 @@ Option.Directory = new Class({
var v = self.input.get('text');
var previous_dir = self.getParentDir();
if(previous_dir != v && previous_dir.length > 1){
if(previous_dir != v && previous_dir.length >= 1 && !json.is_root){
self.back_button.set('data-value', previous_dir)
self.back_button.set('html', '&laquo; '+self.getCurrentDirname(previous_dir))
self.back_button.show()
@@ -909,6 +909,7 @@ Option.Choice = new Class({
var input = self.tag_input.getElement('li:last-child input');
input.fireEvent('focus');
input.focus();
input.setCaretPosition(input.get('value').length);
}
self.el.addEvent('outerClick', function(){
@@ -965,6 +966,12 @@ Option.Choice = new Class({
'onChange': self.setOrder.bind(self),
'onBlur': function(){
self.addLastTag();
},
'onGoLeft': function(){
self.goLeft(this)
},
'onGoRight': function(){
self.goRight(this)
}
});
$(tag).inject(self.tag_input);
@@ -979,6 +986,30 @@ Option.Choice = new Class({
return tag;
},
goLeft: function(from_tag){
var self = this;
from_tag.blur();
var prev_index = self.tags.indexOf(from_tag)-1;
if(prev_index >= 0)
self.tags[prev_index].selectFrom('right')
else
from_tag.focus();
},
goRight: function(from_tag){
var self = this;
from_tag.blur();
var next_index = self.tags.indexOf(from_tag)+1;
if(next_index < self.tags.length)
self.tags[next_index].selectFrom('left')
else
from_tag.focus();
},
setOrder: function(){
var self = this;
@@ -1059,7 +1090,16 @@ Option.Choice.Tag = new Class({
'width': 0
},
'events': {
'keyup': self.is_choice ? null : function(){
'keyup': self.is_choice ? null : function(e){
var current_caret_pos = self.input.getCaretPosition();
if(e.key == 'left' && current_caret_pos == self.last_caret_pos){
self.fireEvent('goLeft');
}
else if (e.key == 'right' && self.last_caret_pos === current_caret_pos){
self.fireEvent('goRight');
}
self.last_caret_pos = self.input.getCaretPosition();
self.setWidth();
self.fireEvent('change');
},
@@ -1081,8 +1121,70 @@ Option.Choice.Tag = new Class({
},
blur: function(){
var self = this;
self.input.blur();
self.selected = false;
self.el.removeClass('selected');
self.input.removeEvents('outerClick');
},
focus: function(){
this.input.focus();
var self = this;
if(!self.is_choice){
this.input.focus();
}
else {
if(self.selected) return;
self.selected = true;
self.el.addClass('selected');
self.input.addEvent('outerClick', self.blur.bind(self));
var temp_input = new Element('input', {
'events': {
'keyup': function(e){
e.stop();
if(e.key == 'right'){
self.fireEvent('goRight');
this.destroy();
}
else if (e.key == 'left'){
self.fireEvent('goLeft');
this.destroy();
}
else if (e.key == 'backspace'){
self.del();
this.destroy();
}
}
},
'styles': {
'height': 0,
'width': 0,
'position': 'absolute',
'top': -200
}
});
self.el.adopt(temp_input)
temp_input.focus();
}
},
selectFrom: function(direction){
var self = this;
if(!direction || self.is_choice){
self.focus();
}
else {
self.focus();
var position = direction == 'left' ? 0 : self.input.get('value').length;
self.input.setCaretPosition(position);
}
},
setWidth: function(){

View File

@@ -14,10 +14,10 @@ Page.Wanted = new Class({
self.wanted = new MovieList({
'identifier': 'wanted',
'status': 'active',
'actions': MovieActions
'actions': MovieActions,
'add_new': true
});
$(self.wanted).inject(self.el);
App.addEvent('library.update', self.wanted.update.bind(self.wanted));
}
}
@@ -73,14 +73,20 @@ window.addEvent('domready', function(){
new Element('option', {
'text': alt.title
}).inject(self.title_select);
if(alt['default'])
self.title_select.set('value', alt.title);
});
Quality.getActiveProfiles().each(function(profile){
new Element('option', {
'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select);
self.profile_select.set('value', (self.movie.profile || {})['id']);
if(self.movie.profile)
self.profile_select.set('value', self.movie.profile.data.id);
});
}

View File

@@ -437,6 +437,7 @@ body > .spinner, .mask{
border-radius:3px;
border: 1px solid #252930;
box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2);
background: rgb(55,62,74);
background-image: -webkit-gradient(
linear,
left bottom,

View File

@@ -362,28 +362,29 @@
border-radius: 2px;
}
.page .tag_input > ul:hover > li.choice {
background: url('../../images/sprite.png') no-repeat 94% -53px, -webkit-gradient(
background: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0, rgba(255,255,255,0.1)),
color-stop(1, rgba(255,255,255,0.3))
);
background: url('../../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient(
background: -moz-linear-gradient(
center top,
rgba(255,255,255,0.3) 0%,
rgba(255,255,255,0.1) 100%
);
}
.page .tag_input > ul > li.choice:hover {
background: url('../../images/sprite.png') no-repeat 94% -53px, -webkit-gradient(
.page .tag_input > ul > li.choice:hover,
.page .tag_input > ul > li.choice.selected {
background: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0, #406db8),
color-stop(1, #5b9bd1)
);
background: url('../../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient(
background: -moz-linear-gradient(
center top,
#5b9bd1 0%,
#406db8 100%
@@ -436,7 +437,8 @@
);
background-size: 65%;
}
.page .tag_input .choice:hover .delete { display: inline-block; }
.page .tag_input .choice:hover .delete,
.page .tag_input .choice.selected .delete { display: inline-block; }
.page .tag_input .choice .delete:hover {
height: 14px;
margin-top: -13px;
@@ -587,4 +589,9 @@
.group_userscript .bookmarklet span {
margin-left: 10px;
display: inline-block;
}
}
.active .group_imdb_automation:not(.disabled) {
background: url('../../images/imdb_watchlist.png') no-repeat right 50px;
min-height: 210px;
}

View File

@@ -31,24 +31,14 @@ load_rc_config ${name}
: ${couchpotato_user:="_sabnzbd"}
: ${couchpotato_dir:="/usr/local/couchpotato"}
: ${couchpotato_chdir:="${couchpotato_dir}"}
: ${couchpotato_pid:="${couchpotato_dir}/couchpotato.pid"}
WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown CouchPotato.
HOST="127.0.0.1" # Set CouchPotato address here.
PORT="8081" # Set CouchPotato port here.
CPAPI="" # Set CouchPotato API key
: ${couchpotato_pid:="/var/run/couchpotato.pid"}
pidfile="${couchpotato_pid}"
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command="/usr/sbin/daemon"
command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/couchpotato.py ${couchpotato_flags}"
# Check for wget and refuse to start without it.
if [ ! -x "${WGET}" ]; then
warn "couchpotato not started: You need wget to safely shut down CouchPotato."
exit 1
fi
command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags} --pid_file=${couchpotato_pid}"
# Ensure user is root when running this script.
if [ `id -u` != "0" ]; then
@@ -59,19 +49,23 @@ fi
verify_couchpotato_pid() {
# Make sure the pid corresponds to the CouchPotato process.
pid=`cat ${couchpotato_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python ${couchpotato_dir}/couchpotato.py"
ps -p ${pid} | grep -q "python ${couchpotato_dir}/CouchPotato.py"
return $?
}
# Try to stop CouchPotato cleanly by calling shutdown over http.
couchpotato_stop() {
echo "Stopping $name"
verify_couchpotato_pid
${WGET} -O - -q "http://${HOST}:${PORT}/${CPAPI}/app.shutdown/" >/dev/null
if [ -n "${pid}" ]; then
kill -SIGTERM ${pid} 2> /dev/null
wait_for_pids ${pid}
kill -9 ${pid} 2> /dev/null
echo "Stopped"
fi
}
couchpotato_status() {