diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 6944924..8ccc136 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -150,6 +150,7 @@ class StatusList(list): 'id': 0, 'status': 'busy', 'downloader': self.provider.getName(), + 'folder': '', } return mergeDicts(defaults, result) diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index 1bc54fd..d0533d5 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -120,6 +120,7 @@ class NZBGet(Downloader): 'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed', 'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'], 'timeleft': str(timedelta(seconds = 0)), + 'folder': item['DestDir'] }) return statuses diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py index e6cbd02..f1f8acc 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/couchpotato/core/downloaders/nzbvortex/main.py @@ -55,7 +55,8 @@ class NZBVortex(Downloader): 'name': item['uiTitle'], 'status': status, 'original_status': item['state'], - 'timeleft':-1, + 'timeleft': -1, + 'folder': item['destinationPath'], }) return statuses diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index 8b37469..f2f217a 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode, ss from couchpotato.core.helpers.variable import cleanHost, mergeDicts from couchpotato.core.logger import CPLog from couchpotato.environment import Env +from datetime import timedelta from urllib2 import URLError import json import traceback @@ -46,19 +47,15 @@ class Sabnzbd(Downloader): log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0)) return False - if sab_data.get('error'): - log.error('Error getting data from SABNZBd: %s', sab_data.get('error')) - return False - log.debug('Result from SAB: %s', sab_data) - if sab_data.get('status'): + if sab_data.get('status') and not sab_data.get('error'): log.info('NZB sent to SAB successfully.') if filedata: return self.downloadReturnId(sab_data.get('nzo_ids')[0]) else: return True else: - log.error(sab_data) + log.error('Error getting data from SABNZBd: %s', sab_data) return False def getAllDownloadStatus(self): @@ -109,7 +106,8 @@ class Sabnzbd(Downloader): 'name': item['name'], 'status': status, 'original_status': item['status'], - 'timeleft': 0, + 'timeleft': str(timedelta(seconds = 0)), + 'folder': item['storage'], }) return statuses diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index 702e61a..6dfbd3f 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -46,11 +46,18 @@ config = [{ { 'name': 'ratio', 'default': 10, - 'type': 'int', + 'type': 'float', 'advanced': True, 'description': 'Stop transfer when reaching ratio', }, { + 'name': 'ratiomode', + 'default': 0, + 'type': 'int', + 'advanced': True, + 'description': '0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.', + }, + { 'name': 'manual', 'default': 0, 'type': 'bool', diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 2383685..31c2f61 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -1,13 +1,15 @@ from base64 import b64encode -from couchpotato.core.downloaders.base import Downloader +from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.helpers.encoding import isInt from couchpotato.core.logger import CPLog +from couchpotato.environment import Env +from datetime import timedelta import httplib import json import os.path import re -import traceback import urllib2 +import shutil log = CPLog(__name__) @@ -19,7 +21,7 @@ class Transmission(Downloader): def download(self, data, movie, filedata = None): - log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type'))) + log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type'))) # Load host from config and split out port. host = self.conf('host').split(':') @@ -45,7 +47,7 @@ class Transmission(Downloader): if self.conf('ratio'): torrent_params = { 'seedRatioLimit': self.conf('ratio'), - 'seedRatioMode': self.conf('ratio') + 'seedRatioMode': self.conf('ratiomode') } if not filedata and data.get('type') == 'torrent': @@ -68,11 +70,99 @@ class Transmission(Downloader): elif torrent_params: 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']) except: log.error('Failed to change settings for transfer: %s', traceback.format_exc()) return False + 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) + return False + + 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 + + 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'] )) + + if not os.path.isdir(Env.setting('from', 'renamer')): + log.debug('Renamer folder has 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'], {}) + + if not os.path.isdir(item['downloadDir']): + raise Exception('Missing folder: %s' % item['downloadDir']) + + else: + log.info('Moving folder from "%s" to "%s"', (item['downloadDir'], Env.setting('from', 'renamer'))) + shutil.move(item['downloadDir'], Env.setting('from', 'renamer')) + + statuses.append({ + 'id': item['hashString'], + 'name': item['downloadDir'], + 'status': 'completed', + 'original_status': item['status'], + 'timeleft': str(timedelta(seconds = 0)), + 'folder': os.path.join(Env.setting('from', 'renamer'), os.path.basename(item['downloadDir'].rstrip(os.path.sep))), + }) + trpc.remove_torrent(item['hashString'], True, {}) + 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['downloadDir'], + 'status': 'failed', + 'original_status': item['status'], + 'timeleft': str(timedelta(seconds = 0)), + }) + else: + statuses.append({ + 'id': item['hashString'], + 'name': item['downloadDir'], + 'status': 'busy', + 'original_status': item['status'], + 'timeleft': str(timedelta(seconds = item['eta'])), # Is ETA in seconds?? + }) + + return statuses class TransmissionRPC(object): @@ -103,6 +193,7 @@ class TransmissionRPC(object): try: open_request = urllib2.urlopen(request) response = json.loads(open_request.read()) + log.debug('request: %s', json.dumps(ojson)) log.debug('response: %s', json.dumps(response)) if response['result'] == 'success': log.debug('Transmission action successfull') @@ -152,3 +243,18 @@ class TransmissionRPC(object): arguments['ids'] = torrent_id post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag} return self._request(post_data) + + def get_alltorrents(self, arguments): + 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} + 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} + return self._request(post_data) diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index c64db13..bca57e7 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -5,6 +5,8 @@ from couchpotato.core.helpers.encoding import isInt, ss from couchpotato.core.logger import CPLog from hashlib import sha1 from multipartpost import MultipartPostHandler +from datetime import timedelta +import os import cookielib import httplib import json @@ -104,6 +106,35 @@ class uTorrent(Downloader): return False statuses = StatusList(self) + download_folder = '' + settings_dict = {} + + try: + data = self.utorrent_api.get_settings() + utorrent_settings = json.loads(data) + + # Create settings dict + for item in utorrent_settings['settings']: + if item[1] == 0: # int + settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0') + elif item[1] == 1: # bool + settings_dict[item[0]] = True if item[2] == 'true' else False + elif item[1] == 2: # string + settings_dict[item[0]] = item[2] + log.debug('uTorrent settings: %s', settings_dict) + + # Get the download path from the uTorrent settings + if settings_dict['dir_completed_download_flag']: + download_folder = settings_dict['dir_completed_download'] + elif settings_dict['dir_active_download_flag']: + download_folder = settings_dict['dir_active_download'] + else: + log.info('No download folder set in uTorrent. Please set a download folder') + return False + + except Exception, err: + log.error('Failed to get settings from uTorrent: %s', err) + return False # Get torrents for item in queue.get('torrents', []): @@ -113,12 +144,18 @@ class uTorrent(Downloader): if item[21] == 'Finished' or item[21] == 'Seeding': status = 'completed' + if settings_dict['dir_add_label']: + release_folder = os.path.join(download_folder, item[11], item[2]) + else: + release_folder = os.path.join(download_folder, item[2]) + statuses.append({ 'id': item[0], 'name': item[2], 'status': status, 'original_status': item[1], - 'timeleft': item[10], + 'timeleft': str(timedelta(seconds = item[10])), + 'folder': release_folder, }) return statuses @@ -195,3 +232,7 @@ class uTorrentAPI(object): def get_status(self): action = "list=1" return self._request(action) + + def get_settings(self): + action = "action=getsettings" + return self._request(action) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 06487aa..de35943 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -2,12 +2,12 @@ from couchpotato import get_session from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import toUnicode, ss -from couchpotato.core.helpers.request import jsonified +from couchpotato.core.helpers.request import getParams, jsonified from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ getImdb from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library, File, Profile, Release +from couchpotato.core.settings.model import Library, File, Profile, Release, ReleaseInfo from couchpotato.environment import Env import errno import os @@ -31,6 +31,17 @@ class Renamer(Plugin): }) addEvent('renamer.scan', self.scan) + + addApiView('renamer.scanfolder', self.scanfolderView, docs = { + 'desc': 'For the renamer to check for new files to rename in a specified folder', + 'params': { + 'movie_folder': {'desc': 'The folder of the movie to scan'}, + 'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'}, + 'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'}, + }, + }) + + addEvent('renamer.scanfolder', self.scanfolder) addEvent('renamer.check_snatched', self.checkSnatched) addEvent('app.load', self.scan) @@ -63,6 +74,26 @@ class Renamer(Plugin): }) def scan(self): + self.scanfolder() + + def scanfolderView(self): + + params = getParams() + movie_folder = params.get('movie_folder', None) + downloader = params.get('downloader', None) + download_id = params.get('download_id', None) + + fireEventAsync('renamer.scanfolder', + movie_folder = movie_folder, + downloader = downloader, + download_id = download_id + ) + + return jsonified({ + 'success': True + }) + + def scanfolder(self, movie_folder = None, downloader = None, download_id = None): if self.isDisabled(): return @@ -71,17 +102,55 @@ class Renamer(Plugin): log.info('Renamer is already running, if you see this often, check the logs above for errors.') return + self.renaming_started = True + # Check to see if the "to" folder is inside the "from" folder. - if not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')): + 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')): log.debug('"To" and "From" have to exist.') return elif self.conf('from') in self.conf('to'): log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.') return + elif (movie_folder and movie_folder in [self.conf('to'), self.conf('from')]): + log.error('The "to" and "from" folders can\'t be inside of or the same as the provided movie folder.') + return - groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True) + # make sure the movie folder name is included in the search + folder = None + movie_files = [] + if movie_folder: + log.info('Scanning movie folder %s...', movie_folder) + movie_folder = movie_folder.rstrip(os.path.sep) + folder = os.path.dirname(movie_folder) - self.renaming_started = True + # Get all files from the specified folder + try: + for root, folders, names in os.walk(movie_folder): + movie_files.extend([os.path.join(root, name) for name in names]) + except: + log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc())) + + db = get_session() + + # Get the release with the downloader ID that was downloded by the downloader + download_quality = None + download_imdb_id = None + if downloader and download_id: + # NOTE TO RUUD: Don't really know how to do this better... but there must be a way...? + rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader) + rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id) + for rlsnfo_dwnld in rlsnfo_dwnlds: + for rlsnfo_id in rlsnfo_ids: + if rlsnfo_id.release == rlsnfo_dwnld.release: + rls = rlsnfo_id.release + + if rls: + download_imdb_id = rls.movie.library.identifier + download_quality = rls.quality.identifier + else: + log.error('Download ID %s from downloader %s not found in releases', (download_id, downloader)) + + groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = movie_files, download_quality = download_quality, download_imdb_id = download_imdb_id, single = True) destination = self.conf('to') folder_name = self.conf('folder_name') @@ -96,8 +165,6 @@ class Renamer(Plugin): downloaded_status = fireEvent('status.get', 'downloaded', single = True) snatched_status = fireEvent('status.get', 'snatched', single = True) - db = get_session() - for group_identifier in groups: group = groups[group_identifier] @@ -609,7 +676,10 @@ class Renamer(Plugin): db.commit() elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) - scan_required = True + if item['id'] and item['downloader'] and item['folder']: + fireEventAsync('renamer.scanfolder', movie_folder = item['folder'], downloader = item['downloader'], download_id = item['id']) + else: + scan_required = True found = True break diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index b2ac938..d714481 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -101,7 +101,7 @@ class Scanner(Plugin): addEvent('scanner.name_year', self.getReleaseNameYear) addEvent('scanner.partnumber', self.getPartNumber) - def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None): + def scan(self, folder = None, files = None, download_imdb_id = None, download_quality = None, simple = False, newer_than = 0, on_found = None): folder = ss(os.path.normpath(folder)) @@ -119,8 +119,7 @@ class Scanner(Plugin): try: files = [] for root, dirs, walk_files in os.walk(folder): - for filename in walk_files: - files.append(os.path.join(root, filename)) + files.extend(os.path.join(root, filename) for filename in walk_files) except: log.error('Failed getting files from %s: %s', (folder, traceback.format_exc())) else: @@ -313,6 +312,12 @@ class Scanner(Plugin): valid_files[identifier] = group del movie_files + + # Make sure only one movie was found if a download ID is provided + if download_imdb_id and download_quality and not len(valid_files) == 1: + log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_imdb_id, len(valid_files))) + download_imdb_id = None + download_quality = None # Determine file types processed_movies = {} @@ -346,7 +351,7 @@ class Scanner(Plugin): continue log.debug('Getting metadata for %s', identifier) - group['meta_data'] = self.getMetaData(group, folder = folder) + group['meta_data'] = self.getMetaData(group, folder = folder, download_quality = download_quality) # Subtitle meta group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {} @@ -376,7 +381,7 @@ class Scanner(Plugin): del group['unsorted_files'] # Determine movie - group['library'] = self.determineMovie(group) + group['library'] = self.determineMovie(group, download_imdb_id = download_imdb_id) if not group['library']: log.error('Unable to determine movie: %s', group['identifiers']) else: @@ -401,7 +406,7 @@ class Scanner(Plugin): return processed_movies - def getMetaData(self, group, folder = ''): + def getMetaData(self, group, folder = '', download_quality = None): data = {} files = list(group['files']['movie']) @@ -423,10 +428,14 @@ class Scanner(Plugin): if data.get('audio'): break + # Use the quality guess first, if that failes use the quality we wanted to download data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True) if not data['quality']: - data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True) - + if download_quality: + data['quality'] = fireEvent('quality.single', download_quality, single = True) + else: + data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True) + data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD' filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0]) @@ -501,17 +510,21 @@ class Scanner(Plugin): return detected_languages - def determineMovie(self, group): - imdb_id = None + def determineMovie(self, group, download_imdb_id = None): + # Get imdb id from downloader + imdb_id = download_imdb_id + if imdb_id: + log.debug('Found movie via imdb id from it\'s download id: %s', download_imdb_id) files = group['files'] # Check for CP(imdb_id) string in the file paths - for cur_file in files['movie']: - imdb_id = self.getCPImdb(cur_file) - if imdb_id: - log.debug('Found movie via CP tag: %s', cur_file) - break + if not imdb_id: + for cur_file in files['movie']: + imdb_id = self.getCPImdb(cur_file) + if imdb_id: + log.debug('Found movie via CP tag: %s', cur_file) + break # Check and see if nfo contains the imdb-id if not imdb_id: