diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py index 4ad37d6..9647d95 100644 --- a/couchpotato/core/_base/_core/main.py +++ b/couchpotato/core/_base/_core/main.py @@ -124,7 +124,7 @@ class Core(Plugin): time.sleep(1) - log.debug('Save to shutdown/restart') + log.debug('Safe to shutdown/restart') try: IOLoop.current().stop() diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 900fd8c..adbfb7e 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -39,6 +39,8 @@ class Downloader(Provider): addEvent('download.enabled_types', self.getEnabledDownloadType) addEvent('download.status', self._getAllDownloadStatus) addEvent('download.remove_failed', self._removeFailed) + addEvent('download.pause', self._pause) + addEvent('download.process_complete', self._processComplete) def getEnabledDownloadType(self): for download_type in self.type: @@ -65,14 +67,30 @@ class Downloader(Provider): if self.isDisabled(manual = True, data = {}): return - if self.conf('delete_failed', default = True): - return self.removeFailed(item) + if item and item.get('downloader') == self.getName(): + if self.conf('delete_failed'): + return self.removeFailed(item) - return False + return False + return def removeFailed(self, item): return + def _processComplete(self, item): + if self.isDisabled(manual = True, data = {}): + return + + if item and item.get('downloader') == self.getName(): + if self.conf('remove_complete'): + return self.processComplete(item = item, delete_files = self.conf('delete_files', default = False)) + + return False + return + + def processComplete(self, item, delete_files): + return + def isCorrectType(self, item_type): is_correct = item_type in self.type @@ -124,6 +142,17 @@ class Downloader(Provider): ((d_manual and manual) or (d_manual is False)) and \ (not data or self.isCorrectType(data.get('type'))) + def _pause(self, item, pause = True): + if self.isDisabled(manual = True, data = {}): + return + + if item and item.get('downloader') == self.getName(): + self.pause(item, pause) + return True + return + + def pause(self, item, pause): + return class StatusList(list): diff --git a/couchpotato/core/downloaders/sabnzbd/__init__.py b/couchpotato/core/downloaders/sabnzbd/__init__.py index f17db9c..8e132b7 100644 --- a/couchpotato/core/downloaders/sabnzbd/__init__.py +++ b/couchpotato/core/downloaders/sabnzbd/__init__.py @@ -42,6 +42,13 @@ config = [{ 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', }, { + 'name': 'remove_complete', + 'label': 'Remove NZB', + 'default': True, + 'type': 'bool', + 'description': 'Remove the NZB from history after it completed.', + }, + { 'name': 'delete_failed', 'default': True, 'type': 'bool', diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index f2f217a..56e9a21 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -129,6 +129,22 @@ class Sabnzbd(Downloader): return True + def processComplete(self, item, delete_files = False): + log.debug('Requesting SabNZBd to remove the NZB %s%s.', (item['name'])) + + try: + self.call({ + 'mode': 'history', + 'name': 'delete', + 'del_files': '0', + 'value': item['id'] + }, use_json = False) + except: + log.error('Failed removing: %s', traceback.format_exc(0)) + return False + + return True + def call(self, request_params, use_json = True, **kwargs): url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, { diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index 6dfbd3f..bca7eae 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -44,18 +44,32 @@ config = [{ 'description': 'Download to this directory. Keep empty for default Transmission download directory.', }, { - 'name': 'ratio', - 'default': 10, - 'type': 'float', + 'name': 'seeding', + 'label': 'Seeding support', + 'default': True, + 'type': 'bool', + 'description': '(Hard)link/copy after download is complete (if enabled in renamer), wait for seeding to finish before (re)moving and set the seeding goal from the torrent providers.', + }, + { + 'name': 'remove_complete', + 'label': 'Remove torrent', + 'default': True, + 'type': 'bool', + 'description': 'Remove the torrent from Transmission after it finished seeding.', + }, + { + 'name': 'delete_files', + 'label': 'Remove files', + 'default': True, + 'type': 'bool', 'advanced': True, - 'description': 'Stop transfer when reaching ratio', + 'description': 'Also remove the leftover files.', }, { - 'name': 'ratiomode', - 'default': 0, - 'type': 'int', - 'advanced': True, - 'description': '0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.', + 'name': 'paused', + 'type': 'bool', + 'default': False, + 'description': 'Add the torrent paused.', }, { 'name': 'manual', @@ -64,6 +78,18 @@ config = [{ 'advanced': True, 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', }, + { + 'name': 'stalled_as_failed', + 'default': True, + 'type': 'bool', + 'description': 'Consider a stalled torrent as failed', + }, + { + 'name': 'delete_failed', + 'default': True, + 'type': 'bool', + 'description': 'Delete a release after the download has failed.', + }, ], } ], diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 6094e89..46b9541 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -1,6 +1,7 @@ from base64 import b64encode from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.helpers.encoding import isInt +from couchpotato.core.helpers.variable import tryInt, tryFloat from couchpotato.core.logger import CPLog from couchpotato.environment import Env from datetime import timedelta @@ -8,7 +9,6 @@ import httplib import json import os.path import re -import traceback import urllib2 log = CPLog(__name__) @@ -18,144 +18,129 @@ class Transmission(Downloader): type = ['torrent', 'torrent_magnet'] log = CPLog(__name__) - - def download(self, data, movie, filedata = None): - - log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type'))) - + trpc = None + + def connect(self): # Load host from config and split out port. host = self.conf('host').split(':') if not isInt(host[1]): log.error('Config properties are not filled in correctly, port is missing.') return False + if not self.trpc: + self.trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) + return self.trpc - # Set parameters for Transmission - params = { - 'paused': self.conf('paused', default = 0), - } + def download(self, data, movie, filedata = None): - if len(self.conf('directory', default = '')) > 0: - folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1] - params['download-dir'] = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep) + log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type'))) - torrent_params = {} - if self.conf('ratio'): - torrent_params = { - 'seedRatioLimit': self.conf('ratio'), - 'seedRatioMode': self.conf('ratiomode') - } + if not self.connect(): + return False if not filedata and data.get('type') == 'torrent': log.error('Failed sending torrent, no data') return False - # Send request to Transmission - try: - trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) - if data.get('type') == 'torrent_magnet': - remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params) - torrent_params['trackerAdd'] = self.torrent_trackers + # Set parameters for adding torrent + params = {} + params['paused'] = self.conf('paused', default = False) + + if self.conf('directory'): + if os.path.isdir(self.conf('directory')): + params['download-dir'] = self.conf('directory') else: - remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params) + log.error('Download directory from Transmission settings: %s doesn\'t exist', self.conf('directory')) - if not remote_torrent: - return False + # Change parameters of torrent + torrent_params = {} + if data.get('seed_ratio') and self.conf('seeding'): + torrent_params['seedRatioLimit'] = tryFloat(data.get('seed_ratio')) + torrent_params['seedRatioMode'] = 1 - # Change settings of added torrents - elif torrent_params: - trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params) + if data.get('seed_time') and self.conf('seeding'): + torrent_params['seedIdleLimit'] = tryInt(data.get('seed_time'))*60 + torrent_params['seedIdleMode'] = 1 - log.info('Torrent sent to Transmission successfully.') - return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) - except: - log.error('Failed to change settings for transfer: %s', traceback.format_exc()) + # Send request to Transmission + if data.get('type') == 'torrent_magnet': + remote_torrent = self.trpc.add_torrent_uri(data.get('url'), arguments = params) + torrent_params['trackerAdd'] = self.torrent_trackers + else: + remote_torrent = self.trpc.add_torrent_file(b64encode(filedata), arguments = params) + + if not remote_torrent: + log.error('Failed sending torrent to Transmission') return False + # Change settings of added torrents + if torrent_params: + self.trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params) + + log.info('Torrent sent to Transmission successfully.') + return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) + def getAllDownloadStatus(self): log.debug('Checking Transmission download status.') - # Load host from config and split out port. - host = self.conf('host').split(':') - if not isInt(host[1]): - log.error('Config properties are not filled in correctly, port is missing.') - return False - - # Go through Queue - try: - trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) - return_params = { - 'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio'] - } - queue = trpc.get_alltorrents(return_params) - except Exception, err: - log.error('Failed getting queue: %s', err) + if not self.connect(): return False - if not queue: - return [] - statuses = StatusList(self) - # Get torrents status - # CouchPotato Status - #status = 'busy' - #status = 'failed' - #status = 'completed' - # Transmission Status - #status = 0 => "Torrent is stopped" - #status = 1 => "Queued to check files" - #status = 2 => "Checking files" - #status = 3 => "Queued to download" - #status = 4 => "Downloading" - #status = 4 => "Queued to seed" - #status = 6 => "Seeding" - #To do : - # add checking file - # manage no peer in a range time => fail + return_params = { + 'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit'] + } + + queue = self.trpc.get_alltorrents(return_params) + if not (queue and queue.get('torrents')): + log.debug('Nothing in queue or error') + return False for item in queue['torrents']: - log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished'])) + log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], item['isFinished'])) if not os.path.isdir(Env.setting('from', 'renamer')): log.error('Renamer "from" folder doesn\'t to exist.') return - if (item['percentDone'] * 100) >= 100 and (item['status'] == 6 or item['status'] == 0) and item['uploadRatio'] > self.conf('ratio'): - try: - trpc.stop_torrent(item['hashString'], {}) - statuses.append({ - 'id': item['hashString'], - 'name': item['name'], - 'status': 'completed', - 'original_status': item['status'], - 'timeleft': str(timedelta(seconds = 0)), - 'folder': os.path.join(item['downloadDir'], item['name']), - }) - except Exception, err: - log.error('Failed to stop and remove torrent "%s" with error: %s', (item['name'], err)) - statuses.append({ - 'id': item['hashString'], - 'name': item['name'], - 'status': 'failed', - 'original_status': item['status'], - 'timeleft': str(timedelta(seconds = 0)), - }) - else: - statuses.append({ - 'id': item['hashString'], - 'name': item['name'], - 'status': 'busy', - 'original_status': item['status'], - 'timeleft': str(timedelta(seconds = item['eta'])), # Is ETA in seconds?? - }) + status = 'busy' + if item['isStalled'] and self.conf('stalled_as_failed'): + status = 'failed' + elif item['status'] == 0 and item['percentDone'] == 1: + status = 'completed' + elif item['status'] in [5, 6]: + status = 'seeding' + + statuses.append({ + 'id': item['hashString'], + 'name': item['name'], + 'status': status, + 'original_status': item['status'], + 'seed_ratio': item['uploadRatio'], + 'timeleft': str(timedelta(seconds = item['eta'])), + 'folder': os.path.join(item['downloadDir'], item['name']), + }) return statuses + def pause(self, item, pause = True): + if pause: + return self.trpc.stop_torrent(item['hashString']) + else: + return self.trpc.start_torrent(item['hashString']) + + def removeFailed(self, item): + log.info('%s failed downloading, deleting...', item['name']) + return self.trpc.remove_torrent(self, item['hashString'], True) + + def processComplete(self, item, delete_files = False): + log.debug('Requesting Transmission to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else '')) + return self.trpc.remove_torrent(self, item['hashString'], delete_files) + class TransmissionRPC(object): """TransmissionRPC lite library""" - def __init__(self, host = 'localhost', port = 9091, username = None, password = None): super(TransmissionRPC, self).__init__() @@ -184,7 +169,7 @@ class TransmissionRPC(object): log.debug('request: %s', json.dumps(ojson)) log.debug('response: %s', json.dumps(response)) if response['result'] == 'success': - log.debug('Transmission action successfull') + log.debug('Transmission action successful') return response['arguments'] else: log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result']) @@ -236,13 +221,15 @@ class TransmissionRPC(object): post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag} return self._request(post_data) - def stop_torrent(self, torrent_id, arguments): - arguments['ids'] = torrent_id - post_data = {'arguments': arguments, 'method': 'torrent-stop', 'tag': self.tag} + def stop_torrent(self, torrent_id): + post_data = {'arguments': {'ids': torrent_id}, 'method': 'torrent-stop', 'tag': self.tag} return self._request(post_data) - def remove_torrent(self, torrent_id, remove_local_data, arguments): - arguments['ids'] = torrent_id - arguments['delete-local-data'] = remove_local_data - post_data = {'arguments': arguments, 'method': 'torrent-remove', 'tag': self.tag} + def start_torrent(self, torrent_id): + post_data = {'arguments': {'ids': torrent_id}, 'method': 'torrent-start', 'tag': self.tag} return self._request(post_data) + + def remove_torrent(self, torrent_id, delete_local_data): + post_data = {'arguments': {'ids': torrent_id, 'delete-local-data': delete_local_data}, 'method': 'torrent-remove', 'tag': self.tag} + return self._request(post_data) + diff --git a/couchpotato/core/downloaders/utorrent/__init__.py b/couchpotato/core/downloaders/utorrent/__init__.py index 2c494eb..f2fcc13 100644 --- a/couchpotato/core/downloaders/utorrent/__init__.py +++ b/couchpotato/core/downloaders/utorrent/__init__.py @@ -37,6 +37,28 @@ config = [{ 'description': 'Label to add torrent as.', }, { + 'name': 'seeding', + 'label': 'Seeding support', + 'default': True, + 'type': 'bool', + 'description': '(Hard)links/copies after download is complete (if enabled in renamer), wait for seeding to finish before (re)moving.', + }, + { + 'name': 'remove_complete', + 'label': 'Remove torrent', + 'default': True, + 'type': 'bool', + 'description': 'Remove the torrent from uTorrent after it finished seeding.', + }, + { + 'name': 'delete_files', + 'label': 'Remove files', + 'default': True, + 'type': 'bool', + 'advanced': True, + 'description': 'Also remove the leftover files.', + }, + { 'name': 'paused', 'type': 'bool', 'default': False, diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index f49859a..738fbad 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -2,6 +2,7 @@ from base64 import b16encode, b32decode from bencode import bencode, bdecode from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.helpers.encoding import isInt, ss +from couchpotato.core.helpers.variable import tryInt, tryFloat from couchpotato.core.logger import CPLog from datetime import timedelta from hashlib import sha1 @@ -23,16 +24,40 @@ class uTorrent(Downloader): type = ['torrent', 'torrent_magnet'] utorrent_api = None - def download(self, data, movie, filedata = None): - - log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type'))) - + def connect(self): # Load host from config and split out port. host = self.conf('host').split(':') if not isInt(host[1]): log.error('Config properties are not filled in correctly, port is missing.') return False + if not self.utorrent_api: + self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) + return self.utorrent_api + + def download(self, data, movie, filedata = None): + + log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type'))) + + if not self.connect(): + return False + + settings = self.utorrent_api.get_settings() + if not settings: + return False + + #Fix settings in case they are not set for CPS compatibility + new_settings = {} + if self.conf('seeding') and not (settings.get('seed_prio_limitul') == 0 and settings['seed_prio_limitul_flag']): + new_settings['seed_prio_limitul'] = 0 + new_settings['seed_prio_limitul_flag'] = True + log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.') + if settings.get('bt.read_only_on_complete'): #This doesnt work as this option seems to be not available through the api + new_settings['bt.read_only_on_complete'] = False + log.info('Updated uTorrent settings to not set the files to read only after completing.') + if new_settings: + self.utorrent_api.set_settings(new_settings) + torrent_params = {} if self.conf('label'): torrent_params['label'] = self.conf('label') @@ -49,75 +74,72 @@ class uTorrent(Downloader): torrent_hash = sha1(bencode(info)).hexdigest().upper() torrent_filename = self.createFileName(data, filedata, movie) + if data.get('seed_ratio') and self.conf('seeding'): + torrent_params['seed_override'] = 1 + torrent_params['seed_ratio'] = tryInt(tryFloat(data['seed_ratio'])*1000) + + if data.get('seed_time') and self.conf('seeding'): + torrent_params['seed_override'] = 1 + torrent_params['seed_time'] = tryInt(data['seed_time'])*3600 + # Convert base 32 to hex if len(torrent_hash) == 32: torrent_hash = b16encode(b32decode(torrent_hash)) # Send request to uTorrent - try: - if not self.utorrent_api: - self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) + if data.get('type') == 'torrent_magnet': + self.utorrent_api.add_torrent_uri(data.get('url')) + else: + self.utorrent_api.add_torrent_file(torrent_filename, filedata) - if data.get('type') == 'torrent_magnet': - self.utorrent_api.add_torrent_uri(data.get('url')) - else: - self.utorrent_api.add_torrent_file(torrent_filename, filedata) + # Change settings of added torrents + self.utorrent_api.set_torrent(torrent_hash, torrent_params) + if self.conf('paused', default = 0): + self.utorrent_api.pause_torrent(torrent_hash) - # Change settings of added torrents - self.utorrent_api.set_torrent(torrent_hash, torrent_params) - if self.conf('paused', default = 0): - self.utorrent_api.pause_torrent(torrent_hash) - return self.downloadReturnId(torrent_hash) - except Exception, err: - log.error('Failed to send torrent to uTorrent: %s', err) - return False + return self.downloadReturnId(torrent_hash) def getAllDownloadStatus(self): log.debug('Checking uTorrent download status.') - # Load host from config and split out port. - host = self.conf('host').split(':') - if not isInt(host[1]): - log.error('Config properties are not filled in correctly, port is missing.') + if not self.connect(): return False - try: - self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) - except Exception, err: - log.error('Failed to get uTorrent object: %s', err) - return False + statuses = StatusList(self) - data = '' - try: - data = self.utorrent_api.get_status() - queue = json.loads(data) - if queue.get('error'): - log.error('Error getting data from uTorrent: %s', queue.get('error')) - return False + data = self.utorrent_api.get_status() + if not data: + log.error('Error getting data from uTorrent') + return False - except Exception, err: - log.error('Failed to get status from uTorrent: %s', err) + queue = json.loads(data) + if queue.get('error'): + log.error('Error getting data from uTorrent: %s', queue.get('error')) return False - if queue.get('torrents', []) == []: + if not queue.get('torrents'): log.debug('Nothing in queue') return False - statuses = StatusList(self) - # Get torrents - for item in queue.get('torrents', []): + for item in queue['torrents']: # item[21] = Paused | Downloading | Seeding | Finished status = 'busy' - if item[21] == 'Finished' or item[21] == 'Seeding': + if 'Finished' in item[21]: status = 'completed' + elif 'Seeding' in item[21]: + if self.conf('seeding'): + status = 'seeding' + else: + status = 'completed' statuses.append({ 'id': item[0], 'name': item[2], 'status': status, + 'seed_ratio': float(item[7])/1000, 'original_status': item[1], 'timeleft': str(timedelta(seconds = item[10])), 'folder': item[26], @@ -125,7 +147,16 @@ class uTorrent(Downloader): return statuses + def pause(self, download_info, pause = True): + if not self.connect(): + return False + return self.utorrent_api.pause_torrent(download_info['id'], pause) + def processComplete(self, item, delete_files = False): + log.debug('Requesting uTorrent to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else '')) + if not self.connect(): + return False + return self.utorrent_api.remove_torrent(item['id'], remove_data = delete_files) class uTorrentAPI(object): @@ -190,10 +221,24 @@ class uTorrentAPI(object): action += "&s=%s&v=%s" % (k, v) return self._request(action) - def pause_torrent(self, hash): - action = "action=pause&hash=%s" % hash + def pause_torrent(self, hash, pause = True): + if pause: + action = "action=pause&hash=%s" % hash + else: + action = "action=unpause&hash=%s" % hash return self._request(action) + def stop_torrent(self, hash): + action = "action=stop&hash=%s" % hash + return self._request(action) + + def remove_torrent(self, hash, remove_data = False): + if remove_data: + action = "action=removedata&hash=%s" % hash + else: + action = "action=remove&hash=%s" % hash + return self._request(action) + def get_status(self): action = "list=1" return self._request(action) @@ -219,3 +264,11 @@ class uTorrentAPI(object): log.error('Failed to get settings from uTorrent: %s', err) return settings_dict + + def set_settings(self, settings_dict = {}): + for key in settings_dict: + if isinstance(settings_dict[key], bool): + settings_dict[key] = 1 if settings_dict[key] else 0 + + action = 'action=setsetting' + ''.join(['&s=%s&v=%s' % (key, value) for (key, value) in settings_dict.items()]) + return self._request(action) diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 60ab96b..adc4ebf 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -425,7 +425,9 @@ } .movies .data .quality .available { background-color: #578bc3; } - .movies .data .quality .snatched { background-color: #369545; } + .movies .data .quality .failed { background-color: #a43d34; } + .movies .data .quality .snatched { background-color: #a2a232; } + .movies .data .quality .seeding { background-color: #0a6819; } .movies .data .quality .done { background-color: #369545; opacity: 1; diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index 155a939..e2bb4b9 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -121,7 +121,7 @@ config = [{ 'label': 'Torrent File Action', 'default': 'move', 'type': 'dropdown', - 'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink'), ('Move & Sym link', 'move_symlink')], + 'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Move & Sym link', 'move_symlink')], 'description': 'Define which kind of file operation you want to use for torrents. Before you start using hard links or sym links, PLEASE read about their possible drawbacks.', 'advanced': True, }, diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index a2fb5dc..1070e4d 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -10,6 +10,7 @@ from couchpotato.core.settings.model import Library, File, Profile, Release, \ ReleaseInfo from couchpotato.environment import Env import errno +import fnmatch import os import re import shutil @@ -38,7 +39,6 @@ class Renamer(Plugin): addEvent('renamer.check_snatched', self.checkSnatched) addEvent('app.load', self.scan) - addEvent('app.load', self.checkSnatched) addEvent('app.load', self.setCrons) # Enable / disable interval @@ -65,18 +65,19 @@ class Renamer(Plugin): downloader = kwargs.get('downloader', None) download_id = kwargs.get('download_id', None) + download_info = {'folder': movie_folder} if movie_folder else None + if download_info: + download_info.update({'id': download_id, 'downloader': downloader} if download_id else {}) + fire_handle = fireEvent if not async else fireEventAsync - fire_handle('renamer.scan', - movie_folder = movie_folder, - download_info = {'id': download_id, 'downloader': downloader} if download_id else None - ) + fire_handle('renamer.scan', download_info) return { 'success': True } - def scan(self, movie_folder = None, download_info = None): + def scan(self, download_info = None): if self.isDisabled(): return @@ -85,6 +86,8 @@ class Renamer(Plugin): log.info('Renamer is already running, if you see this often, check the logs above for errors.') return + movie_folder = download_info and download_info.get('folder') + # Check to see if the "to" folder is inside the "from" folder. if movie_folder and not os.path.isdir(movie_folder) or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')): l = log.debug if movie_folder else log.error @@ -97,6 +100,10 @@ class Renamer(Plugin): log.error('The "to" and "from" folders can\'t be inside of or the same as the provided movie folder.') return + # Make sure a checkSnatched marked all downloads/seeds as such + if not download_info and self.conf('run_every') > 0: + fireEvent('renamer.check_snatched') + self.renaming_started = True # make sure the movie folder name is included in the search @@ -144,7 +151,7 @@ class Renamer(Plugin): # Add _UNKNOWN_ if no library item is connected if not group['library'] or not movie_title: - self.tagDir(group, 'unknown') + self.tagDir(group['parentdir'], 'unknown') continue # Rename the files using the library data else: @@ -192,7 +199,7 @@ class Renamer(Plugin): # Move nfo depending on settings if file_type is 'nfo' and not self.conf('rename_nfo'): log.debug('Skipping, renaming of %s disabled', file_type) - if self.conf('cleanup'): + if self.conf('cleanup') and not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)): for current_file in group['files'][file_type]: remove_files.append(current_file) continue @@ -354,7 +361,7 @@ class Renamer(Plugin): log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label)) # Add exists tag to the .ignore file - self.tagDir(group, 'exists') + self.tagDir(group['parentdir'], 'exists') # Notify on rename fail download_message = 'Renaming of %s (%s) canceled, exists in %s already.' % (movie.library.titles[0].title, group['meta_data']['quality']['label'], release.quality.label) @@ -405,7 +412,7 @@ class Renamer(Plugin): except: log.error('Failed removing %s: %s', (src, traceback.format_exc())) - self.tagDir(group, 'failed_remove') + self.tagDir(group['parentdir'], 'failed_remove') # Delete leftover folder from older releases for delete_folder in delete_folders: @@ -425,14 +432,16 @@ class Renamer(Plugin): self.makeDir(os.path.dirname(dst)) try: - self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(download_info)) + self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(download_info) or self.fileIsAdded(src, group)) group['renamed_files'].append(dst) except: log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc())) - self.tagDir(group, 'failed_rename') + self.tagDir(group['parentdir'], 'failed_rename') - if self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info): - self.tagDir(group, 'renamed already') + # Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent + if (movie_folder and self.conf('from') in movie_folder or not movie_folder) and \ + self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info): + self.tagDir(group['parentdir'], 'renamed_already') # Remove matching releases for release in remove_releases: @@ -480,12 +489,9 @@ class Renamer(Plugin): return rename_files # This adds a file to ignore / tag a release so it is ignored later - def tagDir(self, group, tag): - - ignore_file = None - for movie_file in sorted(list(group['files']['movie'])): - ignore_file = '%s.ignore' % os.path.splitext(movie_file)[0] - break + def tagDir(self, folder, tag): + if not os.path.isdir(folder) or not tag: + return text = """This file is from CouchPotato It has marked this release as "%s" @@ -493,9 +499,27 @@ This file hides the release from the renamer Remove it if you want it to be renamed (again, or at least let it try again) """ % tag - if ignore_file: - self.createFile(ignore_file, text) + self.createFile(os.path.join(folder, '%s.ignore' % tag), text) + + def untagDir(self, folder, tag = None): + if not os.path.isdir(folder): + return + + # Remove any .ignore files + for root, dirnames, filenames in os.walk(folder): + for filename in fnmatch.filter(filenames, '%s.ignore' % tag if tag else '*'): + os.remove((os.path.join(root, filename))) + + def hastagDir(self, folder, tag = None): + if not os.path.isdir(folder): + return False + # Find any .ignore files + for root, dirnames, filenames in os.walk(folder): + if fnmatch.filter(filenames, '%s.ignore' % tag if tag else '*'): + return True + + return False def moveFile(self, old, dest, forcemove = False): dest = ss(dest) @@ -504,8 +528,6 @@ Remove it if you want it to be renamed (again, or at least let it try again) shutil.move(old, dest) elif self.conf('file_action') == 'hardlink': link(old, dest) - elif self.conf('file_action') == 'symlink': - symlink(old, dest) elif self.conf('file_action') == 'copy': shutil.copy(old, dest) elif self.conf('file_action') == 'move_symlink': @@ -584,19 +606,21 @@ Remove it if you want it to be renamed (again, or at least let it try again) if self.checking_snatched: log.debug('Already checking snatched') + return False self.checking_snatched = True - snatched_status, ignored_status, failed_status, done_status = \ - fireEvent('status.get', ['snatched', 'ignored', 'failed', 'done'], single = True) + snatched_status, ignored_status, failed_status, done_status, seeding_status, downloaded_status = \ + fireEvent('status.get', ['snatched', 'ignored', 'failed', 'done', 'seeding', 'downloaded'], single = True) db = get_session() rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all() + rels.extend(db.query(Release).filter_by(status_id = seeding_status.get('id')).all()) + scan_items = [] scan_required = False if rels: - self.checking_snatched = True log.debug('Checking status snatched releases...') statuses = fireEvent('download.status', merge = True) @@ -612,7 +636,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) default_title = getTitle(rel.movie.library) # Check if movie has already completed and is manage tab (legacy db correction) - if rel.movie.status_id == done_status.get('id'): + if rel.movie.status_id == done_status.get('id') and rel.status_id == snatched_status.get('id'): log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title) rel.status_id = ignored_status.get('id') rel.last_edit = int(time.time()) @@ -640,7 +664,35 @@ Remove it if you want it to be renamed (again, or at least let it try again) log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft)) if item['status'] == 'busy': + # Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading + if item['folder'] and self.conf('from') in item['folder']: + self.tagDir(item['folder'], 'downloading') + pass + elif item['status'] == 'seeding': + #If linking setting is enabled, process release + if self.conf('file_action') != 'move' and not rel.movie.status_id == done_status.get('id') and item['id'] and item['downloader'] and item['folder']: + log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (item['name'], item['seed_ratio'])) + + # Remove the downloading tag + self.untagDir(item['folder'], 'downloading') + + rel.status_id = seeding_status.get('id') + rel.last_edit = int(time.time()) + db.commit() + + # Scan and set the torrent to paused if required + item.update({'pause': True, 'scan': True, 'process_complete': False}) + scan_items.append(item) + else: + if rel.status_id != seeding_status.get('id'): + rel.status_id = seeding_status.get('id') + rel.last_edit = int(time.time()) + db.commit() + + #let it seed + log.debug('%s is seeding with ratio: %s', (item['name'], item['seed_ratio'])) + pass elif item['status'] == 'failed': fireEvent('download.remove_failed', item, single = True) rel.status_id = failed_status.get('id') @@ -652,7 +704,35 @@ Remove it if you want it to be renamed (again, or at least let it try again) elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) if item['id'] and item['downloader'] and item['folder']: - fireEventAsync('renamer.scan', movie_folder = item['folder'], download_info = item) + + # If the release has been seeding, process now the seeding is done + if rel.status_id == seeding_status.get('id'): + if rel.movie.status_id == done_status.get('id'): # and self.conf('file_action') != 'move': + # Set the release to done as the movie has already been renamed + rel.status_id = downloaded_status.get('id') + rel.last_edit = int(time.time()) + db.commit() + + # Allow the downloader to clean-up + item.update({'pause': False, 'scan': False, 'process_complete': True}) + scan_items.append(item) + else: + # Set the release to snatched so that the renamer can process the release as if it was never seeding + rel.status_id = snatched_status.get('id') + rel.last_edit = int(time.time()) + db.commit() + + # Scan and Allow the downloader to clean-up + item.update({'pause': False, 'scan': True, 'process_complete': True}) + scan_items.append(item) + + else: + # Remove the downloading tag + self.untagDir(item['folder'], 'downloading') + + # Scan and Allow the downloader to clean-up + item.update({'pause': False, 'scan': True, 'process_complete': True}) + scan_items.append(item) else: scan_required = True @@ -665,6 +745,23 @@ Remove it if you want it to be renamed (again, or at least let it try again) 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 item in scan_items: + # Ask the renamer to scan the item + if item['scan']: + if item['pause'] and self.conf('file_action') == 'move_symlink': + fireEvent('download.pause', item = item, pause = True, single = True) + fireEvent('renamer.scan', download_info = item) + if item['pause'] and self.conf('file_action') == 'move_symlink': + fireEvent('download.pause', item = item, pause = False, single = True) + if item['process_complete']: + #First make sure the files were succesfully processed + if not self.hastagDir(item['folder'], 'failed_rename'): + # Remove the seeding tag if it exists + self.untagDir(item['folder'], 'renamed_already') + # Ask the downloader to process the item + fireEvent('download.process_complete', item = item, single = True) + if scan_required: fireEvent('renamer.scan') @@ -706,3 +803,9 @@ Remove it if you want it to be renamed (again, or at least let it try again) def downloadIsTorrent(self, download_info): return download_info and download_info.get('type') in ['torrent', 'torrent_magnet'] + + def fileIsAdded(self, src, group): + if not group['files'].get('added'): + return False + return src in group['files']['added'] + diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index a400b15..e48d274 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -225,6 +225,10 @@ class Scanner(Plugin): # Remove the found files from the leftover stack leftovers = leftovers - set(found_files) + exts = [getExt(ff) for ff in found_files] + if 'ignore' in exts: + ignored_identifiers.append(identifier) + # Break if CP wants to shut down if self.shuttingDown(): break @@ -251,6 +255,10 @@ class Scanner(Plugin): # Remove the found files from the leftover stack leftovers = leftovers - set([ff]) + ext = getExt(ff) + if ext == 'ignore': + ignored_identifiers.append(new_identifier) + # Break if CP wants to shut down if self.shuttingDown(): break diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py index c8c8f66..8db2bf7 100644 --- a/couchpotato/core/plugins/status/main.py +++ b/couchpotato/core/plugins/status/main.py @@ -23,6 +23,7 @@ class StatusPlugin(Plugin): 'ignored': 'Ignored', 'available': 'Available', 'suggest': 'Suggest', + 'seeding': 'Seeding', } status_cached = {} diff --git a/couchpotato/core/plugins/subtitle/main.py b/couchpotato/core/plugins/subtitle/main.py index c6bef6a..0ea1de3 100644 --- a/couchpotato/core/plugins/subtitle/main.py +++ b/couchpotato/core/plugins/subtitle/main.py @@ -60,6 +60,7 @@ 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['files']['added'].append(d_sub.path) group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2] return True diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 182a031..cb7b16d 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -276,6 +276,8 @@ class ResultList(list): 'type': self.provider.type, 'provider': self.provider.getName(), 'download': self.provider.loginDownload if self.provider.urls.get('login') else self.provider.download, + 'seed_ratio': Env.setting('seed_ratio', section = self.provider.getName().lower(), default = ''), + 'seed_time': Env.setting('seed_time', section = self.provider.getName().lower(), default = ''), 'url': '', 'name': '', 'age': 0, diff --git a/couchpotato/core/providers/torrent/awesomehd/__init__.py b/couchpotato/core/providers/torrent/awesomehd/__init__.py index 5c8c979..e2587a5 100644 --- a/couchpotato/core/providers/torrent/awesomehd/__init__.py +++ b/couchpotato/core/providers/torrent/awesomehd/__init__.py @@ -24,6 +24,20 @@ config = [{ 'default': '', }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'only_internal', 'advanced': True, 'type': 'bool', diff --git a/couchpotato/core/providers/torrent/hdbits/__init__.py b/couchpotato/core/providers/torrent/hdbits/__init__.py index 8a9fc80..0b370e1 100644 --- a/couchpotato/core/providers/torrent/hdbits/__init__.py +++ b/couchpotato/core/providers/torrent/hdbits/__init__.py @@ -32,6 +32,20 @@ config = [{ 'default': '', }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', diff --git a/couchpotato/core/providers/torrent/iptorrents/__init__.py b/couchpotato/core/providers/torrent/iptorrents/__init__.py index 24f9772..c1eea5c 100644 --- a/couchpotato/core/providers/torrent/iptorrents/__init__.py +++ b/couchpotato/core/providers/torrent/iptorrents/__init__.py @@ -35,6 +35,20 @@ config = [{ 'description': 'Only search for [FreeLeech] torrents.', }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', diff --git a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py index 999dbb1..90f9eea 100644 --- a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py +++ b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py @@ -20,6 +20,20 @@ config = [{ 'default': True, }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', diff --git a/couchpotato/core/providers/torrent/passthepopcorn/__init__.py b/couchpotato/core/providers/torrent/passthepopcorn/__init__.py index a535034..f0cb966 100644 --- a/couchpotato/core/providers/torrent/passthepopcorn/__init__.py +++ b/couchpotato/core/providers/torrent/passthepopcorn/__init__.py @@ -63,6 +63,20 @@ config = [{ 'description': 'Require staff-approval for releases to be accepted.' }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', @@ -71,6 +85,6 @@ config = [{ 'description': 'Starting score for each release found via this provider.', } ], -} + } ] }] diff --git a/couchpotato/core/providers/torrent/publichd/__init__.py b/couchpotato/core/providers/torrent/publichd/__init__.py index 3c27cf4..22e2dbb 100644 --- a/couchpotato/core/providers/torrent/publichd/__init__.py +++ b/couchpotato/core/providers/torrent/publichd/__init__.py @@ -20,6 +20,20 @@ config = [{ 'default': True, }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', diff --git a/couchpotato/core/providers/torrent/sceneaccess/__init__.py b/couchpotato/core/providers/torrent/sceneaccess/__init__.py index baad57f..786f28a 100644 --- a/couchpotato/core/providers/torrent/sceneaccess/__init__.py +++ b/couchpotato/core/providers/torrent/sceneaccess/__init__.py @@ -29,6 +29,20 @@ config = [{ 'type': 'password', }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', diff --git a/couchpotato/core/providers/torrent/scenehd/__init__.py b/couchpotato/core/providers/torrent/scenehd/__init__.py index 3cd2132..9b967f4 100644 --- a/couchpotato/core/providers/torrent/scenehd/__init__.py +++ b/couchpotato/core/providers/torrent/scenehd/__init__.py @@ -29,6 +29,20 @@ config = [{ 'type': 'password', }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', diff --git a/couchpotato/core/providers/torrent/thepiratebay/__init__.py b/couchpotato/core/providers/torrent/thepiratebay/__init__.py index f2394dd..2c90243 100644 --- a/couchpotato/core/providers/torrent/thepiratebay/__init__.py +++ b/couchpotato/core/providers/torrent/thepiratebay/__init__.py @@ -26,6 +26,20 @@ config = [{ 'description': 'Domain for requests, keep empty to let CouchPotato pick.', }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', diff --git a/couchpotato/core/providers/torrent/torrentbytes/__init__.py b/couchpotato/core/providers/torrent/torrentbytes/__init__.py index 10e581a..c7f4437 100644 --- a/couchpotato/core/providers/torrent/torrentbytes/__init__.py +++ b/couchpotato/core/providers/torrent/torrentbytes/__init__.py @@ -29,6 +29,20 @@ config = [{ 'type': 'password', }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', diff --git a/couchpotato/core/providers/torrent/torrentday/__init__.py b/couchpotato/core/providers/torrent/torrentday/__init__.py index de715b5..d3bbaa1 100644 --- a/couchpotato/core/providers/torrent/torrentday/__init__.py +++ b/couchpotato/core/providers/torrent/torrentday/__init__.py @@ -29,6 +29,20 @@ config = [{ 'type': 'password', }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', diff --git a/couchpotato/core/providers/torrent/torrentleech/__init__.py b/couchpotato/core/providers/torrent/torrentleech/__init__.py index fa048d5..d3ee761 100644 --- a/couchpotato/core/providers/torrent/torrentleech/__init__.py +++ b/couchpotato/core/providers/torrent/torrentleech/__init__.py @@ -29,6 +29,20 @@ config = [{ 'type': 'password', }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', diff --git a/couchpotato/core/providers/torrent/torrentshack/__init__.py b/couchpotato/core/providers/torrent/torrentshack/__init__.py index 203e099..69ad176 100644 --- a/couchpotato/core/providers/torrent/torrentshack/__init__.py +++ b/couchpotato/core/providers/torrent/torrentshack/__init__.py @@ -28,6 +28,20 @@ config = [{ 'type': 'password', }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'scene_only', 'type': 'bool', 'default': False, diff --git a/couchpotato/core/providers/torrent/yify/__init__.py b/couchpotato/core/providers/torrent/yify/__init__.py index 70d6568..f953e80 100644 --- a/couchpotato/core/providers/torrent/yify/__init__.py +++ b/couchpotato/core/providers/torrent/yify/__init__.py @@ -20,6 +20,20 @@ config = [{ 'default': 0 }, { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Torrent will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Torrent will not be (re)moved until this seed time (in hours) is met.', + }, + { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index 8e6cced..db90c30 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -1,4 +1,4 @@ -var CouchPotato = new Class({ +var CouchPotato = new Class({ Implements: [Events, Options], @@ -179,7 +179,7 @@ var CouchPotato = new Class({ shutdown: function(){ var self = this; - self.blockPage('You have shutdown. This is what suppose to happen ;)'); + self.blockPage('You have shutdown. This is what is supposed to happen ;)'); Api.request('app.shutdown', { 'onComplete': self.blockPage.bind(self) });