diff --git a/couchpotato/core/downloaders/deluge/__init__.py b/couchpotato/core/downloaders/deluge/__init__.py new file mode 100644 index 00000000..c7aa26e6 --- /dev/null +++ b/couchpotato/core/downloaders/deluge/__init__.py @@ -0,0 +1,90 @@ +from .main import Deluge + +def start(): + return Deluge() + +config = [{ + 'name': 'deluge', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'deluge', + 'label': 'Deluge', + 'description': 'Use Deluge to download torrents.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'torrent', + }, + { + 'name': 'host', + 'default': 'localhost:58846', + 'description': 'Hostname with port. Usually localhost:58846', + }, + { + 'name': 'username', + }, + { + 'name': 'password', + 'type': 'password', + }, + { + 'name': 'directory', + 'type': 'directory', + 'description': 'Download to this directory. Keep empty for default Deluge download directory.', + }, + { + 'name': 'completed_directory', + 'type': 'directory', + 'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.', + 'advanced': True, + }, + { + 'name': 'label', + 'description': 'Label to add to torrents in the Deluge UI.', + }, + { + 'name': 'remove_complete', + 'label': 'Remove torrent', + 'type': 'bool', + 'default': True, + 'advanced': True, + 'description': 'Remove the torrent from Deluge after it has finished seeding.', + }, + { + 'name': 'delete_files', + 'label': 'Remove files', + 'default': True, + 'type': 'bool', + 'advanced': True, + 'description': 'Also remove the leftover files.', + }, + { + 'name': 'paused', + 'type': 'bool', + 'advanced': True, + 'default': False, + 'description': 'Add the torrent paused.', + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + { + 'name': 'delete_failed', + 'default': True, + 'advanced': True, + 'type': 'bool', + 'description': 'Delete a release after the download has failed.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/deluge/main.py b/couchpotato/core/downloaders/deluge/main.py new file mode 100644 index 00000000..e8e0b459 --- /dev/null +++ b/couchpotato/core/downloaders/deluge/main.py @@ -0,0 +1,239 @@ +from base64 import b64encode +from couchpotato.core.helpers.variable import tryInt, tryFloat +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 +from synchronousdeluge import DelugeClient +import os.path +import traceback + +log = CPLog(__name__) + + +class Deluge(Downloader): + + protocol = ['torrent', 'torrent_magnet'] + log = CPLog(__name__) + drpc = 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.drpc: + self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) + + return self.drpc + + def download(self, data, movie, filedata = None): + + log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol'))) + + if not self.connect(): + return False + + if not filedata and data.get('protocol') == 'torrent': + log.error('Failed sending torrent, no data') + return False + + # Set parameters for Deluge + options = { + 'add_paused': self.conf('paused', default = 0), + 'label': self.conf('label') + } + + if self.conf('directory'): + if os.path.isdir(self.conf('directory')): + options['download_location'] = self.conf('directory') + else: + log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory')) + + if self.conf('completed_directory'): + if os.path.isdir(self.conf('completed_directory')): + options['move_completed'] = 1 + options['move_completed_path'] = self.conf('completed_directory') + else: + log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory')) + + if data.get('seed_ratio'): + options['stop_at_ratio'] = 1 + options['stop_ratio'] = tryFloat(data.get('seed_ratio')) + +# Deluge only has seed time as a global option. Might be added in +# in a future API release. +# if data.get('seed_time'): + + # Send request to Deluge + if data.get('protocol') == 'torrent_magnet': + remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options) + else: + remote_torrent = self.drpc.add_torrent_file(movie, b64encode(filedata), options) + + if not remote_torrent: + log.error('Failed sending torrent to Deluge') + return False + + log.info('Torrent sent to Deluge successfully.') + return self.downloadReturnId(remote_torrent) + + def getAllDownloadStatus(self): + + log.debug('Checking Deluge download status.') + + if not self.connect(): + return False + + statuses = StatusList(self) + + queue = self.drpc.get_alltorrents() + + if not (queue and queue.get('torrents')): + log.debug('Nothing in queue or error') + return False + + for torrent_id in queue: + item = queue[torrent_id] + log.debug('name=%s / id=%s / save_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / conf_ratio=%s/ is_seed=%s / is_finished=%s', (item['name'], item['hash'], item['save_path'], item['hash'], item['progress'], item['state'], item['eta'], item['ratio'], self.conf('ratio'), item['is_seed'], item['is_finished'])) + + if not os.path.isdir(Env.setting('from', 'renamer')): + log.error('Renamer "from" folder doesn\'t to exist.') + return + + status = 'busy' + # Deluge seems to set both is_seed and is_finished once everything has been downloaded. + if item['is_seed'] or item['is_finished']: + status = 'seeding' + elif item['is_seed'] and item['is_finished'] and item['paused']: + status = 'completed' + + download_dir = item['save_path'] + if item['move_on_completed']: + download_dir = item['move_completed_path'] + + statuses.append({ + 'id': item['hash'], + 'name': item['name'], + 'status': status, + 'original_status': item['state'], + 'seed_ratio': item['ratio'], + 'timeleft': str(timedelta(seconds = item['eta'])), + 'folder': os.path.join(download_dir, item['name']), + }) + + return statuses + + def pause(self, item, pause = True): + if pause: + return self.drpc.pause_torrent([item['id']]) + else: + return self.drpc.resume_torrent([item['id']]) + + def removeFailed(self, item): + log.info('%s failed downloading, deleting...', item['name']) + return self.drpc.remove_torrent(item['id'], True) + + def processComplete(self, item, delete_files = False): + log.debug('Requesting Deluge to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else '')) + return self.drpc.remove_torrent(item['id'], remove_local_data = delete_files) + +class DelugeRPC(object): + + host = 'localhost' + port = 58846 + username = None + password = None + client = None + + def __init__(self, host = 'localhost', port = 58846, username = None, password = None): + super(DelugeRPC, self).__init__() + + self.host = host + self.port = port + self.username = username + self.password = password + + def connect(self): + self.client = DelugeClient() + self.client.connect(self.host, int(self.port), self.username, self.password) + + def add_torrent_magnet(self, torrent, options): + torrent_id = False + try: + self.connect() + torrent_id = self.client.core.add_torrent_magnet(torrent, options).get() + if options['label']: + self.client.label.set_torrent(torrent_id, options['label']).get() + except Exception, err: + log.error('Failed to add torrent magnet: %s %s', err, traceback.format_exc()) + finally: + if self.client: + self.disconnect() + + return torrent_id + + def add_torrent_file(self, movie, torrent, options): + torrent_id = False + try: + self.connect() + torrent_id = self.client.core.add_torrent_file(movie, torrent, options).get() + if options['label']: + self.client.label.set_torrent(torrent_id, options['label']).get() + except Exception, err: + log.error('Failed to add torrent file: %s %s', err, traceback.format_exc()) + finally: + if self.client: + self.disconnect() + + return torrent_id + + def get_alltorrents(self): + ret = False + try: + self.connect() + ret = self.client.core.get_torrents_status({}, {}).get() + except Exception, err: + log.error('Failed to get all torrents: %s %s', err, traceback.format_exc()) + finally: + if self.client: + self.disconnect() + return ret + + def pause_torrent(self, torrent_ids): + try: + self.connect() + self.client.core.pause_torrent(torrent_ids).get() + except Exception, err: + log.error('Failed to pause torrent: %s %s', err, traceback.format_exc()) + finally: + if self.client: + self.disconnect() + + def resume_torrent(self, torrent_ids): + try: + self.connect() + self.client.core.resume_torrent(torrent_ids).get() + except Exception, err: + log.error('Failed to resume torrent: %s %s', err, traceback.format_exc()) + finally: + if self.client: + self.disconnect() + + def remove_torrent(self, torrent_id, remove_local_data): + ret = False + try: + self.connect() + ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get() + except Exception, err: + log.error('Failed to remove torrent: %s %s', err, traceback.format_exc()) + finally: + if self.client: + self.disconnect() + return ret + + def disconnect(self): + self.client.disconnect() diff --git a/couchpotato/core/downloaders/rtorrent/__init__.py b/couchpotato/core/downloaders/rtorrent/__init__.py new file mode 100755 index 00000000..efc2234b --- /dev/null +++ b/couchpotato/core/downloaders/rtorrent/__init__.py @@ -0,0 +1,71 @@ +from .main import rTorrent + +def start(): + return rTorrent() + +config = [{ + 'name': 'rtorrent', + 'groups': [ + { + 'tab': 'downloaders', + 'list': 'download_providers', + 'name': 'rtorrent', + 'label': 'rTorrent', + 'description': '', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + 'radio_group': 'torrent', + }, + { + 'name': 'url', + 'default': 'http://localhost:80/RPC2', + }, + { + 'name': 'username', + }, + { + 'name': 'password', + 'type': 'password', + }, + { + 'name': 'label', + 'description': 'Label to apply on added torrents.', + }, + { + 'name': 'remove_complete', + 'label': 'Remove torrent', + 'default': False, + 'advanced': True, + 'type': 'bool', + 'description': 'Remove the torrent after it finishes seeding.', + }, + { + 'name': 'delete_files', + 'label': 'Remove files', + 'default': True, + 'type': 'bool', + 'advanced': True, + 'description': 'Also remove the leftover files.', + }, + { + 'name': 'paused', + 'type': 'bool', + 'advanced': True, + 'default': False, + 'description': 'Add the torrent paused.', + }, + { + 'name': 'manual', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py new file mode 100755 index 00000000..c2a9b8af --- /dev/null +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -0,0 +1,202 @@ +from base64 import b16encode, b32decode +from datetime import timedelta +from hashlib import sha1 +import shutil +from rtorrent.err import MethodError + +from bencode import bencode, bdecode +from couchpotato.core.downloaders.base import Downloader, StatusList +from couchpotato.core.logger import CPLog +from rtorrent import RTorrent + + +log = CPLog(__name__) + + +class rTorrent(Downloader): + protocol = ['torrent', 'torrent_magnet'] + rt = None + + def connect(self): + # Already connected? + if self.rt is not None: + return self.rt + + # Ensure url is set + if not self.conf('url'): + log.error('Config properties are not filled in correctly, url is missing.') + return False + + if self.conf('username') and self.conf('password'): + self.rt = RTorrent( + self.conf('url'), + self.conf('username'), + self.conf('password') + ) + else: + self.rt = RTorrent(self.conf('url')) + + return self.rt + + def _update_provider_group(self, name, data): + if data.get('seed_time'): + log.info('seeding time ignored, not supported') + + if not name: + return False + + if not self.connect(): + return False + + views = self.rt.get_views() + + if name not in views: + self.rt.create_group(name) + + group = self.rt.get_group(name) + + try: + if data.get('seed_ratio'): + ratio = int(float(data.get('seed_ratio')) * 100) + log.debug('Updating provider ratio to %s, group name: %s', (ratio, name)) + + # Explicitly set all group options to ensure it is setup correctly + group.set_upload('1M') + group.set_min(ratio) + group.set_max(ratio) + group.set_command('d.stop') + group.enable() + else: + # Reset group action and disable it + group.set_command() + group.disable() + except MethodError, err: + log.error('Unable to set group options: %s', err.message) + return False + + return True + + + def download(self, data, movie, filedata = None): + log.debug('Sending "%s" to rTorrent.', (data.get('name'))) + + if not self.connect(): + return False + + group_name = 'cp_' + data.get('provider').lower() + if not self._update_provider_group(group_name, data): + return False + + torrent_params = {} + if self.conf('label'): + torrent_params['label'] = self.conf('label') + + if not filedata and data.get('protocol') == 'torrent': + log.error('Failed sending torrent, no data') + return False + + # Try download magnet torrents + if data.get('protocol') == 'torrent_magnet': + filedata = self.magnetToTorrent(data.get('url')) + + if filedata is False: + return False + + data['protocol'] = 'torrent' + + info = bdecode(filedata)["info"] + torrent_hash = sha1(bencode(info)).hexdigest().upper() + + # Convert base 32 to hex + if len(torrent_hash) == 32: + torrent_hash = b16encode(b32decode(torrent_hash)) + + # Send request to rTorrent + try: + # Send torrent to rTorrent + torrent = self.rt.load_torrent(filedata) + + # Set label + if self.conf('label'): + torrent.set_custom(1, self.conf('label')) + + # Set Ratio Group + torrent.set_visible(group_name) + + # Start torrent + if not self.conf('paused', default = 0): + torrent.start() + + return self.downloadReturnId(torrent_hash) + except Exception, err: + log.error('Failed to send torrent to rTorrent: %s', err) + return False + + def getAllDownloadStatus(self): + log.debug('Checking rTorrent download status.') + + if not self.connect(): + return False + + try: + torrents = self.rt.get_torrents() + + statuses = StatusList(self) + + for item in torrents: + status = 'busy' + if item.complete: + if item.active: + status = 'seeding' + else: + status = 'completed' + + statuses.append({ + 'id': item.info_hash, + 'name': item.name, + 'status': status, + 'seed_ratio': item.ratio, + 'original_status': item.state, + 'timeleft': str(timedelta(seconds = float(item.left_bytes) / item.down_rate)) + if item.down_rate > 0 else -1, + 'folder': item.directory + }) + + return statuses + + except Exception, err: + log.error('Failed to get status from rTorrent: %s', err) + return False + + def pause(self, download_info, pause = True): + if not self.connect(): + return False + + torrent = self.rt.find_torrent(download_info['id']) + if torrent is None: + return False + + if pause: + return torrent.pause() + return torrent.resume() + + def removeFailed(self, item): + log.info('%s failed downloading, deleting...', item['name']) + return self.processComplete(item, delete_files = True) + + def processComplete(self, item, delete_files): + log.debug('Requesting rTorrent to remove the torrent %s%s.', + (item['name'], ' and cleanup the downloaded files' if delete_files else '')) + if not self.connect(): + return False + + torrent = self.rt.find_torrent(item['id']) + if torrent is None: + return False + + torrent.erase() # just removes the torrent, doesn't delete data + + if delete_files: + shutil.rmtree(item['folder'], True) + + return True diff --git a/couchpotato/core/downloaders/transmission/__init__.py b/couchpotato/core/downloaders/transmission/__init__.py index d0e8279e..f96e628e 100644 --- a/couchpotato/core/downloaders/transmission/__init__.py +++ b/couchpotato/core/downloaders/transmission/__init__.py @@ -47,7 +47,7 @@ config = [{ { 'name': 'remove_complete', 'label': 'Remove torrent', - 'default': False, + 'default': True, 'advanced': True, 'type': 'bool', 'description': 'Remove the torrent from Transmission after it finished seeding.', diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 89c7099b..fc8c1b93 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -129,9 +129,9 @@ class Transmission(Downloader): def pause(self, item, pause = True): if pause: - return self.trpc.stop_torrent(item['hashString']) + return self.trpc.stop_torrent(item['id']) else: - return self.trpc.start_torrent(item['hashString']) + return self.trpc.start_torrent(item['id']) def removeFailed(self, item): log.info('%s failed downloading, deleting...', item['name']) diff --git a/couchpotato/core/downloaders/utorrent/__init__.py b/couchpotato/core/downloaders/utorrent/__init__.py index 6a1da36b..d45e2e6c 100644 --- a/couchpotato/core/downloaders/utorrent/__init__.py +++ b/couchpotato/core/downloaders/utorrent/__init__.py @@ -39,7 +39,7 @@ config = [{ { 'name': 'remove_complete', 'label': 'Remove torrent', - 'default': False, + 'default': True, 'advanced': True, 'type': 'bool', 'description': 'Remove the torrent from uTorrent after it finished seeding.', diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index d5cc64f7..3e5df647 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -10,7 +10,9 @@ from multipartpost import MultipartPostHandler import cookielib import httplib import json +import os import re +import stat import time import urllib import urllib2 @@ -52,7 +54,7 @@ class uTorrent(Downloader): 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 + if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function new_settings['bt.read_only_on_complete'] = False log.info('Updated uTorrent settings to not set the files to read only after completing.') @@ -93,7 +95,7 @@ class uTorrent(Downloader): else: self.utorrent_api.add_torrent_file(torrent_filename, filedata) - # Change settings of added torrents + # Change settings of added torrent self.utorrent_api.set_torrent(torrent_hash, torrent_params) if self.conf('paused', default = 0): self.utorrent_api.pause_torrent(torrent_hash) @@ -130,8 +132,10 @@ class uTorrent(Downloader): status = 'busy' if 'Finished' in item[21]: status = 'completed' + self.removeReadOnly(item[26]) elif 'Seeding' in item[21]: status = 'seeding' + self.removeReadOnly(item[26]) statuses.append({ 'id': item[0], @@ -145,10 +149,10 @@ class uTorrent(Downloader): return statuses - def pause(self, download_info, pause = True): + def pause(self, item, pause = True): if not self.connect(): return False - return self.utorrent_api.pause_torrent(download_info['id'], pause) + return self.utorrent_api.pause_torrent(item['id'], pause) def removeFailed(self, item): log.info('%s failed downloading, deleting...', item['name']) @@ -161,6 +165,13 @@ class uTorrent(Downloader): if not self.connect(): return False return self.utorrent_api.remove_torrent(item['id'], remove_data = delete_files) + + def removeReadOnly(self, folder): + #Removes all read-only flags in a folder + if folder and os.path.isdir(folder): + for root, folders, filenames in os.walk(folder): + for filename in filenames: + os.chmod(os.path.join(root, filename), stat.S_IWRITE) class uTorrentAPI(object): diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 381889c0..e6c9f842 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -128,7 +128,7 @@ def getImdb(txt, check_inside = True, multiple = False): try: ids = re.findall('(tt\d{7})', txt) if multiple: - return ids if len(ids) > 0 else [] + return list(set(ids)) if len(ids) > 0 else [] return ids[0] except IndexError: pass @@ -140,7 +140,11 @@ def tryInt(s): except: return 0 def tryFloat(s): - try: return float(s) if '.' in s else tryInt(s) + try: + if isinstance(s, str): + return float(s) if '.' in s else tryInt(s) + else: + return float(s) except: return 0 def natsortKey(s): diff --git a/couchpotato/core/loader.py b/couchpotato/core/loader.py index 36f5fca8..b179f919 100644 --- a/couchpotato/core/loader.py +++ b/couchpotato/core/loader.py @@ -6,20 +6,20 @@ import traceback log = CPLog(__name__) -class Loader(object): +class Loader(object): plugins = {} providers = {} modules = {} - def addPath(self, root, base_path, priority, recursive=False): + def addPath(self, root, base_path, priority, recursive = False): for filename in os.listdir(os.path.join(root, *base_path)): path = os.path.join(os.path.join(root, *base_path), filename) if os.path.isdir(path) and filename[:2] != '__': if not u'__init__.py' in os.listdir(path): return - new_base_path = ''.join(s + '.' for s in base_path) + filename - self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path) + new_base_path = ''.join(s + '.' for s in base_path) + filename + self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path) if recursive: self.addPath(root, base_path + [filename], priority, recursive = True) @@ -98,14 +98,16 @@ class Loader(object): for cur_file in glob.glob(os.path.join(dir_name, '*')): name = os.path.basename(cur_file) - if os.path.isdir(os.path.join(dir_name, name)): + if os.path.isdir(os.path.join(dir_name, name)) and name != 'static': module_name = '%s.%s' % (module, name) self.addModule(priority, plugin_type, module_name, name) def loadSettings(self, module, name, save = True): + if not hasattr(module, 'config'): - log.warning('Skip loading settings for plugin %s as it has no config section' % module.__file__) + log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__) return False + try: for section in module.config: fireEvent('settings.options', section['name'], section) @@ -120,8 +122,9 @@ class Loader(object): return False def loadPlugins(self, module, name): + if not hasattr(module, 'start'): - log.warning('Skip startup for plugin %s as it has no start section' % module.__file__) + log.debug('Skip startup for plugin %s as it has no start section' % module.__file__) return False try: module.start() @@ -150,7 +153,7 @@ class Loader(object): m = getattr(m, sub) return m except ImportError: - log.warning("Skip loading module plugin '%s' as it seems not to be a module." % name) + log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc())) return None except: raise diff --git a/couchpotato/core/media/_base/library/__init__.py b/couchpotato/core/media/_base/library/__init__.py index 588a42d7..553eff5a 100644 --- a/couchpotato/core/media/_base/library/__init__.py +++ b/couchpotato/core/media/_base/library/__init__.py @@ -1,5 +1,4 @@ from couchpotato.core.event import addEvent -from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin diff --git a/couchpotato/core/notifications/xbmc/__init__.py b/couchpotato/core/notifications/xbmc/__init__.py index e3c467ce..dafa0f63 100644 --- a/couchpotato/core/notifications/xbmc/__init__.py +++ b/couchpotato/core/notifications/xbmc/__init__.py @@ -38,6 +38,14 @@ config = [{ 'advanced': True, 'description': 'Only update the first host when movie snatched, useful for synced XBMC', }, + { + 'name': 'remote_dir_scan', + 'label': 'Remote Folder Scan', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.', + }, { 'name': 'on_snatch', 'default': 0, diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index ad6fa605..34a9c1da 100755 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -13,7 +13,7 @@ log = CPLog(__name__) class XBMC(Notification): - listen_to = ['renamer.after'] + listen_to = ['renamer.after', 'movie.snatched'] use_json_notifications = {} http_time_between_calls = 0 @@ -33,15 +33,19 @@ class XBMC(Notification): ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}), ] - if not self.conf('only_first') or hosts.index(host) == 0: - calls.append(('VideoLibrary.Scan', {})) + if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0): + param = {} + if self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0]): + param = {'directory': data['destination_dir']} + + calls.append(('VideoLibrary.Scan', param)) max_successful += len(calls) response = self.request(host, calls) else: response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message}) - if not self.conf('only_first') or hosts.index(host) == 0: + if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0): response += self.request(host, [('VideoLibrary.Scan', {})]) max_successful += 1 diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py index 80e12850..92547cb0 100644 --- a/couchpotato/core/plugins/automation/main.py +++ b/couchpotato/core/plugins/automation/main.py @@ -26,6 +26,10 @@ class Automation(Plugin): movie_ids = [] for imdb_id in movies: + + if self.shuttingDown(): + break + prop_name = 'automation.added.%s' % imdb_id added = Env.prop(prop_name, default = False) if not added: @@ -35,5 +39,11 @@ class Automation(Plugin): Env.prop(prop_name, True) for movie_id in movie_ids: + + if self.shuttingDown(): + break + movie_dict = fireEvent('movie.get', movie_id, single = True) fireEvent('movie.searcher.single', movie_dict) + + return True \ No newline at end of file diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index fb2d16be..34668ccd 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -174,7 +174,7 @@ class Release(Plugin): # Get matching provider provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) - if item['type'] != 'torrent_magnet': + if item['protocol'] != 'torrent_magnet': item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({ diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py old mode 100644 new mode 100755 index 04cd970d..6472a2df --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -27,6 +27,7 @@ rename_options = { 'imdb_id': 'IMDB id (tt0123456)', 'cd': 'CD number (cd1)', 'cd_nr': 'Just the cd nr. (1)', + 'mpaa': 'MPAA Rating', }, } @@ -72,6 +73,12 @@ config = [{ 'type': 'choice', 'options': rename_options }, + { + 'name': 'unrar', + 'type': 'bool', + 'description': 'Extract rar files if found.', + 'default': False, + }, { 'name': 'cleanup', 'type': 'bool', @@ -119,10 +126,10 @@ config = [{ { 'name': 'file_action', 'label': 'Torrent File Action', - 'default': 'move', + 'default': 'link', 'type': 'dropdown', - '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.', + 'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')], + 'description': 'Link or Copy after downloading completed (and allow for seeding), or Move after seeding completed. Link first tries hard link, then sym link and falls back to Copy.', 'advanced': True, }, { diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index cfcd1759..43e7b5fe 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -9,6 +9,8 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Library, File, Profile, Release, \ ReleaseInfo from couchpotato.environment import Env +from unrar2 import RarFile, RarInfo +from unrar2.rar_exceptions import * import errno import fnmatch import os @@ -126,6 +128,11 @@ class Renamer(Plugin): # Extend the download info with info stored in the downloaded release download_info = self.extendDownloadInfo(download_info) + # Unpack any archives + if self.conf('unrar'): + folder, movie_folder, files, extr_files = self.extractFiles(folder = folder, movie_folder = movie_folder, files = files, \ + cleanup = self.conf('cleanup') and not self.downloadIsTorrent(download_info)) + groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'), files = files, download_info = download_info, return_ignored = False, single = True) @@ -179,6 +186,9 @@ class Renamer(Plugin): group['before_rename'] = [] fireEvent('renamer.before', group) + # Add extracted files to the before_rename list + group['before_rename'].extend(extr_files) + # Remove weird chars from moviename movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title) @@ -205,6 +215,7 @@ class Renamer(Plugin): 'imdb_id': library['identifier'], 'cd': '', 'cd_nr': '', + 'mpaa': library['info'].get('mpaa', ''), } for file_type in group['files']: @@ -212,8 +223,8 @@ 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') and not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)): - for current_file in group['files'][file_type]: + for current_file in group['files'][file_type]: + if self.conf('cleanup') and (not self.downloadIsTorrent(download_info) or self.fileIsAdded(current_file, group)): remove_files.append(current_file) continue @@ -393,14 +404,15 @@ class Renamer(Plugin): db.commit() # Remove leftover files - if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers and \ - not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)): - log.debug('Removing leftover files') - for current_file in group['files']['leftover']: - remove_files.append(current_file) - elif not remove_leftovers: # Don't remove anything + if not remove_leftovers: # Don't remove anything break + log.debug('Removing leftover files') + for current_file in group['files']['leftover']: + if self.conf('cleanup') and not self.conf('move_leftover') and \ + (not self.downloadIsTorrent(download_info) or self.fileIsAdded(current_file, group)): + remove_files.append(current_file) + # Remove files delete_folders = [] for src in remove_files: @@ -451,8 +463,7 @@ class Renamer(Plugin): self.tagDir(group, 'failed_rename') # Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent - if self.movieInFromFolder(movie_folder) and \ - self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info): + if self.movieInFromFolder(movie_folder) and self.downloadIsTorrent(download_info): self.tagDir(group, 'renamed_already') # Remove matching releases @@ -463,8 +474,7 @@ class Renamer(Plugin): except: log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc())) - if group['dirname'] and group['parentdir'] and \ - not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)): + if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(download_info): try: log.info('Deleting folder: %s', group['parentdir']) self.deleteEmptyFolder(group['parentdir']) @@ -524,22 +534,22 @@ Remove it if you want it to be renamed (again, or at least let it try again) if ignore_file: self.createFile(ignore_file, text) - def untagDir(self, folder, tag = None): + def untagDir(self, folder, tag = ''): 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 '*'): + for filename in fnmatch.filter(filenames, '*%s.ignore' % tag): os.remove((os.path.join(root, filename))) - def hastagDir(self, folder, tag = None): + def hastagDir(self, folder, tag = ''): 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 '*'): + if fnmatch.filter(filenames, '*%s.ignore' % tag): return True return False @@ -549,17 +559,23 @@ Remove it if you want it to be renamed (again, or at least let it try again) try: if forcemove: shutil.move(old, dest) - elif self.conf('file_action') == 'hardlink': - try: - link(old, dest) - except: - log.error('Couldn\'t hardlink file "%s" to "%s". Copying instead. Error: %s. ', (old, dest, traceback.format_exc())) - shutil.copy(old, dest) elif self.conf('file_action') == 'copy': shutil.copy(old, dest) - elif self.conf('file_action') == 'move_symlink': - shutil.move(old, dest) - symlink(dest, old) + elif self.conf('file_action') == 'link': + # First try to hardlink + try: + log.debug('Hardlinking file "%s" to "%s"...', (old, dest)) + link(old, dest) + except: + # Try to simlink next + log.debug('Couldn\'t hardlink file "%s" to "%s". Simlinking instead. Error: %s. ', (old, dest, traceback.format_exc())) + shutil.copy(old, dest) + try: + symlink(dest, old + '.link') + os.unlink(old) + os.rename(old + '.link', old) + except: + log.error('Couldn\'t symlink file "%s" to "%s". Copied instead. Error: %s. ', (old, dest, traceback.format_exc())) else: shutil.move(old, dest) @@ -764,10 +780,10 @@ Remove it if you want it to be renamed (again, or at least let it try again) 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': + if item['pause'] and self.conf('file_action') == 'link': 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': + if item['pause'] and self.conf('file_action') == 'link': fireEvent('download.pause', item = item, pause = False, single = True) if item['process_complete']: #First make sure the files were succesfully processed @@ -811,13 +827,13 @@ Remove it if you want it to be renamed (again, or at least let it try again) download_info.update({ 'imdb_id': rls.movie.library.identifier, 'quality': rls.quality.identifier, - 'type': rls_dict.get('info', {}).get('type') + 'protocol': rls_dict.get('info', {}).get('protocol') or rls_dict.get('info', {}).get('type'), }) return download_info def downloadIsTorrent(self, download_info): - return download_info and download_info.get('type') in ['torrent', 'torrent_magnet'] + return download_info and download_info.get('protocol') in ['torrent', 'torrent_magnet'] def fileIsAdded(self, src, group): if not group or not group.get('before_rename'): @@ -826,6 +842,130 @@ Remove it if you want it to be renamed (again, or at least let it try again) def statusInfoComplete(self, item): return item['id'] and item['downloader'] and item['folder'] - + def movieInFromFolder(self, movie_folder): return movie_folder and self.conf('from') in movie_folder or not movie_folder + + def extractFiles(self, folder = None, movie_folder = None, files = [], cleanup = False): + + # RegEx for finding rar files + archive_regex = '(?P^(?P(?:(?!\.part\d+\.rar$).)*)\.(?:(?:part0*1\.)?rar)$)' + restfile_regex = '(^%s\.(?:part(?!0*1\.rar$)\d+\.rar$|[rstuvw]\d+$))' + extr_files = [] + + # Check input variables + if not folder: + folder = self.conf('from') + + check_file_date = True + if movie_folder: + check_file_date = False + + if not files: + for root, folders, names in os.walk(folder): + files.extend([os.path.join(root, name) for name in names]) + + # Find all archive files + archives = [re.search(archive_regex, name).groupdict() for name in files if re.search(archive_regex, name)] + + #Extract all found archives + for archive in archives: + # Check if it has already been processed by CPS + if (self.hastagDir(os.path.dirname(archive['file']))): + continue + + # Find all related archive files + archive['files'] = [name for name in files if re.search(restfile_regex % re.escape(archive['base']), name)] + archive['files'].append(archive['file']) + + # Check if archive is fresh and maybe still copying/moving/downloading, ignore files newer than 1 minute + if check_file_date: + file_too_new = False + for cur_file in archive['files']: + if not os.path.isfile(cur_file): + file_too_new = time.time() + break + file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)] + for t in file_time: + if t > time.time() - 60: + file_too_new = tryInt(time.time() - t) + break + + if file_too_new: + break + + if file_too_new: + try: + time_string = time.ctime(file_time[0]) + except: + try: + time_string = time.ctime(file_time[1]) + except: + time_string = 'unknown' + + log.info('Archive seems to be still copying/moving/downloading or just copied/moved/downloaded (created on %s), ignoring for now: %s', (time_string, os.path.basename(archive['file']))) + continue + + log.info('Archive %s found. Extracting...', os.path.basename(archive['file'])) + try: + rar_handle = RarFile(archive['file']) + extr_path = os.path.join(self.conf('from'), os.path.relpath(os.path.dirname(archive['file']), folder)) + self.makeDir(extr_path) + for packedinfo in rar_handle.infolist(): + if not packedinfo.isdir and not os.path.isfile(os.path.join(extr_path, os.path.basename(packedinfo.filename))): + log.debug('Extracting %s...', packedinfo.filename) + rar_handle.extract(condition = [packedinfo.index], path = extr_path, withSubpath = False, overwrite = False) + extr_files.append(os.path.join(extr_path, os.path.basename(packedinfo.filename))) + del rar_handle + except Exception, e: + log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc())) + continue + + # Delete the archive files + for filename in archive['files']: + if cleanup: + try: + os.remove(filename) + except Exception, e: + log.error('Failed to remove %s: %s %s', (filename, e, traceback.format_exc())) + continue + files.remove(filename) + + # Move the rest of the files and folders if any files are extracted to the from folder (only if folder was provided) + if extr_files and os.path.normpath(os.path.normcase(folder)) != os.path.normpath(os.path.normcase(self.conf('from'))): + for leftoverfile in list(files): + move_to = os.path.join(self.conf('from'), os.path.relpath(leftoverfile, folder)) + + try: + self.makeDir(os.path.dirname(move_to)) + self.moveFile(leftoverfile, move_to, cleanup) + except Exception, e: + log.error('Failed moving left over file %s to %s: %s %s',(leftoverfile, move_to, e, traceback.format_exc())) + # As we probably tried to overwrite the nfo file, check if it exists and then remove the original + if os.path.isfile(move_to): + if cleanup: + log.info('Deleting left over file %s instead...', leftoverfile) + os.unlink(leftoverfile) + else: + continue + + files.remove(leftoverfile) + extr_files.append(move_to) + + if cleanup: + # Remove all left over folders + log.debug('Removing old movie folder %s...', movie_folder) + self.deleteEmptyFolder(movie_folder) + + movie_folder = os.path.join(self.conf('from'), os.path.relpath(movie_folder, folder)) + folder = self.conf('from') + + if extr_files: + files.extend(extr_files) + + # Cleanup files and folder if movie_folder was not provided + if not movie_folder: + files = [] + folder = None + + return (folder, movie_folder, files, extr_files) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 4ac50e1c..a45367ef 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -277,7 +277,7 @@ class Scanner(Plugin): except: break - # Check if movie is fresh and maybe still unpacking, ignore files new then 1 minute + # Check if movie is fresh and maybe still unpacking, ignore files newer than 1 minute file_too_new = False for cur_file in group['unsorted_files']: if not os.path.isfile(cur_file): @@ -329,14 +329,17 @@ class Scanner(Plugin): del movie_files + total_found = len(valid_files) + # Make sure only one movie was found if a download ID is provided - if download_info and not len(valid_files) == 1: + if download_info and total_found == 0: + log.info('Download ID provided (%s), but no groups found! Make sure the download contains valid media files (fully extracted).', download_info.get('imdb_id')) + elif download_info and total_found > 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) while True and not self.shuttingDown(): try: identifier, group = valid_files.popitem() diff --git a/couchpotato/core/providers/automation/imdb/__init__.py b/couchpotato/core/providers/automation/imdb/__init__.py index a0013c4a..546cba97 100644 --- a/couchpotato/core/providers/automation/imdb/__init__.py +++ b/couchpotato/core/providers/automation/imdb/__init__.py @@ -9,7 +9,7 @@ config = [{ { 'tab': 'automation', 'list': 'watchlist_providers', - 'name': 'imdb_automation', + 'name': 'imdb_automation_watchlist', 'label': 'IMDB', 'description': 'From any public IMDB watchlists. Url should be the CSV link.', 'options': [ @@ -30,5 +30,33 @@ config = [{ }, ], }, + { + 'tab': 'automation', + 'list': 'automation_providers', + 'name': 'imdb_automation_charts', + 'label': 'IMDB', + 'description': 'Import movies from IMDB Charts', + 'options': [ + { + 'name': 'automation_providers_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'automation_charts_theater', + 'type': 'bool', + 'label': 'In Theaters', + 'description': 'New Movies In-Theaters chart', + 'default': True, + }, + { + 'name': 'automation_charts_top250', + 'type': 'bool', + 'label': 'TOP 250', + 'description': 'IMDB TOP 250 chart', + 'default': True, + }, + ], + }, ], }] diff --git a/couchpotato/core/providers/automation/imdb/main.py b/couchpotato/core/providers/automation/imdb/main.py index 75a2d75c..c4aef7f1 100644 --- a/couchpotato/core/providers/automation/imdb/main.py +++ b/couchpotato/core/providers/automation/imdb/main.py @@ -1,38 +1,100 @@ +import traceback + +from bs4 import BeautifulSoup +from couchpotato import fireEvent from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.variable import getImdb, splitString, tryInt + from couchpotato.core.logger import CPLog from couchpotato.core.providers.automation.base import Automation -import traceback + +from couchpotato.core.providers.base import MultiProvider + log = CPLog(__name__) -class IMDB(Automation, RSS): +class IMDB(MultiProvider): + + def getTypes(self): + return [IMDBWatchlist, IMDBAutomation] + + +class IMDBBase(Automation, RSS): interval = 1800 + def getInfo(self, imdb_id): + return fireEvent('movie.info', identifier = imdb_id, merge = True) + + +class IMDBWatchlist(IMDBBase): + + enabled_option = 'automation_enabled' + def getIMDBids(self): movies = [] - enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))] - urls = splitString(self.conf('automation_urls')) + watchlist_enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))] + watchlist_urls = splitString(self.conf('automation_urls')) index = -1 - for url in urls: + for watchlist_url in watchlist_urls: index += 1 - if not enablers[index]: + if not watchlist_enablers[index]: continue try: - rss_data = self.getHTMLData(url) + log.debug('Started IMDB watchlists: %s', watchlist_url) + rss_data = self.getHTMLData(watchlist_url) imdbs = getImdb(rss_data, multiple = True) if rss_data else [] for imdb in imdbs: movies.append(imdb) + if self.shuttingDown(): + break + except: log.error('Failed loading IMDB watchlist: %s %s', (url, traceback.format_exc())) return movies + + +class IMDBAutomation(IMDBBase): + + enabled_option = 'automation_providers_enabled' + + chart_urls = { + 'theater': 'http://www.imdb.com/movies-in-theaters/', + 'top250': 'http://www.imdb.com/chart/top', + } + + def getIMDBids(self): + + movies = [] + + for url in self.chart_urls: + if self.conf('automation_charts_%s' % url): + data = self.getHTMLData(self.chart_urls[url]) + if data: + html = BeautifulSoup(data) + + try: + result_div = html.find('div', attrs = {'id': 'main'}) + imdb_ids = getImdb(str(result_div), multiple = True) + + for imdb_id in imdb_ids: + info = self.getInfo(imdb_id) + if info and self.isMinimalMovie(info): + movies.append(imdb_id) + + if self.shuttingDown(): + break + + except: + log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc())) + + return movies diff --git a/couchpotato/core/providers/automation/rottentomatoes/__init__.py b/couchpotato/core/providers/automation/rottentomatoes/__init__.py index 83a545b6..4675fac2 100644 --- a/couchpotato/core/providers/automation/rottentomatoes/__init__.py +++ b/couchpotato/core/providers/automation/rottentomatoes/__init__.py @@ -11,19 +11,31 @@ config = [{ 'list': 'automation_providers', 'name': 'rottentomatoes_automation', 'label': 'Rottentomatoes', - 'description': 'Imports movies from the rottentomatoes "in theaters"-feed.', + 'description': 'Imports movies from rottentomatoes rss feeds specified below.', 'options': [ { 'name': 'automation_enabled', 'default': False, 'type': 'enabler', }, + { + 'name': 'automation_urls_use', + 'label': 'Use', + 'default': '1', + }, + { + 'name': 'automation_urls', + 'label': 'url', + 'type': 'combined', + 'combine': ['automation_urls_use', 'automation_urls'], + 'default': 'http://www.rottentomatoes.com/syndication/rss/in_theaters.xml', + }, { 'name': 'tomatometer_percent', 'default': '80', 'label': 'Tomatometer', 'description': 'Use as extra scoring requirement', - } + }, ], }, ], diff --git a/couchpotato/core/providers/automation/rottentomatoes/main.py b/couchpotato/core/providers/automation/rottentomatoes/main.py index 9842d4c9..69611705 100644 --- a/couchpotato/core/providers/automation/rottentomatoes/main.py +++ b/couchpotato/core/providers/automation/rottentomatoes/main.py @@ -1,5 +1,5 @@ from couchpotato.core.helpers.rss import RSS -from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.logger import CPLog from couchpotato.core.providers.automation.base import Automation from xml.etree.ElementTree import QName @@ -11,38 +11,42 @@ log = CPLog(__name__) class Rottentomatoes(Automation, RSS): interval = 1800 - urls = { - 'namespace': 'http://www.rottentomatoes.com/xmlns/rtmovie/', - 'theater': 'http://www.rottentomatoes.com/syndication/rss/in_theaters.xml', - } def getIMDBids(self): movies = [] - rss_movies = self.getRSSData(self.urls['theater']) - rating_tag = str(QName(self.urls['namespace'], 'tomatometer_percent')) + rotten_tomatoes_namespace = 'http://www.rottentomatoes.com/xmlns/rtmovie/' + urls = dict(zip(splitString(self.conf('automation_urls')), [tryInt(x) for x in splitString(self.conf('automation_urls_use'))])) - for movie in rss_movies: + for url in urls: - value = self.getTextElement(movie, "title") - result = re.search('(?<=%\s).*', value) + if not urls[url]: + continue - if result: + rss_movies = self.getRSSData(url) + rating_tag = str(QName(rotten_tomatoes_namespace, 'tomatometer_percent')) - log.info2('Something smells...') - rating = tryInt(self.getTextElement(movie, rating_tag)) - name = result.group(0) + for movie in rss_movies: - if rating < tryInt(self.conf('tomatometer_percent')): - log.info2('%s seems to be rotten...', name) - else: + value = self.getTextElement(movie, "title") + result = re.search('(?<=%\s).*', value) - log.info2('Found %s fresh enough movies, enqueuing: %s', (rating, name)) - year = datetime.datetime.now().strftime("%Y") - imdb = self.search(name, year) + if result: - if imdb and self.isMinimalMovie(imdb): - movies.append(imdb['imdb']) + log.info2('Something smells...') + rating = tryInt(self.getTextElement(movie, rating_tag)) + name = result.group(0) + + if rating < tryInt(self.conf('tomatometer_percent')): + log.info2('%s seems to be rotten...', name) + else: + + log.info2('Found %s fresh enough movies, enqueuing: %s', (rating, name)) + year = datetime.datetime.now().strftime("%Y") + imdb = self.search(name, year) + + if imdb and self.isMinimalMovie(imdb): + movies.append(imdb['imdb']) return movies diff --git a/couchpotato/core/providers/movie/__init__.py b/couchpotato/core/providers/info/__init__.py similarity index 100% rename from couchpotato/core/providers/movie/__init__.py rename to couchpotato/core/providers/info/__init__.py diff --git a/couchpotato/core/providers/info/_modifier/__init__.py b/couchpotato/core/providers/info/_modifier/__init__.py new file mode 100644 index 00000000..6242e963 --- /dev/null +++ b/couchpotato/core/providers/info/_modifier/__init__.py @@ -0,0 +1,7 @@ +from .main import InfoResultModifier + +def start(): + + return InfoResultModifier() + +config = [] diff --git a/couchpotato/core/providers/movie/_modifier/main.py b/couchpotato/core/providers/info/_modifier/main.py similarity index 90% rename from couchpotato/core/providers/movie/_modifier/main.py rename to couchpotato/core/providers/info/_modifier/main.py index 6efbc1ef..083fc3e7 100644 --- a/couchpotato/core/providers/movie/_modifier/main.py +++ b/couchpotato/core/providers/info/_modifier/main.py @@ -3,6 +3,7 @@ from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.variable import mergeDicts, randomString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin +from couchpotato.core.providers.base import MultiProvider from couchpotato.core.settings.model import Library import copy import traceback @@ -10,7 +11,17 @@ import traceback log = CPLog(__name__) -class MovieResultModifier(Plugin): +class InfoResultModifier(MultiProvider): + + def getTypes(self): + return [Movie, Show] + + +class ModifierBase(Plugin): + pass + + +class Movie(ModifierBase): default_info = { 'tmdb_id': 0, @@ -28,6 +39,7 @@ class MovieResultModifier(Plugin): 'tagline': '', 'imdb': '', 'genres': [], + 'mpaa': None } def __init__(self): @@ -92,3 +104,7 @@ class MovieResultModifier(Plugin): if result and result.get('imdb'): return mergeDicts(result, self.getLibraryTags(result['imdb'])) return result + + +class Show(ModifierBase): + pass \ No newline at end of file diff --git a/couchpotato/core/providers/movie/base.py b/couchpotato/core/providers/info/base.py similarity index 67% rename from couchpotato/core/providers/movie/base.py rename to couchpotato/core/providers/info/base.py index 1b43ab8a..efb16621 100644 --- a/couchpotato/core/providers/movie/base.py +++ b/couchpotato/core/providers/info/base.py @@ -3,3 +3,7 @@ from couchpotato.core.providers.base import Provider class MovieProvider(Provider): type = 'movie' + + +class ShowProvider(Provider): + type = 'show' diff --git a/couchpotato/core/providers/movie/couchpotatoapi/__init__.py b/couchpotato/core/providers/info/couchpotatoapi/__init__.py similarity index 100% rename from couchpotato/core/providers/movie/couchpotatoapi/__init__.py rename to couchpotato/core/providers/info/couchpotatoapi/__init__.py diff --git a/couchpotato/core/providers/movie/couchpotatoapi/main.py b/couchpotato/core/providers/info/couchpotatoapi/main.py similarity index 98% rename from couchpotato/core/providers/movie/couchpotatoapi/main.py rename to couchpotato/core/providers/info/couchpotatoapi/main.py index 9f76381a..cdbc513a 100644 --- a/couchpotato/core/providers/movie/couchpotatoapi/main.py +++ b/couchpotato/core/providers/info/couchpotatoapi/main.py @@ -1,7 +1,7 @@ from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.logger import CPLog -from couchpotato.core.providers.movie.base import MovieProvider +from couchpotato.core.providers.info.base import MovieProvider from couchpotato.environment import Env import time diff --git a/couchpotato/core/providers/movie/omdbapi/__init__.py b/couchpotato/core/providers/info/omdbapi/__init__.py similarity index 100% rename from couchpotato/core/providers/movie/omdbapi/__init__.py rename to couchpotato/core/providers/info/omdbapi/__init__.py diff --git a/couchpotato/core/providers/movie/omdbapi/main.py b/couchpotato/core/providers/info/omdbapi/main.py old mode 100644 new mode 100755 similarity index 97% rename from couchpotato/core/providers/movie/omdbapi/main.py rename to couchpotato/core/providers/info/omdbapi/main.py index 89990747..2726ef51 --- a/couchpotato/core/providers/movie/omdbapi/main.py +++ b/couchpotato/core/providers/info/omdbapi/main.py @@ -2,7 +2,7 @@ from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString from couchpotato.core.logger import CPLog -from couchpotato.core.providers.movie.base import MovieProvider +from couchpotato.core.providers.info.base import MovieProvider import json import re import traceback @@ -95,6 +95,7 @@ class OMDBAPI(MovieProvider): #'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', '').replace(',', ''))), }, 'imdb': str(movie.get('imdbID', '')), + 'mpaa': str(movie.get('Rated', '')), 'runtime': self.runtimeToMinutes(movie.get('Runtime', '')), 'released': movie.get('Released'), 'year': year if isinstance(year, (int)) else None, diff --git a/couchpotato/core/providers/movie/themoviedb/__init__.py b/couchpotato/core/providers/info/themoviedb/__init__.py similarity index 100% rename from couchpotato/core/providers/movie/themoviedb/__init__.py rename to couchpotato/core/providers/info/themoviedb/__init__.py diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/info/themoviedb/main.py similarity index 98% rename from couchpotato/core/providers/movie/themoviedb/main.py rename to couchpotato/core/providers/info/themoviedb/main.py index 735419c3..e2ff9377 100644 --- a/couchpotato/core/providers/movie/themoviedb/main.py +++ b/couchpotato/core/providers/info/themoviedb/main.py @@ -1,7 +1,7 @@ from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import simplifyString, toUnicode from couchpotato.core.logger import CPLog -from couchpotato.core.providers.movie.base import MovieProvider +from couchpotato.core.providers.info.base import MovieProvider from themoviedb import tmdb import traceback @@ -167,6 +167,7 @@ class TheMovieDb(MovieProvider): 'backdrop_original': [backdrop_original] if backdrop_original else [], }, 'imdb': movie.get('imdb_id'), + 'mpaa': movie.get('certification', ''), 'runtime': movie.get('runtime'), 'released': movie.get('released'), 'year': year, diff --git a/couchpotato/core/providers/show/thetvdb/__init__.py b/couchpotato/core/providers/info/thetvdb/__init__.py similarity index 100% rename from couchpotato/core/providers/show/thetvdb/__init__.py rename to couchpotato/core/providers/info/thetvdb/__init__.py diff --git a/couchpotato/core/providers/show/thetvdb/main.py b/couchpotato/core/providers/info/thetvdb/main.py similarity index 99% rename from couchpotato/core/providers/show/thetvdb/main.py rename to couchpotato/core/providers/info/thetvdb/main.py index 767fb7f7..b056c60c 100644 --- a/couchpotato/core/providers/show/thetvdb/main.py +++ b/couchpotato/core/providers/info/thetvdb/main.py @@ -1,7 +1,7 @@ from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import simplifyString, toUnicode from couchpotato.core.logger import CPLog -from couchpotato.core.providers.show.base import ShowProvider +from couchpotato.core.providers.info.base import ShowProvider from tvdb_api import tvdb_api, tvdb_exceptions from datetime import datetime import traceback diff --git a/couchpotato/core/providers/movie/_modifier/__init__.py b/couchpotato/core/providers/movie/_modifier/__init__.py deleted file mode 100644 index 3bdf5e0d..00000000 --- a/couchpotato/core/providers/movie/_modifier/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import MovieResultModifier - -def start(): - - return MovieResultModifier() - -config = [] diff --git a/couchpotato/core/providers/show/_modifier/__init__.py b/couchpotato/core/providers/show/_modifier/__init__.py deleted file mode 100644 index 54d59197..00000000 --- a/couchpotato/core/providers/show/_modifier/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .main import ShowResultModifier - -def start(): - - return ShowResultModifier() - -config = [] diff --git a/couchpotato/core/providers/show/_modifier/main.py b/couchpotato/core/providers/show/_modifier/main.py deleted file mode 100644 index 41be3ecb..00000000 --- a/couchpotato/core/providers/show/_modifier/main.py +++ /dev/null @@ -1,94 +0,0 @@ -from couchpotato import get_session -from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.variable import mergeDicts, randomString -from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library -import copy -import traceback - -log = CPLog(__name__) - - -class ShowResultModifier(Plugin): - - default_info = { - 'tmdb_id': 0, - 'titles': [], - 'original_title': '', - 'year': 0, - 'images': { - 'poster': [], - 'backdrop': [], - 'poster_original': [], - 'backdrop_original': [] - }, - 'runtime': 0, - 'plot': '', - 'tagline': '', - 'imdb': '', - 'genres': [], - } - - def __init__(self): - addEvent('result.modify.show.search', self.combineOnIMDB) - addEvent('result.modify.show.info', self.checkLibrary) - - def combineOnIMDB(self, results): - - temp = {} - order = [] - - # Combine on imdb id - for item in results: - random_string = randomString() - imdb = item.get('imdb', random_string) - imdb = imdb if imdb else random_string - - if not temp.get(imdb): - temp[imdb] = self.getLibraryTags(imdb) - order.append(imdb) - - # Merge dicts - temp[imdb] = mergeDicts(temp[imdb], item) - - # Make it a list again - temp_list = [temp[x] for x in order] - - return temp_list - - def getLibraryTags(self, imdb): - - temp = { - 'in_wanted': False, - 'in_library': False, - } - - # Add release info from current library - db = get_session() - try: - l = db.query(Library).filter_by(identifier = imdb).first() - if l: - - # Statuses - active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) - - for movie in l.media: - if movie.status_id == active_status['id']: - temp['in_wanted'] = fireEvent('movie.get', movie.id, single = True) - - for release in movie.releases: - if release.status_id == done_status['id']: - temp['in_library'] = fireEvent('movie.get', movie.id, single = True) - except: - log.error('Tried getting more info on searched movies: %s', traceback.format_exc()) - - return temp - - def checkLibrary(self, result): - - result = mergeDicts(copy.deepcopy(self.default_info), copy.deepcopy(result)) - - if result and result.get('imdb'): - return mergeDicts(result, self.getLibraryTags(result['imdb'])) - return result diff --git a/couchpotato/core/providers/show/base.py b/couchpotato/core/providers/show/base.py deleted file mode 100644 index 95ab0dcc..00000000 --- a/couchpotato/core/providers/show/base.py +++ /dev/null @@ -1,5 +0,0 @@ -from couchpotato.core.providers.base import Provider - - -class ShowProvider(Provider): - type = 'show' diff --git a/couchpotato/core/providers/torrent/sceneaccess/main.py b/couchpotato/core/providers/torrent/sceneaccess/main.py index 166ded09..f3c6ebdb 100644 --- a/couchpotato/core/providers/torrent/sceneaccess/main.py +++ b/couchpotato/core/providers/torrent/sceneaccess/main.py @@ -3,7 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import MultiProvider -from couchpotato.core.providers.movie.base import MovieProvider +from couchpotato.core.providers.info.base import MovieProvider from couchpotato.core.providers.torrent.base import TorrentProvider import traceback diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 3fd1a420..82e7f385 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -214,7 +214,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # app.debug = development config = { 'use_reloader': reloader, - 'port': tryInt(Env.setting('port', default = 5000)), + 'port': tryInt(Env.setting('port', default = 5050)), 'host': host if host and len(host) > 0 else '0.0.0.0', 'ssl_cert': Env.setting('ssl_cert', default = None), 'ssl_key': Env.setting('ssl_key', default = None), diff --git a/couchpotato/templates/index.html b/couchpotato/templates/index.html index f9bc4634..d45dcb9b 100644 --- a/couchpotato/templates/index.html +++ b/couchpotato/templates/index.html @@ -22,17 +22,18 @@