diff --git a/couchpotato/core/_base/scheduler/main.py b/couchpotato/core/_base/scheduler/main.py index 4102552e..2c97e1b4 100644 --- a/couchpotato/core/_base/scheduler/main.py +++ b/couchpotato/core/_base/scheduler/main.py @@ -16,51 +16,19 @@ class Scheduler(Plugin): addEvent('schedule.cron', self.cron) addEvent('schedule.interval', self.interval) - addEvent('schedule.start', self.start) - addEvent('schedule.restart', self.start) - - addEvent('app.load', self.start) + addEvent('schedule.remove', self.remove) self.sched = Sched(misfire_grace_time = 60) - - def remove(self, identifier): - for type in ['interval', 'cron']: - try: - self.sched.unschedule_job(getattr(self, type)[identifier]['job']) - log.debug('%s unscheduled %s', (type.capitalize(), identifier)) - except: - pass - - def start(self): - - # Stop all running - self.stop() - - # Crons - for identifier in self.crons: - try: - self.remove(identifier) - cron = self.crons[identifier] - job = self.sched.add_cron_job(cron['handle'], day = cron['day'], hour = cron['hour'], minute = cron['minute']) - cron['job'] = job - except ValueError, e: - log.error('Failed adding cronjob: %s', e) - - # Intervals - for identifier in self.intervals: - try: - self.remove(identifier) - interval = self.intervals[identifier] - job = self.sched.add_interval_job(interval['handle'], hours = interval['hours'], minutes = interval['minutes'], seconds = interval['seconds']) - interval['job'] = job - except ValueError, e: - log.error('Failed adding interval cronjob: %s', e) - - # Start it - log.debug('Starting scheduler') self.sched.start() self.started = True - log.debug('Scheduler started') + + def remove(self, identifier): + for cron_type in ['intervals', 'crons']: + try: + self.sched.unschedule_job(getattr(self, cron_type)[identifier]['job']) + log.debug('%s unscheduled %s', (cron_type.capitalize(), identifier)) + except: + pass def doShutdown(self): super(Scheduler, self).doShutdown() @@ -82,6 +50,7 @@ class Scheduler(Plugin): 'day': day, 'hour': hour, 'minute': minute, + 'job': self.sched.add_cron_job(handle, day = day, hour = hour, minute = minute) } def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0): @@ -93,4 +62,5 @@ class Scheduler(Plugin): 'hours': hours, 'minutes': minutes, 'seconds': seconds, + 'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds) } diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 18d2c303..07031556 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -32,7 +32,6 @@ class Updater(Plugin): else: self.updater = SourceUpdater() - fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6) addEvent('app.load', self.autoUpdate) addEvent('updater.info', self.info) @@ -52,6 +51,15 @@ class Updater(Plugin): 'return': {'type': 'see updater.info'} }) + addEvent('setting.save.updater.enabled.after', self.setCrons) + + def setCrons(self): + + fireEvent('schedule.remove', 'updater.check', single = True) + if self.isEnabled(): + fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6) + self.autoUpdate() # Check after enabling + def autoUpdate(self): if self.check() and self.conf('automatic') and not self.updater.update_failed: if self.updater.doUpdate(): diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 69449249..8ccc1369 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 1bc54fd5..d0533d5c 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 e6cbd027..f1f8acc6 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 8b374698..f2f217a1 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 210a0d9e..6dfbd3f9 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -41,15 +41,22 @@ config = [{ { 'name': 'directory', 'type': 'directory', - 'description': 'Where should Transmission saved the downloaded files?', + 'description': 'Download to this directory. Keep empty for default Transmission download directory.', }, { '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, diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 6c4607fb..8cf47380 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -1,11 +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 shutil +import traceback import urllib2 log = CPLog(__name__) @@ -18,7 +22,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(':') @@ -27,22 +31,24 @@ class Transmission(Downloader): return False # Set parameters for Transmission - folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1] - folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep) - - # Create the empty folder to download too - self.makeDir(folder_path) - params = { 'paused': self.conf('paused', default = 0), - 'download-dir': folder_path } + if len(self.conf('directory', default = '')) > 0: + folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1] + folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep) + + # Create the empty folder to download too + self.makeDir(folder_path) + + params['download-dir'] = folder_path + torrent_params = {} 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': @@ -58,15 +64,97 @@ class Transmission(Downloader): else: remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params) + if not remote_torrent: + return False + # Change settings of added torrents - if torrent_params: + 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 Exception, err: - log.error('Failed to change settings for transfer: %s', err) + 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.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?? + }) + + return statuses class TransmissionRPC(object): @@ -97,6 +185,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') @@ -146,3 +235,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 c64db135..bca57e71 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/notifications/base.py b/couchpotato/core/notifications/base.py index dda3dade..9e80aea8 100644 --- a/couchpotato/core/notifications/base.py +++ b/couchpotato/core/notifications/base.py @@ -44,7 +44,7 @@ class Notification(Provider): def _notify(self, *args, **kwargs): if self.isEnabled(): - self.notify(*args, **kwargs) + return self.notify(*args, **kwargs) def notify(self, message = '', data = {}, listener = None): pass diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py index f4ede40d..67bae1d1 100644 --- a/couchpotato/core/plugins/automation/main.py +++ b/couchpotato/core/plugins/automation/main.py @@ -10,11 +10,16 @@ class Automation(Plugin): def __init__(self): - fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12)) + addEvent('app.load', self.setCrons) if not Env.get('dev'): addEvent('app.load', self.addMovies) + addEvent('setting.save.automation.hour.after', self.setCrons) + + def setCrons(self): + fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12)) + def addMovies(self): movies = fireEvent('automation.get_movies', merge = True) diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py index 0dc01783..87898f52 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file/main.py @@ -9,6 +9,8 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.scanner.main import Scanner from couchpotato.core.settings.model import FileType, File from couchpotato.environment import Env +from flask.helpers import send_file +from werkzeug.exceptions import NotFound import os.path import time import traceback @@ -81,11 +83,13 @@ class FileManager(Plugin): def showCacheFile(self, filename = ''): - cache_dir = Env.get('cache_dir') - filename = os.path.basename(filename) + file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename)) - from flask.helpers import send_from_directory - return send_from_directory(cache_dir, filename) + if not os.path.isfile(file_path): + log.error('File "%s" not found', file_path) + raise NotFound() + + return send_file(file_path, conditional = True) def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}): diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index c32ad81d..cf0b046c 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -24,7 +24,7 @@ class QualityPlugin(Plugin): {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, - {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']}, + {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index 22277ba5..f90828bc 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -112,6 +112,15 @@ config = [{ 'label': 'Separator', 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', }, + { + 'name': 'file_action', + 'label': 'File Action', + 'default': 'move', + 'type': 'dropdown', + 'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink')], + 'description': 'Define which kind of file operation you want to use. Before you start using hard links or sym links, PLEASE read about their possible drawbacks.', + 'advanced': True, + }, { 'advanced': True, 'name': 'ntfs_permission', diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index aba20724..26aa9dea 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -2,14 +2,16 @@ 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 linktastic.linktastic as linktastic import os import re import shutil @@ -27,7 +29,12 @@ class Renamer(Plugin): def __init__(self): addApiView('renamer.scan', self.scanView, docs = { - 'desc': 'For the renamer to check for new files to rename', + 'desc': 'For the renamer to check for new files to rename in a folder', + 'params': { + 'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'}, + 'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'}, + 'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'}, + }, }) addEvent('renamer.scan', self.scan) @@ -35,22 +42,43 @@ class Renamer(Plugin): addEvent('app.load', self.scan) addEvent('app.load', self.checkSnatched) + addEvent('app.load', self.setCrons) - if self.conf('run_every') > 0: - fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every')) + # Enable / disable interval + addEvent('setting.save.renamer.enabled.after', self.setCrons) + addEvent('setting.save.renamer.run_every.after', self.setCrons) + addEvent('setting.save.renamer.force_every.after', self.setCrons) - if self.conf('force_every') > 0: - fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every')) + def setCrons(self): + + fireEvent('schedule.remove', 'renamer.check_snatched') + if self.isEnabled() and self.conf('run_every') > 0: + fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'), single = True) + + fireEvent('schedule.remove', 'renamer.check_snatched_forced') + if self.isEnabled() and self.conf('force_every') > 0: + fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'), single = True) + + return True def scanView(self): - fireEventAsync('renamer.scan') + params = getParams() + movie_folder = params.get('movie_folder', None) + downloader = params.get('downloader', None) + download_id = params.get('download_id', None) + + fireEventAsync('renamer.scan', + movie_folder = movie_folder, + downloader = downloader, + download_id = download_id + ) return jsonified({ 'success': True }) - def scan(self): + def scan(self, movie_folder = None, downloader = None, download_id = None): if self.isDisabled(): return @@ -60,17 +88,61 @@ class Renamer(Plugin): return # 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')): - log.debug('"To" and "From" have to exist.') + 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.error('"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 - - groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True) + 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 self.renaming_started = 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) + + # 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_info = None + if download_id and downloader: + rls = None + + rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = downloader).all() + rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_id).all() + + for rlsnfo_dwnld in rlsnfo_dwnlds: + for rlsnfo_id in rlsnfo_ids: + if rlsnfo_id.release == rlsnfo_dwnld.release: + rls = rlsnfo_id.release + break + if rls: break + + if rls: + download_info = { + 'imdb_id': rls.movie.library.identifier, + '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_info = download_info, return_ignored = False, single = True) + destination = self.conf('to') folder_name = self.conf('folder_name') file_name = self.conf('file_name') @@ -84,8 +156,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] @@ -381,6 +451,9 @@ class Renamer(Plugin): log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc())) self.tagDir(group, 'failed_rename') + if self.conf('file_action') != 'move': + self.tagDir(group, 'renamed already') + # Remove matching releases for release in remove_releases: log.debug('Removing release %s', release.identifier) @@ -426,36 +499,35 @@ 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): - rename_files = {} + ignore_file = None + for movie_file in sorted(list(group['files']['movie'])): + ignore_file = '%s.ignore' % os.path.splitext(movie_file)[0] + break - if group['dirname']: - rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_%s_%s' % (tag.upper(), group['dirname'])) - else: # Add it to filename - for file_type in group['files']: - for rename_me in group['files'][file_type]: - filename = os.path.basename(rename_me) - rename_files[rename_me] = rename_me.replace(filename, '_%s_%s' % (tag.upper(), filename)) + text = """This file is from CouchPotato +It has marked this release as "%s" +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 - for src in rename_files: - if rename_files[src]: - dst = rename_files[src] - log.info('Renaming "%s" to "%s"', (src, dst)) + if ignore_file: + self.createFile(ignore_file, text) - # Create dir - self.makeDir(os.path.dirname(dst)) - - try: - self.moveFile(src, dst) - except: - log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc())) - raise def moveFile(self, old, dest): dest = ss(dest) try: - shutil.move(old, dest) + if self.conf('file_action') == 'hardlink': + linktastic.link(old, dest) + elif self.conf('file_action') == 'symlink': + linktastic.symlink(old, dest) + elif self.conf('file_action') == 'copy': + shutil.copy(old, dest) + else: + shutil.move(old, dest) try: os.chmod(dest, Env.getPermission('file')) @@ -524,6 +596,7 @@ class Renamer(Plugin): loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc())) def checkSnatched(self): + if self.checking_snatched: log.debug('Already checking snatched') @@ -597,7 +670,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.scan', 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 b2ac938c..79fe0018 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_info = None, simple = False, newer_than = 0, return_ignored = True, 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: @@ -178,17 +177,25 @@ class Scanner(Plugin): # Group files minus extension + ignored_identifiers = [] for identifier, group in movie_files.iteritems(): if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier) log.debug('Grouping files: %s', identifier) + has_ignored = 0 for file_path in group['unsorted_files']: - wo_ext = file_path[:-(len(getExt(file_path)) + 1)] + ext = getExt(file_path) + wo_ext = file_path[:-(len(ext) + 1)] found_files = set([i for i in leftovers if wo_ext in i]) group['unsorted_files'].extend(found_files) leftovers = leftovers - found_files + has_ignored += 1 if ext == 'ignore' else 0 + + if has_ignored > 0: + ignored_identifiers.append(identifier) + # Break if CP wants to shut down if self.shuttingDown(): break @@ -314,6 +321,11 @@ class Scanner(Plugin): del movie_files + # Make sure only one movie was found if a download ID is provided + if download_info and not len(valid_files) == 1: + log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_info.get('imdb_id'), len(valid_files))) + download_info = None + # Determine file types processed_movies = {} total_found = len(valid_files) @@ -323,15 +335,17 @@ class Scanner(Plugin): except: break + if return_ignored is False and identifier in ignored_identifiers: + log.debug('Ignore file found, ignoring release: %s' % identifier) + continue + # Group extra (and easy) files first - # images = self.getImages(group['unsorted_files']) group['files'] = { 'movie_extra': self.getMovieExtras(group['unsorted_files']), 'subtitle': self.getSubtitles(group['unsorted_files']), 'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']), 'nfo': self.getNfo(group['unsorted_files']), 'trailer': self.getTrailers(group['unsorted_files']), - #'backdrop': images['backdrop'], 'leftover': set(group['unsorted_files']), } @@ -346,7 +360,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_info = download_info) # Subtitle meta group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {} @@ -376,7 +390,7 @@ class Scanner(Plugin): del group['unsorted_files'] # Determine movie - group['library'] = self.determineMovie(group) + group['library'] = self.determineMovie(group, download_info = download_info) if not group['library']: log.error('Unable to determine movie: %s', group['identifiers']) else: @@ -401,7 +415,7 @@ class Scanner(Plugin): return processed_movies - def getMetaData(self, group, folder = ''): + def getMetaData(self, group, folder = '', download_info = None): data = {} files = list(group['files']['movie']) @@ -423,9 +437,13 @@ 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_info and download_info.get('quality'): + data['quality'] = fireEvent('quality.single', download_info.get('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' @@ -501,17 +519,22 @@ class Scanner(Plugin): return detected_languages - def determineMovie(self, group): - imdb_id = None + def determineMovie(self, group, download_info = None): + + # Get imdb id from downloader + imdb_id = download_info and download_info.get('imdb_id') + if imdb_id: + log.debug('Found movie via imdb id from it\'s download id: %s', download_info.get('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: diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index a34b0b47..ad33c6ff 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -50,7 +50,12 @@ class Searcher(Plugin): }"""}, }) - # Schedule cronjob + addEvent('app.load', self.setCrons) + addEvent('setting.save.searcher.cron_day.after', self.setCrons) + addEvent('setting.save.searcher.cron_hour.after', self.setCrons) + addEvent('setting.save.searcher.cron_minute.after', self.setCrons) + + def setCrons(self): fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) def allMoviesView(self): @@ -366,7 +371,7 @@ class Searcher(Plugin): return search_types - def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs): + def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs): imdb_results = kwargs.get('imdb_results', False) retention = Env.setting('retention', section = 'nzb') diff --git a/couchpotato/core/providers/movie/couchpotatoapi/main.py b/couchpotato/core/providers/movie/couchpotatoapi/main.py index fc16e8ab..8415c2d0 100644 --- a/couchpotato/core/providers/movie/couchpotatoapi/main.py +++ b/couchpotato/core/providers/movie/couchpotatoapi/main.py @@ -5,6 +5,7 @@ from couchpotato.core.helpers.request import jsonified, getParams from couchpotato.core.logger import CPLog from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.settings.model import Movie +from couchpotato.environment import Env import time log = CPLog(__name__) @@ -96,4 +97,5 @@ class CouchPotatoApi(MovieProvider): 'X-CP-Version': fireEvent('app.version', single = True), 'X-CP-API': self.api_version, 'X-CP-Time': time.time(), + 'X-CP-Identifier': '+%s' % Env.setting('api_key', 'core')[:10], # Use first 10 as identifier, so we don't need to use IP address in api stats } diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py index 2349590f..625da1ac 100644 --- a/couchpotato/core/providers/nzb/newznab/__init__.py +++ b/couchpotato/core/providers/nzb/newznab/__init__.py @@ -12,9 +12,10 @@ config = [{ 'list': 'nzb_providers', 'name': 'newznab', 'order': 10, - 'description': 'Enable NewzNab providers such as NZB.su, \ + 'description': 'Enable NewzNab such as NZB.su, \ NZBs.org, DOGnzb.cr, \ - Spotweb or NZBGeek', + Spotweb, NZBGeek, \ + SmackDown, NZBFinder', 'wizard': True, 'options': [ { @@ -23,18 +24,18 @@ config = [{ }, { 'name': 'use', - 'default': '0,0,0,0' + 'default': '0,0,0,0,0,0' }, { 'name': 'host', - 'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info', + 'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws', 'description': 'The hostname of your newznab provider', }, { 'name': 'extra_score', 'advanced': True, 'label': 'Extra Score', - 'default': '0,0,0,0', + 'default': '0,0,0,0,0,0', 'description': 'Starting score for each release found via this provider.', }, { diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index 00f77a64..45443fcb 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -189,6 +189,9 @@ class Settings(object): self.set(section, option, (new_value if new_value else value).encode('unicode_escape')) self.save() + # After save (for re-interval etc) + fireEvent('setting.save.%s.%s.after' % (section, option), single = True) + return jsonified({ 'success': True, }) diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html deleted file mode 100644 index 1d618066..00000000 --- a/couchpotato/templates/_desktop.html +++ /dev/null @@ -1,78 +0,0 @@ - - -
- {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %} - {% endfor %} - {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %} - {% endfor %} - - {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %} - {% endfor %} - {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %} - {% endfor %} - - - - - - - -