Merge branches 'develop' and 'manual_scan' of https://github.com/RuudBurger/CouchPotatoServer into manual_scan
Conflicts: couchpotato/core/plugins/renamer/main.py
This commit is contained in:
commit
d49c663c64
225 files changed
+22249
-10130
No files matched your search
@@ -34,6 +34,8 @@ class ClientScript(Plugin):
|
||||
'scripts/library/question.js',
|
||||
'scripts/library/scrollspy.js',
|
||||
'scripts/library/spin.js',
|
||||
'scripts/library/Array.stableSort.js',
|
||||
'scripts/library/async.js',
|
||||
'scripts/couchpotato.js',
|
||||
'scripts/api.js',
|
||||
'scripts/library/history.js',
|
||||
|
||||
@@ -31,13 +31,13 @@ class Scheduler(Plugin):
|
||||
pass
|
||||
|
||||
def doShutdown(self):
|
||||
super(Scheduler, self).doShutdown()
|
||||
self.stop()
|
||||
return super(Scheduler, self).doShutdown()
|
||||
|
||||
def stop(self):
|
||||
if self.started:
|
||||
log.debug('Stopping scheduler')
|
||||
self.sched.shutdown()
|
||||
self.sched.shutdown(wait = False)
|
||||
log.debug('Scheduler stopped')
|
||||
self.started = False
|
||||
|
||||
|
||||
@@ -183,9 +183,6 @@ class GitUpdater(BaseUpdater):
|
||||
def doUpdate(self):
|
||||
|
||||
try:
|
||||
log.debug('Stashing local changes')
|
||||
self.repo.saveStash()
|
||||
|
||||
log.info('Updating to latest version')
|
||||
self.repo.pull()
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
|
||||
self.doUpdate();
|
||||
else {
|
||||
App.unBlockPage();
|
||||
App.fireEvent('message', 'No updates available');
|
||||
App.on('message', 'No updates available');
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -49,21 +49,26 @@ class Downloader(Provider):
|
||||
|
||||
return []
|
||||
|
||||
def _download(self, data = None, movie = None, manual = False, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def _download(self, data = None, media = None, manual = False, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
if self.isDisabled(manual, data):
|
||||
return
|
||||
return self.download(data = data, movie = movie, filedata = filedata)
|
||||
return self.download(data = data, media = media, filedata = filedata)
|
||||
|
||||
def _getAllDownloadStatus(self):
|
||||
def _getAllDownloadStatus(self, download_ids):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
|
||||
return self.getAllDownloadStatus()
|
||||
ids = [download_id['id'] for download_id in download_ids if download_id['downloader'] == self.getName()]
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
if ids:
|
||||
return self.getAllDownloadStatus(ids)
|
||||
else:
|
||||
return
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
return
|
||||
|
||||
def _removeFailed(self, release_download):
|
||||
|
||||
@@ -13,7 +13,7 @@ config = [{
|
||||
'list': 'download_providers',
|
||||
'name': 'blackhole',
|
||||
'label': 'Black hole',
|
||||
'description': 'Download the NZB/Torrent to a specific folder.',
|
||||
'description': 'Download the NZB/Torrent to a specific folder. <em>Note: Seeding and copying/linking features do <strong>not</strong> work with Black hole</em>.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -12,8 +12,8 @@ class Blackhole(Downloader):
|
||||
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
@@ -33,7 +33,7 @@ class Blackhole(Downloader):
|
||||
log.error('No nzb/torrent available: %s', data.get('url'))
|
||||
return False
|
||||
|
||||
file_name = self.createFileName(data, filedata, movie)
|
||||
file_name = self.createFileName(data, filedata, media)
|
||||
full_path = os.path.join(directory, file_name)
|
||||
|
||||
if self.conf('create_subdir'):
|
||||
@@ -51,10 +51,10 @@ class Blackhole(Downloader):
|
||||
with open(full_path, 'wb') as f:
|
||||
f.write(filedata)
|
||||
os.chmod(full_path, Env.getPermission('file'))
|
||||
return True
|
||||
return self.downloadReturnId('')
|
||||
else:
|
||||
log.info('File %s already exists.', full_path)
|
||||
return True
|
||||
return self.downloadReturnId('')
|
||||
|
||||
except:
|
||||
log.error('Failed to download to blackhole %s', traceback.format_exc())
|
||||
|
||||
@@ -32,7 +32,10 @@ class Deluge(Downloader):
|
||||
|
||||
return self.drpc
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
|
||||
|
||||
if not self.connect():
|
||||
@@ -73,7 +76,7 @@ class Deluge(Downloader):
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
|
||||
else:
|
||||
filename = self.createFileName(data, filedata, movie)
|
||||
filename = self.createFileName(data, filedata, media)
|
||||
remote_torrent = self.drpc.add_torrent_file(filename, filedata, options)
|
||||
|
||||
if not remote_torrent:
|
||||
@@ -83,7 +86,7 @@ class Deluge(Downloader):
|
||||
log.info('Torrent sent to Deluge successfully.')
|
||||
return self.downloadReturnId(remote_torrent)
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking Deluge download status.')
|
||||
|
||||
@@ -100,38 +103,39 @@ class Deluge(Downloader):
|
||||
|
||||
for torrent_id in queue:
|
||||
torrent = queue[torrent_id]
|
||||
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
|
||||
|
||||
# Deluge has no easy way to work out if a torrent is stalled or failing.
|
||||
#status = 'failed'
|
||||
status = 'busy'
|
||||
if torrent['is_seed'] and tryFloat(torrent['ratio']) < tryFloat(torrent['stop_ratio']):
|
||||
# We have torrent['seeding_time'] to work out what the seeding time is, but we do not
|
||||
# have access to the downloader seed_time, as with deluge we have no way to pass it
|
||||
# when the torrent is added. So Deluge will only look at the ratio.
|
||||
# See above comment in download().
|
||||
status = 'seeding'
|
||||
elif torrent['is_seed'] and torrent['is_finished'] and torrent['paused'] and torrent['state'] == 'Paused':
|
||||
status = 'completed'
|
||||
|
||||
download_dir = sp(torrent['save_path'])
|
||||
if torrent['move_on_completed']:
|
||||
download_dir = torrent['move_completed_path']
|
||||
|
||||
torrent_files = []
|
||||
for file_item in torrent['files']:
|
||||
torrent_files.append(os.path.join(download_dir), sp(file_item['path']))
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent['hash'],
|
||||
'name': torrent['name'],
|
||||
'status': status,
|
||||
'original_status': torrent['state'],
|
||||
'seed_ratio': torrent['ratio'],
|
||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||
'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])),
|
||||
'files': '|'.join(torrent_files),
|
||||
})
|
||||
if torrent['hash'] in ids:
|
||||
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
|
||||
|
||||
# Deluge has no easy way to work out if a torrent is stalled or failing.
|
||||
#status = 'failed'
|
||||
status = 'busy'
|
||||
if torrent['is_seed'] and tryFloat(torrent['ratio']) < tryFloat(torrent['stop_ratio']):
|
||||
# We have torrent['seeding_time'] to work out what the seeding time is, but we do not
|
||||
# have access to the downloader seed_time, as with deluge we have no way to pass it
|
||||
# when the torrent is added. So Deluge will only look at the ratio.
|
||||
# See above comment in download().
|
||||
status = 'seeding'
|
||||
elif torrent['is_seed'] and torrent['is_finished'] and torrent['paused'] and torrent['state'] == 'Paused':
|
||||
status = 'completed'
|
||||
|
||||
download_dir = sp(torrent['save_path'])
|
||||
if torrent['move_on_completed']:
|
||||
download_dir = torrent['move_completed_path']
|
||||
|
||||
torrent_files = []
|
||||
for file_item in torrent['files']:
|
||||
torrent_files.append(sp(os.path.join(download_dir, file_item['path'])))
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent['hash'],
|
||||
'name': torrent['name'],
|
||||
'status': status,
|
||||
'original_status': torrent['state'],
|
||||
'seed_ratio': torrent['ratio'],
|
||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||
'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])),
|
||||
'files': '|'.join(torrent_files),
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ class NZBGet(Downloader):
|
||||
|
||||
url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc'
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
if not filedata:
|
||||
@@ -30,7 +30,7 @@ class NZBGet(Downloader):
|
||||
log.info('Sending "%s" to NZBGet.', data.get('name'))
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
|
||||
nzb_name = ss('%s.nzb' % self.createNzbName(data, media))
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
@@ -67,7 +67,7 @@ class NZBGet(Downloader):
|
||||
log.error('NZBGet could not add %s to the queue.', nzb_name)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking NZBGet download status.')
|
||||
|
||||
@@ -102,51 +102,54 @@ class NZBGet(Downloader):
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
|
||||
for nzb in groups:
|
||||
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
|
||||
try:
|
||||
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
except:
|
||||
nzb_id = nzb['NZBID']
|
||||
|
||||
|
||||
timeleft = -1
|
||||
try:
|
||||
if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
||||
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||
except:
|
||||
pass
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'original_status': 'DOWNLOADING' if nzb['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': timeleft,
|
||||
})
|
||||
if nzb_id in ids:
|
||||
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
|
||||
timeleft = -1
|
||||
try:
|
||||
if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
||||
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||
except:
|
||||
pass
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'original_status': 'DOWNLOADING' if nzb['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': timeleft,
|
||||
})
|
||||
|
||||
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
|
||||
release_downloads.append({
|
||||
'id': nzb['NZBID'],
|
||||
'name': nzb['NZBFilename'],
|
||||
'original_status': nzb['Stage'],
|
||||
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
|
||||
})
|
||||
if nzb['NZBID'] in ids:
|
||||
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
|
||||
release_downloads.append({
|
||||
'id': nzb['NZBID'],
|
||||
'name': nzb['NZBFilename'],
|
||||
'original_status': nzb['Stage'],
|
||||
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
|
||||
})
|
||||
|
||||
for nzb in history:
|
||||
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
|
||||
try:
|
||||
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
except:
|
||||
nzb_id = nzb['NZBID']
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
|
||||
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(nzb['DestDir'])
|
||||
})
|
||||
|
||||
if nzb_id in ids:
|
||||
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
|
||||
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(nzb['DestDir'])
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ from uuid import uuid4
|
||||
import hashlib
|
||||
import httplib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
@@ -23,44 +25,46 @@ class NZBVortex(Downloader):
|
||||
api_level = None
|
||||
session_id = None
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
# Send the nzb
|
||||
try:
|
||||
nzb_filename = self.createFileName(data, filedata, movie)
|
||||
nzb_filename = self.createFileName(data, filedata, media)
|
||||
self.call('nzb/add', params = {'file': (nzb_filename, filedata)}, multipart = True)
|
||||
|
||||
time.sleep(10)
|
||||
raw_statuses = self.call('nzb')
|
||||
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if nzb['name'] == nzb_filename][0]
|
||||
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(item['nzbFileName']) == nzb_filename][0]
|
||||
return self.downloadReturnId(nzb_id)
|
||||
except:
|
||||
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
raw_statuses = self.call('nzb')
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
for nzb in raw_statuses.get('nzbs', []):
|
||||
if nzb['id'] in ids:
|
||||
|
||||
# Check status
|
||||
status = 'busy'
|
||||
if nzb['state'] == 20:
|
||||
status = 'completed'
|
||||
elif nzb['state'] in [21, 22, 24]:
|
||||
status = 'failed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['id'],
|
||||
'name': nzb['uiTitle'],
|
||||
'status': status,
|
||||
'original_status': nzb['state'],
|
||||
'timeleft':-1,
|
||||
'folder': sp(nzb['destinationPath']),
|
||||
})
|
||||
# Check status
|
||||
status = 'busy'
|
||||
if nzb['state'] == 20:
|
||||
status = 'completed'
|
||||
elif nzb['state'] in [21, 22, 24]:
|
||||
status = 'failed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['id'],
|
||||
'name': nzb['uiTitle'],
|
||||
'status': status,
|
||||
'original_status': nzb['state'],
|
||||
'timeleft':-1,
|
||||
'folder': sp(nzb['destinationPath']),
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ class Pneumatic(Downloader):
|
||||
protocol = ['nzb']
|
||||
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
@@ -25,7 +25,7 @@ class Pneumatic(Downloader):
|
||||
log.error('No nzb available!')
|
||||
return False
|
||||
|
||||
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
|
||||
fullPath = os.path.join(directory, self.createFileName(data, filedata, media))
|
||||
|
||||
try:
|
||||
if not os.path.isfile(fullPath):
|
||||
@@ -33,7 +33,7 @@ class Pneumatic(Downloader):
|
||||
with open(fullPath, 'wb') as f:
|
||||
f.write(filedata)
|
||||
|
||||
nzb_name = self.createNzbName(data, movie)
|
||||
nzb_name = self.createNzbName(data, media)
|
||||
strm_path = os.path.join(directory, nzb_name)
|
||||
|
||||
strm_file = open(strm_path + '.strm', 'wb')
|
||||
@@ -41,11 +41,11 @@ class Pneumatic(Downloader):
|
||||
strm_file.write(strmContent)
|
||||
strm_file.close()
|
||||
|
||||
return True
|
||||
return self.downloadReturnId('')
|
||||
|
||||
else:
|
||||
log.info('File %s already exists.', fullPath)
|
||||
return True
|
||||
return self.downloadReturnId('')
|
||||
|
||||
except:
|
||||
log.error('Failed to download .strm: %s', traceback.format_exc())
|
||||
|
||||
@@ -58,14 +58,6 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'append_label',
|
||||
'label': 'Append Label',
|
||||
'default': False,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Append label to download location. Requires you to set the download location above.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
|
||||
@@ -77,7 +77,10 @@ class rTorrent(Downloader):
|
||||
return True
|
||||
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
|
||||
|
||||
if not self.connect():
|
||||
@@ -125,9 +128,7 @@ class rTorrent(Downloader):
|
||||
if self.conf('label'):
|
||||
torrent.set_custom(1, self.conf('label'))
|
||||
|
||||
if self.conf('directory') and self.conf('append_label'):
|
||||
torrent.set_directory(os.path.join(self.conf('directory'), self.conf('label')))
|
||||
elif self.conf('directory'):
|
||||
if self.conf('directory'):
|
||||
torrent.set_directory(self.conf('directory'))
|
||||
|
||||
# Set Ratio Group
|
||||
@@ -142,7 +143,7 @@ class rTorrent(Downloader):
|
||||
log.error('Failed to send torrent to rTorrent: %s', err)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def getAllDownloadStatus(self, ids):
|
||||
log.debug('Checking rTorrent download status.')
|
||||
|
||||
if not self.connect():
|
||||
@@ -154,27 +155,28 @@ class rTorrent(Downloader):
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
|
||||
for torrent in torrents:
|
||||
torrent_files = []
|
||||
for file_item in torrent.get_files():
|
||||
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
|
||||
|
||||
status = 'busy'
|
||||
if torrent.complete:
|
||||
if torrent.active:
|
||||
status = 'seeding'
|
||||
else:
|
||||
status = 'completed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent.info_hash,
|
||||
'name': torrent.name,
|
||||
'status': status,
|
||||
'seed_ratio': torrent.ratio,
|
||||
'original_status': torrent.state,
|
||||
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
|
||||
'folder': sp(torrent.directory),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
if torrent.info_hash in ids:
|
||||
torrent_files = []
|
||||
for file_item in torrent.get_files():
|
||||
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
|
||||
|
||||
status = 'busy'
|
||||
if torrent.complete:
|
||||
if torrent.active:
|
||||
status = 'seeding'
|
||||
else:
|
||||
status = 'completed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent.info_hash,
|
||||
'name': torrent.name,
|
||||
'status': status,
|
||||
'seed_ratio': torrent.ratio,
|
||||
'original_status': torrent.state,
|
||||
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
|
||||
'folder': sp(torrent.directory),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ class Sabnzbd(Downloader):
|
||||
|
||||
protocol = ['nzb']
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
||||
@@ -25,7 +25,7 @@ class Sabnzbd(Downloader):
|
||||
req_params = {
|
||||
'cat': self.conf('category'),
|
||||
'mode': 'addurl',
|
||||
'nzbname': self.createNzbName(data, movie),
|
||||
'nzbname': self.createNzbName(data, media),
|
||||
'priority': self.conf('priority'),
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class Sabnzbd(Downloader):
|
||||
return False
|
||||
|
||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||
nzb_filename = self.createFileName(data, filedata, movie)
|
||||
nzb_filename = self.createFileName(data, filedata, media)
|
||||
req_params['mode'] = 'addfile'
|
||||
else:
|
||||
req_params['name'] = data.get('url')
|
||||
@@ -64,7 +64,7 @@ class Sabnzbd(Downloader):
|
||||
log.error('Error getting data from SABNZBd: %s', sab_data)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking SABnzbd download status.')
|
||||
|
||||
@@ -91,35 +91,36 @@ class Sabnzbd(Downloader):
|
||||
|
||||
# Get busy releases
|
||||
for nzb in queue.get('slots', []):
|
||||
status = 'busy'
|
||||
if 'ENCRYPTED / ' in nzb['filename']:
|
||||
status = 'failed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['filename'],
|
||||
'status': status,
|
||||
'original_status': nzb['status'],
|
||||
'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
|
||||
})
|
||||
if nzb['nzo_id'] in ids:
|
||||
status = 'busy'
|
||||
if 'ENCRYPTED / ' in nzb['filename']:
|
||||
status = 'failed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['filename'],
|
||||
'status': status,
|
||||
'original_status': nzb['status'],
|
||||
'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
|
||||
})
|
||||
|
||||
# Get old releases
|
||||
for nzb in history.get('slots', []):
|
||||
|
||||
status = 'busy'
|
||||
if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()):
|
||||
status = 'failed'
|
||||
elif nzb['status'] == 'Completed':
|
||||
status = 'completed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['name'],
|
||||
'status': status,
|
||||
'original_status': nzb['status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']),
|
||||
})
|
||||
if nzb['nzo_id'] in ids:
|
||||
status = 'busy'
|
||||
if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()):
|
||||
status = 'failed'
|
||||
elif nzb['status'] == 'Completed':
|
||||
status = 'completed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['name'],
|
||||
'status': status,
|
||||
'original_status': nzb['status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']),
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import isInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
import json
|
||||
import requests
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -12,8 +13,8 @@ class Synology(Downloader):
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
response = False
|
||||
@@ -34,14 +35,14 @@ class Synology(Downloader):
|
||||
elif data['protocol'] in ['nzb', 'torrent']:
|
||||
log.info('Adding %s' % data['protocol'])
|
||||
if not filedata:
|
||||
log.error('No %s data found' % data['protocol'])
|
||||
log.error('No %s data found', data['protocol'])
|
||||
else:
|
||||
filename = data['name'] + '.' + data['protocol']
|
||||
response = srpc.create_task(filename = filename, filedata = filedata)
|
||||
except Exception, err:
|
||||
log.error('Exception while adding torrent: %s', err)
|
||||
except:
|
||||
log.error('Exception while adding torrent: %s', traceback.format_exc())
|
||||
finally:
|
||||
return response
|
||||
return self.downloadReturnId('') if response else False
|
||||
|
||||
def getEnabledProtocol(self):
|
||||
if self.conf('use_for') == 'both':
|
||||
|
||||
@@ -31,7 +31,9 @@ class Transmission(Downloader):
|
||||
|
||||
return self.trpc
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol')))
|
||||
|
||||
@@ -81,7 +83,7 @@ class Transmission(Downloader):
|
||||
log.info('Torrent sent to Transmission successfully.')
|
||||
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking Transmission download status.')
|
||||
|
||||
@@ -100,31 +102,32 @@ class Transmission(Downloader):
|
||||
return False
|
||||
|
||||
for torrent in queue['torrents']:
|
||||
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s',
|
||||
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent['eta'], torrent['uploadRatio'], torrent['isFinished']))
|
||||
|
||||
torrent_files = []
|
||||
for file_item in torrent['files']:
|
||||
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name'])))
|
||||
|
||||
status = 'busy'
|
||||
if torrent.get('isStalled') and self.conf('stalled_as_failed'):
|
||||
status = 'failed'
|
||||
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
|
||||
status = 'completed'
|
||||
elif torrent['status'] in [5, 6]:
|
||||
status = 'seeding'
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent['hashString'],
|
||||
'name': torrent['name'],
|
||||
'status': status,
|
||||
'original_status': torrent['status'],
|
||||
'seed_ratio': torrent['uploadRatio'],
|
||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
if torrent['hashString'] in ids:
|
||||
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s',
|
||||
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent['eta'], torrent['uploadRatio'], torrent['isFinished']))
|
||||
|
||||
torrent_files = []
|
||||
for file_item in torrent['files']:
|
||||
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name'])))
|
||||
|
||||
status = 'busy'
|
||||
if torrent.get('isStalled') and self.conf('stalled_as_failed'):
|
||||
status = 'failed'
|
||||
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
|
||||
status = 'completed'
|
||||
elif torrent['status'] in [5, 6]:
|
||||
status = 'seeding'
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent['hashString'],
|
||||
'name': torrent['name'],
|
||||
'status': status,
|
||||
'original_status': torrent['status'],
|
||||
'seed_ratio': torrent['uploadRatio'],
|
||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ class uTorrent(Downloader):
|
||||
|
||||
return self.utorrent_api
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('protocol')))
|
||||
@@ -77,7 +77,8 @@ class uTorrent(Downloader):
|
||||
else:
|
||||
info = bdecode(filedata)["info"]
|
||||
torrent_hash = sha1(benc(info)).hexdigest().upper()
|
||||
torrent_filename = self.createFileName(data, filedata, movie)
|
||||
|
||||
torrent_filename = self.createFileName(data, filedata, media)
|
||||
|
||||
if data.get('seed_ratio'):
|
||||
torrent_params['seed_override'] = 1
|
||||
@@ -104,7 +105,7 @@ class uTorrent(Downloader):
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking uTorrent download status.')
|
||||
|
||||
@@ -129,47 +130,48 @@ class uTorrent(Downloader):
|
||||
|
||||
# Get torrents
|
||||
for torrent in queue['torrents']:
|
||||
if torrent[0] in ids:
|
||||
|
||||
#Get files of the torrent
|
||||
torrent_files = []
|
||||
try:
|
||||
torrent_files = json.loads(self.utorrent_api.get_files(torrent[0]))
|
||||
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]]
|
||||
except:
|
||||
log.debug('Failed getting files from torrent: %s', torrent[2])
|
||||
|
||||
status_flags = {
|
||||
"STARTED" : 1,
|
||||
"CHECKING" : 2,
|
||||
"CHECK-START" : 4,
|
||||
"CHECKED" : 8,
|
||||
"ERROR" : 16,
|
||||
"PAUSED" : 32,
|
||||
"QUEUED" : 64,
|
||||
"LOADED" : 128
|
||||
}
|
||||
|
||||
status = 'busy'
|
||||
if (torrent[1] & status_flags["STARTED"] or torrent[1] & status_flags["QUEUED"]) and torrent[4] == 1000:
|
||||
status = 'seeding'
|
||||
elif (torrent[1] & status_flags["ERROR"]):
|
||||
status = 'failed'
|
||||
elif torrent[4] == 1000:
|
||||
status = 'completed'
|
||||
|
||||
if not status == 'busy':
|
||||
self.removeReadOnly(torrent_files)
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent[0],
|
||||
'name': torrent[2],
|
||||
'status': status,
|
||||
'seed_ratio': float(torrent[7]) / 1000,
|
||||
'original_status': torrent[1],
|
||||
'timeleft': str(timedelta(seconds = torrent[10])),
|
||||
'folder': sp(torrent[26]),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
#Get files of the torrent
|
||||
torrent_files = []
|
||||
try:
|
||||
torrent_files = json.loads(self.utorrent_api.get_files(torrent[0]))
|
||||
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]]
|
||||
except:
|
||||
log.debug('Failed getting files from torrent: %s', torrent[2])
|
||||
|
||||
status_flags = {
|
||||
"STARTED" : 1,
|
||||
"CHECKING" : 2,
|
||||
"CHECK-START" : 4,
|
||||
"CHECKED" : 8,
|
||||
"ERROR" : 16,
|
||||
"PAUSED" : 32,
|
||||
"QUEUED" : 64,
|
||||
"LOADED" : 128
|
||||
}
|
||||
|
||||
status = 'busy'
|
||||
if (torrent[1] & status_flags["STARTED"] or torrent[1] & status_flags["QUEUED"]) and torrent[4] == 1000:
|
||||
status = 'seeding'
|
||||
elif (torrent[1] & status_flags["ERROR"]):
|
||||
status = 'failed'
|
||||
elif torrent[4] == 1000:
|
||||
status = 'completed'
|
||||
|
||||
if not status == 'busy':
|
||||
self.removeReadOnly(torrent_files)
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent[0],
|
||||
'name': torrent[2],
|
||||
'status': status,
|
||||
'seed_ratio': float(torrent[7]) / 1000,
|
||||
'original_status': torrent[1],
|
||||
'timeleft': str(timedelta(seconds = torrent[10])),
|
||||
'folder': sp(torrent[26]),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
|
||||
@@ -49,8 +49,21 @@ def ss(original, *args):
|
||||
return u_original.encode('UTF-8')
|
||||
|
||||
def sp(path, *args):
|
||||
|
||||
# Standardise encoding, normalise case, path and strip trailing '/' or '\'
|
||||
return os.path.normcase(os.path.normpath(ss(path, *args))).rstrip(os.path.sep)
|
||||
if not path or len(path) == 0:
|
||||
return path
|
||||
|
||||
# convert windows path (from remote box) to *nix path
|
||||
if os.path.sep == '/' and '\\' in path:
|
||||
path = '/' + path.replace(':', '').replace('\\', '/')
|
||||
|
||||
path = os.path.normcase(os.path.normpath(ss(path, *args)))
|
||||
|
||||
if path != os.path.sep:
|
||||
path = path.rstrip(os.path.sep)
|
||||
|
||||
return path
|
||||
|
||||
def ek(original, *args):
|
||||
if isinstance(original, (str, unicode)):
|
||||
|
||||
@@ -11,6 +11,9 @@ import sys
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
def fnEscape(pattern):
|
||||
return pattern.replace('[','[[').replace(']','[]]').replace('[[','[[]')
|
||||
|
||||
def link(src, dst):
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
|
||||
@@ -38,7 +38,7 @@ class MediaBase(Plugin):
|
||||
def notifyFront():
|
||||
db = get_session()
|
||||
media = db.query(Media).filter_by(id = media_id).first()
|
||||
fireEvent('notify.frontend', type = '%s.update.%s' % (media.type, media.id), data = media.to_dict(self.default_dict))
|
||||
fireEvent('notify.frontend', type = '%s.update' % media.type, data = media.to_dict(self.default_dict))
|
||||
db.expire_all()
|
||||
|
||||
return notifyFront
|
||||
@@ -1,10 +1,15 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media import MediaBase
|
||||
from couchpotato.core.settings.model import Media
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, Release, \
|
||||
Media
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
from sqlalchemy.sql.expression import or_, asc, not_, desc
|
||||
from string import ascii_lowercase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -20,7 +25,49 @@ class MediaPlugin(MediaBase):
|
||||
}
|
||||
})
|
||||
|
||||
addEvent('app.load', self.addSingleRefresh)
|
||||
addApiView('media.list', self.listView, docs = {
|
||||
'desc': 'List media',
|
||||
'params': {
|
||||
'type': {'type': 'string', 'desc': 'Media type to filter on.'},
|
||||
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
|
||||
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
|
||||
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
|
||||
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
|
||||
'search': {'desc': 'Search movie title'},
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'empty': bool, any movies returned or not,
|
||||
'media': array, media found,
|
||||
}"""}
|
||||
})
|
||||
|
||||
addApiView('media.get', self.getView, docs = {
|
||||
'desc': 'Get media by id',
|
||||
'params': {
|
||||
'id': {'desc': 'The id of the media'},
|
||||
}
|
||||
})
|
||||
|
||||
addApiView('media.delete', self.deleteView, docs = {
|
||||
'desc': 'Delete a media from the wanted list',
|
||||
'params': {
|
||||
'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'},
|
||||
'delete_from': {'desc': 'Delete media from this page', 'type': 'string: all (default), wanted, manage'},
|
||||
}
|
||||
})
|
||||
|
||||
addApiView('media.available_chars', self.charView)
|
||||
|
||||
addEvent('app.load', self.addSingleRefreshView)
|
||||
addEvent('app.load', self.addSingleListView)
|
||||
addEvent('app.load', self.addSingleCharView)
|
||||
addEvent('app.load', self.addSingleDeleteView)
|
||||
|
||||
addEvent('media.get', self.get)
|
||||
addEvent('media.list', self.list)
|
||||
addEvent('media.delete', self.delete)
|
||||
addEvent('media.restatus', self.restatus)
|
||||
|
||||
def refresh(self, id = '', **kwargs):
|
||||
db = get_session()
|
||||
@@ -34,7 +81,7 @@ class MediaPlugin(MediaBase):
|
||||
for title in media.library.titles:
|
||||
if title.default: default_title = title.title
|
||||
|
||||
fireEvent('notify.frontend', type = '%s.busy.%s' % (media.type, x), data = True)
|
||||
fireEvent('notify.frontend', type = '%s.busy' % media.type, data = {'id': x})
|
||||
fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||
|
||||
db.expire_all()
|
||||
@@ -43,7 +90,369 @@ class MediaPlugin(MediaBase):
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def addSingleRefresh(self):
|
||||
def addSingleRefreshView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
addApiView('%s.refresh' % media_type, self.refresh)
|
||||
|
||||
def get(self, media_id):
|
||||
|
||||
db = get_session()
|
||||
|
||||
imdb_id = getImdb(str(media_id))
|
||||
|
||||
if imdb_id:
|
||||
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
|
||||
else:
|
||||
m = db.query(Media).filter_by(id = media_id).first()
|
||||
|
||||
results = None
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return results
|
||||
|
||||
def getView(self, id = None, **kwargs):
|
||||
|
||||
media = self.get(id) if id else None
|
||||
|
||||
return {
|
||||
'success': media is not None,
|
||||
'media': media,
|
||||
}
|
||||
|
||||
def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if status and not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
if types and not isinstance(types, (list, tuple)):
|
||||
types = [types]
|
||||
|
||||
# query movie ids
|
||||
q = db.query(Media) \
|
||||
.with_entities(Media.id) \
|
||||
.group_by(Media.id)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Media.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.join(Media.releases)
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Release.status_id.in_(statuses))
|
||||
|
||||
# Filter on type
|
||||
if types and len(types) > 0:
|
||||
try: q = q.filter(Media.type.in_(types))
|
||||
except: pass
|
||||
|
||||
# Only join when searching / ordering
|
||||
if starts_with or search or order != 'release_order':
|
||||
q = q.join(Media.library, Library.titles) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
# Add search filters
|
||||
filter_or = []
|
||||
if starts_with:
|
||||
starts_with = toUnicode(starts_with.lower())
|
||||
if starts_with in ascii_lowercase:
|
||||
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
|
||||
else:
|
||||
ignore = []
|
||||
for letter in ascii_lowercase:
|
||||
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
|
||||
filter_or.append(not_(or_(*ignore)))
|
||||
|
||||
if search:
|
||||
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
||||
|
||||
if len(filter_or) > 0:
|
||||
q = q.filter(or_(*filter_or))
|
||||
|
||||
total_count = q.count()
|
||||
if total_count == 0:
|
||||
return 0, []
|
||||
|
||||
if order == 'release_order':
|
||||
q = q.order_by(desc(Release.last_edit))
|
||||
else:
|
||||
q = q.order_by(asc(LibraryTitle.simple_title))
|
||||
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q = q.limit(limit).offset(offset)
|
||||
|
||||
# Get all media_ids in sorted order
|
||||
media_ids = [m.id for m in q.all()]
|
||||
|
||||
# List release statuses
|
||||
releases = db.query(Release) \
|
||||
.filter(Release.movie_id.in_(media_ids)) \
|
||||
.all()
|
||||
|
||||
release_statuses = dict((m, set()) for m in media_ids)
|
||||
releases_count = dict((m, 0) for m in media_ids)
|
||||
for release in releases:
|
||||
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id))
|
||||
releases_count[release.movie_id] += 1
|
||||
|
||||
# Get main movie data
|
||||
q2 = db.query(Media) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
q2 = q2.filter(Media.id.in_(media_ids))
|
||||
|
||||
results = q2.all()
|
||||
|
||||
# Create dict by movie id
|
||||
movie_dict = {}
|
||||
for movie in results:
|
||||
movie_dict[movie.id] = movie
|
||||
|
||||
# List movies based on media_ids order
|
||||
movies = []
|
||||
for media_id in media_ids:
|
||||
|
||||
releases = []
|
||||
for r in release_statuses.get(media_id):
|
||||
x = splitString(r)
|
||||
releases.append({'status_id': x[0], 'quality_id': x[1]})
|
||||
|
||||
# Merge releases with movie dict
|
||||
movies.append(mergeDicts(movie_dict[media_id].to_dict({
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
}), {
|
||||
'releases': releases,
|
||||
'releases_count': releases_count.get(media_id),
|
||||
}))
|
||||
|
||||
db.expire_all()
|
||||
return total_count, movies
|
||||
|
||||
def listView(self, **kwargs):
|
||||
|
||||
types = splitString(kwargs.get('types'))
|
||||
status = splitString(kwargs.get('status'))
|
||||
release_status = splitString(kwargs.get('release_status'))
|
||||
limit_offset = kwargs.get('limit_offset')
|
||||
starts_with = kwargs.get('starts_with')
|
||||
search = kwargs.get('search')
|
||||
order = kwargs.get('order')
|
||||
|
||||
total_movies, movies = self.list(
|
||||
types = types,
|
||||
status = status,
|
||||
release_status = release_status,
|
||||
limit_offset = limit_offset,
|
||||
starts_with = starts_with,
|
||||
search = search,
|
||||
order = order
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
}
|
||||
|
||||
def addSingleListView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
def tempList(*args, **kwargs):
|
||||
return self.listView(types = media_type, *args, **kwargs)
|
||||
addApiView('%s.list' % media_type, tempList)
|
||||
|
||||
def availableChars(self, types = None, status = None, release_status = None):
|
||||
|
||||
types = types or []
|
||||
status = status or []
|
||||
release_status = release_status or []
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
if types and not isinstance(types, (list, tuple)):
|
||||
types = [types]
|
||||
|
||||
q = db.query(Media)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Media.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.join(Media.releases) \
|
||||
.filter(Release.status_id.in_(statuses))
|
||||
|
||||
# Filter on type
|
||||
if types and len(types) > 0:
|
||||
try: q = q.filter(Media.type.in_(types))
|
||||
except: pass
|
||||
|
||||
q = q.join(Library, LibraryTitle) \
|
||||
.with_entities(LibraryTitle.simple_title) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
titles = q.all()
|
||||
|
||||
chars = set()
|
||||
for title in titles:
|
||||
try:
|
||||
char = title[0][0]
|
||||
char = char if char in ascii_lowercase else '#'
|
||||
chars.add(str(char))
|
||||
except:
|
||||
log.error('Failed getting title for %s', title.libraries_id)
|
||||
|
||||
if len(chars) == 25:
|
||||
break
|
||||
|
||||
db.expire_all()
|
||||
return ''.join(sorted(chars))
|
||||
|
||||
def charView(self, **kwargs):
|
||||
|
||||
type = splitString(kwargs.get('type', 'movie'))
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
chars = self.availableChars(type, status, release_status)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(chars) == 0,
|
||||
'chars': chars,
|
||||
}
|
||||
|
||||
def addSingleCharView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
def tempChar(*args, **kwargs):
|
||||
return self.charView(types = media_type, *args, **kwargs)
|
||||
addApiView('%s.available_chars' % media_type, tempChar)
|
||||
|
||||
def delete(self, media_id, delete_from = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
media = db.query(Media).filter_by(id = media_id).first()
|
||||
if media:
|
||||
deleted = False
|
||||
if delete_from == 'all':
|
||||
db.delete(media)
|
||||
db.commit()
|
||||
deleted = True
|
||||
else:
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
total_releases = len(media.releases)
|
||||
total_deleted = 0
|
||||
new_movie_status = None
|
||||
for release in media.releases:
|
||||
if delete_from in ['wanted', 'snatched', 'late']:
|
||||
if release.status_id != done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'done'
|
||||
elif delete_from == 'manage':
|
||||
if release.status_id == done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'active'
|
||||
db.commit()
|
||||
|
||||
if total_releases == total_deleted:
|
||||
db.delete(media)
|
||||
db.commit()
|
||||
deleted = True
|
||||
elif new_movie_status:
|
||||
new_status = fireEvent('status.get', new_movie_status, single = True)
|
||||
media.profile_id = None
|
||||
media.status_id = new_status.get('id')
|
||||
db.commit()
|
||||
else:
|
||||
fireEvent('media.restatus', media.id, single = True)
|
||||
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict())
|
||||
|
||||
db.expire_all()
|
||||
return True
|
||||
|
||||
def deleteView(self, id = '', **kwargs):
|
||||
|
||||
ids = splitString(id)
|
||||
for media_id in ids:
|
||||
self.delete(media_id, delete_from = kwargs.get('delete_from', 'all'))
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def addSingleDeleteView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
def tempDelete(*args, **kwargs):
|
||||
return self.deleteView(types = media_type, *args, **kwargs)
|
||||
addApiView('%s.delete' % media_type, tempDelete)
|
||||
|
||||
def restatus(self, media_id):
|
||||
|
||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
m = db.query(Media).filter_by(id = media_id).first()
|
||||
if not m or len(m.library.titles) == 0:
|
||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||
return False
|
||||
|
||||
log.debug('Changing status for %s', m.library.titles[0].title)
|
||||
if not m.profile:
|
||||
m.status_id = done_status.get('id')
|
||||
else:
|
||||
move_to_wanted = True
|
||||
|
||||
for t in m.profile.types:
|
||||
for release in m.releases:
|
||||
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
|
||||
move_to_wanted = False
|
||||
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
|
||||
@@ -147,7 +147,7 @@ class Searcher(SearcherBase):
|
||||
except: pass
|
||||
|
||||
# Match longest name between []
|
||||
try: check_names.append(max(check_name.split('['), key = len))
|
||||
try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', check_name), key = len).strip())
|
||||
except: pass
|
||||
|
||||
for check_name in list(set(check_names)):
|
||||
|
||||
@@ -2,15 +2,10 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
|
||||
mergeDicts
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.movie import MovieTypeBase
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, Media, \
|
||||
Release
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
from sqlalchemy.sql.expression import or_, asc, not_, desc
|
||||
from string import ascii_lowercase
|
||||
from couchpotato.core.settings.model import Media
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -26,33 +21,12 @@ class MovieBase(MovieTypeBase):
|
||||
super(MovieBase, self).__init__()
|
||||
self.initType()
|
||||
|
||||
addApiView('movie.list', self.listView, docs = {
|
||||
'desc': 'List movies in wanted list',
|
||||
'params': {
|
||||
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
|
||||
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
|
||||
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
|
||||
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
|
||||
'search': {'desc': 'Search movie title'},
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'empty': bool, any movies returned or not,
|
||||
'movies': array, movies found,
|
||||
}"""}
|
||||
})
|
||||
addApiView('movie.get', self.getView, docs = {
|
||||
'desc': 'Get a movie by id',
|
||||
'params': {
|
||||
'id': {'desc': 'The id of the movie'},
|
||||
}
|
||||
})
|
||||
addApiView('movie.available_chars', self.charView)
|
||||
addApiView('movie.add', self.addView, docs = {
|
||||
'desc': 'Add new movie to the wanted list',
|
||||
'params': {
|
||||
'identifier': {'desc': 'IMDB id of the movie your want to add.'},
|
||||
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
|
||||
'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'},
|
||||
'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
|
||||
}
|
||||
})
|
||||
@@ -64,255 +38,8 @@ class MovieBase(MovieTypeBase):
|
||||
'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
|
||||
}
|
||||
})
|
||||
addApiView('movie.delete', self.deleteView, docs = {
|
||||
'desc': 'Delete a movie from the wanted list',
|
||||
'params': {
|
||||
'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'},
|
||||
'delete_from': {'desc': 'Delete movie from this page', 'type': 'string: all (default), wanted, manage'},
|
||||
}
|
||||
})
|
||||
|
||||
addEvent('movie.add', self.add)
|
||||
addEvent('movie.delete', self.delete)
|
||||
addEvent('movie.get', self.get)
|
||||
addEvent('movie.list', self.list)
|
||||
addEvent('movie.restatus', self.restatus)
|
||||
|
||||
def getView(self, id = None, **kwargs):
|
||||
|
||||
movie = self.get(id) if id else None
|
||||
|
||||
return {
|
||||
'success': movie is not None,
|
||||
'movie': movie,
|
||||
}
|
||||
|
||||
def get(self, movie_id):
|
||||
|
||||
db = get_session()
|
||||
|
||||
imdb_id = getImdb(str(movie_id))
|
||||
|
||||
if imdb_id:
|
||||
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
|
||||
else:
|
||||
m = db.query(Media).filter_by(id = movie_id).first()
|
||||
|
||||
results = None
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return results
|
||||
|
||||
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if status and not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
# query movie ids
|
||||
q = db.query(Media) \
|
||||
.with_entities(Media.id) \
|
||||
.group_by(Media.id)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Media.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.join(Media.releases)
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Release.status_id.in_(statuses))
|
||||
|
||||
# Only join when searching / ordering
|
||||
if starts_with or search or order != 'release_order':
|
||||
q = q.join(Media.library, Library.titles) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
# Add search filters
|
||||
filter_or = []
|
||||
if starts_with:
|
||||
starts_with = toUnicode(starts_with.lower())
|
||||
if starts_with in ascii_lowercase:
|
||||
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
|
||||
else:
|
||||
ignore = []
|
||||
for letter in ascii_lowercase:
|
||||
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
|
||||
filter_or.append(not_(or_(*ignore)))
|
||||
|
||||
if search:
|
||||
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
||||
|
||||
if len(filter_or) > 0:
|
||||
q = q.filter(or_(*filter_or))
|
||||
|
||||
total_count = q.count()
|
||||
if total_count == 0:
|
||||
return 0, []
|
||||
|
||||
if order == 'release_order':
|
||||
q = q.order_by(desc(Release.last_edit))
|
||||
else:
|
||||
q = q.order_by(asc(LibraryTitle.simple_title))
|
||||
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q = q.limit(limit).offset(offset)
|
||||
|
||||
# Get all movie_ids in sorted order
|
||||
movie_ids = [m.id for m in q.all()]
|
||||
|
||||
# List release statuses
|
||||
releases = db.query(Release) \
|
||||
.filter(Release.movie_id.in_(movie_ids)) \
|
||||
.all()
|
||||
|
||||
release_statuses = dict((m, set()) for m in movie_ids)
|
||||
releases_count = dict((m, 0) for m in movie_ids)
|
||||
for release in releases:
|
||||
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id))
|
||||
releases_count[release.movie_id] += 1
|
||||
|
||||
# Get main movie data
|
||||
q2 = db.query(Media) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
q2 = q2.filter(Media.id.in_(movie_ids))
|
||||
|
||||
results = q2.all()
|
||||
|
||||
# Create dict by movie id
|
||||
movie_dict = {}
|
||||
for movie in results:
|
||||
movie_dict[movie.id] = movie
|
||||
|
||||
# List movies based on movie_ids order
|
||||
movies = []
|
||||
for movie_id in movie_ids:
|
||||
|
||||
releases = []
|
||||
for r in release_statuses.get(movie_id):
|
||||
x = splitString(r)
|
||||
releases.append({'status_id': x[0], 'quality_id': x[1]})
|
||||
|
||||
# Merge releases with movie dict
|
||||
movies.append(mergeDicts(movie_dict[movie_id].to_dict({
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
}), {
|
||||
'releases': releases,
|
||||
'releases_count': releases_count.get(movie_id),
|
||||
}))
|
||||
|
||||
db.expire_all()
|
||||
return total_count, movies
|
||||
|
||||
def availableChars(self, status = None, release_status = None):
|
||||
|
||||
status = status or []
|
||||
release_status = release_status or []
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
q = db.query(Media)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Media.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.join(Media.releases) \
|
||||
.filter(Release.status_id.in_(statuses))
|
||||
|
||||
q = q.join(Library, LibraryTitle) \
|
||||
.with_entities(LibraryTitle.simple_title) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
titles = q.all()
|
||||
|
||||
chars = set()
|
||||
for title in titles:
|
||||
try:
|
||||
char = title[0][0]
|
||||
char = char if char in ascii_lowercase else '#'
|
||||
chars.add(str(char))
|
||||
except:
|
||||
log.error('Failed getting title for %s', title.libraries_id)
|
||||
|
||||
if len(chars) == 25:
|
||||
break
|
||||
|
||||
db.expire_all()
|
||||
return ''.join(sorted(chars))
|
||||
|
||||
def listView(self, **kwargs):
|
||||
|
||||
status = splitString(kwargs.get('status'))
|
||||
release_status = splitString(kwargs.get('release_status'))
|
||||
limit_offset = kwargs.get('limit_offset')
|
||||
starts_with = kwargs.get('starts_with')
|
||||
search = kwargs.get('search')
|
||||
order = kwargs.get('order')
|
||||
|
||||
total_movies, movies = self.list(
|
||||
status = status,
|
||||
release_status = release_status,
|
||||
limit_offset = limit_offset,
|
||||
starts_with = starts_with,
|
||||
search = search,
|
||||
order = order
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
}
|
||||
|
||||
def charView(self, **kwargs):
|
||||
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
chars = self.availableChars(status, release_status)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(chars) == 0,
|
||||
'chars': chars,
|
||||
}
|
||||
|
||||
def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||
if not params: params = {}
|
||||
@@ -421,9 +148,9 @@ class MovieBase(MovieTypeBase):
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
for media_id in ids:
|
||||
|
||||
m = db.query(Media).filter_by(id = movie_id).first()
|
||||
m = db.query(Media).filter_by(id = media_id).first()
|
||||
if not m:
|
||||
continue
|
||||
|
||||
@@ -446,98 +173,12 @@ class MovieBase(MovieTypeBase):
|
||||
|
||||
db.commit()
|
||||
|
||||
fireEvent('movie.restatus', m.id)
|
||||
fireEvent('media.restatus', m.id)
|
||||
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id))
|
||||
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def deleteView(self, id = '', **kwargs):
|
||||
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def delete(self, movie_id, delete_from = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
movie = db.query(Media).filter_by(id = movie_id).first()
|
||||
if movie:
|
||||
deleted = False
|
||||
if delete_from == 'all':
|
||||
db.delete(movie)
|
||||
db.commit()
|
||||
deleted = True
|
||||
else:
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
total_releases = len(movie.releases)
|
||||
total_deleted = 0
|
||||
new_movie_status = None
|
||||
for release in movie.releases:
|
||||
if delete_from in ['wanted', 'snatched', 'late']:
|
||||
if release.status_id != done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'done'
|
||||
elif delete_from == 'manage':
|
||||
if release.status_id == done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'active'
|
||||
db.commit()
|
||||
|
||||
if total_releases == total_deleted:
|
||||
db.delete(movie)
|
||||
db.commit()
|
||||
deleted = True
|
||||
elif new_movie_status:
|
||||
new_status = fireEvent('status.get', new_movie_status, single = True)
|
||||
movie.profile_id = None
|
||||
movie.status_id = new_status.get('id')
|
||||
db.commit()
|
||||
else:
|
||||
fireEvent('movie.restatus', movie.id, single = True)
|
||||
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
||||
|
||||
db.expire_all()
|
||||
return True
|
||||
|
||||
def restatus(self, movie_id):
|
||||
|
||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
m = db.query(Media).filter_by(id = movie_id).first()
|
||||
if not m or len(m.library.titles) == 0:
|
||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||
return False
|
||||
|
||||
log.debug('Changing status for %s', m.library.titles[0].title)
|
||||
if not m.profile:
|
||||
m.status_id = done_status.get('id')
|
||||
else:
|
||||
move_to_wanted = True
|
||||
|
||||
for t in m.profile.types:
|
||||
for release in m.releases:
|
||||
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
|
||||
move_to_wanted = False
|
||||
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
@@ -52,8 +52,8 @@ var MovieList = new Class({
|
||||
|
||||
self.getMovies();
|
||||
|
||||
App.addEvent('movie.added', self.movieAdded.bind(self))
|
||||
App.addEvent('movie.deleted', self.movieDeleted.bind(self))
|
||||
App.on('movie.added', self.movieAdded.bind(self))
|
||||
App.on('movie.deleted', self.movieDeleted.bind(self))
|
||||
},
|
||||
|
||||
movieDeleted: function(notification){
|
||||
@@ -65,6 +65,7 @@ var MovieList = new Class({
|
||||
movie.destroy();
|
||||
delete self.movies_added[notification.data.id];
|
||||
self.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -75,6 +76,7 @@ var MovieList = new Class({
|
||||
movieAdded: function(notification){
|
||||
var self = this;
|
||||
|
||||
self.fireEvent('movieAdded', notification);
|
||||
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
|
||||
window.scroll(0,0);
|
||||
self.createMovie(notification.data, 'top');
|
||||
@@ -279,7 +281,7 @@ var MovieList = new Class({
|
||||
|
||||
// Get available chars and highlight
|
||||
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
|
||||
Api.request('movie.available_chars', {
|
||||
Api.request('media.available_chars', {
|
||||
'data': Object.merge({
|
||||
'status': self.options.status
|
||||
}, self.filter),
|
||||
@@ -370,7 +372,7 @@ var MovieList = new Class({
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
this.set('text', 'Deleting..')
|
||||
Api.request('movie.delete', {
|
||||
Api.request('media.delete', {
|
||||
'data': {
|
||||
'id': ids.join(','),
|
||||
'delete_from': self.options.identifier
|
||||
@@ -390,6 +392,7 @@ var MovieList = new Class({
|
||||
self.movies.erase(movie);
|
||||
movie.destroy();
|
||||
self.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
});
|
||||
|
||||
self.calculateSelected();
|
||||
@@ -547,8 +550,9 @@ var MovieList = new Class({
|
||||
|
||||
}
|
||||
|
||||
Api.request(self.options.api_call || 'movie.list', {
|
||||
Api.request(self.options.api_call || 'media.list', {
|
||||
'data': Object.merge({
|
||||
'type': 'movie',
|
||||
'status': self.options.status,
|
||||
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
|
||||
}, self.filter),
|
||||
|
||||
@@ -126,7 +126,9 @@ MA.Release = new Class({
|
||||
else
|
||||
self.showHelper();
|
||||
|
||||
App.addEvent('movie.searcher.ended.'+self.movie.data.id, function(notification){
|
||||
App.on('movie.searcher.ended', function(notification){
|
||||
if(self.movie.data.id != notification.data.id) return;
|
||||
|
||||
self.releases = null;
|
||||
if(self.options_container){
|
||||
self.options_container.destroy();
|
||||
@@ -250,12 +252,14 @@ MA.Release = new Class({
|
||||
else if(!self.next_release && status.identifier == 'available'){
|
||||
self.next_release = release;
|
||||
}
|
||||
|
||||
|
||||
var update_handle = function(notification) {
|
||||
var q = self.movie.quality.getElement('.q_id' + release.quality_id),
|
||||
if(notification.data.id != release.id) return;
|
||||
|
||||
var q = self.movie.quality.getElement('.q_id' + release.quality_id),
|
||||
status = Status.get(release.status_id),
|
||||
new_status = Status.get(notification.data);
|
||||
|
||||
new_status = Status.get(notification.data.status_id);
|
||||
|
||||
release.status_id = new_status.id
|
||||
release.el.set('class', 'item ' + new_status.identifier);
|
||||
|
||||
@@ -272,7 +276,7 @@ MA.Release = new Class({
|
||||
}
|
||||
}
|
||||
|
||||
App.addEvent('release.update_status.' + release.id, update_handle);
|
||||
App.on('release.update_status', update_handle);
|
||||
|
||||
});
|
||||
|
||||
@@ -285,7 +289,7 @@ MA.Release = new Class({
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
|
||||
|
||||
|
||||
var nr = self.next_release,
|
||||
lr = self.last_release;
|
||||
|
||||
@@ -427,7 +431,7 @@ MA.Release = new Class({
|
||||
markMovieDone: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('movie.delete', {
|
||||
Api.request('media.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': 'wanted'
|
||||
@@ -446,7 +450,7 @@ MA.Release = new Class({
|
||||
|
||||
},
|
||||
|
||||
tryNextRelease: function(movie_id){
|
||||
tryNextRelease: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('movie.searcher.try_next', {
|
||||
@@ -817,7 +821,7 @@ MA.Delete = new Class({
|
||||
self.callChain();
|
||||
},
|
||||
function(){
|
||||
Api.request('movie.delete', {
|
||||
Api.request('media.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': self.movie.list.options.identifier
|
||||
|
||||
@@ -1036,7 +1036,7 @@
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 85%;
|
||||
direction: rtl;
|
||||
direction: ltr;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,23 +23,49 @@ var Movie = new Class({
|
||||
addEvents: function(){
|
||||
var self = this;
|
||||
|
||||
App.addEvent('movie.update.'+self.data.id, function(notification){
|
||||
self.global_events = {}
|
||||
|
||||
// Do refresh with new data
|
||||
self.global_events['movie.update'] = function(notification){
|
||||
if(self.data.id != notification.data.id) return;
|
||||
|
||||
self.busy(false);
|
||||
self.removeView();
|
||||
self.update.delay(2000, self, notification);
|
||||
});
|
||||
}
|
||||
App.on('movie.update', self.global_events['movie.update']);
|
||||
|
||||
// Add spinner on load / search
|
||||
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||
App.addEvent(listener+'.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
self.global_events[listener] = function(notification){
|
||||
if(notification.data && self.data.id == notification.data.id)
|
||||
self.busy(true)
|
||||
});
|
||||
}
|
||||
App.on(listener, self.global_events[listener]);
|
||||
})
|
||||
|
||||
App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
// Remove spinner
|
||||
self.global_events['movie.searcher.ended'] = function(notification){
|
||||
if(notification.data && self.data.id == notification.data.id)
|
||||
self.busy(false)
|
||||
});
|
||||
}
|
||||
App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
|
||||
|
||||
// Reload when releases have updated
|
||||
self.global_events['release.update_status'] = function(notification){
|
||||
var data = notification.data
|
||||
if(data && self.data.id == data.movie_id){
|
||||
|
||||
if(!self.data.releases)
|
||||
self.data.releases = [];
|
||||
|
||||
self.data.releases.push({'quality_id': data.quality_id, 'status_id': data.status_id});
|
||||
self.updateReleases();
|
||||
}
|
||||
}
|
||||
|
||||
App.on('release.update_status', self.global_events['release.update_status']);
|
||||
|
||||
},
|
||||
|
||||
destroy: function(){
|
||||
@@ -52,10 +78,9 @@ var Movie = new Class({
|
||||
self.list.checkIfEmpty();
|
||||
|
||||
// Remove events
|
||||
App.removeEvents('movie.update.'+self.data.id);
|
||||
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||
App.removeEvents(listener+'.'+self.data.id);
|
||||
})
|
||||
Object.each(self.global_events, function(handle, listener){
|
||||
App.off(listener, handle);
|
||||
});
|
||||
},
|
||||
|
||||
busy: function(set_busy, timeout){
|
||||
@@ -179,21 +204,7 @@ var Movie = new Class({
|
||||
});
|
||||
|
||||
// Add releases
|
||||
if(self.data.releases)
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||
status = Status.get(release.status_id);
|
||||
|
||||
if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done'))
|
||||
var q = self.addQuality(release.quality_id)
|
||||
|
||||
if (status && q && !q.hasClass(status.identifier)){
|
||||
q.addClass(status.identifier);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
|
||||
}
|
||||
|
||||
});
|
||||
self.updateReleases();
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
|
||||
@@ -203,6 +214,26 @@ var Movie = new Class({
|
||||
|
||||
},
|
||||
|
||||
updateReleases: function(){
|
||||
var self = this;
|
||||
if(!self.data.releases || self.data.releases.length == 0) return;
|
||||
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||
status = Status.get(release.status_id);
|
||||
|
||||
if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done'))
|
||||
var q = self.addQuality(release.quality_id)
|
||||
|
||||
if (status && q && !q.hasClass(status.identifier)){
|
||||
q.addClass(status.identifier);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
addQuality: function(quality_id){
|
||||
var self = this;
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ class MovieLibraryPlugin(LibraryBase):
|
||||
else:
|
||||
dates = library.info.get('release_date')
|
||||
|
||||
if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||
if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates:
|
||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||
library.info.update({'release_date': dates })
|
||||
db.commit()
|
||||
|
||||
@@ -145,10 +145,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
default_title = getTitle(movie['library'])
|
||||
if not default_title:
|
||||
log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
|
||||
fireEvent('movie.delete', movie['id'], single = True)
|
||||
fireEvent('media.delete', movie['id'], single = True)
|
||||
return
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title)
|
||||
|
||||
|
||||
ret = False
|
||||
@@ -192,7 +192,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
|
||||
else:
|
||||
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
|
||||
fireEvent('movie.restatus', movie['id'])
|
||||
fireEvent('media.restatus', movie['id'])
|
||||
break
|
||||
|
||||
# Break if CP wants to shut down
|
||||
@@ -202,7 +202,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
if len(too_early_to_search) > 0:
|
||||
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.ended.%s' % movie['id'], data = True)
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['id']})
|
||||
|
||||
return ret
|
||||
|
||||
@@ -318,14 +318,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
'success': trynext
|
||||
}
|
||||
|
||||
def tryNextRelease(self, movie_id, manual = False):
|
||||
def tryNextRelease(self, media_id, manual = False):
|
||||
|
||||
snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True)
|
||||
|
||||
try:
|
||||
db = get_session()
|
||||
rels = db.query(Release) \
|
||||
.filter_by(movie_id = movie_id) \
|
||||
.filter_by(movie_id = media_id) \
|
||||
.filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \
|
||||
.all()
|
||||
|
||||
@@ -333,7 +333,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
rel.status_id = ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||
movie_dict = fireEvent('media.get', media_id = media_id, single = True)
|
||||
log.info('Trying next release for: %s', getTitle(movie_dict['library']))
|
||||
fireEvent('movie.searcher.single', movie_dict, manual = manual)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Notification(Provider):
|
||||
listen_to = [
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
'core.message',
|
||||
'core.message.important',
|
||||
]
|
||||
dont_listen_to = []
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@ class CoreNotifier(Notification):
|
||||
|
||||
m_lock = None
|
||||
|
||||
listen_to = [
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
'core.message', 'core.message.important',
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(CoreNotifier, self).__init__()
|
||||
|
||||
@@ -121,7 +127,10 @@ class CoreNotifier(Notification):
|
||||
|
||||
for message in messages:
|
||||
if message.get('time') > last_check:
|
||||
fireEvent('core.message', message = message.get('message'), data = message)
|
||||
message['sticky'] = True # Always sticky core messages
|
||||
|
||||
message_type = 'core.message.important' if message.get('important') else 'core.message'
|
||||
fireEvent(message_type, message = message.get('message'), data = message)
|
||||
|
||||
if last_check < message.get('time'):
|
||||
last_check = message.get('time')
|
||||
|
||||
@@ -10,8 +10,8 @@ var NotificationBase = new Class({
|
||||
// Listener
|
||||
App.addEvent('unload', self.stopPoll.bind(self));
|
||||
App.addEvent('reload', self.startInterval.bind(self, [true]));
|
||||
App.addEvent('notification', self.notify.bind(self));
|
||||
App.addEvent('message', self.showMessage.bind(self));
|
||||
App.on('notification', self.notify.bind(self));
|
||||
App.on('message', self.showMessage.bind(self));
|
||||
|
||||
// Add test buttons to settings page
|
||||
App.addEvent('load', self.addTestButtons.bind(self));
|
||||
@@ -50,9 +50,9 @@ var NotificationBase = new Class({
|
||||
, 'top');
|
||||
self.notifications.include(result);
|
||||
|
||||
if(result.data.important !== undefined && !result.read){
|
||||
if((result.data.important !== undefined || result.data.sticky !== undefined) && !result.read){
|
||||
var sticky = true
|
||||
App.fireEvent('message', [result.message, sticky, result])
|
||||
App.trigger('message', [result.message, sticky, result])
|
||||
}
|
||||
else if(!result.read){
|
||||
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
|
||||
@@ -147,7 +147,7 @@ var NotificationBase = new Class({
|
||||
// Process data
|
||||
if(json){
|
||||
Array.each(json.result, function(result){
|
||||
App.fireEvent(result.type, result);
|
||||
App.trigger(result.type, result);
|
||||
if(result.message && result.read === undefined)
|
||||
self.showMessage(result.message);
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from couchpotato.environment import Env
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate, make_msgid
|
||||
import smtplib
|
||||
import traceback
|
||||
|
||||
@@ -30,6 +31,8 @@ class Email(Notification):
|
||||
message['Subject'] = self.default_title
|
||||
message['From'] = from_address
|
||||
message['To'] = to_address
|
||||
message['Date'] = formatdate(localtime = 1)
|
||||
message['Message-ID'] = make_msgid()
|
||||
|
||||
try:
|
||||
# Open the SMTP connection, via SSL if requested
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from .main import Pushbullet
|
||||
|
||||
def start():
|
||||
return Pushbullet()
|
||||
|
||||
config = [{
|
||||
'name': 'pushbullet',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'pushbullet',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'User API Key'
|
||||
},
|
||||
{
|
||||
'name': 'devices',
|
||||
'default': '',
|
||||
'advanced': True,
|
||||
'description': 'IDs of devices to send notifications to, empty = all devices'
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,86 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import base64
|
||||
import json
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Pushbullet(Notification):
|
||||
|
||||
url = 'https://api.pushbullet.com/api/%s'
|
||||
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
devices = self.getDevices()
|
||||
if devices is None:
|
||||
return False
|
||||
|
||||
# Get all the device IDs linked to this user
|
||||
if not len(devices):
|
||||
response = self.request('devices')
|
||||
if not response:
|
||||
return False
|
||||
|
||||
devices += [device.get('id') for device in response['devices']]
|
||||
|
||||
successful = 0
|
||||
for device in devices:
|
||||
response = self.request(
|
||||
'pushes',
|
||||
cache = False,
|
||||
device_id = device,
|
||||
type = 'note',
|
||||
title = self.default_title,
|
||||
body = toUnicode(message)
|
||||
)
|
||||
|
||||
if response:
|
||||
successful += 1
|
||||
else:
|
||||
log.error('Unable to push notification to Pushbullet device with ID %s' % device)
|
||||
|
||||
return successful == len(devices)
|
||||
|
||||
def getDevices(self):
|
||||
devices = [d.strip() for d in self.conf('devices').split(',')]
|
||||
|
||||
# Remove empty items
|
||||
devices = [d for d in devices if len(d)]
|
||||
|
||||
# Break on any ids that aren't integers
|
||||
valid_devices = []
|
||||
|
||||
for device_id in devices:
|
||||
d = tryInt(device_id, None)
|
||||
|
||||
if not d:
|
||||
log.error('Device ID "%s" is not valid', device_id)
|
||||
return None
|
||||
|
||||
valid_devices.append(d)
|
||||
|
||||
return valid_devices
|
||||
|
||||
def request(self, method, cache = True, **kwargs):
|
||||
try:
|
||||
base64string = base64.encodestring('%s:' % self.conf('api_key'))[:-1]
|
||||
|
||||
headers = {
|
||||
"Authorization": "Basic %s" % base64string
|
||||
}
|
||||
|
||||
if cache:
|
||||
return self.getJsonData(self.url % method, headers = headers, params = kwargs)
|
||||
else:
|
||||
data = self.urlopen(self.url % method, headers = headers, params = kwargs)
|
||||
return json.loads(data)
|
||||
|
||||
except Exception, ex:
|
||||
log.error('Pushbullet request failed')
|
||||
log.debug(ex)
|
||||
|
||||
return None
|
||||
@@ -46,6 +46,14 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.',
|
||||
},
|
||||
{
|
||||
'name': 'force_full_scan',
|
||||
'label': 'Always do a full scan',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Do a full scan instead of only the new movie. Useful if the XBMC path is different from the path CPS uses.',
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
|
||||
@@ -36,7 +36,7 @@ class XBMC(Notification):
|
||||
|
||||
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
|
||||
param = {}
|
||||
if self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0]):
|
||||
if not self.conf('force_full_scan') and (self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0])):
|
||||
param = {'directory': data['destination_dir']}
|
||||
|
||||
calls.append(('VideoLibrary.Scan', param))
|
||||
|
||||
@@ -41,7 +41,7 @@ config = [{
|
||||
'label': 'Required Genres',
|
||||
'default': '',
|
||||
'placeholder': 'Example: Action, Crime & Drama',
|
||||
'description': 'Ignore movies that don\'t contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"'
|
||||
'description': ('Ignore movies that don\'t contain at least one set of genres.', 'Sets are separated by "," and each word within a set must be separated with "&"')
|
||||
},
|
||||
{
|
||||
'name': 'ignored_genres',
|
||||
|
||||
@@ -43,7 +43,7 @@ class Automation(Plugin):
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||
movie_dict = fireEvent('media.get', movie_id, single = True)
|
||||
fireEvent('movie.searcher.single', movie_dict)
|
||||
|
||||
return True
|
||||
return True
|
||||
@@ -289,19 +289,19 @@ class Plugin(object):
|
||||
Env.get('cache').set(cache_key_md5, value, timeout)
|
||||
return value
|
||||
|
||||
def createNzbName(self, data, movie):
|
||||
tag = self.cpTag(movie)
|
||||
def createNzbName(self, data, media):
|
||||
tag = self.cpTag(media)
|
||||
return '%s%s' % (toSafeString(toUnicode(data.get('name'))[:127 - len(tag)]), tag)
|
||||
|
||||
def createFileName(self, data, filedata, movie):
|
||||
name = sp(os.path.join(self.createNzbName(data, movie)))
|
||||
def createFileName(self, data, filedata, media):
|
||||
name = sp(os.path.join(self.createNzbName(data, media)))
|
||||
if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
||||
return '%s.%s' % (name, 'rar')
|
||||
return '%s.%s' % (name, data.get('protocol'))
|
||||
|
||||
def cpTag(self, movie):
|
||||
def cpTag(self, media):
|
||||
if Env.setting('enabled', 'renamer'):
|
||||
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
|
||||
return '.cp(' + media['library'].get('identifier') + ')' if media['library'].get('identifier') else ''
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.helpers.variable import splitString, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -79,6 +79,7 @@ class Manage(Plugin):
|
||||
try:
|
||||
|
||||
directories = self.directories()
|
||||
directories.sort()
|
||||
added_identifiers = []
|
||||
|
||||
# Add some progress
|
||||
@@ -111,22 +112,20 @@ class Manage(Plugin):
|
||||
if self.conf('cleanup') and full and not self.shuttingDown():
|
||||
|
||||
# Get movies with done status
|
||||
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
|
||||
total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', single = True)
|
||||
|
||||
for done_movie in done_movies:
|
||||
if done_movie['library']['identifier'] not in added_identifiers:
|
||||
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
|
||||
fireEvent('media.delete', media_id = done_movie['id'], delete_from = 'all')
|
||||
else:
|
||||
|
||||
releases = fireEvent('release.for_movie', id = done_movie.get('id'), single = True)
|
||||
|
||||
for release in releases:
|
||||
if len(release.get('files', [])) == 0:
|
||||
fireEvent('release.delete', release['id'])
|
||||
else:
|
||||
if len(release.get('files', [])) > 0:
|
||||
for release_file in release.get('files', []):
|
||||
# Remove release not available anymore
|
||||
if not os.path.isfile(ss(release_file['path'])):
|
||||
if not os.path.isfile(sp(release_file['path'])):
|
||||
fireEvent('release.clean', release['id'])
|
||||
break
|
||||
|
||||
@@ -201,7 +200,7 @@ class Manage(Plugin):
|
||||
|
||||
self.in_progress[folder]['to_go'] -= 1
|
||||
total = self.in_progress[folder]['total']
|
||||
movie_dict = fireEvent('movie.get', identifier, single = True)
|
||||
movie_dict = fireEvent('media.get', identifier, single = True)
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library']))
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
|
||||
from couchpotato.core.helpers.variable import mergeDicts, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Quality, Profile, ProfileType
|
||||
@@ -19,14 +19,14 @@ class QualityPlugin(Plugin):
|
||||
{'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': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']},
|
||||
{'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']},
|
||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
|
||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
|
||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r')]},
|
||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p'], 'ext':['avi', 'mpg', 'mpeg'], 'tags': ['webrip', ('web', 'rip')]},
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
|
||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':[], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': ['webrip', ('web', 'rip')]},
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':[]},
|
||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':[]},
|
||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':[]},
|
||||
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]}
|
||||
]
|
||||
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
|
||||
|
||||
@@ -50,6 +50,8 @@ class QualityPlugin(Plugin):
|
||||
|
||||
addEvent('app.initialize', self.fill, priority = 10)
|
||||
|
||||
addEvent('app.test', self.doTest)
|
||||
|
||||
def preReleases(self):
|
||||
return self.pre_releases
|
||||
|
||||
@@ -165,9 +167,10 @@ class QualityPlugin(Plugin):
|
||||
if not extra: extra = {}
|
||||
|
||||
# Create hash for cache
|
||||
cache_key = md5(str([f.replace('.' + getExt(f), '') for f in files]))
|
||||
cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files])
|
||||
cached = self.getCache(cache_key)
|
||||
if cached and len(extra) == 0: return cached
|
||||
if cached and len(extra) == 0:
|
||||
return cached
|
||||
|
||||
qualities = self.all()
|
||||
|
||||
@@ -228,11 +231,6 @@ class QualityPlugin(Plugin):
|
||||
if len(set(words) & set(alt)) == len(alt):
|
||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||
score += points.get(tag_type)
|
||||
elif len(set(words) & set(alt)) > 0:
|
||||
partial = list(set(words) & set(alt))[0]
|
||||
if len(partial) > 2:
|
||||
log.debug('Found %s via partial %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||
score += points.get(tag_type) / 3
|
||||
|
||||
if (isinstance(alt, (str, unicode)) and ss(alt.lower()) in cur_file.lower()):
|
||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||
@@ -285,3 +283,33 @@ class QualityPlugin(Plugin):
|
||||
if add_score != 0:
|
||||
for allow in quality.get('allow', []):
|
||||
score[allow] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5
|
||||
|
||||
def doTest(self):
|
||||
|
||||
tests = {
|
||||
'Movie Name (1999)-DVD-Rip.avi': 'dvdrip',
|
||||
'Movie Name 1999 720p Bluray.mkv': '720p',
|
||||
'Movie Name 1999 BR-Rip 720p.avi': 'brrip',
|
||||
'Movie Name 1999 720p Web Rip.avi': 'scr',
|
||||
'Movie Name 1999 Web DL.avi': 'brrip',
|
||||
'Movie.Name.1999.1080p.WEBRip.H264-Group': 'scr',
|
||||
'Movie.Name.1999.DVDRip-Group': 'dvdrip',
|
||||
'Movie.Name.1999.DVD-Rip-Group': 'dvdrip',
|
||||
'Movie.Name.1999.DVD-R-Group': 'dvdr',
|
||||
}
|
||||
|
||||
correct = 0
|
||||
for name in tests:
|
||||
success = self.guess([name]).get('identifier') == tests[name]
|
||||
if not success:
|
||||
log.error('%s failed check, thinks it\'s %s', (name, self.guess([name]).get('identifier')))
|
||||
|
||||
correct += success
|
||||
|
||||
if correct == len(tests):
|
||||
log.info('Quality test successful')
|
||||
return True
|
||||
else:
|
||||
log.error('Quality test failed: %s out of %s succeeded', (correct, len(tests)))
|
||||
|
||||
|
||||
@@ -100,14 +100,14 @@ class Release(Plugin):
|
||||
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
|
||||
|
||||
# Add movie
|
||||
movie = db.query(Media).filter_by(library_id = group['library'].get('id')).first()
|
||||
if not movie:
|
||||
movie = Media(
|
||||
media = db.query(Media).filter_by(library_id = group['library'].get('id')).first()
|
||||
if not media:
|
||||
media = Media(
|
||||
library_id = group['library'].get('id'),
|
||||
profile_id = 0,
|
||||
status_id = done_status.get('id')
|
||||
)
|
||||
db.add(movie)
|
||||
db.add(media)
|
||||
db.commit()
|
||||
|
||||
# Add Release
|
||||
@@ -120,7 +120,7 @@ class Release(Plugin):
|
||||
if not rel:
|
||||
rel = Relea(
|
||||
identifier = identifier,
|
||||
movie = movie,
|
||||
movie = media,
|
||||
quality_id = group['meta_data']['quality'].get('id'),
|
||||
status_id = done_status.get('id')
|
||||
)
|
||||
@@ -142,7 +142,7 @@ class Release(Plugin):
|
||||
except:
|
||||
log.debug('Failed to attach "%s" to release: %s', (added_files, traceback.format_exc()))
|
||||
|
||||
fireEvent('movie.restatus', movie.id)
|
||||
fireEvent('media.restatus', media.id)
|
||||
|
||||
return True
|
||||
|
||||
@@ -269,7 +269,7 @@ class Release(Plugin):
|
||||
if filedata == 'try_next':
|
||||
return filedata
|
||||
|
||||
download_result = fireEvent('download', data = data, movie = media, manual = manual, filedata = filedata, single = True)
|
||||
download_result = fireEvent('download', data = data, media = media, manual = manual, filedata = filedata, single = True)
|
||||
log.debug('Downloader result: %s', download_result)
|
||||
|
||||
if download_result:
|
||||
@@ -288,7 +288,7 @@ class Release(Plugin):
|
||||
value = toUnicode(download_result.get(key))
|
||||
)
|
||||
rls.info.append(rls_info)
|
||||
db.commit()
|
||||
db.commit()
|
||||
|
||||
log_movie = '%s (%s) in %s' % (getTitle(media['library']), media['library']['year'], rls.quality.label)
|
||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||
@@ -446,6 +446,6 @@ class Release(Plugin):
|
||||
db.commit()
|
||||
|
||||
#Update all movie info as there is no release update function
|
||||
fireEvent('notify.frontend', type = 'release.update_status.%s' % rel.id, data = status.get('id'))
|
||||
fireEvent('notify.frontend', type = 'release.update_status', data = rel.to_dict())
|
||||
|
||||
return True
|
||||
@@ -93,7 +93,7 @@ config = [{
|
||||
'default': 1,
|
||||
'type': 'int',
|
||||
'unit': 'min(s)',
|
||||
'description': 'Detect movie status every X minutes. Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled',
|
||||
'description': ('Detect movie status every X minutes.', 'Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled'),
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
@@ -122,13 +122,13 @@ config = [{
|
||||
'advanced': True,
|
||||
'name': 'separator',
|
||||
'label': 'File-Separator',
|
||||
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
|
||||
'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'),
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
'name': 'foldersep',
|
||||
'label': 'Folder-Separator',
|
||||
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
|
||||
'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'),
|
||||
},
|
||||
{
|
||||
'name': 'file_action',
|
||||
@@ -136,7 +136,7 @@ config = [{
|
||||
'default': 'link',
|
||||
'type': 'dropdown',
|
||||
'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
|
||||
'description': '<strong>Link</strong> or <strong>Copy</strong> after downloading completed (and allow for seeding), or <strong>Move</strong> after seeding completed. Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy.',
|
||||
'description': ('<strong>Link</strong>, <strong>Copy</strong> or <strong>Move</strong> after download completed.', 'Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy. It is perfered to use link when downloading torrents as it will save you space, while still beeing able to seed.'),
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss, sp
|
||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||
getImdb, link, symlink, tryInt, splitString
|
||||
getImdb, link, symlink, tryInt, splitString, fnEscape
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, File, Profile, Release, \
|
||||
@@ -30,10 +30,10 @@ class Renamer(Plugin):
|
||||
'desc': 'For the renamer to check for new files to rename in a folder',
|
||||
'params': {
|
||||
'async': {'desc': 'Optional: Set to 1 if you dont want to fire the renamer.scan asynchronous.'},
|
||||
'base_folder': {'desc': 'Optional: The folder to find releases in. Leave empty for default folder.'},
|
||||
'movie_folder': {'desc': 'Optional: The folder of a specific release to scan. Don\'t use in combination with base_folder.'},
|
||||
'media_folder': {'desc': 'Optional: The folder of the media to scan. Keep empty for default renamer folder.'},
|
||||
'files': {'desc': 'Optional: Provide the release files if more releases are in the same media_folder, delimited with a \'|\'. Note that no dedicated release folder is expected for releases with one file.'},
|
||||
'downloader' : {'desc': 'Optional: The downloader the release has been downloaded with. \'download_id\' is required with this option.'},
|
||||
'download_id': {'desc': 'Optional: The nzb/torrent ID of the release in movie_folder. \'downloader\' is required with this option.'},
|
||||
'download_id': {'desc': 'Optional: The nzb/torrent ID of the release in media_folder. \'downloader\' is required with this option.'},
|
||||
'status': {'desc': 'Optional: The status of the release: \'completed\' (default) or \'seeding\''},
|
||||
},
|
||||
})
|
||||
@@ -64,26 +64,31 @@ class Renamer(Plugin):
|
||||
def scanView(self, **kwargs):
|
||||
|
||||
async = tryInt(kwargs.get('async', 0))
|
||||
movie_folder = sp(kwargs.get('movie_folder'))
|
||||
base_folder = sp(kwargs.get('base_folder'))
|
||||
media_folder = sp(kwargs.get('media_folder'))
|
||||
|
||||
# Backwards compatibility, to be removed after a few versions :)
|
||||
if not media_folder:
|
||||
media_folder = sp(kwargs.get('movie_folder'))
|
||||
|
||||
downloader = kwargs.get('downloader')
|
||||
download_id = kwargs.get('download_id')
|
||||
files = '|'.join([sp(filename) for filename in splitString(kwargs.get('files'), '|')])
|
||||
status = kwargs.get('status', 'completed')
|
||||
|
||||
release_download = None
|
||||
if not base_folder and movie_folder:
|
||||
release_download = {'folder': movie_folder}
|
||||
release_download.update({'id': download_id, 'downloader': downloader, 'status': status} if download_id else {})
|
||||
release_download = {'folder': media_folder} if media_folder else None
|
||||
if release_download:
|
||||
release_download.update({'id': download_id, 'downloader': downloader, 'status': status, 'files': files} if download_id else {})
|
||||
|
||||
fire_handle = fireEvent if not async else fireEventAsync
|
||||
|
||||
fire_handle('renamer.scan', base_folder = base_folder, release_download = release_download)
|
||||
fire_handle('renamer.scan', release_download)
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
||||
|
||||
def scan(self, base_folder = None, release_download = None):
|
||||
def scan(self, release_download = None):
|
||||
if not release_download: release_download = {}
|
||||
|
||||
if self.isDisabled():
|
||||
return
|
||||
@@ -92,11 +97,11 @@ class Renamer(Plugin):
|
||||
log.info('Renamer is already running, if you see this often, check the logs above for errors.')
|
||||
return
|
||||
|
||||
from_folder = sp(self.conf('from') if not base_folder else base_folder)
|
||||
from_folder = sp(self.conf('from'))
|
||||
to_folder = sp(self.conf('to'))
|
||||
|
||||
# Get movie folder to process
|
||||
movie_folder = sp(release_download and release_download.get('folder'))
|
||||
# Get media folder to process
|
||||
media_folder = release_download.get('folder')
|
||||
|
||||
# Get all folders that should not be processed
|
||||
no_process = [to_folder]
|
||||
@@ -104,7 +109,7 @@ class Renamer(Plugin):
|
||||
no_process.extend([item['destination'] for item in cat_list])
|
||||
try:
|
||||
if Env.setting('library', section = 'manage').strip():
|
||||
no_process.extend(splitString(Env.setting('library', section = 'manage'), '::'))
|
||||
no_process.extend([sp(manage_folder) for manage_folder in splitString(Env.setting('library', section = 'manage'), '::')])
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -114,40 +119,40 @@ class Renamer(Plugin):
|
||||
return
|
||||
else:
|
||||
for item in no_process:
|
||||
if from_folder in item:
|
||||
log.error('To protect your data, the movie libraries can\'t be inside of or the same as the "from" folder.')
|
||||
if '%s%s' % (from_folder, os.path.sep) in item:
|
||||
log.error('To protect your data, the media libraries can\'t be inside of or the same as the "from" folder.')
|
||||
return
|
||||
|
||||
# Check to see if the no_process folders are inside the provided movie_folder
|
||||
if movie_folder and not os.path.isdir(movie_folder):
|
||||
log.debug('The provided movie folder %s does not exist. Trying to find it in the \'from\' folder.', movie_folder)
|
||||
# Check to see if the no_process folders are inside the provided media_folder
|
||||
if media_folder and not os.path.isdir(media_folder):
|
||||
log.debug('The provided media folder %s does not exist. Trying to find it in the \'from\' folder.', media_folder)
|
||||
|
||||
# Update to the from folder
|
||||
if len(release_download.get('files')) == 1:
|
||||
new_movie_folder = from_folder
|
||||
if len(splitString(release_download.get('files'), '|')) == 1:
|
||||
new_media_folder = from_folder
|
||||
else:
|
||||
new_movie_folder = sp(os.path.join(from_folder, os.path.basename(movie_folder)))
|
||||
new_media_folder = os.path.join(from_folder, os.path.basename(media_folder))
|
||||
|
||||
if not os.path.isdir(new_movie_folder):
|
||||
log.error('The provided movie folder %s does not exist and could also not be found in the \'from\' folder.', movie_folder)
|
||||
if not os.path.isdir(new_media_folder):
|
||||
log.error('The provided media folder %s does not exist and could also not be found in the \'from\' folder.', media_folder)
|
||||
return
|
||||
|
||||
# Update the files
|
||||
new_files = [os.path.join(new_movie_folder, os.path.relpath(filename, movie_folder)) for filename in splitString(release_download.get('files'), '|')]
|
||||
new_files = [os.path.join(new_media_folder, os.path.relpath(filename, media_folder)) for filename in splitString(release_download.get('files'), '|')]
|
||||
if new_files and not os.path.isfile(new_files[0]):
|
||||
log.error('The provided movie folder %s does not exist and its files could also not be found in the \'from\' folder.', movie_folder)
|
||||
log.error('The provided media folder %s does not exist and its files could also not be found in the \'from\' folder.', media_folder)
|
||||
return
|
||||
|
||||
# Update release_download info to the from folder
|
||||
log.debug('Release %s found in the \'from\' folder.', movie_folder)
|
||||
release_download['folder'] = new_movie_folder
|
||||
log.debug('Release %s found in the \'from\' folder.', media_folder)
|
||||
release_download['folder'] = new_media_folder
|
||||
release_download['files'] = '|'.join(new_files)
|
||||
movie_folder = new_movie_folder
|
||||
media_folder = new_media_folder
|
||||
|
||||
if movie_folder:
|
||||
if media_folder:
|
||||
for item in no_process:
|
||||
if movie_folder in item:
|
||||
log.error('To protect your data, the movie libraries can\'t be inside of or the same as the provided movie folder.')
|
||||
if '%s%s' % (media_folder, os.path.sep) in item:
|
||||
log.error('To protect your data, the media libraries can\'t be inside of or the same as the provided media folder.')
|
||||
return
|
||||
|
||||
# Make sure a checkSnatched marked all downloads/seeds as such
|
||||
@@ -156,26 +161,26 @@ class Renamer(Plugin):
|
||||
|
||||
self.renaming_started = True
|
||||
|
||||
# make sure the movie folder name is included in the search
|
||||
# make sure the media folder name is included in the search
|
||||
folder = None
|
||||
files = []
|
||||
if movie_folder:
|
||||
log.info('Scanning movie folder %s...', movie_folder)
|
||||
folder = os.path.dirname(movie_folder)
|
||||
if media_folder:
|
||||
log.info('Scanning media folder %s...', media_folder)
|
||||
folder = os.path.dirname(media_folder)
|
||||
|
||||
if release_download.get('files', ''):
|
||||
files = splitString(release_download['files'], '|')
|
||||
|
||||
# If there is only one file in the torrent, the downloader did not create a subfolder
|
||||
if len(files) == 1:
|
||||
folder = movie_folder
|
||||
folder = media_folder
|
||||
else:
|
||||
# Get all files from the specified folder
|
||||
try:
|
||||
for root, folders, names in os.walk(movie_folder):
|
||||
files.extend([os.path.join(root, name) for name in names])
|
||||
for root, folders, names in os.walk(media_folder):
|
||||
files.extend([sp(os.path.join(root, name)) for name in names])
|
||||
except:
|
||||
log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc()))
|
||||
log.error('Failed getting files from %s: %s', (media_folder, traceback.format_exc()))
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -185,7 +190,7 @@ class Renamer(Plugin):
|
||||
# Unpack any archives
|
||||
extr_files = None
|
||||
if self.conf('unrar'):
|
||||
folder, movie_folder, files, extr_files = self.extractFiles(folder = folder, movie_folder = movie_folder, files = files,
|
||||
folder, media_folder, files, extr_files = self.extractFiles(folder = folder, media_folder = media_folder, files = files,
|
||||
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(release_download))
|
||||
|
||||
groups = fireEvent('scanner.scan', folder = folder if folder else from_folder,
|
||||
@@ -496,8 +501,8 @@ class Renamer(Plugin):
|
||||
if os.path.isfile(src):
|
||||
os.remove(src)
|
||||
|
||||
parent_dir = sp(os.path.dirname(src))
|
||||
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not parent_dir in [destination, movie_folder] and not from_folder in parent_dir:
|
||||
parent_dir = os.path.dirname(src)
|
||||
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not '%s%s' % (parent_dir, os.path.sep) in [destination, media_folder] and not '%s%s' % (from_folder, os.path.sep) in parent_dir:
|
||||
delete_folders.append(parent_dir)
|
||||
|
||||
except:
|
||||
@@ -529,7 +534,7 @@ class Renamer(Plugin):
|
||||
self.tagRelease(group = group, tag = 'failed_rename')
|
||||
|
||||
# Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent
|
||||
if self.movieInFromFolder(movie_folder) and self.downloadIsTorrent(release_download):
|
||||
if self.movieInFromFolder(media_folder) and self.downloadIsTorrent(release_download):
|
||||
self.tagRelease(group = group, tag = 'renamed_already')
|
||||
|
||||
# Remove matching releases
|
||||
@@ -541,12 +546,12 @@ class Renamer(Plugin):
|
||||
log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
|
||||
|
||||
if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(release_download):
|
||||
if movie_folder:
|
||||
if media_folder:
|
||||
# Delete the movie folder
|
||||
group_folder = movie_folder
|
||||
group_folder = media_folder
|
||||
else:
|
||||
# Delete the first empty subfolder in the tree relative to the 'from' folder
|
||||
group_folder = sp(os.path.join(from_folder, os.path.relpath(group['parentdir'], from_folder)).split(os.path.sep)[0])
|
||||
group_folder = sp(os.path.join(from_folder, os.path.relpath(group['parentdir'], from_folder).split(os.path.sep)[0]))
|
||||
|
||||
try:
|
||||
log.info('Deleting folder: %s', group_folder)
|
||||
@@ -606,7 +611,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
elif isinstance(release_download, dict):
|
||||
# Tag download_files if they are known
|
||||
if release_download['files']:
|
||||
tag_files = release_download['files'].split('|')
|
||||
tag_files = splitString(release_download['files'], '|')
|
||||
|
||||
# Tag all files in release folder
|
||||
else:
|
||||
@@ -614,6 +619,11 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
tag_files.extend([os.path.join(root, name) for name in names])
|
||||
|
||||
for filename in tag_files:
|
||||
|
||||
# Dont tag .ignore files
|
||||
if os.path.splitext(filename)[1] == '.ignore':
|
||||
continue
|
||||
|
||||
tag_filename = '%s.%s.ignore' % (os.path.splitext(filename)[0], tag)
|
||||
if not os.path.isfile(tag_filename):
|
||||
self.createFile(tag_filename, text)
|
||||
@@ -630,21 +640,21 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
|
||||
# Untag download_files if they are known
|
||||
if release_download['files']:
|
||||
tag_files = release_download['files'].split('|')
|
||||
tag_files = splitString(release_download['files'], '|')
|
||||
|
||||
# Untag all files in release folder
|
||||
else:
|
||||
for root, folders, names in os.walk(release_download['folder']):
|
||||
tag_files.extend([os.path.join(root, name) for name in names if not os.path.splitext(name)[1] == '.ignore'])
|
||||
tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore'])
|
||||
|
||||
# Find all .ignore files in folder
|
||||
ignore_files = []
|
||||
for root, dirnames, filenames in os.walk(folder):
|
||||
ignore_files.extend(fnmatch.filter([os.path.join(root, filename) for filename in filenames], '*%s.ignore' % tag))
|
||||
ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag))
|
||||
|
||||
# Match all found ignore files with the tag_files and delete if found
|
||||
for tag_file in tag_files:
|
||||
ignore_file = fnmatch.filter(ignore_files, '%s.%s.ignore' % (re.escape(os.path.splitext(tag_file)[0]), tag if tag else '*'))
|
||||
ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*')))
|
||||
for filename in ignore_file:
|
||||
try:
|
||||
os.remove(filename)
|
||||
@@ -664,20 +674,20 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
|
||||
# Find tag on download_files if they are known
|
||||
if release_download['files']:
|
||||
tag_files = release_download['files'].split('|')
|
||||
tag_files = splitString(release_download['files'], '|')
|
||||
|
||||
# Find tag on all files in release folder
|
||||
else:
|
||||
for root, folders, names in os.walk(release_download['folder']):
|
||||
tag_files.extend([os.path.join(root, name) for name in names if not os.path.splitext(name)[1] == '.ignore'])
|
||||
tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore'])
|
||||
|
||||
# Find all .ignore files in folder
|
||||
for root, dirnames, filenames in os.walk(folder):
|
||||
ignore_files.extend(fnmatch.filter([os.path.join(root, filename) for filename in filenames], '*%s.ignore' % tag))
|
||||
ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag))
|
||||
|
||||
# Match all found ignore files with the tag_files and return True found
|
||||
for tag_file in tag_files:
|
||||
ignore_file = fnmatch.filter(ignore_files, '%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*'))
|
||||
ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*')))
|
||||
if ignore_file:
|
||||
return True
|
||||
|
||||
@@ -772,7 +782,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
return string
|
||||
|
||||
def deleteEmptyFolder(self, folder, show_error = True):
|
||||
folder = ss(folder)
|
||||
folder = sp(folder)
|
||||
|
||||
loge = log.error if show_error else log.debug
|
||||
for root, dirs, files in os.walk(folder):
|
||||
@@ -806,126 +816,159 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
Release.status_id.in_([snatched_status.get('id'), seeding_status.get('id'), missing_status.get('id')])
|
||||
).all()
|
||||
|
||||
if not rels:
|
||||
#No releases found that need status checking
|
||||
self.checking_snatched = False
|
||||
return True
|
||||
|
||||
# Collect all download information with the download IDs from the releases
|
||||
download_ids = []
|
||||
try:
|
||||
for rel in rels:
|
||||
rel_dict = rel.to_dict({'info': {}})
|
||||
if rel_dict['info'].get('download_id') and rel_dict['info'].get('download_downloader'):
|
||||
download_ids.append({'id': rel_dict['info']['download_id'], 'downloader': rel_dict['info']['download_downloader']})
|
||||
except:
|
||||
log.error('Error getting download IDs from database')
|
||||
self.checking_snatched = False
|
||||
return False
|
||||
|
||||
release_downloads = fireEvent('download.status', download_ids, merge = True)
|
||||
if not release_downloads:
|
||||
log.debug('Download status functionality is not implemented for any active downloaders.')
|
||||
fireEvent('renamer.scan')
|
||||
|
||||
self.checking_snatched = False
|
||||
return True
|
||||
|
||||
scan_releases = []
|
||||
scan_required = False
|
||||
|
||||
if rels:
|
||||
log.debug('Checking status snatched releases...')
|
||||
log.debug('Checking status snatched releases...')
|
||||
|
||||
release_downloads = fireEvent('download.status', merge = True)
|
||||
if not release_downloads:
|
||||
log.debug('Download status functionality is not implemented for active downloaders.')
|
||||
scan_required = True
|
||||
else:
|
||||
try:
|
||||
for rel in rels:
|
||||
rel_dict = rel.to_dict({'info': {}})
|
||||
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
|
||||
try:
|
||||
for rel in rels:
|
||||
rel_dict = rel.to_dict({'info': {}})
|
||||
movie_dict = fireEvent('media.get', media_id = rel.movie_id, single = True)
|
||||
|
||||
if not isinstance(rel_dict['info'], (dict)):
|
||||
log.error('Faulty release found without any info, ignoring.')
|
||||
if not isinstance(rel_dict['info'], (dict)):
|
||||
log.error('Faulty release found without any info, ignoring.')
|
||||
fireEvent('release.update_status', rel.id, status = ignored_status, single = True)
|
||||
continue
|
||||
|
||||
# Check if download ID is available
|
||||
if not rel_dict['info'].get('download_id') or not rel_dict['info'].get('download_downloader'):
|
||||
log.debug('Download status functionality is not implemented for downloader (%s) of release %s.', (rel_dict['info'].get('download_downloader', 'unknown'), rel_dict['info']['name']))
|
||||
scan_required = True
|
||||
|
||||
# Continue with next release
|
||||
continue
|
||||
|
||||
# Find release in downloaders
|
||||
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
|
||||
|
||||
for release_download in release_downloads:
|
||||
found_release = False
|
||||
if rel_dict['info'].get('download_id'):
|
||||
if release_download['id'] == rel_dict['info']['download_id'] and release_download['downloader'] == rel_dict['info']['download_downloader']:
|
||||
log.debug('Found release by id: %s', release_download['id'])
|
||||
found_release = True
|
||||
break
|
||||
else:
|
||||
if release_download['name'] == nzbname or rel_dict['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']:
|
||||
log.debug('Found release by release name or imdb ID: %s', release_download['name'])
|
||||
found_release = True
|
||||
break
|
||||
|
||||
if not found_release:
|
||||
log.info('%s not found in downloaders', nzbname)
|
||||
|
||||
#Check status if already missing and for how long, if > 1 week, set to ignored else to missing
|
||||
if rel.status_id == missing_status.get('id'):
|
||||
if rel.last_edit < int(time.time()) - 7 * 24 * 60 * 60:
|
||||
fireEvent('release.update_status', rel.id, status = ignored_status, single = True)
|
||||
continue
|
||||
else:
|
||||
# Set the release to missing
|
||||
fireEvent('release.update_status', rel.id, status = missing_status, single = True)
|
||||
|
||||
# check status
|
||||
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
|
||||
# Continue with next release
|
||||
continue
|
||||
|
||||
found = False
|
||||
for release_download in release_downloads:
|
||||
found_release = False
|
||||
if rel_dict['info'].get('download_id'):
|
||||
if release_download['id'] == rel_dict['info']['download_id'] and release_download['downloader'] == rel_dict['info']['download_downloader']:
|
||||
log.debug('Found release by id: %s', release_download['id'])
|
||||
found_release = True
|
||||
# Log that we found the release
|
||||
timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft']
|
||||
log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft))
|
||||
|
||||
# Check status of release
|
||||
if release_download['status'] == 'busy':
|
||||
# Set the release to snatched if it was missing before
|
||||
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
|
||||
|
||||
# Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading
|
||||
if self.movieInFromFolder(release_download['folder']):
|
||||
self.tagRelease(release_download = release_download, tag = 'downloading')
|
||||
|
||||
elif release_download['status'] == 'seeding':
|
||||
#If linking setting is enabled, process release
|
||||
if self.conf('file_action') != 'move' and not rel.status_id == seeding_status.get('id') and self.statusInfoComplete(release_download):
|
||||
log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio']))
|
||||
|
||||
# Remove the downloading tag
|
||||
self.untagRelease(release_download = release_download, tag = 'downloading')
|
||||
|
||||
# Scan and set the torrent to paused if required
|
||||
release_download.update({'pause': True, 'scan': True, 'process_complete': False})
|
||||
scan_releases.append(release_download)
|
||||
else:
|
||||
#let it seed
|
||||
log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio']))
|
||||
|
||||
# Set the release to seeding
|
||||
fireEvent('release.update_status', rel.id, status = seeding_status, single = True)
|
||||
|
||||
elif release_download['status'] == 'failed':
|
||||
# Set the release to failed
|
||||
fireEvent('release.update_status', rel.id, status = failed_status, single = True)
|
||||
|
||||
fireEvent('download.remove_failed', release_download, single = True)
|
||||
|
||||
if self.conf('next_on_failed'):
|
||||
fireEvent('movie.searcher.try_next_release', media_id = rel.movie_id)
|
||||
|
||||
elif release_download['status'] == 'completed':
|
||||
log.info('Download of %s completed!', release_download['name'])
|
||||
|
||||
#Make sure the downloader sent over a path to look in
|
||||
if self.statusInfoComplete(release_download):
|
||||
|
||||
# If the release has been seeding, process now the seeding is done
|
||||
if rel.status_id == seeding_status.get('id'):
|
||||
if self.conf('file_action') != 'move':
|
||||
# Set the release to done as the movie has already been renamed
|
||||
fireEvent('release.update_status', rel.id, status = downloaded_status, single = True)
|
||||
|
||||
# Allow the downloader to clean-up
|
||||
release_download.update({'pause': False, 'scan': False, 'process_complete': True})
|
||||
scan_releases.append(release_download)
|
||||
else:
|
||||
if release_download['name'] == nzbname or rel_dict['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']:
|
||||
found_release = True
|
||||
# Scan and Allow the downloader to clean-up
|
||||
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
|
||||
scan_releases.append(release_download)
|
||||
|
||||
if found_release:
|
||||
timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft']
|
||||
log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft))
|
||||
else:
|
||||
# Set the release to snatched if it was missing before
|
||||
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
|
||||
|
||||
if release_download['status'] == 'busy':
|
||||
# Set the release to snatched if it was missing before
|
||||
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
|
||||
# Remove the downloading tag
|
||||
self.untagRelease(release_download = release_download, tag = 'downloading')
|
||||
|
||||
# Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading
|
||||
if self.movieInFromFolder(release_download['folder']):
|
||||
self.tagRelease(release_download = release_download, tag = 'downloading')
|
||||
# Scan and Allow the downloader to clean-up
|
||||
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
|
||||
scan_releases.append(release_download)
|
||||
else:
|
||||
scan_required = True
|
||||
|
||||
elif release_download['status'] == 'seeding':
|
||||
#If linking setting is enabled, process release
|
||||
if self.conf('file_action') != 'move' and not rel.status_id == seeding_status.get('id') and self.statusInfoComplete(release_download):
|
||||
log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio']))
|
||||
|
||||
# Remove the downloading tag
|
||||
self.untagRelease(release_download = release_download, tag = 'downloading')
|
||||
|
||||
# Scan and set the torrent to paused if required
|
||||
release_download.update({'pause': True, 'scan': True, 'process_complete': False})
|
||||
scan_releases.append(release_download)
|
||||
else:
|
||||
#let it seed
|
||||
log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio']))
|
||||
|
||||
# Set the release to seeding
|
||||
fireEvent('release.update_status', rel.id, status = seeding_status, single = True)
|
||||
|
||||
elif release_download['status'] == 'failed':
|
||||
# Set the release to failed
|
||||
fireEvent('release.update_status', rel.id, status = failed_status, single = True)
|
||||
|
||||
fireEvent('download.remove_failed', release_download, single = True)
|
||||
|
||||
if self.conf('next_on_failed'):
|
||||
fireEvent('movie.searcher.try_next_release', movie_id = rel.movie_id)
|
||||
elif release_download['status'] == 'completed':
|
||||
log.info('Download of %s completed!', release_download['name'])
|
||||
if self.statusInfoComplete(release_download):
|
||||
|
||||
# If the release has been seeding, process now the seeding is done
|
||||
if rel.status_id == seeding_status.get('id'):
|
||||
if self.conf('file_action') != 'move':
|
||||
# Set the release to done as the movie has already been renamed
|
||||
fireEvent('release.update_status', rel.id, status = downloaded_status, single = True)
|
||||
|
||||
# Allow the downloader to clean-up
|
||||
release_download.update({'pause': False, 'scan': False, 'process_complete': True})
|
||||
scan_releases.append(release_download)
|
||||
else:
|
||||
# Scan and Allow the downloader to clean-up
|
||||
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
|
||||
scan_releases.append(release_download)
|
||||
|
||||
else:
|
||||
# Set the release to snatched if it was missing before
|
||||
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
|
||||
|
||||
# Remove the downloading tag
|
||||
self.untagRelease(release_download = release_download, tag = 'downloading')
|
||||
|
||||
# Scan and Allow the downloader to clean-up
|
||||
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
|
||||
scan_releases.append(release_download)
|
||||
else:
|
||||
scan_required = True
|
||||
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
log.info('%s not found in downloaders', nzbname)
|
||||
|
||||
#Check status if already missing and for how long, if > 1 week, set to ignored else to missing
|
||||
if rel.status_id == missing_status.get('id'):
|
||||
if rel.last_edit < int(time.time()) - 7 * 24 * 60 * 60:
|
||||
fireEvent('release.update_status', rel.id, status = ignored_status, single = True)
|
||||
else:
|
||||
# Set the release to missing
|
||||
fireEvent('release.update_status', rel.id, status = missing_status, single = True)
|
||||
|
||||
except:
|
||||
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
|
||||
except:
|
||||
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
|
||||
|
||||
# The following can either be done here, or inside the scanner if we pass it scan_items in one go
|
||||
for release_download in scan_releases:
|
||||
@@ -948,7 +991,6 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
fireEvent('renamer.scan')
|
||||
|
||||
self.checking_snatched = False
|
||||
|
||||
return True
|
||||
|
||||
def extendReleaseDownload(self, release_download):
|
||||
@@ -995,10 +1037,10 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
def statusInfoComplete(self, release_download):
|
||||
return release_download['id'] and release_download['downloader'] and release_download['folder']
|
||||
|
||||
def movieInFromFolder(self, movie_folder):
|
||||
return (movie_folder and sp(self.conf('from')) in movie_folder) or not movie_folder
|
||||
def movieInFromFolder(self, media_folder):
|
||||
return media_folder and '%s%s' % (sp(self.conf('from')), os.path.sep) in sp(media_folder) or not media_folder
|
||||
|
||||
def extractFiles(self, folder = None, movie_folder = None, files = None, cleanup = False):
|
||||
def extractFiles(self, folder = None, media_folder = None, files = None, cleanup = False):
|
||||
if not files: files = []
|
||||
|
||||
# RegEx for finding rar files
|
||||
@@ -1013,12 +1055,12 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
folder = from_folder
|
||||
|
||||
check_file_date = True
|
||||
if movie_folder:
|
||||
if media_folder:
|
||||
check_file_date = False
|
||||
|
||||
if not files:
|
||||
for root, folders, names in os.walk(folder):
|
||||
files.extend([os.path.join(root, name) for name in names])
|
||||
files.extend([sp(os.path.join(root, name)) for name in names])
|
||||
|
||||
# Find all archive files
|
||||
archives = [re.search(archive_regex, name).groupdict() for name in files if re.search(archive_regex, name)]
|
||||
@@ -1064,13 +1106,13 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
log.info('Archive %s found. Extracting...', os.path.basename(archive['file']))
|
||||
try:
|
||||
rar_handle = RarFile(archive['file'])
|
||||
extr_path = sp(os.path.join(from_folder, os.path.relpath(os.path.dirname(archive['file']), folder)))
|
||||
extr_path = os.path.join(from_folder, os.path.relpath(os.path.dirname(archive['file']), folder))
|
||||
self.makeDir(extr_path)
|
||||
for packedinfo in rar_handle.infolist():
|
||||
if not packedinfo.isdir and not os.path.isfile(os.path.join(extr_path, os.path.basename(packedinfo.filename))):
|
||||
if not packedinfo.isdir and not os.path.isfile(sp(os.path.join(extr_path, os.path.basename(packedinfo.filename)))):
|
||||
log.debug('Extracting %s...', packedinfo.filename)
|
||||
rar_handle.extract(condition = [packedinfo.index], path = extr_path, withSubpath = False, overwrite = False)
|
||||
extr_files.append(os.path.join(extr_path, os.path.basename(packedinfo.filename)))
|
||||
extr_files.append(sp(os.path.join(extr_path, os.path.basename(packedinfo.filename))))
|
||||
del rar_handle
|
||||
except Exception, e:
|
||||
log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc()))
|
||||
@@ -1087,9 +1129,9 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
files.remove(filename)
|
||||
|
||||
# Move the rest of the files and folders if any files are extracted to the from folder (only if folder was provided)
|
||||
if extr_files and sp(folder) != from_folder:
|
||||
if extr_files and folder != from_folder:
|
||||
for leftoverfile in list(files):
|
||||
move_to = sp(os.path.join(from_folder, os.path.relpath(leftoverfile, folder)))
|
||||
move_to = os.path.join(from_folder, os.path.relpath(leftoverfile, folder))
|
||||
|
||||
try:
|
||||
self.makeDir(os.path.dirname(move_to))
|
||||
@@ -1109,18 +1151,18 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
|
||||
if cleanup:
|
||||
# Remove all left over folders
|
||||
log.debug('Removing old movie folder %s...', movie_folder)
|
||||
self.deleteEmptyFolder(movie_folder)
|
||||
log.debug('Removing old movie folder %s...', media_folder)
|
||||
self.deleteEmptyFolder(media_folder)
|
||||
|
||||
movie_folder = sp(os.path.join(from_folder, os.path.relpath(movie_folder, folder)))
|
||||
media_folder = os.path.join(from_folder, os.path.relpath(media_folder, folder))
|
||||
folder = from_folder
|
||||
|
||||
if extr_files:
|
||||
files.extend(extr_files)
|
||||
|
||||
# Cleanup files and folder if movie_folder was not provided
|
||||
if not movie_folder:
|
||||
# Cleanup files and folder if media_folder was not provided
|
||||
if not media_folder:
|
||||
files = []
|
||||
folder = None
|
||||
|
||||
return folder, movie_folder, files, extr_files
|
||||
return folder, media_folder, files, extr_files
|
||||
@@ -124,7 +124,7 @@ class Scanner(Plugin):
|
||||
try:
|
||||
files = []
|
||||
for root, dirs, walk_files in os.walk(folder):
|
||||
files.extend([os.path.join(root, filename) for filename in walk_files])
|
||||
files.extend([sp(os.path.join(root, filename)) for filename in walk_files])
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
@@ -134,7 +134,7 @@ class Scanner(Plugin):
|
||||
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
|
||||
else:
|
||||
check_file_date = False
|
||||
files = [ss(x) for x in files]
|
||||
files = [sp(x) for x in files]
|
||||
|
||||
|
||||
for file_path in files:
|
||||
@@ -454,7 +454,7 @@ class Scanner(Plugin):
|
||||
data['resolution_width'] = meta.get('resolution_width', 720)
|
||||
data['resolution_height'] = meta.get('resolution_height', 480)
|
||||
data['audio_channels'] = meta.get('audio_channels', 2.0)
|
||||
data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480)
|
||||
data['aspect'] = round(float(meta.get('resolution_width', 720)) / meta.get('resolution_height', 480), 2)
|
||||
except:
|
||||
log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
|
||||
pass
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import getTitle, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \
|
||||
sizeScore, providerScore, duplicateScore, partialIgnoredScore, namePositionScore, \
|
||||
halfMultipartScore
|
||||
halfMultipartScore, sceneScore
|
||||
from couchpotato.environment import Env
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -62,4 +62,7 @@ class Score(Plugin):
|
||||
if extra_score:
|
||||
score += extra_score(nzb)
|
||||
|
||||
# Scene / Nuke scoring
|
||||
score += sceneScore(nzb['name'])
|
||||
|
||||
return score
|
||||
@@ -1,8 +1,13 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
import re
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
name_scores = [
|
||||
# Tags
|
||||
@@ -160,3 +165,38 @@ def halfMultipartScore(nzb_name):
|
||||
return -30
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def sceneScore(nzb_name):
|
||||
|
||||
check_names = [nzb_name]
|
||||
|
||||
# Match names between "
|
||||
try: check_names.append(re.search(r'([\'"])[^\1]*\1', nzb_name).group(0))
|
||||
except: pass
|
||||
|
||||
# Match longest name between []
|
||||
try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', nzb_name), key = len).strip())
|
||||
except: pass
|
||||
|
||||
for name in check_names:
|
||||
|
||||
# Strip twice, remove possible file extensions
|
||||
name = name.lower().strip(' "\'\.-_\[\]')
|
||||
name = re.sub('\.([a-z0-9]{0,4})$', '', name)
|
||||
name = name.strip(' "\'\.-_\[\]')
|
||||
|
||||
# Make sure year and groupname is in there
|
||||
year = re.findall('(?P<year>19[0-9]{2}|20[0-9]{2})', name)
|
||||
group = re.findall('\-([a-z0-9]+)$', name)
|
||||
|
||||
if len(year) > 0 and len(group) > 0:
|
||||
try:
|
||||
validate = fireEvent('release.validate', name, single = True)
|
||||
if validate and tryInt(validate.get('score')) != 0:
|
||||
log.debug('Release "%s" scored %s, reason: %s', (nzb_name, validate['score'], validate['reasons']))
|
||||
return tryInt(validate.get('score'))
|
||||
except:
|
||||
log.error('Failed scoring scene: %s', traceback.format_exc())
|
||||
|
||||
return 0
|
||||
@@ -20,7 +20,7 @@ config = [{
|
||||
},
|
||||
{
|
||||
'name': 'languages',
|
||||
'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>',
|
||||
'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',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.encoding import toUnicode, sp
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -58,9 +58,9 @@ class Subtitle(Plugin):
|
||||
|
||||
for d_sub in downloaded:
|
||||
log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files))
|
||||
group['files']['subtitle'].append(d_sub.path)
|
||||
group['before_rename'].append(d_sub.path)
|
||||
group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2]
|
||||
group['files']['subtitle'].append(sp(d_sub.path))
|
||||
group['before_rename'].append(sp(d_sub.path))
|
||||
group['subtitle_language'][sp(d_sub.path)] = [d_sub.language.alpha2]
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -264,14 +264,14 @@ class ResultList(list):
|
||||
|
||||
result_ids = None
|
||||
provider = None
|
||||
movie = None
|
||||
media = None
|
||||
quality = None
|
||||
|
||||
def __init__(self, provider, movie, quality, **kwargs):
|
||||
def __init__(self, provider, media, quality, **kwargs):
|
||||
|
||||
self.result_ids = []
|
||||
self.provider = provider
|
||||
self.movie = movie
|
||||
self.media = media
|
||||
self.quality = quality
|
||||
self.kwargs = kwargs
|
||||
|
||||
@@ -285,13 +285,13 @@ class ResultList(list):
|
||||
|
||||
new_result = self.fillResult(result)
|
||||
|
||||
is_correct = fireEvent('searcher.correct_release', new_result, self.movie, self.quality,
|
||||
is_correct = fireEvent('searcher.correct_release', new_result, self.media, self.quality,
|
||||
imdb_results = self.kwargs.get('imdb_results', False), single = True)
|
||||
|
||||
if is_correct and new_result['id'] not in self.result_ids:
|
||||
is_correct_weight = float(is_correct)
|
||||
|
||||
new_result['score'] += fireEvent('score.calculate', new_result, self.movie, single = True)
|
||||
new_result['score'] += fireEvent('score.calculate', new_result, self.media, single = True)
|
||||
|
||||
old_score = new_result['score']
|
||||
new_result['score'] = int(old_score * is_correct_weight)
|
||||
|
||||
@@ -93,11 +93,11 @@ class MovieResultModifier(Plugin):
|
||||
|
||||
for movie in l.movies:
|
||||
if movie.status_id == active_status['id']:
|
||||
temp['in_wanted'] = fireEvent('movie.get', movie.id, single = True)
|
||||
temp['in_wanted'] = fireEvent('media.get', movie.id, single = True)
|
||||
|
||||
for release in movie.releases:
|
||||
if release.status_id == done_status['id']:
|
||||
temp['in_library'] = fireEvent('movie.get', movie.id, single = True)
|
||||
temp['in_library'] = fireEvent('media.get', movie.id, single = True)
|
||||
except:
|
||||
log.error('Tried getting more info on searched movies: %s', traceback.format_exc())
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.info.base import MovieProvider
|
||||
from couchpotato.environment import Env
|
||||
import base64
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -11,6 +12,7 @@ log = CPLog(__name__)
|
||||
class CouchPotatoApi(MovieProvider):
|
||||
|
||||
urls = {
|
||||
'validate': 'https://api.couchpota.to/validate/%s/',
|
||||
'search': 'https://api.couchpota.to/search/%s/',
|
||||
'info': 'https://api.couchpota.to/info/%s/',
|
||||
'is_movie': 'https://api.couchpota.to/ismovie/%s/',
|
||||
@@ -30,6 +32,8 @@ class CouchPotatoApi(MovieProvider):
|
||||
addEvent('movie.suggest', self.getSuggestions)
|
||||
addEvent('movie.is_movie', self.isMovie)
|
||||
|
||||
addEvent('release.validate', self.validate)
|
||||
|
||||
addEvent('cp.source_url', self.getSourceUrl)
|
||||
addEvent('cp.messages', self.getMessages)
|
||||
|
||||
@@ -51,6 +55,14 @@ class CouchPotatoApi(MovieProvider):
|
||||
def search(self, q, limit = 5):
|
||||
return self.getJsonData(self.urls['search'] % tryUrlencode(q) + ('?limit=%s' % limit), headers = self.getRequestHeaders())
|
||||
|
||||
def validate(self, name = None):
|
||||
|
||||
if not name:
|
||||
return
|
||||
|
||||
name_enc = base64.b64encode(name)
|
||||
return self.getJsonData(self.urls['validate'] % name_enc, headers = self.getRequestHeaders())
|
||||
|
||||
def isMovie(self, identifier = None):
|
||||
|
||||
if not identifier:
|
||||
|
||||
@@ -84,6 +84,10 @@ class OMDBAPI(MovieProvider):
|
||||
|
||||
year = tryInt(movie.get('Year', ''))
|
||||
|
||||
actors = {}
|
||||
for actor in splitString(movie.get('Actors', '')):
|
||||
actors[actor] = '' #omdb does not return actor roles
|
||||
|
||||
movie_data = {
|
||||
'type': 'movie',
|
||||
'via_imdb': True,
|
||||
@@ -105,7 +109,7 @@ class OMDBAPI(MovieProvider):
|
||||
'genres': splitString(movie.get('Genre', '')),
|
||||
'directors': splitString(movie.get('Director', '')),
|
||||
'writers': splitString(movie.get('Writer', '')),
|
||||
'actors': splitString(movie.get('Actors', '')),
|
||||
'actor_roles': actors,
|
||||
}
|
||||
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
|
||||
except:
|
||||
|
||||
@@ -92,6 +92,13 @@ class TheMovieDb(MovieProvider):
|
||||
poster_original = self.getImage(movie, type = 'poster', size = 'original')
|
||||
backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
|
||||
|
||||
images = {
|
||||
'poster': [poster] if poster else [],
|
||||
#'backdrop': [backdrop] if backdrop else [],
|
||||
'poster_original': [poster_original] if poster_original else [],
|
||||
'backdrop_original': [backdrop_original] if backdrop_original else [],
|
||||
}
|
||||
|
||||
# Genres
|
||||
try:
|
||||
genres = [genre.name for genre in movie.genres]
|
||||
@@ -103,18 +110,22 @@ class TheMovieDb(MovieProvider):
|
||||
if not movie.releasedate or year == '1900' or year.lower() == 'none':
|
||||
year = None
|
||||
|
||||
# Gather actors data
|
||||
actors = {}
|
||||
for cast_item in movie.cast:
|
||||
try:
|
||||
actors[toUnicode(cast_item.name)] = toUnicode(cast_item.character)
|
||||
images['actor %s' % toUnicode(cast_item.name)] = self.getImage(cast_item, type = 'profile', size = 'original')
|
||||
except:
|
||||
log.debug('Error getting cast info for %s: %s', (cast_item, traceback.format_exc()))
|
||||
|
||||
movie_data = {
|
||||
'type': 'movie',
|
||||
'via_tmdb': True,
|
||||
'tmdb_id': movie.id,
|
||||
'titles': [toUnicode(movie.title)],
|
||||
'original_title': movie.originaltitle,
|
||||
'images': {
|
||||
'poster': [poster] if poster else [],
|
||||
#'backdrop': [backdrop] if backdrop else [],
|
||||
'poster_original': [poster_original] if poster_original else [],
|
||||
'backdrop_original': [backdrop_original] if backdrop_original else [],
|
||||
},
|
||||
'images': images,
|
||||
'imdb': movie.imdb,
|
||||
'runtime': movie.runtime,
|
||||
'released': str(movie.releasedate),
|
||||
@@ -122,6 +133,7 @@ class TheMovieDb(MovieProvider):
|
||||
'plot': movie.overview,
|
||||
'genres': genres,
|
||||
'collection': getattr(movie.collection, 'name', None),
|
||||
'actor_roles': actors
|
||||
}
|
||||
|
||||
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
|
||||
@@ -145,7 +157,7 @@ class TheMovieDb(MovieProvider):
|
||||
try:
|
||||
image_url = getattr(movie, type).geturl(size = 'original')
|
||||
except:
|
||||
log.debug('Failed getting %s.%s for "%s"', (type, size, movie.title))
|
||||
log.debug('Failed getting %s.%s for "%s"', (type, size, movie))
|
||||
|
||||
return image_url
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class XBMC(MetaDataBase):
|
||||
name = type
|
||||
|
||||
try:
|
||||
if data['library'].get(type):
|
||||
if movie_info.get(type):
|
||||
el = SubElement(nfoxml, name)
|
||||
el.text = toUnicode(movie_info.get(type, ''))
|
||||
except:
|
||||
@@ -89,10 +89,18 @@ class XBMC(MetaDataBase):
|
||||
genres.text = toUnicode(genre)
|
||||
|
||||
# Actors
|
||||
for actor in movie_info.get('actors', []):
|
||||
actors = SubElement(nfoxml, 'actor')
|
||||
name = SubElement(actors, 'name')
|
||||
name.text = toUnicode(actor)
|
||||
for actor_name in movie_info.get('actor_roles', {}):
|
||||
role_name = movie_info['actor_roles'][actor_name]
|
||||
|
||||
actor = SubElement(nfoxml, 'actor')
|
||||
name = SubElement(actor, 'name')
|
||||
name.text = toUnicode(actor_name)
|
||||
if role_name:
|
||||
role = SubElement(actor, 'role')
|
||||
role.text = toUnicode(role_name)
|
||||
if movie_info['images'].get('actor %s' % actor_name, ''):
|
||||
thumb = SubElement(actor, 'thumb')
|
||||
thumb.text = toUnicode(movie_info['images'].get('actor %s' % actor_name))
|
||||
|
||||
# Directors
|
||||
for director_name in movie_info.get('directors', []):
|
||||
@@ -112,6 +120,51 @@ class XBMC(MetaDataBase):
|
||||
sorttitle = SubElement(nfoxml, 'sorttitle')
|
||||
sorttitle.text = '%s %s' % (toUnicode(collection_name), movie_info.get('year'))
|
||||
|
||||
# Images
|
||||
for image_url in movie_info['images']['poster_original']:
|
||||
image = SubElement(nfoxml, 'thumb')
|
||||
image.text = toUnicode(image_url)
|
||||
fanart = SubElement(nfoxml, 'fanart')
|
||||
for image_url in movie_info['images']['backdrop_original']:
|
||||
image = SubElement(fanart, 'thumb')
|
||||
image.text = toUnicode(image_url)
|
||||
|
||||
# Add trailer if found
|
||||
trailer_found = False
|
||||
if data.get('renamed_files'):
|
||||
for filename in data.get('renamed_files'):
|
||||
if 'trailer' in filename:
|
||||
trailer = SubElement(nfoxml, 'trailer')
|
||||
trailer.text = toUnicode(filename)
|
||||
trailer_found = True
|
||||
if not trailer_found and data['files'].get('trailer'):
|
||||
trailer = SubElement(nfoxml, 'trailer')
|
||||
trailer.text = toUnicode(data['files']['trailer'][0])
|
||||
|
||||
# Add file metadata
|
||||
fileinfo = SubElement(nfoxml, 'fileinfo')
|
||||
streamdetails = SubElement(fileinfo, 'streamdetails')
|
||||
|
||||
# Video data
|
||||
if data['meta_data'].get('video'):
|
||||
video = SubElement(streamdetails, 'video')
|
||||
codec = SubElement(video, 'codec')
|
||||
codec.text = toUnicode(data['meta_data']['video'])
|
||||
aspect = SubElement(video, 'aspect')
|
||||
aspect.text = str(data['meta_data']['aspect'])
|
||||
width = SubElement(video, 'width')
|
||||
width.text = str(data['meta_data']['resolution_width'])
|
||||
height = SubElement(video, 'height')
|
||||
height.text = str(data['meta_data']['resolution_height'])
|
||||
|
||||
# Audio data
|
||||
if data['meta_data'].get('audio'):
|
||||
audio = SubElement(streamdetails, 'audio')
|
||||
codec = SubElement(audio, 'codec')
|
||||
codec.text = toUnicode(data['meta_data'].get('audio'))
|
||||
channels = SubElement(audio, 'channels')
|
||||
channels.text = toUnicode(data['meta_data'].get('audio_channels'))
|
||||
|
||||
# Clean up the xml and return it
|
||||
nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
|
||||
xml_string = nfoxml.toprettyxml(indent = ' ')
|
||||
|
||||
@@ -10,6 +10,7 @@ from urllib2 import HTTPError
|
||||
from urlparse import urlparse
|
||||
import time
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -159,7 +160,15 @@ class Newznab(NZBProvider, RSS):
|
||||
return 'try_next'
|
||||
|
||||
try:
|
||||
data = self.urlopen(url, show_error = False)
|
||||
# Get final redirected url
|
||||
log.debug('Checking %s for redirects.', url)
|
||||
req = urllib2.Request(url)
|
||||
res = urllib2.urlopen(req)
|
||||
finalurl = res.geturl()
|
||||
if finalurl != url:
|
||||
log.debug('Redirect url used: %s', finalurl)
|
||||
|
||||
data = self.urlopen(finalurl, show_error = False)
|
||||
self.limits_reached[host] = False
|
||||
return data
|
||||
except HTTPError, e:
|
||||
|
||||
@@ -4,7 +4,7 @@ def start():
|
||||
return BiTHDTV()
|
||||
|
||||
config = [{
|
||||
'name': 'BiT-HDTV',
|
||||
'name': 'bithdtv',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from .main import TorrentPotato
|
||||
|
||||
def start():
|
||||
return TorrentPotato()
|
||||
|
||||
config = [{
|
||||
'name': 'torrentpotato',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentPotato',
|
||||
'order': 10,
|
||||
'description': 'CouchPotato torrent provider. Checkout <a href="https://github.com/RuudBurger/CouchPotatoServer/wiki/CouchPotato-Torrent-Provider">the wiki page about this provider</a> for more info.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'use',
|
||||
'default': ''
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': '',
|
||||
'description': 'The url path of your TorrentPotato provider.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'default': '0',
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
},
|
||||
{
|
||||
'name': 'name',
|
||||
'label': 'Username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'default': '1',
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'default': '40',
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'pass_key',
|
||||
'default': ',',
|
||||
'label': 'Pass Key',
|
||||
'description': 'Can be found on your profile page',
|
||||
'type': 'combined',
|
||||
'combine': ['use', 'host', 'pass_key', 'name', 'seed_ratio', 'seed_time', 'extra_score'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,129 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import ResultList
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
from urlparse import urlparse
|
||||
import re
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class TorrentPotato(TorrentProvider):
|
||||
|
||||
urls = {}
|
||||
limits_reached = {}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
def search(self, movie, quality):
|
||||
hosts = self.getHosts()
|
||||
|
||||
results = ResultList(self, movie, quality, imdb_results = True)
|
||||
|
||||
for host in hosts:
|
||||
if self.isDisabled(host):
|
||||
continue
|
||||
|
||||
self._searchOnHost(host, movie, quality, results)
|
||||
|
||||
return results
|
||||
|
||||
def _searchOnHost(self, host, movie, quality, results):
|
||||
|
||||
arguments = tryUrlencode({
|
||||
'user': host['name'],
|
||||
'passkey': host['pass_key'],
|
||||
'imdbid': movie['library']['identifier']
|
||||
})
|
||||
url = '%s?%s' % (host['host'], arguments)
|
||||
|
||||
torrents = self.getJsonData(url, cache_timeout = 1800)
|
||||
|
||||
if torrents:
|
||||
try:
|
||||
if torrents.get('error'):
|
||||
log.error('%s: %s', (torrents.get('error'), host['host']))
|
||||
elif torrents.get('results'):
|
||||
for torrent in torrents.get('results', []):
|
||||
results.append({
|
||||
'id': torrent.get('torrent_id'),
|
||||
'protocol': 'torrent' if re.match('^(http|https|ftp)://.*$', torrent.get('download_url')) else 'torrent_magnet',
|
||||
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
||||
'name': toUnicode(torrent.get('release_name')),
|
||||
'url': torrent.get('download_url'),
|
||||
'detail_url': torrent.get('details_url'),
|
||||
'size': torrent.get('size'),
|
||||
'score': host['extra_score'],
|
||||
'seeders': torrent.get('seeders'),
|
||||
'leechers': torrent.get('leechers'),
|
||||
'seed_ratio': host['seed_ratio'],
|
||||
'seed_time': host['seed_time'],
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (host['host'], traceback.format_exc()))
|
||||
|
||||
def getHosts(self):
|
||||
|
||||
uses = splitString(str(self.conf('use')), clean = False)
|
||||
hosts = splitString(self.conf('host'), clean = False)
|
||||
names = splitString(self.conf('name'), clean = False)
|
||||
seed_times = splitString(self.conf('seed_time'), clean = False)
|
||||
seed_ratios = splitString(self.conf('seed_ratio'), clean = False)
|
||||
pass_keys = splitString(self.conf('pass_key'), clean = False)
|
||||
extra_score = splitString(self.conf('extra_score'), clean = False)
|
||||
|
||||
list = []
|
||||
for nr in range(len(hosts)):
|
||||
|
||||
try: key = pass_keys[nr]
|
||||
except: key = ''
|
||||
|
||||
try: host = hosts[nr]
|
||||
except: host = ''
|
||||
|
||||
try: name = names[nr]
|
||||
except: name = ''
|
||||
|
||||
try: ratio = seed_ratios[nr]
|
||||
except: ratio = ''
|
||||
|
||||
try: seed_time = seed_times[nr]
|
||||
except: seed_time = ''
|
||||
|
||||
list.append({
|
||||
'use': uses[nr],
|
||||
'host': host,
|
||||
'name': name,
|
||||
'seed_ratio': tryFloat(ratio),
|
||||
'seed_time': tryInt(seed_time),
|
||||
'pass_key': key,
|
||||
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
|
||||
})
|
||||
|
||||
return list
|
||||
|
||||
def belongsTo(self, url, provider = None, host = None):
|
||||
|
||||
hosts = self.getHosts()
|
||||
|
||||
for host in hosts:
|
||||
result = super(TorrentPotato, self).belongsTo(url, host = host['host'], provider = provider)
|
||||
if result:
|
||||
return result
|
||||
|
||||
def isDisabled(self, host = None):
|
||||
return not self.isEnabled(host)
|
||||
|
||||
def isEnabled(self, host = None):
|
||||
|
||||
# Return true if at least one is enabled and no host is given
|
||||
if host is None:
|
||||
for host in self.getHosts():
|
||||
if self.isEnabled(host):
|
||||
return True
|
||||
return False
|
||||
|
||||
return TorrentProvider.isEnabled(self) and host['host'] and host['pass_key'] and int(host['use'])
|
||||
@@ -78,6 +78,7 @@ class Env(object):
|
||||
return s.get(attr, default = default, section = section, type = type)
|
||||
|
||||
# Set setting
|
||||
s.addSection(section)
|
||||
s.set(section, attr, value)
|
||||
s.save()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from couchpotato.core.helpers.variable import getDataDir, tryInt
|
||||
from logging import handlers
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.web import Application, StaticFileHandler, RedirectHandler
|
||||
from uuid import uuid4
|
||||
import locale
|
||||
import logging
|
||||
import os.path
|
||||
@@ -144,7 +145,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
Env.set('dev', development)
|
||||
|
||||
# Disable logging for some modules
|
||||
for logger_name in ['enzyme', 'guessit', 'subliminal', 'apscheduler']:
|
||||
for logger_name in ['enzyme', 'guessit', 'subliminal', 'apscheduler', 'tornado']:
|
||||
logging.getLogger(logger_name).setLevel(logging.ERROR)
|
||||
|
||||
for logger_name in ['gntp', 'migrate']:
|
||||
@@ -215,6 +216,10 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
Env.set('web_base', web_base)
|
||||
|
||||
api_key = Env.setting('api_key')
|
||||
if not api_key:
|
||||
api_key = uuid4().hex
|
||||
Env.setting('api_key', value = api_key)
|
||||
|
||||
api_base = r'%sapi/%s/' % (web_base, api_key)
|
||||
Env.set('api_base', api_base)
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
pages: [],
|
||||
block: [],
|
||||
|
||||
initialize: function(){
|
||||
var self = this;
|
||||
|
||||
self.global_events = {};
|
||||
},
|
||||
|
||||
setup: function(options) {
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
@@ -30,7 +36,7 @@
|
||||
History.addEvent('change', self.openPage.bind(self));
|
||||
self.c.addEvent('click:relay(a[href^=/]:not([target]))', self.pushState.bind(self));
|
||||
self.c.addEvent('click:relay(a[href^=http])', self.openDerefered.bind(self));
|
||||
|
||||
|
||||
// Check if device is touchenabled
|
||||
self.touch_device = 'ontouchstart' in window || navigator.msMaxTouchPoints;
|
||||
if(self.touch_device)
|
||||
@@ -55,7 +61,7 @@
|
||||
History.push(url);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
isMac: function(){
|
||||
return Browser.Platform.mac
|
||||
},
|
||||
@@ -111,7 +117,7 @@
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
|
||||
setting_links.each(function(a){
|
||||
self.block.more.addLink(a)
|
||||
});
|
||||
@@ -336,6 +342,66 @@
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* Global events
|
||||
*/
|
||||
on: function(name, handle){
|
||||
var self = this;
|
||||
|
||||
if(!self.global_events[name])
|
||||
self.global_events[name] = [];
|
||||
|
||||
self.global_events[name].push(handle);
|
||||
|
||||
},
|
||||
|
||||
trigger: function(name, args, on_complete){
|
||||
var self = this;
|
||||
|
||||
if(!self.global_events[name]){ return; }
|
||||
|
||||
if(!on_complete && typeOf(args) == 'function'){
|
||||
on_complete = args;
|
||||
args = {};
|
||||
}
|
||||
|
||||
// Create parallel callback
|
||||
var callbacks = [];
|
||||
self.global_events[name].each(function(handle, nr){
|
||||
|
||||
callbacks.push(function(callback){
|
||||
var results = handle(args || {});
|
||||
callback(null, results || null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Fire events
|
||||
async.parallel(callbacks, function(err, results){
|
||||
if(err) p(err);
|
||||
|
||||
if(on_complete)
|
||||
on_complete(results);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
off: function(name, handle){
|
||||
var self = this;
|
||||
|
||||
if(!self.global_events[name]) return;
|
||||
|
||||
// Remove single
|
||||
if(handle){
|
||||
self.global_events[name] = self.global_events[name].erase(handle);
|
||||
}
|
||||
// Reset full event
|
||||
else {
|
||||
self.global_events[name] = [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@@ -503,7 +569,7 @@ function randomString(length, extra) {
|
||||
case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break;
|
||||
}
|
||||
});
|
||||
return this.sort(comparer);
|
||||
return this.stableSort(comparer);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
---
|
||||
|
||||
script: Array.stableSort.js
|
||||
|
||||
description: Add a stable sort algorithm for all browsers
|
||||
|
||||
license: MIT-style license.
|
||||
|
||||
authors:
|
||||
- Yorick Sijsling
|
||||
|
||||
requires:
|
||||
core/1.3: '*'
|
||||
|
||||
provides:
|
||||
- [Array.stableSort, Array.mergeSort]
|
||||
|
||||
...
|
||||
*/
|
||||
|
||||
(function() {
|
||||
|
||||
var defaultSortFunction = function(a, b) {
|
||||
return a > b ? 1 : (a < b ? -1 : 0);
|
||||
}
|
||||
|
||||
Array.implement({
|
||||
|
||||
stableSort: function(compare) {
|
||||
// I would love some real feature recognition. Problem is that an unstable algorithm sometimes/often gives the same result as an unstable algorithm.
|
||||
return (Browser.chrome || Browser.firefox2 || Browser.opera9) ? this.mergeSort(compare) : this.sort(compare);
|
||||
},
|
||||
|
||||
mergeSort: function(compare, token) {
|
||||
compare = compare || defaultSortFunction;
|
||||
if (this.length > 1) {
|
||||
// Split and sort both parts
|
||||
var right = this.splice(Math.floor(this.length / 2)).mergeSort(compare);
|
||||
var left = this.splice(0).mergeSort(compare); // 'this' is now empty.
|
||||
|
||||
// Merge parts together
|
||||
while (left.length > 0 || right.length > 0) {
|
||||
this.push(
|
||||
right.length === 0 ? left.shift()
|
||||
: left.length === 0 ? right.shift()
|
||||
: compare(left[0], right[0]) > 0 ? right.shift()
|
||||
: left.shift());
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,955 @@
|
||||
/*global setImmediate: false, setTimeout: false, console: false */
|
||||
(function () {
|
||||
|
||||
var async = {};
|
||||
|
||||
// global on the server, window in the browser
|
||||
var root, previous_async;
|
||||
|
||||
root = this;
|
||||
if (root != null) {
|
||||
previous_async = root.async;
|
||||
}
|
||||
|
||||
async.noConflict = function () {
|
||||
root.async = previous_async;
|
||||
return async;
|
||||
};
|
||||
|
||||
function only_once(fn) {
|
||||
var called = false;
|
||||
return function() {
|
||||
if (called) throw new Error("Callback was already called.");
|
||||
called = true;
|
||||
fn.apply(root, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
//// cross-browser compatiblity functions ////
|
||||
|
||||
var _each = function (arr, iterator) {
|
||||
if (arr.forEach) {
|
||||
return arr.forEach(iterator);
|
||||
}
|
||||
for (var i = 0; i < arr.length; i += 1) {
|
||||
iterator(arr[i], i, arr);
|
||||
}
|
||||
};
|
||||
|
||||
var _map = function (arr, iterator) {
|
||||
if (arr.map) {
|
||||
return arr.map(iterator);
|
||||
}
|
||||
var results = [];
|
||||
_each(arr, function (x, i, a) {
|
||||
results.push(iterator(x, i, a));
|
||||
});
|
||||
return results;
|
||||
};
|
||||
|
||||
var _reduce = function (arr, iterator, memo) {
|
||||
if (arr.reduce) {
|
||||
return arr.reduce(iterator, memo);
|
||||
}
|
||||
_each(arr, function (x, i, a) {
|
||||
memo = iterator(memo, x, i, a);
|
||||
});
|
||||
return memo;
|
||||
};
|
||||
|
||||
var _keys = function (obj) {
|
||||
if (Object.keys) {
|
||||
return Object.keys(obj);
|
||||
}
|
||||
var keys = [];
|
||||
for (var k in obj) {
|
||||
if (obj.hasOwnProperty(k)) {
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
//// exported async module functions ////
|
||||
|
||||
//// nextTick implementation with browser-compatible fallback ////
|
||||
if (typeof process === 'undefined' || !(process.nextTick)) {
|
||||
if (typeof setImmediate === 'function') {
|
||||
async.nextTick = function (fn) {
|
||||
// not a direct alias for IE10 compatibility
|
||||
setImmediate(fn);
|
||||
};
|
||||
async.setImmediate = async.nextTick;
|
||||
}
|
||||
else {
|
||||
async.nextTick = function (fn) {
|
||||
setTimeout(fn, 0);
|
||||
};
|
||||
async.setImmediate = async.nextTick;
|
||||
}
|
||||
}
|
||||
else {
|
||||
async.nextTick = process.nextTick;
|
||||
if (typeof setImmediate !== 'undefined') {
|
||||
async.setImmediate = setImmediate;
|
||||
}
|
||||
else {
|
||||
async.setImmediate = async.nextTick;
|
||||
}
|
||||
}
|
||||
|
||||
async.each = function (arr, iterator, callback) {
|
||||
callback = callback || function () {};
|
||||
if (!arr.length) {
|
||||
return callback();
|
||||
}
|
||||
var completed = 0;
|
||||
_each(arr, function (x) {
|
||||
iterator(x, only_once(function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
completed += 1;
|
||||
if (completed >= arr.length) {
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
};
|
||||
async.forEach = async.each;
|
||||
|
||||
async.eachSeries = function (arr, iterator, callback) {
|
||||
callback = callback || function () {};
|
||||
if (!arr.length) {
|
||||
return callback();
|
||||
}
|
||||
var completed = 0;
|
||||
var iterate = function () {
|
||||
iterator(arr[completed], function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
completed += 1;
|
||||
if (completed >= arr.length) {
|
||||
callback(null);
|
||||
}
|
||||
else {
|
||||
iterate();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
iterate();
|
||||
};
|
||||
async.forEachSeries = async.eachSeries;
|
||||
|
||||
async.eachLimit = function (arr, limit, iterator, callback) {
|
||||
var fn = _eachLimit(limit);
|
||||
fn.apply(null, [arr, iterator, callback]);
|
||||
};
|
||||
async.forEachLimit = async.eachLimit;
|
||||
|
||||
var _eachLimit = function (limit) {
|
||||
|
||||
return function (arr, iterator, callback) {
|
||||
callback = callback || function () {};
|
||||
if (!arr.length || limit <= 0) {
|
||||
return callback();
|
||||
}
|
||||
var completed = 0;
|
||||
var started = 0;
|
||||
var running = 0;
|
||||
|
||||
(function replenish () {
|
||||
if (completed >= arr.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
while (running < limit && started < arr.length) {
|
||||
started += 1;
|
||||
running += 1;
|
||||
iterator(arr[started - 1], function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
completed += 1;
|
||||
running -= 1;
|
||||
if (completed >= arr.length) {
|
||||
callback();
|
||||
}
|
||||
else {
|
||||
replenish();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
var doParallel = function (fn) {
|
||||
return function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
return fn.apply(null, [async.each].concat(args));
|
||||
};
|
||||
};
|
||||
var doParallelLimit = function(limit, fn) {
|
||||
return function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
return fn.apply(null, [_eachLimit(limit)].concat(args));
|
||||
};
|
||||
};
|
||||
var doSeries = function (fn) {
|
||||
return function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
return fn.apply(null, [async.eachSeries].concat(args));
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
var _asyncMap = function (eachfn, arr, iterator, callback) {
|
||||
var results = [];
|
||||
arr = _map(arr, function (x, i) {
|
||||
return {index: i, value: x};
|
||||
});
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x.value, function (err, v) {
|
||||
results[x.index] = v;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, results);
|
||||
});
|
||||
};
|
||||
async.map = doParallel(_asyncMap);
|
||||
async.mapSeries = doSeries(_asyncMap);
|
||||
async.mapLimit = function (arr, limit, iterator, callback) {
|
||||
return _mapLimit(limit)(arr, iterator, callback);
|
||||
};
|
||||
|
||||
var _mapLimit = function(limit) {
|
||||
return doParallelLimit(limit, _asyncMap);
|
||||
};
|
||||
|
||||
// reduce only has a series version, as doing reduce in parallel won't
|
||||
// work in many situations.
|
||||
async.reduce = function (arr, memo, iterator, callback) {
|
||||
async.eachSeries(arr, function (x, callback) {
|
||||
iterator(memo, x, function (err, v) {
|
||||
memo = v;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, memo);
|
||||
});
|
||||
};
|
||||
// inject alias
|
||||
async.inject = async.reduce;
|
||||
// foldl alias
|
||||
async.foldl = async.reduce;
|
||||
|
||||
async.reduceRight = function (arr, memo, iterator, callback) {
|
||||
var reversed = _map(arr, function (x) {
|
||||
return x;
|
||||
}).reverse();
|
||||
async.reduce(reversed, memo, iterator, callback);
|
||||
};
|
||||
// foldr alias
|
||||
async.foldr = async.reduceRight;
|
||||
|
||||
var _filter = function (eachfn, arr, iterator, callback) {
|
||||
var results = [];
|
||||
arr = _map(arr, function (x, i) {
|
||||
return {index: i, value: x};
|
||||
});
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x.value, function (v) {
|
||||
if (v) {
|
||||
results.push(x);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
callback(_map(results.sort(function (a, b) {
|
||||
return a.index - b.index;
|
||||
}), function (x) {
|
||||
return x.value;
|
||||
}));
|
||||
});
|
||||
};
|
||||
async.filter = doParallel(_filter);
|
||||
async.filterSeries = doSeries(_filter);
|
||||
// select alias
|
||||
async.select = async.filter;
|
||||
async.selectSeries = async.filterSeries;
|
||||
|
||||
var _reject = function (eachfn, arr, iterator, callback) {
|
||||
var results = [];
|
||||
arr = _map(arr, function (x, i) {
|
||||
return {index: i, value: x};
|
||||
});
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x.value, function (v) {
|
||||
if (!v) {
|
||||
results.push(x);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
callback(_map(results.sort(function (a, b) {
|
||||
return a.index - b.index;
|
||||
}), function (x) {
|
||||
return x.value;
|
||||
}));
|
||||
});
|
||||
};
|
||||
async.reject = doParallel(_reject);
|
||||
async.rejectSeries = doSeries(_reject);
|
||||
|
||||
var _detect = function (eachfn, arr, iterator, main_callback) {
|
||||
eachfn(arr, function (x, callback) {
|
||||
iterator(x, function (result) {
|
||||
if (result) {
|
||||
main_callback(x);
|
||||
main_callback = function () {};
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}, function (err) {
|
||||
main_callback();
|
||||
});
|
||||
};
|
||||
async.detect = doParallel(_detect);
|
||||
async.detectSeries = doSeries(_detect);
|
||||
|
||||
async.some = function (arr, iterator, main_callback) {
|
||||
async.each(arr, function (x, callback) {
|
||||
iterator(x, function (v) {
|
||||
if (v) {
|
||||
main_callback(true);
|
||||
main_callback = function () {};
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
main_callback(false);
|
||||
});
|
||||
};
|
||||
// any alias
|
||||
async.any = async.some;
|
||||
|
||||
async.every = function (arr, iterator, main_callback) {
|
||||
async.each(arr, function (x, callback) {
|
||||
iterator(x, function (v) {
|
||||
if (!v) {
|
||||
main_callback(false);
|
||||
main_callback = function () {};
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}, function (err) {
|
||||
main_callback(true);
|
||||
});
|
||||
};
|
||||
// all alias
|
||||
async.all = async.every;
|
||||
|
||||
async.sortBy = function (arr, iterator, callback) {
|
||||
async.map(arr, function (x, callback) {
|
||||
iterator(x, function (err, criteria) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
}
|
||||
else {
|
||||
callback(null, {value: x, criteria: criteria});
|
||||
}
|
||||
});
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
else {
|
||||
var fn = function (left, right) {
|
||||
var a = left.criteria, b = right.criteria;
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
};
|
||||
callback(null, _map(results.sort(fn), function (x) {
|
||||
return x.value;
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async.auto = function (tasks, callback) {
|
||||
callback = callback || function () {};
|
||||
var keys = _keys(tasks);
|
||||
if (!keys.length) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
var results = {};
|
||||
|
||||
var listeners = [];
|
||||
var addListener = function (fn) {
|
||||
listeners.unshift(fn);
|
||||
};
|
||||
var removeListener = function (fn) {
|
||||
for (var i = 0; i < listeners.length; i += 1) {
|
||||
if (listeners[i] === fn) {
|
||||
listeners.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
var taskComplete = function () {
|
||||
_each(listeners.slice(0), function (fn) {
|
||||
fn();
|
||||
});
|
||||
};
|
||||
|
||||
addListener(function () {
|
||||
if (_keys(results).length === keys.length) {
|
||||
callback(null, results);
|
||||
callback = function () {};
|
||||
}
|
||||
});
|
||||
|
||||
_each(keys, function (k) {
|
||||
var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k];
|
||||
var taskCallback = function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
if (err) {
|
||||
var safeResults = {};
|
||||
_each(_keys(results), function(rkey) {
|
||||
safeResults[rkey] = results[rkey];
|
||||
});
|
||||
safeResults[k] = args;
|
||||
callback(err, safeResults);
|
||||
// stop subsequent errors hitting callback multiple times
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
results[k] = args;
|
||||
async.setImmediate(taskComplete);
|
||||
}
|
||||
};
|
||||
var requires = task.slice(0, Math.abs(task.length - 1)) || [];
|
||||
var ready = function () {
|
||||
return _reduce(requires, function (a, x) {
|
||||
return (a && results.hasOwnProperty(x));
|
||||
}, true) && !results.hasOwnProperty(k);
|
||||
};
|
||||
if (ready()) {
|
||||
task[task.length - 1](taskCallback, results);
|
||||
}
|
||||
else {
|
||||
var listener = function () {
|
||||
if (ready()) {
|
||||
removeListener(listener);
|
||||
task[task.length - 1](taskCallback, results);
|
||||
}
|
||||
};
|
||||
addListener(listener);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async.waterfall = function (tasks, callback) {
|
||||
callback = callback || function () {};
|
||||
if (tasks.constructor !== Array) {
|
||||
var err = new Error('First argument to waterfall must be an array of functions');
|
||||
return callback(err);
|
||||
}
|
||||
if (!tasks.length) {
|
||||
return callback();
|
||||
}
|
||||
var wrapIterator = function (iterator) {
|
||||
return function (err) {
|
||||
if (err) {
|
||||
callback.apply(null, arguments);
|
||||
callback = function () {};
|
||||
}
|
||||
else {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
var next = iterator.next();
|
||||
if (next) {
|
||||
args.push(wrapIterator(next));
|
||||
}
|
||||
else {
|
||||
args.push(callback);
|
||||
}
|
||||
async.setImmediate(function () {
|
||||
iterator.apply(null, args);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
wrapIterator(async.iterator(tasks))();
|
||||
};
|
||||
|
||||
var _parallel = function(eachfn, tasks, callback) {
|
||||
callback = callback || function () {};
|
||||
if (tasks.constructor === Array) {
|
||||
eachfn.map(tasks, function (fn, callback) {
|
||||
if (fn) {
|
||||
fn(function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
callback.call(null, err, args);
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
}
|
||||
else {
|
||||
var results = {};
|
||||
eachfn.each(_keys(tasks), function (k, callback) {
|
||||
tasks[k](function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
results[k] = args;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, results);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async.parallel = function (tasks, callback) {
|
||||
_parallel({ map: async.map, each: async.each }, tasks, callback);
|
||||
};
|
||||
|
||||
async.parallelLimit = function(tasks, limit, callback) {
|
||||
_parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback);
|
||||
};
|
||||
|
||||
async.series = function (tasks, callback) {
|
||||
callback = callback || function () {};
|
||||
if (tasks.constructor === Array) {
|
||||
async.mapSeries(tasks, function (fn, callback) {
|
||||
if (fn) {
|
||||
fn(function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
callback.call(null, err, args);
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
}
|
||||
else {
|
||||
var results = {};
|
||||
async.eachSeries(_keys(tasks), function (k, callback) {
|
||||
tasks[k](function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (args.length <= 1) {
|
||||
args = args[0];
|
||||
}
|
||||
results[k] = args;
|
||||
callback(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, results);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async.iterator = function (tasks) {
|
||||
var makeCallback = function (index) {
|
||||
var fn = function () {
|
||||
if (tasks.length) {
|
||||
tasks[index].apply(null, arguments);
|
||||
}
|
||||
return fn.next();
|
||||
};
|
||||
fn.next = function () {
|
||||
return (index < tasks.length - 1) ? makeCallback(index + 1): null;
|
||||
};
|
||||
return fn;
|
||||
};
|
||||
return makeCallback(0);
|
||||
};
|
||||
|
||||
async.apply = function (fn) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
return function () {
|
||||
return fn.apply(
|
||||
null, args.concat(Array.prototype.slice.call(arguments))
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
var _concat = function (eachfn, arr, fn, callback) {
|
||||
var r = [];
|
||||
eachfn(arr, function (x, cb) {
|
||||
fn(x, function (err, y) {
|
||||
r = r.concat(y || []);
|
||||
cb(err);
|
||||
});
|
||||
}, function (err) {
|
||||
callback(err, r);
|
||||
});
|
||||
};
|
||||
async.concat = doParallel(_concat);
|
||||
async.concatSeries = doSeries(_concat);
|
||||
|
||||
async.whilst = function (test, iterator, callback) {
|
||||
if (test()) {
|
||||
iterator(function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.whilst(test, iterator, callback);
|
||||
});
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
async.doWhilst = function (iterator, test, callback) {
|
||||
iterator(function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (test()) {
|
||||
async.doWhilst(iterator, test, callback);
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async.until = function (test, iterator, callback) {
|
||||
if (!test()) {
|
||||
iterator(function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.until(test, iterator, callback);
|
||||
});
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
async.doUntil = function (iterator, test, callback) {
|
||||
iterator(function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!test()) {
|
||||
async.doUntil(iterator, test, callback);
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async.queue = function (worker, concurrency) {
|
||||
if (concurrency === undefined) {
|
||||
concurrency = 1;
|
||||
}
|
||||
function _insert(q, data, pos, callback) {
|
||||
if(data.constructor !== Array) {
|
||||
data = [data];
|
||||
}
|
||||
_each(data, function(task) {
|
||||
var item = {
|
||||
data: task,
|
||||
callback: typeof callback === 'function' ? callback : null
|
||||
};
|
||||
|
||||
if (pos) {
|
||||
q.tasks.unshift(item);
|
||||
} else {
|
||||
q.tasks.push(item);
|
||||
}
|
||||
|
||||
if (q.saturated && q.tasks.length === concurrency) {
|
||||
q.saturated();
|
||||
}
|
||||
async.setImmediate(q.process);
|
||||
});
|
||||
}
|
||||
|
||||
var workers = 0;
|
||||
var q = {
|
||||
tasks: [],
|
||||
concurrency: concurrency,
|
||||
saturated: null,
|
||||
empty: null,
|
||||
drain: null,
|
||||
push: function (data, callback) {
|
||||
_insert(q, data, false, callback);
|
||||
},
|
||||
unshift: function (data, callback) {
|
||||
_insert(q, data, true, callback);
|
||||
},
|
||||
process: function () {
|
||||
if (workers < q.concurrency && q.tasks.length) {
|
||||
var task = q.tasks.shift();
|
||||
if (q.empty && q.tasks.length === 0) {
|
||||
q.empty();
|
||||
}
|
||||
workers += 1;
|
||||
var next = function () {
|
||||
workers -= 1;
|
||||
if (task.callback) {
|
||||
task.callback.apply(task, arguments);
|
||||
}
|
||||
if (q.drain && q.tasks.length + workers === 0) {
|
||||
q.drain();
|
||||
}
|
||||
q.process();
|
||||
};
|
||||
var cb = only_once(next);
|
||||
worker(task.data, cb);
|
||||
}
|
||||
},
|
||||
length: function () {
|
||||
return q.tasks.length;
|
||||
},
|
||||
running: function () {
|
||||
return workers;
|
||||
}
|
||||
};
|
||||
return q;
|
||||
};
|
||||
|
||||
async.cargo = function (worker, payload) {
|
||||
var working = false,
|
||||
tasks = [];
|
||||
|
||||
var cargo = {
|
||||
tasks: tasks,
|
||||
payload: payload,
|
||||
saturated: null,
|
||||
empty: null,
|
||||
drain: null,
|
||||
push: function (data, callback) {
|
||||
if(data.constructor !== Array) {
|
||||
data = [data];
|
||||
}
|
||||
_each(data, function(task) {
|
||||
tasks.push({
|
||||
data: task,
|
||||
callback: typeof callback === 'function' ? callback : null
|
||||
});
|
||||
if (cargo.saturated && tasks.length === payload) {
|
||||
cargo.saturated();
|
||||
}
|
||||
});
|
||||
async.setImmediate(cargo.process);
|
||||
},
|
||||
process: function process() {
|
||||
if (working) return;
|
||||
if (tasks.length === 0) {
|
||||
if(cargo.drain) cargo.drain();
|
||||
return;
|
||||
}
|
||||
|
||||
var ts = typeof payload === 'number'
|
||||
? tasks.splice(0, payload)
|
||||
: tasks.splice(0);
|
||||
|
||||
var ds = _map(ts, function (task) {
|
||||
return task.data;
|
||||
});
|
||||
|
||||
if(cargo.empty) cargo.empty();
|
||||
working = true;
|
||||
worker(ds, function () {
|
||||
working = false;
|
||||
|
||||
var args = arguments;
|
||||
_each(ts, function (data) {
|
||||
if (data.callback) {
|
||||
data.callback.apply(null, args);
|
||||
}
|
||||
});
|
||||
|
||||
process();
|
||||
});
|
||||
},
|
||||
length: function () {
|
||||
return tasks.length;
|
||||
},
|
||||
running: function () {
|
||||
return working;
|
||||
}
|
||||
};
|
||||
return cargo;
|
||||
};
|
||||
|
||||
var _console_fn = function (name) {
|
||||
return function (fn) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
fn.apply(null, args.concat([function (err) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (typeof console !== 'undefined') {
|
||||
if (err) {
|
||||
if (console.error) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
else if (console[name]) {
|
||||
_each(args, function (x) {
|
||||
console[name](x);
|
||||
});
|
||||
}
|
||||
}
|
||||
}]));
|
||||
};
|
||||
};
|
||||
async.log = _console_fn('log');
|
||||
async.dir = _console_fn('dir');
|
||||
/*async.info = _console_fn('info');
|
||||
async.warn = _console_fn('warn');
|
||||
async.error = _console_fn('error');*/
|
||||
|
||||
async.memoize = function (fn, hasher) {
|
||||
var memo = {};
|
||||
var queues = {};
|
||||
hasher = hasher || function (x) {
|
||||
return x;
|
||||
};
|
||||
var memoized = function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var callback = args.pop();
|
||||
var key = hasher.apply(null, args);
|
||||
if (key in memo) {
|
||||
callback.apply(null, memo[key]);
|
||||
}
|
||||
else if (key in queues) {
|
||||
queues[key].push(callback);
|
||||
}
|
||||
else {
|
||||
queues[key] = [callback];
|
||||
fn.apply(null, args.concat([function () {
|
||||
memo[key] = arguments;
|
||||
var q = queues[key];
|
||||
delete queues[key];
|
||||
for (var i = 0, l = q.length; i < l; i++) {
|
||||
q[i].apply(null, arguments);
|
||||
}
|
||||
}]));
|
||||
}
|
||||
};
|
||||
memoized.memo = memo;
|
||||
memoized.unmemoized = fn;
|
||||
return memoized;
|
||||
};
|
||||
|
||||
async.unmemoize = function (fn) {
|
||||
return function () {
|
||||
return (fn.unmemoized || fn).apply(null, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
async.times = function (count, iterator, callback) {
|
||||
var counter = [];
|
||||
for (var i = 0; i < count; i++) {
|
||||
counter.push(i);
|
||||
}
|
||||
return async.map(counter, iterator, callback);
|
||||
};
|
||||
|
||||
async.timesSeries = function (count, iterator, callback) {
|
||||
var counter = [];
|
||||
for (var i = 0; i < count; i++) {
|
||||
counter.push(i);
|
||||
}
|
||||
return async.mapSeries(counter, iterator, callback);
|
||||
};
|
||||
|
||||
async.compose = function (/* functions... */) {
|
||||
var fns = Array.prototype.reverse.call(arguments);
|
||||
return function () {
|
||||
var that = this;
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var callback = args.pop();
|
||||
async.reduce(fns, args, function (newargs, fn, cb) {
|
||||
fn.apply(that, newargs.concat([function () {
|
||||
var err = arguments[0];
|
||||
var nextargs = Array.prototype.slice.call(arguments, 1);
|
||||
cb(err, nextargs);
|
||||
}]))
|
||||
},
|
||||
function (err, results) {
|
||||
callback.apply(that, [err].concat(results));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
var _applyEach = function (eachfn, fns /*args...*/) {
|
||||
var go = function () {
|
||||
var that = this;
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var callback = args.pop();
|
||||
return eachfn(fns, function (fn, cb) {
|
||||
fn.apply(that, args.concat([cb]));
|
||||
},
|
||||
callback);
|
||||
};
|
||||
if (arguments.length > 2) {
|
||||
var args = Array.prototype.slice.call(arguments, 2);
|
||||
return go.apply(this, args);
|
||||
}
|
||||
else {
|
||||
return go;
|
||||
}
|
||||
};
|
||||
async.applyEach = doParallel(_applyEach);
|
||||
async.applyEachSeries = doSeries(_applyEach);
|
||||
|
||||
async.forever = function (fn, callback) {
|
||||
function next(err) {
|
||||
if (err) {
|
||||
if (callback) {
|
||||
return callback(err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
fn(next);
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// AMD / RequireJS
|
||||
if (typeof define !== 'undefined' && define.amd) {
|
||||
define([], function () {
|
||||
return async;
|
||||
});
|
||||
}
|
||||
// Node.js
|
||||
else if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = async;
|
||||
}
|
||||
// included directly via <script> tag
|
||||
else {
|
||||
root.async = async;
|
||||
}
|
||||
|
||||
}());
|
||||
@@ -1,5 +1,5 @@
|
||||
// MooTools: the javascript framework.
|
||||
// Load this file's selection again by visiting: http://mootools.net/more/43db227db7a621ebb062ee621432ae3d
|
||||
// Load this file's selection again by visiting: http://mootools.net/more/0f75cfbac1aabbedaba7630beef8d10c
|
||||
// Or build this file again with packager using: packager build More/Events.Pseudos More/Date More/Date.Extras More/Element.Forms More/Element.Position More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical
|
||||
/*
|
||||
---
|
||||
|
||||
@@ -57,6 +57,19 @@ Page.Home = new Class({
|
||||
'limit': null,
|
||||
'onLoaded': function(){
|
||||
self.chain.callChain();
|
||||
},
|
||||
'onMovieAdded': function(notification){
|
||||
|
||||
// Track movie added
|
||||
var after_search = function(data){
|
||||
if(notification.data.id != data.data.id) return;
|
||||
|
||||
// Force update after search
|
||||
self.available_list.update();
|
||||
App.off('movie.searcher.ended', after_search);
|
||||
}
|
||||
App.on('movie.searcher.ended', after_search);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ Page.Manage = new Class({
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Capture progress so we can use it in our *each* closure
|
||||
var progress = json.progress
|
||||
|
||||
// Don't add loader when page is loading still
|
||||
if(!self.list.navigation)
|
||||
@@ -112,10 +114,13 @@ Page.Manage = new Class({
|
||||
|
||||
self.progress_container.empty();
|
||||
|
||||
Object.each(json.progress, function(progress, folder){
|
||||
var sorted_table = self.parseProgress(json.progress)
|
||||
|
||||
sorted_table.each(function(folder){
|
||||
var folder_progress = progress[folder]
|
||||
new Element('div').adopt(
|
||||
new Element('span.folder', {'text': folder}),
|
||||
new Element('span.percentage', {'text': progress.total ? (((progress.total-progress.to_go)/progress.total)*100).round() + '%' : '0%'})
|
||||
new Element('span.percentage', {'text': folder_progress.total ? (((folder_progress.total-folder_progress.to_go)/folder_progress.total)*100).round() + '%' : '0%'})
|
||||
).inject(self.progress_container)
|
||||
});
|
||||
|
||||
@@ -124,7 +129,17 @@ Page.Manage = new Class({
|
||||
})
|
||||
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
parseProgress: function (progress_object) {
|
||||
var folder, temp_array = [];
|
||||
|
||||
for (folder in progress_object) {
|
||||
if (progress_object.hasOwnProperty(folder)) {
|
||||
temp_array.push(folder)
|
||||
}
|
||||
}
|
||||
return temp_array.stableSort()
|
||||
}
|
||||
|
||||
});
|
||||
@@ -111,6 +111,10 @@ Page.Settings = new Class({
|
||||
Cookie.write('advanced_toggle_checked', +self.advanced_toggle.checked, {'duration': 365});
|
||||
},
|
||||
|
||||
sortByOrder: function(a, b){
|
||||
return (a.order || 100) - (b.order || 100)
|
||||
},
|
||||
|
||||
create: function(json){
|
||||
var self = this;
|
||||
|
||||
@@ -141,13 +145,11 @@ Page.Settings = new Class({
|
||||
options.include(section);
|
||||
});
|
||||
|
||||
options.sort(function(a, b){
|
||||
return (a.order || 100) - (b.order || 100)
|
||||
}).each(function(section){
|
||||
options.stableSort(self.sortByOrder).each(function(section){
|
||||
var section_name = section.section_name;
|
||||
|
||||
// Add groups to content
|
||||
section.groups.sortBy('order').each(function(group){
|
||||
section.groups.stableSort(self.sortByOrder).each(function(group){
|
||||
if(group.hidden) return;
|
||||
|
||||
if(self.wizard_only && !group.wizard)
|
||||
@@ -184,9 +186,7 @@ Page.Settings = new Class({
|
||||
}
|
||||
|
||||
// Add options to group
|
||||
group.options.sort(function(a, b){
|
||||
return (a.order || 100) - (b.order || 100)
|
||||
}).each(function(option){
|
||||
group.options.stableSort(self.sortByOrder).each(function(option){
|
||||
if(option.hidden) return;
|
||||
var class_name = (option.type || 'string').capitalize();
|
||||
var input = new Option[class_name](section_name, option.name, self.getValue(section_name, option.name), option);
|
||||
@@ -265,16 +265,27 @@ Page.Settings = new Class({
|
||||
},
|
||||
|
||||
createGroup: function(group){
|
||||
|
||||
if((typeOf(group.description) == 'array')){
|
||||
var hint = new Element('span.hint.more_hint', {
|
||||
'html': group.description[0]
|
||||
});
|
||||
|
||||
createTooltip(group.description[1]).inject(hint, 'top');
|
||||
}
|
||||
else {
|
||||
var hint = new Element('span.hint', {
|
||||
'html': group.description || ''
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return new Element('fieldset', {
|
||||
'class': (group.advanced ? 'inlineLabels advanced' : 'inlineLabels') + ' group_' + (group.name || '') + ' subtab_' + (group.subtab || '')
|
||||
}).adopt(
|
||||
}).grab(
|
||||
new Element('h2', {
|
||||
'text': group.label || (group.name).capitalize()
|
||||
}).adopt(
|
||||
new Element('span.hint', {
|
||||
'html': group.description || ''
|
||||
})
|
||||
)
|
||||
}).grab(hint)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -343,10 +354,22 @@ var OptionBase = new Class({
|
||||
|
||||
createHint: function(){
|
||||
var self = this;
|
||||
if(self.options.description)
|
||||
new Element('p.formHint', {
|
||||
'html': self.options.description
|
||||
}).inject(self.el);
|
||||
if(self.options.description){
|
||||
|
||||
|
||||
if((typeOf(self.options.description) == 'array')){
|
||||
var hint = new Element('p.formHint.more_hint', {
|
||||
'html': self.options.description[0]
|
||||
}).inject(self.el);
|
||||
|
||||
createTooltip(self.options.description[1]).inject(hint, 'top');
|
||||
}
|
||||
else {
|
||||
var hint = new Element('p.formHint', {
|
||||
'html': self.options.description || ''
|
||||
}).inject(self.el)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
afterInject: function(){
|
||||
@@ -1412,4 +1435,25 @@ Option.Combined = new Class({
|
||||
self.saveCombined();
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
var createTooltip = function(description){
|
||||
|
||||
var tip = new Element('div.tooltip', {
|
||||
'events': {
|
||||
'mouseenter': function(){
|
||||
tip.addClass('shown')
|
||||
},
|
||||
'mouseleave': function(){
|
||||
tip.removeClass('shown')
|
||||
}
|
||||
}
|
||||
}).adopt(
|
||||
new Element('a.icon2.info'),
|
||||
new Element('div.tip', {
|
||||
'html': description
|
||||
})
|
||||
);
|
||||
|
||||
return tip;
|
||||
}
|
||||
@@ -130,7 +130,7 @@
|
||||
.page .ctrlHolder.focused:first-child, .page .ctrlHolder:first-child{ background-color: transparent; }
|
||||
|
||||
.page .ctrlHolder .formHint {
|
||||
width: 47%;
|
||||
width: 46%;
|
||||
margin: -18px 0;
|
||||
color: #fff !important;
|
||||
display: inline-block;
|
||||
@@ -547,12 +547,31 @@
|
||||
.page .combined_table .head abbr:first-child {
|
||||
display: none;
|
||||
}
|
||||
.page .combined_table .head abbr.host {
|
||||
margin-right: 190px;
|
||||
.page .combined_table .head abbr.host { margin-right: 120px; }
|
||||
.page .combined_table input.host { width: 140px; }
|
||||
.page .section_newznab .combined_table .head abbr.host { margin-right: 200px; }
|
||||
.page .section_newznab .combined_table input.host { width: 220px; }
|
||||
|
||||
.page .combined_table .head abbr.name { margin-right: 57px; }
|
||||
.page .combined_table input.name { width: 120px; }
|
||||
.page .combined_table .head abbr.api_key { margin-right: 75px; }
|
||||
|
||||
.page .combined_table .head abbr.pass_key { margin-right: 71px; }
|
||||
.page .combined_table input.pass_key { width: 113px; }
|
||||
|
||||
.page .section_newznab .combined_table .head abbr.api_key { margin-right: 185px; }
|
||||
.page .section_newznab .combined_table input.api_key { width: 223px; }
|
||||
|
||||
.page .combined_table .seed_ratio,
|
||||
.page .combined_table .seed_time {
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.page .combined_table .head abbr.api_key {
|
||||
margin-right: 171px;
|
||||
.page .combined_table .seed_time {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.page .combined_table .head .extra_score,
|
||||
.page .combined_table .extra_score {
|
||||
width: 70px;
|
||||
@@ -701,4 +720,46 @@
|
||||
.active .group_imdb_automation:not(.disabled) {
|
||||
background: url('../images/imdb_watchlist.png') no-repeat right 50px;
|
||||
min-height: 210px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.tooltip > a {
|
||||
opacity: .3;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tooltip:hover > a {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltip div {
|
||||
background: #FFF;
|
||||
color: #000;
|
||||
padding: 10px;
|
||||
width: 380px;
|
||||
z-index: 200;
|
||||
position: absolute;
|
||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||
margin-top: 40px;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tooltip.shown div {
|
||||
margin-top: 10px;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.tooltip div a {
|
||||
color: #5b9bd1;
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
|
||||
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %}
|
||||
<link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
|
||||
|
||||
Reference in new issue
Block a user