Merge branch 'refs/heads/develop'
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', []):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,6 @@ class Prowl(Notification):
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
data = {
|
||||
'apikey': self.conf('api_key'),
|
||||
|
||||
@@ -12,7 +12,6 @@ class Pushalot(Notification):
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
data = {
|
||||
'AuthorizationToken': self.conf('auth_token'),
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ class Toasty(Notification):
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
data = {
|
||||
'title': self.default_title,
|
||||
|
||||
30
couchpotato/core/notifications/trakt/__init__.py
Normal file
30
couchpotato/core/notifications/trakt/__init__.py
Normal 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',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
46
couchpotato/core/notifications/trakt/main.py
Normal file
46
couchpotato/core/notifications/trakt/main.py
Normal 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
|
||||
@@ -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'))
|
||||
|
||||
|
||||
@@ -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'))
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')]},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
34
couchpotato/core/providers/automation/letterboxd/__init__.py
Normal file
34
couchpotato/core/providers/automation/letterboxd/__init__.py
Normal 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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
49
couchpotato/core/providers/automation/letterboxd/main.py
Normal file
49
couchpotato/core/providers/automation/letterboxd/main.py
Normal 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
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user