Merge branch 'refs/heads/develop'

This commit is contained in:
Ruud
2013-03-26 21:10:27 +01:00
44 changed files with 536 additions and 113 deletions

View File

@@ -70,7 +70,7 @@ config = [{
'name': 'development',
'default': 0,
'type': 'bool',
'description': 'Disables some checks/downloads for faster reloading.',
'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
},
{
'name': 'data_dir',

View File

@@ -1,5 +1,6 @@
from base64 import b32decode, b16encode
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
import random
@@ -103,6 +104,12 @@ class Downloader(Provider):
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
return False
def downloadReturnId(self, download_id):
return {
'downloader': self.getName(),
'id': download_id
}
def isDisabled(self, manual, data):
return not self.isEnabled(manual, data)
@@ -116,3 +123,34 @@ class Downloader(Provider):
return super(Downloader, self).isEnabled() and \
((d_manual and manual) or (d_manual is False)) and \
(not data or self.isCorrectType(data.get('type')))
class StatusList(list):
provider = None
def __init__(self, provider, **kwargs):
self.provider = provider
self.kwargs = kwargs
super(StatusList, self).__init__()
def extend(self, results):
for r in results:
self.append(r)
def append(self, result):
new_result = self.fillResult(result)
super(StatusList, self).append(new_result)
def fillResult(self, result):
defaults = {
'id': 0,
'status': 'busy',
'downloader': self.provider.getName(),
}
return mergeDicts(defaults, result)

View File

@@ -48,6 +48,12 @@ config = [{
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
{
'name': 'delete_failed',
'default': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],

View File

@@ -1,9 +1,11 @@
from base64 import standard_b64encode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from datetime import timedelta
import re
import shutil
import socket
import traceback
import xmlrpclib
@@ -50,7 +52,107 @@ class NZBGet(Downloader):
if xml_response:
log.info('NZB sent successfully to NZBGet')
return True
groups = rpc.listgroups()
nzb_id = [item['NZBID'] for item in groups if item['NZBFilename'] == nzb_name][0]
return self.downloadReturnId(nzb_id)
else:
log.error('NZBGet could not add %s to the queue.', nzb_name)
return False
def getAllDownloadStatus(self):
log.debug('Checking NZBGet download status.')
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
rpc = xmlrpclib.ServerProxy(url)
try:
if rpc.writelog('INFO', 'CouchPotato connected to check status'):
log.info('Successfully connected to NZBGet')
else:
log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False
except xmlrpclib.ProtocolError, e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
log.error('Protocol Error: %s', e)
return False
# Get NZBGet data
try:
status = rpc.status()
groups = rpc.listgroups()
queue = rpc.postqueue(0)
history = rpc.history()
except:
log.error('Failed getting data: %s', traceback.format_exc(1))
return False
statuses = StatusList(self)
for item in groups:
log.debug('Found %s in NZBGet download queue', item['NZBFilename'])
statuses.append({
'id': item['NZBID'],
'name': item['NZBFilename'],
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1,
})
for item in queue:
log.debug('Found %s in NZBGet postprocessing queue', item['NZBFilename'])
statuses.append({
'id': item['NZBID'],
'name': item['NZBFilename'],
'original_status': item['Stage'],
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
})
for item in history:
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log']))
statuses.append({
'id': item['NZBID'],
'name': item['NZBFilename'],
'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed',
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
'timeleft': str(timedelta(seconds = 0)),
})
return statuses
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
rpc = xmlrpclib.ServerProxy(url)
try:
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
log.info('Successfully connected to NZBGet')
else:
log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False
except xmlrpclib.ProtocolError, e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
log.error('Protocol Error: %s', e)
return False
try:
history = rpc.history()
if rpc.editqueue('HistoryDelete', 0, "", [tryInt(item['id'])]):
path = [hist['DestDir'] for hist in history if hist['NZBID'] == item['id']][0]
shutil.rmtree(path, True)
except:
log.error('Failed deleting: %s', traceback.format_exc(0))
return False
return True

View File

@@ -1,5 +1,5 @@
from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
@@ -29,7 +29,9 @@ class NZBVortex(Downloader):
nzb_filename = self.createFileName(data, filedata, movie)
self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
return True
raw_statuses = self.call('nzb')
nzb_id = [item['id'] for item in raw_statuses.get('nzbs', []) if item['name'] == nzb_filename][0]
return self.downloadReturnId(nzb_id)
except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False
@@ -38,7 +40,7 @@ class NZBVortex(Downloader):
raw_statuses = self.call('nzb')
statuses = []
statuses = StatusList(self)
for item in raw_statuses.get('nzbs', []):
# Check status

View File

@@ -1,4 +1,4 @@
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
from couchpotato.core.logger import CPLog
@@ -17,8 +17,7 @@ class Sabnzbd(Downloader):
log.info('Sending "%s" to SABnzbd.', data.get('name'))
params = {
'apikey': self.conf('api_key'),
req_params = {
'cat': self.conf('category'),
'mode': 'addurl',
'nzbname': self.createNzbName(data, movie),
@@ -31,17 +30,15 @@ class Sabnzbd(Downloader):
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
nzb_filename = self.createFileName(data, filedata, movie)
params['mode'] = 'addfile'
req_params['mode'] = 'addfile'
else:
params['name'] = data.get('url')
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
req_params['name'] = data.get('url')
try:
if params.get('mode') is 'addfile':
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
if req_params.get('mode') is 'addfile':
sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
else:
sab = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
sab_data = self.call(req_params)
except URLError:
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
return False
@@ -49,17 +46,19 @@ class Sabnzbd(Downloader):
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
return False
result = sab.strip()
if not result:
log.error('SABnzbd didn\'t return anything.')
if sab_data.get('error'):
log.error('Error getting data from SABNZBd: %s', sab_data.get('error'))
return False
log.debug('Result text from SAB: %s', result[:40])
if result[:2] == 'ok':
log.debug('Result from SAB: %s', sab_data)
if sab_data.get('status'):
log.info('NZB sent to SAB successfully.')
return True
if filedata:
return self.downloadReturnId(sab_data.get('nzo_ids')[0])
else:
return True
else:
log.error(result[:40])
log.error(sab_data)
return False
def getAllDownloadStatus(self):
@@ -85,14 +84,13 @@ class Sabnzbd(Downloader):
log.error('Failed getting history json: %s', traceback.format_exc(1))
return False
statuses = []
statuses = StatusList(self)
# Get busy releases
for item in queue.get('slots', []):
statuses.append({
'id': item['nzo_id'],
'name': item['filename'],
'status': 'busy',
'original_status': item['status'],
'timeleft': item['timeleft'] if not queue['paused'] else -1,
})
@@ -133,21 +131,21 @@ class Sabnzbd(Downloader):
return True
def call(self, params, use_json = True):
def call(self, request_params, use_json = True, **kwargs):
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
'apikey': self.conf('api_key'),
'output': 'json'
}))
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs)
if use_json:
d = json.loads(data)
if d.get('error'):
log.error('Error getting data from SABNZBd: %s', d.get('error'))
return {}
return d[params['mode']]
return d.get(request_params['mode']) or d
else:
return data

View File

@@ -62,7 +62,7 @@ class Transmission(Downloader):
if torrent_params:
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
return True
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
except Exception, err:
log.error('Failed to change settings for transfer: %s', err)
return False

View File

@@ -1,6 +1,6 @@
from base64 import b16encode, b32decode
from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.logger import CPLog
from hashlib import sha1
@@ -66,7 +66,7 @@ class uTorrent(Downloader):
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
if self.conf('paused', default = 0):
self.utorrent_api.pause_torrent(torrent_hash)
return True
return self.downloadReturnId(torrent_hash)
except Exception, err:
log.error('Failed to send torrent to uTorrent: %s', err)
return False
@@ -103,7 +103,7 @@ class uTorrent(Downloader):
log.debug('Nothing in queue')
return False
statuses = []
statuses = StatusList(self)
# Get torrents
for item in queue.get('torrents', []):

View File

@@ -110,7 +110,7 @@ def fireEvent(name, *args, **kwargs):
if isinstance(results[0], dict):
merged = {}
for result in results:
merged = mergeDicts(merged, result)
merged = mergeDicts(merged, result, prepend_list = True)
results = merged
# Lists

View File

@@ -53,7 +53,7 @@ def getDataDir():
def isDict(object):
return isinstance(object, dict)
def mergeDicts(a, b):
def mergeDicts(a, b, prepend_list = False):
assert isDict(a), isDict(b)
dst = a.copy()
@@ -67,7 +67,7 @@ def mergeDicts(a, b):
if isDict(current_src[key]) and isDict(current_dst[key]):
stack.append((current_dst[key], current_src[key]))
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
current_dst[key].extend(current_src[key])
current_dst[key] = current_src[key] + current_dst[key] if prepend_list else current_dst[key] + current_src[key]
current_dst[key] = removeListDuplicates(current_dst[key])
else:
current_dst[key] = current_src[key]

View File

@@ -2,13 +2,15 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env
log = CPLog(__name__)
class Notification(Plugin):
class Notification(Provider):
type = 'notification'
default_title = Env.get('appname')
test_message = 'ZOMG Lazors Pewpewpew!'
@@ -20,7 +22,7 @@ class Notification(Plugin):
dont_listen_to = []
def __init__(self):
addEvent('notify.%s' % self.getName().lower(), self.notify)
addEvent('notify.%s' % self.getName().lower(), self._notify)
addApiView(self.testNotifyName(), self.test)
@@ -33,13 +35,17 @@ class Notification(Plugin):
def notify(message = None, group = {}, data = None):
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
return
return self.notify(message = message, data = data if data else group, listener = listener)
return self._notify(message = message, data = data if data else group, listener = listener)
return notify
def getNotificationImage(self, size = 'small'):
return 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/notify.couch.%s.png' % size
def _notify(self, *args, **kwargs):
if self.isEnabled():
self.notify(*args, **kwargs)
def notify(self, message = '', data = {}, listener = None):
pass
@@ -49,7 +55,7 @@ class Notification(Plugin):
log.info('Sending test to %s', test_type)
success = self.notify(
success = self._notify(
message = self.test_message,
data = {},
listener = 'test'

View File

@@ -11,7 +11,6 @@ class Boxcar(Notification):
url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
try:
message = message.strip()

View File

@@ -12,7 +12,6 @@ log = CPLog(__name__)
class Email(Notification):
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
# Extract all the settings from settings
from_address = self.conf('from')
@@ -50,6 +49,5 @@ class Email(Notification):
return True
except:
log.error('E-mail failed: %s', traceback.format_exc())
return False
return False

View File

@@ -44,7 +44,6 @@ class Growl(Notification):
log.error('Failed register of growl: %s', traceback.format_exc())
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
self.register()

View File

@@ -13,7 +13,6 @@ class Notifo(Notification):
url = 'https://api.notifo.com/v1/send_notification'
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
try:
params = {

View File

@@ -9,7 +9,6 @@ log = CPLog(__name__)
class NotifyMyAndroid(Notification):
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
nma = pynma.PyNMA()
keys = splitString(self.conf('api_key'))

View File

@@ -9,7 +9,6 @@ log = CPLog(__name__)
class NotifyMyWP(Notification):
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
keys = splitString(self.conf('api_key'))
p = PyNMWP(keys, self.conf('dev_key'))

View File

@@ -46,7 +46,6 @@ class Plex(Notification):
return True
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")]
successful = 0

View File

@@ -13,7 +13,6 @@ class Prowl(Notification):
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = {
'apikey': self.conf('api_key'),

View File

@@ -12,7 +12,6 @@ class Pushalot(Notification):
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = {
'AuthorizationToken': self.conf('auth_token'),

View File

@@ -11,7 +11,6 @@ class Pushover(Notification):
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
http_handler = HTTPSConnection("api.pushover.net:443")

View File

@@ -12,7 +12,6 @@ class Toasty(Notification):
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = {
'title': self.default_title,

View File

@@ -0,0 +1,30 @@
from .main import Trakt
def start():
return Trakt()
config = [{
'name': 'trakt',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'trakt',
'label': 'Trakt',
'description': 'add movies to your collection once downloaded. Fill in your username and password in the <a href="../automation/">Automation Trakt settings</a>',
'options': [
{
'name': 'notification_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'remove_watchlist_enabled',
'label': 'Remove from watchlist',
'default': False,
'type': 'bool',
},
],
}
],
}]

View File

@@ -0,0 +1,46 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
class Trakt(Notification):
urls = {
'base': 'http://api.trakt.tv/%s',
'library': 'movie/library/%s',
'unwatchlist': 'movie/unwatchlist/%s',
}
listen_to = ['movie.downloaded']
def notify(self, message = '', data = {}, listener = None):
post_data = {
'username': self.conf('automation_username'),
'password' : self.conf('automation_password'),
'movies': [{
'imdb_id': data['library']['identifier'],
'title': data['library']['titles'][0]['title'],
'year': data['library']['year']
}] if data else []
}
result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data)
if self.conf('remove_watchlist_enabled'):
result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data)
return result
def call(self, method_url, post_data):
try:
response = self.getJsonData(self.urls['base'] % method_url, params = post_data, cache_timeout = 1)
if response:
if response.get('status') == "success":
log.info('Successfully called Trakt')
return True
except:
pass
log.error('Failed to call trakt, check your login.')
return False

View File

@@ -32,7 +32,6 @@ class Twitter(Notification):
addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials)
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret'))

View File

@@ -15,7 +15,6 @@ class XBMC(Notification):
use_json_notifications = {}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
hosts = splitString(self.conf('host'))

View File

@@ -370,6 +370,7 @@ class MoviePlugin(Plugin):
status_active = fireEvent('status.add', 'active', single = True)
snatched_status = fireEvent('status.add', 'snatched', single = True)
ignored_status = fireEvent('status.add', 'ignored', single = True)
done_status = fireEvent('status.add', 'done', single = True)
downloaded_status = fireEvent('status.add', 'downloaded', single = True)
default_profile = fireEvent('profile.default', single = True)
@@ -397,7 +398,7 @@ class MoviePlugin(Plugin):
# Clean snatched history
for release in m.releases:
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id')]:
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
if params.get('ignore_previous', False):
release.status_id = ignored_status.get('id')
else:

View File

@@ -94,36 +94,30 @@ MA.Release = new Class({
}
else {
var buttons_done = false;
var releases = self.movie.data.releases.sortBy('-info.score');
self.movie.data.releases.sortBy('-info.score').each(function(release){
if(buttons_done) return;
var status = Status.get(release.status_id);
for(x in releases){
var release = releases[x],
status = Status.get(release.status_id);
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
self.hide_on_click = false;
self.show();
buttons_done = true;
self.showHelper();
break;
}
});
}
}
},
show: function(e){
createReleases: function(){
var self = this;
if(e)
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.release_container = new Element('div.releases.table').adopt(
self.trynext_container = new Element('div.buttons.try_container')
)
).inject(self.movie, 'top');
);
// Header
new Element('div.item.head').adopt(
@@ -238,9 +232,71 @@ MA.Release = new Class({
}
},
show: function(e){
var self = this;
if(e)
(e).preventDefault();
self.createReleases();
self.options_container.inject(self.movie, 'top');
self.movie.slide('in', self.options_container);
},
showHelper: function(e){
var self = this;
if(e)
(e).preventDefault();
self.createReleases();
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
if(self.next_release || self.last_release){
self.trynext_container.adopt(
self.next_release ? [new Element('a.icon.readd', {
'text': self.last_release ? 'Download another release' : 'Download the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('a.icon.download', {
'text': 'pick one yourself',
'events': {
'click': function(){
self.movie.quality.fireEvent('click');
}
}
})] : null,
new Element('a.icon.completed', {
'text': 'mark this movie done',
'events': {
'click': function(){
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id'),
'delete_from': 'wanted'
},
'onComplete': function(){
var movie = $(self.movie);
movie.set('tween', {
'duration': 300,
'onComplete': function(){
self.movie.destroy()
}
});
movie.tween('height', 0);
}
});
}
}
})
)
}
},
get: function(release, type){
return release.info[type] || 'n/a'
},
@@ -251,14 +307,15 @@ MA.Release = new Class({
var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon');
icon.addClass('spinner');
self.movie.busy(true);
Api.request('release.download', {
'data': {
'id': release.id
},
'onComplete': function(json){
icon.removeClass('spinner')
self.movie.busy(false);
if(json.success)
icon.addClass('completed');
else
@@ -281,6 +338,8 @@ MA.Release = new Class({
tryNextRelease: function(movie_id){
var self = this;
self.createReleases();
if(self.last_release)
self.ignore(self.last_release);

View File

@@ -39,7 +39,7 @@
overflow: hidden;
width: 100%;
height: 180px;
transition: all 0.2s linear;
transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
}
.movies.list_list .movie:not(.details_view),
@@ -95,7 +95,7 @@
width: 938px;
box-shadow: none;
border: 0;
background: none;
background: #4e5969;
}
.movies.thumbs_list .data {
@@ -322,8 +322,8 @@
right: 10px;
}
.movies .data:hover .action { opacity: 0.6; }
.movies .data:hover .action:hover { opacity: 1; }
.movies .movie:hover .action { opacity: 0.6; }
.movies .movie:hover .action:hover { opacity: 1; }
.movies.mass_edit_list .data .actions {
display: none;
}
@@ -338,8 +338,8 @@
opacity: 0;
}
.movies.list_list .movie:not(.details_view) .data:hover .actions,
.movies.mass_edit_list .data:hover .actions {
.movies.list_list .movie:not(.details_view):hover .actions,
.movies.mass_edit_list .movie:hover .actions {
margin: 0;
background: #4e5969;
top: 2px;
@@ -510,6 +510,29 @@
.movies .movie .releases .last_release > :first-child {
margin-left: -6px;
}
.movies .movie .trynext {
display: inline;
position: absolute;
right: 135px;
z-index: 2;
opacity: 0;
text-shadow: none;
background: #4e5969;
}
.movies .movie:hover .trynext {
opacity: 1;
}
.movies .movie .trynext a {
background-position: 5px center;
padding: 0 5px 0 25px;
margin-right: 10px;
color: #FFF;
border-radius: 2px;
}
.movies .movie .trynext a:hover {
background-color: #369545;
}
.movies .load_more {
display: block;

View File

@@ -19,8 +19,8 @@ class QualityPlugin(Plugin):
qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
{'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', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},

View File

@@ -160,7 +160,9 @@ class Release(Plugin):
db = get_session()
id = getParam('id')
status_snatched = fireEvent('status.add', 'snatched', single = True)
snatched_status = fireEvent('status.add', 'snatched', single = True)
done_status = fireEvent('status.get', 'done', single = True)
rel = db.query(Relea).filter_by(id = id).first()
if rel:
@@ -168,6 +170,8 @@ class Release(Plugin):
for info in rel.info:
item[info.identifier] = info.value
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Snatching "%s"' % item['name'])
# Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
@@ -182,8 +186,14 @@ class Release(Plugin):
}), manual = True, single = True)
if success:
rel.status_id = status_snatched.get('id')
db.commit()
db.expunge_all()
rel = db.query(Relea).filter_by(id = id).first() # Get release again
if rel.status_id != done_status.get('id'):
rel.status_id = snatched_status.get('id')
db.commit()
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return jsonified({
'success': success

View File

@@ -13,7 +13,7 @@ rename_options = {
'thename': 'The Moviename',
'year': 'Year (2011)',
'first': 'First letter (M)',
'quality': 'Quality (720P)',
'quality': 'Quality (720p)',
'video': 'Video (x264)',
'audio': 'Audio (DTS)',
'group': 'Releasegroup name',

View File

@@ -571,8 +571,16 @@ class Renamer(Plugin):
found = False
for item in statuses:
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
found_release = False
if rel_dict['info'].get('download_id'):
if item['id'] == rel_dict['info']['download_id'] and item['downloader'] == rel_dict['info']['download_downloader']:
log.debug('Found release by id: %s', item['id'])
found_release = True
else:
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
found_release = True
if found_release:
timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))

View File

@@ -11,6 +11,7 @@ from subliminal.videos import Video
import enzyme
import os
import re
import threading
import time
import traceback
@@ -74,7 +75,7 @@ class Scanner(Plugin):
'hdtv': ['hdtv']
}
clean = '[ _\,\.\(\)\[\]\-](french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
clean = '[ _\,\.\(\)\[\]\-](extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
multipart_regex = [
'[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
'[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1
@@ -388,6 +389,11 @@ class Scanner(Plugin):
if on_found:
on_found(group, total_found, total_found - len(processed_movies))
# Wait for all the async events calm down a bit
while threading.activeCount() > 100 and not self.shuttingDown():
log.debug('Too many threads active, waiting a few seconds')
time.sleep(10)
if len(processed_movies) > 0:
log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
else:

View File

@@ -285,10 +285,10 @@ class Searcher(Plugin):
if filedata == 'try_next':
return filedata
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
if successful:
download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
log.debug('Downloader result: %s', download_result)
if download_result:
try:
# Mark release as snatched
db = get_session()
@@ -298,6 +298,15 @@ class Searcher(Plugin):
done_status = fireEvent('status.get', 'done', single = True)
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
# Save download-id info if returned
if isinstance(download_result, dict):
for key in download_result:
rls_info = ReleaseInfo(
identifier = 'download_%s' % key,
value = toUnicode(download_result.get(key))
)
rls.info.append(rls_info)
db.commit()
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
@@ -333,7 +342,7 @@ class Searcher(Plugin):
return True
log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', '')))
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('type', '')))
return False

View File

@@ -20,7 +20,7 @@ config = [{
},
{
'name': 'languages',
'description': 'Comma separated, 2 letter country code. Example: en, nl',
'description': 'Comma separated, 2 letter country code. Example: en, nl. See the codes at <a href="http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes">on Wikipedia</a>',
},
# {
# 'name': 'automatic',

View File

@@ -22,7 +22,7 @@ config = [{
'name': 'quality',
'default': '720p',
'type': 'dropdown',
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
'values': [('1080p', '1080p'), ('720p', '720p'), ('480P', '480p')],
},
{
'name': 'name',

View File

@@ -0,0 +1,34 @@
from .main import Letterboxd
def start():
return Letterboxd()
config = [{
'name': 'letterboxd',
'groups': [
{
'tab': 'automation',
'list': 'watchlist_providers',
'name': 'letterboxd_automation',
'label': 'Letterboxd',
'description': 'Import movies from any public <a href="http://letterboxd.com/">Letterboxd</a> watchlist',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_urls_use',
'label': 'Use',
},
{
'name': 'automation_urls',
'label': 'Username',
'type': 'combined',
'combine': ['automation_urls_use', 'automation_urls'],
},
],
},
],
}]

View File

@@ -0,0 +1,49 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
import re
log = CPLog(__name__)
class Letterboxd(Automation):
url = 'http://letterboxd.com/%s/watchlist/'
pattern = re.compile(r'(.*)\((\d*)\)')
def getIMDBids(self):
urls = splitString(self.conf('automation_urls'))
if len(urls) == 0:
return []
movies = []
for movie in self.getWatchlist():
imdb_id = self.search(movie.get('title'), movie.get('year'), imdb_only = True)
movies.append(imdb_id)
return movies
def getWatchlist(self):
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
urls = splitString(self.conf('automation_urls'))
index = -1
movies = []
for username in urls:
index += 1
if not enablers[index]:
continue
soup = BeautifulSoup(self.getHTMLData(self.url % username))
for movie in soup.find_all('a', attrs = { 'class': 'frame' }):
match = filter(None, self.pattern.split(movie['title']))
movies.append({'title': match[0], 'year': match[1] })
return movies

View File

@@ -76,7 +76,7 @@ class LibraryTitle(Entity):
title = Field(Unicode)
simple_title = Field(Unicode, index = True)
default = Field(Boolean, index = True)
default = Field(Boolean, default = False, index = True)
language = OneToMany('Language')
libraries = ManyToOne('Library')
@@ -141,12 +141,12 @@ class Status(Entity):
class Quality(Entity):
"""Quality name of a release, DVD, 720P, DVD-Rip etc"""
"""Quality name of a release, DVD, 720p, DVD-Rip etc"""
using_options(order_by = 'order')
identifier = Field(String(20), unique = True)
label = Field(Unicode(20))
order = Field(Integer, index = True)
order = Field(Integer, default = 0, index = True)
size_min = Field(Integer)
size_max = Field(Integer)
@@ -160,21 +160,27 @@ class Profile(Entity):
using_options(order_by = 'order')
label = Field(Unicode(50))
order = Field(Integer, index = True)
core = Field(Boolean)
hide = Field(Boolean)
order = Field(Integer, default = 0, index = True)
core = Field(Boolean, default = False)
hide = Field(Boolean, default = False)
movie = OneToMany('Movie')
types = OneToMany('ProfileType', cascade = 'all, delete-orphan')
def to_dict(self, deep = {}, exclude = []):
orig_dict = super(Profile, self).to_dict(deep = deep, exclude = exclude)
orig_dict['core'] = orig_dict.get('core') or False
orig_dict['hide'] = orig_dict.get('hide') or False
return orig_dict
class ProfileType(Entity):
""""""
using_options(order_by = 'order')
order = Field(Integer, index = True)
finish = Field(Boolean)
wait_for = Field(Integer)
order = Field(Integer, default = 0, index = True)
finish = Field(Boolean, default = True)
wait_for = Field(Integer, default = 0)
quality = ManyToOne('Quality')
profile = ManyToOne('Profile')
@@ -185,7 +191,7 @@ class File(Entity):
path = Field(Unicode(255), nullable = False, unique = True)
part = Field(Integer, default = 1)
available = Field(Boolean)
available = Field(Boolean, default = True)
type = ManyToOne('FileType')
properties = OneToMany('FileProperty')

View File

@@ -190,9 +190,12 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
version_control(db, repo, version = latest_db_version)
current_db_version = db_version(db, repo)
if current_db_version < latest_db_version and not development:
log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version))
upgrade(db, repo)
if current_db_version < latest_db_version:
if development:
log.error('There is a database migration ready, but you are running development mode, so it won\'t be used. If you see this, you are stupid. Please disable development mode.')
else:
log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version))
upgrade(db, repo)
# Configure Database
from couchpotato.core.settings.model import setup

View File

@@ -23,7 +23,7 @@ Page.Home = new Class({
'identifier': 'snatched',
'load_more': false,
'view': 'list',
'actions': [MA.IMDB, MA.Trailer, MA.Files, MA.Release, MA.Edit, MA.Readd, MA.Refresh, MA.Delete],
'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Refresh, MA.Delete],
'title': 'Snatched & Available',
'on_empty_element': new Element('div'),
'filter': {

View File

@@ -297,7 +297,7 @@ Page.Settings = new Class({
return group_el
},
createList: function(content_container){
return new Element('div.option_list').grab(
new Element('h3', {
@@ -1283,6 +1283,7 @@ Option.Combined = new Class({
self.values = {}
self.inputs = {}
self.items = []
self.labels = {}
self.options.combine.each(function(name){
@@ -1302,9 +1303,10 @@ Option.Combined = new Class({
var head = new Element('div.head').inject(self.combined_list)
Object.each(self.inputs, function(input, name){
self.labels[name] = input.getPrevious().get('text')
new Element('abbr', {
'class': name,
'text': input.getPrevious().get('text'),
'text': self.labels[name],
//'title': input.getNext().get('text')
}).inject(head)
})
@@ -1367,7 +1369,7 @@ Option.Combined = new Class({
value_count++;
new Element('input[type=text].inlay.'+name, {
'value': value,
'placeholder': name,
'placeholder': self.labels[name] || name,
'events': {
'keyup': self.saveCombined.bind(self),
'change': self.saveCombined.bind(self)

View File

@@ -134,10 +134,9 @@ body > .spinner, .mask{
text-decoration: none;
font-weight: bold;
line-height: 1;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0,0,0,0.5);
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0,0,0,0.3);
text-shadow: 0 -1px 1px rgba(0,0,0,0.25);
border-bottom: 1px solid rgba(0,0,0,0.25);
cursor: pointer;
}
.button.red { background-color: #ff0000; }