From 461a0b364592577466246c979425774921c811c0 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Wed, 17 Apr 2013 23:19:30 +0200 Subject: [PATCH 01/10] Seeding support Design intent: - Option to turn seeding support on or off - After torrent downloading is complete the seeding phase starts, seeding parameters can be set per torrent provide (0 disables them) - When the seeding phase starts the checkSnatched function renames all files if (sym)linking/copying is used. The movie is set to done (!), the release to seeding status. - Note that Direct symlink functionality is removed as the original file needs to end up in the movies store and not the downloader store (if the downloader cleans up his files, the original is deleted and the symlinks are useless) - checkSnatched waits until downloader sets the download to completed (met the seeding parameters) - When completed, checkSnatched intiates the renamer if move is used, or if linking is used asks the downloader to remove the torrent and clean-up it's files and sets the release to downloaded - Updated some of the .ignore file behavior to allow the downloader to remove its files Known items/issues: - only implemented for uTorrent and Transmission - text in downloader settings is too long and messes up the layout... To do (after this PR): - implement for other torrent downloaders - complete download removal for NZBs (remove from history in sabNZBd) - failed download management for torrents (no seeders, takes too long, etc.) - unrar support Updates: - Added transmission support - Simplified uTorrent - Added checkSnatched to renamer to make sure the poller is always first - Updated default values and removed advanced option tag for providers - Updated the tagger to allow removing of ignore tags and tagging when the group is not known - Added tagging of downloading torrents - fixed subtitles being leftover after seeding --- couchpotato/core/_base/_core/main.py | 2 +- couchpotato/core/downloaders/base.py | 35 +++- .../core/downloaders/transmission/__init__.py | 32 +++- couchpotato/core/downloaders/transmission/main.py | 195 ++++++++++----------- couchpotato/core/downloaders/utorrent/__init__.py | 22 +++ couchpotato/core/downloaders/utorrent/main.py | 131 +++++++++----- couchpotato/core/plugins/renamer/__init__.py | 2 +- couchpotato/core/plugins/renamer/main.py | 156 ++++++++++++++--- couchpotato/core/plugins/scanner/main.py | 8 + couchpotato/core/plugins/status/main.py | 1 + couchpotato/core/plugins/subtitle/main.py | 1 + couchpotato/core/providers/base.py | 2 + .../core/providers/torrent/hdbits/__init__.py | 14 ++ .../core/providers/torrent/iptorrents/__init__.py | 14 ++ .../providers/torrent/kickasstorrents/__init__.py | 14 ++ .../providers/torrent/passthepopcorn/__init__.py | 16 +- .../core/providers/torrent/publichd/__init__.py | 14 ++ .../core/providers/torrent/sceneaccess/__init__.py | 14 ++ .../core/providers/torrent/scenehd/__init__.py | 14 ++ .../providers/torrent/thepiratebay/__init__.py | 14 ++ .../providers/torrent/torrentbytes/__init__.py | 14 ++ .../core/providers/torrent/torrentday/__init__.py | 14 ++ .../providers/torrent/torrentleech/__init__.py | 14 ++ .../providers/torrent/torrentshack/__init__.py | 14 ++ couchpotato/static/scripts/couchpotato.js | 4 +- 25 files changed, 564 insertions(+), 197 deletions(-) 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..5ffad3d 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')) + + 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/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index 6dfbd3f..8f19e34 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)links/copies after download is complete (if enabled in renamer), wait for seeding to finish before (re)moving. Set the seeding goal in 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', diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 6094e89..dc798d2 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,123 @@ 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', '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['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 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 +163,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 +215,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..674a52a 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. Stop seeding manually in uTorrent, or check the option Queueing->When uTorrent reaches the seeding goal->Limit the upload rate and set it to 0 to stop seeding after the seeding goal set in the torrent providers is met.', + }, + { + '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..cc02b80 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,28 @@ 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 + torrent_params = {} if self.conf('label'): torrent_params['label'] = self.conf('label') @@ -49,75 +62,82 @@ 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) + + # Check if uTorrent completes the torrent if seeding goal is met. + # Note that CPS can also check if the goal has been met but for now it should be done by uTorrent + if not (settings.get('seed_prio_limitul') == 0 and settings['seed_prio_limitul_flag']): + log.info('With the current settings uTorrent does not set torrents that completed the seed ratio and time to complete. Please stop them manually in uTorrent or check the option Queueing->When uTorrent reaches the seeding goal->Limit the upload rate and set it to 0') + + if data.get('seed_time') and self.conf('seeding'): + torrent_params['seed_override'] = 1 + torrent_params['seed_time'] = tryInt(data['seed_time'])*3600 + + # Check if uTorrent completes the torrent if seeding goal is met. + # Note that CPS can also check if the goal has been met but for now it should be done by uTorrent + if not (settings.get('seed_prio_limitul') == 0 and settings['seed_prio_limitul_flag']): + log.info('With the current settings uTorrent does not set torrents that completed the seed ratio and time to complete. Please stop them manually in uTorrent or check the option Queueing->When uTorrent reaches the seeding goal->Limit the upload rate and set it to 0') + # 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 +145,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 +219,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) 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 c219f17..76f6c67 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,30 @@ 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: + #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 +699,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 +740,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 +798,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'] + \ No newline at end of file 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/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/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) }); From 7ed43da42530a5a6c29d1a7b0bd7edb90756a551 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Sat, 22 Jun 2013 16:22:33 +0200 Subject: [PATCH 02/10] Also set seeding status in case nothing is done --- couchpotato/core/plugins/renamer/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 76f6c67..92d7f90 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -685,6 +685,11 @@ Remove it if you want it to be renamed (again, or at least let it try again) 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 From cdee08bd367e623fa0eb218f2534fda9fb24da99 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Sat, 22 Jun 2013 16:24:59 +0200 Subject: [PATCH 03/10] Add status colours in dashboard --- couchpotato/core/plugins/movie/static/movie.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 0460047..dccd6cd 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; From 628c0e5dcc42fa75e377e3a209a9a99093beccff Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Wed, 26 Jun 2013 19:52:39 +0200 Subject: [PATCH 04/10] Add yify torrent provider --- couchpotato/core/providers/torrent/yify/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/couchpotato/core/providers/torrent/yify/__init__.py b/couchpotato/core/providers/torrent/yify/__init__.py index f747751..23f9e34 100644 --- a/couchpotato/core/providers/torrent/yify/__init__.py +++ b/couchpotato/core/providers/torrent/yify/__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', From 84e9f9794d8f750b8de0afa5609d956f0bfc2c51 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Wed, 26 Jun 2013 19:53:28 +0200 Subject: [PATCH 05/10] Add awesomehd torrent provider --- couchpotato/core/providers/torrent/awesomehd/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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', From 18a88eab510d04863aa75c5c9fb19c5bf6819072 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Wed, 26 Jun 2013 20:02:25 +0200 Subject: [PATCH 06/10] Textual change --- couchpotato/core/downloaders/transmission/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index 8f19e34..7805dd0 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -48,7 +48,7 @@ config = [{ '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. Set the seeding goal in the torrent providers.', + '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', From cfd23c395a8fdbc585efd6548b5f142e07d9df57 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Sat, 29 Jun 2013 10:23:08 +0200 Subject: [PATCH 07/10] Add failed download handling to Transmission --- couchpotato/core/downloaders/transmission/__init__.py | 12 ++++++++++++ couchpotato/core/downloaders/transmission/main.py | 10 ++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index 7805dd0..bca7eae 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -78,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 dc798d2..46b9541 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -89,7 +89,7 @@ class Transmission(Downloader): statuses = StatusList(self) return_params = { - 'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit'] + 'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit'] } queue = self.trpc.get_alltorrents(return_params) @@ -105,7 +105,9 @@ class Transmission(Downloader): return status = 'busy' - if item['status'] == 0 and item['percentDone'] == 1: + 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' @@ -128,6 +130,10 @@ class Transmission(Downloader): 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) From 7411670e2279827e494ef4195c4ea76527b8c27d Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Sat, 29 Jun 2013 10:36:02 +0200 Subject: [PATCH 08/10] Added complete download removal to SabNZBd --- couchpotato/core/downloaders/base.py | 2 +- couchpotato/core/downloaders/sabnzbd/__init__.py | 7 +++++++ couchpotato/core/downloaders/sabnzbd/main.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 5ffad3d..adbfb7e 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -83,7 +83,7 @@ class Downloader(Provider): if item and item.get('downloader') == self.getName(): if self.conf('remove_complete'): - return self.processComplete(item = item, delete_files = self.conf('delete_files')) + return self.processComplete(item = item, delete_files = self.conf('delete_files', default = False)) return False return diff --git a/couchpotato/core/downloaders/sabnzbd/__init__.py b/couchpotato/core/downloaders/sabnzbd/__init__.py index f17db9c..7fcec9e 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 torrent', + 'default': True, + 'type': 'bool', + 'description': 'Remove the NZB from SabNZBd 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, { From 7d9920691fedc7e5c83bd00c68d4430d63f10b11 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Sat, 29 Jun 2013 22:50:25 +0200 Subject: [PATCH 09/10] Fix uTorrent settings automatically Note that this might not be the way we want to go? --- couchpotato/core/downloaders/utorrent/__init__.py | 2 +- couchpotato/core/downloaders/utorrent/main.py | 30 +++++++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/couchpotato/core/downloaders/utorrent/__init__.py b/couchpotato/core/downloaders/utorrent/__init__.py index 674a52a..f2fcc13 100644 --- a/couchpotato/core/downloaders/utorrent/__init__.py +++ b/couchpotato/core/downloaders/utorrent/__init__.py @@ -41,7 +41,7 @@ config = [{ '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. Stop seeding manually in uTorrent, or check the option Queueing->When uTorrent reaches the seeding goal->Limit the upload rate and set it to 0 to stop seeding after the seeding goal set in the torrent providers is met.', + 'description': '(Hard)links/copies after download is complete (if enabled in renamer), wait for seeding to finish before (re)moving.', }, { 'name': 'remove_complete', diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index cc02b80..738fbad 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -46,6 +46,18 @@ class uTorrent(Downloader): 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') @@ -65,21 +77,11 @@ class uTorrent(Downloader): if data.get('seed_ratio') and self.conf('seeding'): torrent_params['seed_override'] = 1 torrent_params['seed_ratio'] = tryInt(tryFloat(data['seed_ratio'])*1000) - - # Check if uTorrent completes the torrent if seeding goal is met. - # Note that CPS can also check if the goal has been met but for now it should be done by uTorrent - if not (settings.get('seed_prio_limitul') == 0 and settings['seed_prio_limitul_flag']): - log.info('With the current settings uTorrent does not set torrents that completed the seed ratio and time to complete. Please stop them manually in uTorrent or check the option Queueing->When uTorrent reaches the seeding goal->Limit the upload rate and set it to 0') if data.get('seed_time') and self.conf('seeding'): torrent_params['seed_override'] = 1 torrent_params['seed_time'] = tryInt(data['seed_time'])*3600 - # Check if uTorrent completes the torrent if seeding goal is met. - # Note that CPS can also check if the goal has been met but for now it should be done by uTorrent - if not (settings.get('seed_prio_limitul') == 0 and settings['seed_prio_limitul_flag']): - log.info('With the current settings uTorrent does not set torrents that completed the seed ratio and time to complete. Please stop them manually in uTorrent or check the option Queueing->When uTorrent reaches the seeding goal->Limit the upload rate and set it to 0') - # Convert base 32 to hex if len(torrent_hash) == 32: torrent_hash = b16encode(b32decode(torrent_hash)) @@ -262,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) From 998e487fe8e1db068f33103c48e21f4709b2a3b7 Mon Sep 17 00:00:00 2001 From: mano3m <-> Date: Sun, 30 Jun 2013 10:14:08 +0200 Subject: [PATCH 10/10] NZBs are not torrents :) --- couchpotato/core/downloaders/sabnzbd/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/sabnzbd/__init__.py b/couchpotato/core/downloaders/sabnzbd/__init__.py index 7fcec9e..8e132b7 100644 --- a/couchpotato/core/downloaders/sabnzbd/__init__.py +++ b/couchpotato/core/downloaders/sabnzbd/__init__.py @@ -43,10 +43,10 @@ config = [{ }, { 'name': 'remove_complete', - 'label': 'Remove torrent', + 'label': 'Remove NZB', 'default': True, 'type': 'bool', - 'description': 'Remove the NZB from SabNZBd history after it completed.', + 'description': 'Remove the NZB from history after it completed.', }, { 'name': 'delete_failed',