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 @@