diff --git a/couchpotato/core/_base/_core.py b/couchpotato/core/_base/_core.py
index 852c42c2..6bfb441d 100644
--- a/couchpotato/core/_base/_core.py
+++ b/couchpotato/core/_base/_core.py
@@ -286,13 +286,13 @@ config = [{
'name': 'permission_folder',
'default': '0755',
'label': 'Folder CHMOD',
- 'description': 'Can be either decimal (493) or octal (leading zero: 0755)',
+ 'description': 'Can be either decimal (493) or octal (leading zero: 0755). Calculate the correct value',
},
{
'name': 'permission_file',
'default': '0755',
'label': 'File CHMOD',
- 'description': 'Same as Folder CHMOD but for files',
+ 'description': 'See Folder CHMOD description, but for files',
},
],
},
diff --git a/couchpotato/core/downloaders/transmission.py b/couchpotato/core/downloaders/transmission.py
index 409efaa0..d6112a91 100644
--- a/couchpotato/core/downloaders/transmission.py
+++ b/couchpotato/core/downloaders/transmission.py
@@ -25,7 +25,7 @@ class Transmission(DownloaderBase):
def connect(self):
# Load host from config and split out port.
- host = cleanHost(self.conf('host'), protocol = False).split(':')
+ host = cleanHost(self.conf('host')).rstrip('/').rsplit(':', 1)
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
@@ -162,11 +162,11 @@ class Transmission(DownloaderBase):
class TransmissionRPC(object):
"""TransmissionRPC lite library"""
- def __init__(self, host = 'localhost', port = 9091, rpc_url = 'transmission', username = None, password = None):
+ def __init__(self, host = 'http://localhost', port = 9091, rpc_url = 'transmission', username = None, password = None):
super(TransmissionRPC, self).__init__()
- self.url = 'http://' + host + ':' + str(port) + '/' + rpc_url + '/rpc'
+ self.url = host + ':' + str(port) + '/' + rpc_url + '/rpc'
self.tag = 0
self.session_id = 0
self.session = {}
@@ -274,8 +274,8 @@ config = [{
},
{
'name': 'host',
- 'default': 'localhost:9091',
- 'description': 'Hostname with port. Usually localhost:9091',
+ 'default': 'http://localhost:9091',
+ 'description': 'Hostname with port. Usually http://localhost:9091',
},
{
'name': 'rpc_url',
diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py
index db68da2f..9e064b7b 100755
--- a/couchpotato/core/helpers/variable.py
+++ b/couchpotato/core/helpers/variable.py
@@ -382,6 +382,28 @@ def getFreeSpace(directories):
return free_space
+def getSize(paths):
+
+ single = not isinstance(paths, (tuple, list))
+ if single:
+ paths = [paths]
+
+ total_size = 0
+ for path in paths:
+ path = sp(path)
+
+ if os.path.isdir(path):
+ total_size = 0
+ for dirpath, _, filenames in os.walk(path):
+ for f in filenames:
+ total_size += os.path.getsize(sp(os.path.join(dirpath, f)))
+
+ elif os.path.isfile(path):
+ total_size += os.path.getsize(path)
+
+ return total_size / 1048576 # MB
+
+
def find(func, iterable):
for item in iterable:
if func(item):
diff --git a/couchpotato/core/media/_base/providers/torrent/torrentshack.py b/couchpotato/core/media/_base/providers/torrent/torrentshack.py
index 226993cc..f56017f5 100644
--- a/couchpotato/core/media/_base/providers/torrent/torrentshack.py
+++ b/couchpotato/core/media/_base/providers/torrent/torrentshack.py
@@ -42,6 +42,7 @@ class Base(TorrentProvider):
link = result.find('span', attrs = {'class': 'torrent_name_link'}).parent
url = result.find('td', attrs = {'class': 'torrent_td'}).find('a')
+ tds = result.find_all('td')
results.append({
'id': link['href'].replace('torrents.php?torrentid=', ''),
@@ -49,8 +50,8 @@ class Base(TorrentProvider):
'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % link['href'],
'size': self.parseSize(result.find_all('td')[5].string),
- 'seeders': tryInt(result.find_all('td')[7].string),
- 'leechers': tryInt(result.find_all('td')[8].string),
+ 'seeders': tryInt(tds[len(tds)-2].string),
+ 'leechers': tryInt(tds[len(tds)-1].string),
})
except:
diff --git a/couchpotato/core/media/_base/searcher/__init__.py b/couchpotato/core/media/_base/searcher/__init__.py
index bf69b950..0e3655e8 100644
--- a/couchpotato/core/media/_base/searcher/__init__.py
+++ b/couchpotato/core/media/_base/searcher/__init__.py
@@ -73,4 +73,24 @@ config = [{
],
},
],
+}, {
+ 'name': 'torrent',
+ 'groups': [
+ {
+ 'tab': 'searcher',
+ 'name': 'searcher',
+ 'wizard': True,
+ 'options': [
+ {
+ 'name': 'minimum_seeders',
+ 'advanced': True,
+ 'label': 'Minimum seeders',
+ 'description': 'Ignore torrents with seeders below this number',
+ 'default': 1,
+ 'type': 'int',
+ 'unit': 'seeders'
+ },
+ ],
+ },
+ ],
}]
diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py
index 75e10c7d..1b0881b5 100755
--- a/couchpotato/core/media/movie/_base/main.py
+++ b/couchpotato/core/media/movie/_base/main.py
@@ -27,6 +27,10 @@ class MovieBase(MovieTypeBase):
addApiView('movie.add', self.addView, docs = {
'desc': 'Add new movie to the wanted list',
+ 'return': {'type': 'object', 'example': """{
+ 'success': True,
+ 'movie': object
+}"""},
'params': {
'identifier': {'desc': 'IMDB id of the movie your want to add.'},
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
@@ -150,7 +154,7 @@ class MovieBase(MovieTypeBase):
for release in fireEvent('release.for_media', m['_id'], single = True):
if release.get('status') in ['downloaded', 'snatched', 'seeding', 'done']:
if params.get('ignore_previous', False):
- fireEvent('release.update_status', m['_id'], status = 'ignored')
+ fireEvent('release.update_status', release['_id'], status = 'ignored')
else:
fireEvent('release.delete', release['_id'], single = True)
diff --git a/couchpotato/core/media/movie/providers/info/couchpotatoapi.py b/couchpotato/core/media/movie/providers/info/couchpotatoapi.py
index 4c65bf8c..51afbaef 100644
--- a/couchpotato/core/media/movie/providers/info/couchpotatoapi.py
+++ b/couchpotato/core/media/movie/providers/info/couchpotatoapi.py
@@ -2,7 +2,7 @@ import base64
import time
from couchpotato.core.event import addEvent, fireEvent
-from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.base import MovieProvider
from couchpotato.environment import Env
@@ -66,7 +66,7 @@ class CouchPotatoApi(MovieProvider):
if not name:
return
- name_enc = base64.b64encode(name)
+ name_enc = base64.b64encode(ss(name))
return self.getJsonData(self.urls['validate'] % name_enc, headers = self.getRequestHeaders())
def isMovie(self, identifier = None):
diff --git a/couchpotato/core/media/movie/providers/userscript/filmstarts.py b/couchpotato/core/media/movie/providers/userscript/filmstarts.py
index 59027e03..4e61f299 100644
--- a/couchpotato/core/media/movie/providers/userscript/filmstarts.py
+++ b/couchpotato/core/media/movie/providers/userscript/filmstarts.py
@@ -25,6 +25,6 @@ class Filmstarts(UserscriptBase):
name = html.find("meta", {"property":"og:title"})['content']
# Year of production is not available in the meta data, so get it from the table
- year = table.find("tr", text="Produktionsjahr").parent.parent.parent.td.text
+ year = table.find(text="Produktionsjahr").parent.parent.next_sibling.text
- return self.search(name, year)
\ No newline at end of file
+ return self.search(name, year)
diff --git a/couchpotato/core/notifications/growl.py b/couchpotato/core/notifications/growl.py
index e60e7ef7..a0081a23 100644
--- a/couchpotato/core/notifications/growl.py
+++ b/couchpotato/core/notifications/growl.py
@@ -34,9 +34,9 @@ class Growl(Notification):
self.growl = notifier.GrowlNotifier(
applicationName = Env.get('appname'),
- notifications = ["Updates"],
- defaultNotifications = ["Updates"],
- applicationIcon = '%s/static/images/couch.png' % fireEvent('app.api_url', single = True),
+ notifications = ['Updates'],
+ defaultNotifications = ['Updates'],
+ applicationIcon = self.getNotificationImage('medium'),
hostname = hostname if hostname else 'localhost',
password = password if password else None,
port = port if port else 23053
@@ -56,7 +56,7 @@ class Growl(Notification):
try:
self.growl.notify(
- noteType = "Updates",
+ noteType = 'Updates',
title = self.default_title,
description = message,
sticky = False,
diff --git a/couchpotato/core/notifications/trakt.py b/couchpotato/core/notifications/trakt.py
index 8f35deab..fe170be8 100644
--- a/couchpotato/core/notifications/trakt.py
+++ b/couchpotato/core/notifications/trakt.py
@@ -1,4 +1,4 @@
-from couchpotato.core.helpers.variable import getTitle
+from couchpotato.core.helpers.variable import getTitle, getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
@@ -16,7 +16,7 @@ class Trakt(Notification):
'test': 'account/test/%s',
}
- listen_to = ['movie.downloaded']
+ listen_to = ['movie.snatched']
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
@@ -38,7 +38,7 @@ class Trakt(Notification):
'username': self.conf('automation_username'),
'password': self.conf('automation_password'),
'movies': [{
- 'imdb_id': data['identifier'],
+ 'imdb_id': getIdentifier(data),
'title': getTitle(data),
'year': data['info']['year']
}] if data else []
diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py
index 8a4cada7..5054b325 100644
--- a/couchpotato/core/plugins/base.py
+++ b/couchpotato/core/plugins/base.py
@@ -121,15 +121,31 @@ class Plugin(object):
if os.path.exists(path):
log.debug('%s already exists, overwriting file with new version', path)
- try:
- f = open(path, 'w+' if not binary else 'w+b')
- f.write(content)
- f.close()
- os.chmod(path, Env.getPermission('file'))
- except:
- log.error('Unable writing to file "%s": %s', (path, traceback.format_exc()))
- if os.path.isfile(path):
- os.remove(path)
+ write_type = 'w+' if not binary else 'w+b'
+
+ # Stream file using response object
+ if isinstance(content, requests.models.Response):
+
+ # Write file to temp
+ with open('%s.tmp' % path, write_type) as f:
+ for chunk in content.iter_content(chunk_size = 1048576):
+ if chunk: # filter out keep-alive new chunks
+ f.write(chunk)
+ f.flush()
+
+ # Rename to destination
+ os.rename('%s.tmp' % path, path)
+
+ else:
+ try:
+ f = open(path, write_type)
+ f.write(content)
+ f.close()
+ os.chmod(path, Env.getPermission('file'))
+ except:
+ log.error('Unable writing to file "%s": %s', (path, traceback.format_exc()))
+ if os.path.isfile(path):
+ os.remove(path)
def makeDir(self, path):
path = sp(path)
@@ -146,21 +162,17 @@ class Plugin(object):
folder = sp(folder)
for item in os.listdir(folder):
- full_folder = os.path.join(folder, item)
+ full_folder = sp(os.path.join(folder, item))
if not only_clean or (item in only_clean and os.path.isdir(full_folder)):
- for root, dirs, files in os.walk(full_folder):
+ for subfolder, dirs, files in os.walk(full_folder, topdown = False):
- for dir_name in dirs:
- full_path = os.path.join(root, dir_name)
-
- if len(os.listdir(full_path)) == 0:
- try:
- os.rmdir(full_path)
- except:
- if show_error:
- log.info2('Couldn\'t remove directory %s: %s', (full_path, traceback.format_exc()))
+ try:
+ os.rmdir(subfolder)
+ except:
+ if show_error:
+ log.info2('Couldn\'t remove directory %s: %s', (subfolder, traceback.format_exc()))
try:
os.rmdir(folder)
@@ -169,7 +181,7 @@ class Plugin(object):
log.error('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
# http request
- def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True):
+ def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True, stream = False):
url = quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
if not headers: headers = {}
@@ -210,6 +222,7 @@ class Plugin(object):
'timeout': timeout,
'files': files,
'verify': False, #verify_ssl, Disable for now as to many wrongly implemented certificates..
+ 'stream': stream,
}
method = 'post' if len(data) > 0 or files else 'get'
@@ -218,7 +231,7 @@ class Plugin(object):
status_code = response.status_code
if response.status_code == requests.codes.ok:
- data = response.content
+ data = response if stream else response.content
else:
response.raise_for_status()
diff --git a/couchpotato/core/plugins/category/main.py b/couchpotato/core/plugins/category/main.py
index a0852cc1..4abc94c0 100644
--- a/couchpotato/core/plugins/category/main.py
+++ b/couchpotato/core/plugins/category/main.py
@@ -27,7 +27,7 @@ class CategoryPlugin(Plugin):
'desc': 'List all available categories',
'return': {'type': 'object', 'example': """{
'success': True,
- 'list': array, categories
+ 'categories': array, categories
}"""}
})
diff --git a/couchpotato/core/plugins/file.py b/couchpotato/core/plugins/file.py
index 80c073fc..db6787df 100644
--- a/couchpotato/core/plugins/file.py
+++ b/couchpotato/core/plugins/file.py
@@ -64,6 +64,9 @@ class FileManager(Plugin):
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = None):
if not urlopen_kwargs: urlopen_kwargs = {}
+ # Return response object to stream download
+ urlopen_kwargs['stream'] = True
+
if not dest: # to Cache
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
@@ -107,4 +110,4 @@ class FileManager(Plugin):
else:
log.info('Subfolder test succeeded')
- return failed == 0
\ No newline at end of file
+ return failed == 0
diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py
index 0385e226..815cb404 100644
--- a/couchpotato/core/plugins/release/main.py
+++ b/couchpotato/core/plugins/release/main.py
@@ -8,7 +8,7 @@ from couchpotato import md5, get_db
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import toUnicode, sp
-from couchpotato.core.helpers.variable import getTitle
+from couchpotato.core.helpers.variable import getTitle, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from .index import ReleaseIndex, ReleaseStatusIndex, ReleaseIDIndex, ReleaseDownloadIndex
@@ -380,6 +380,7 @@ class Release(Plugin):
wait_for = False
let_through = False
filtered_results = []
+ minimum_seeders = tryInt(Env.setting('minimum_seeders', section = 'torrent', default = 1))
# Filter out ignored and other releases we don't want
for rel in results:
@@ -396,8 +397,8 @@ class Release(Plugin):
log.info('Ignored, size "%sMB" to low: %s', (rel['size'], rel['name']))
continue
- if 'seeders' in rel and rel.get('seeders') <= 0:
- log.info('Ignored, no seeders: %s', (rel['name']))
+ if 'seeders' in rel and rel.get('seeders') < minimum_seeders:
+ log.info('Ignored, not enough seeders, has %s needs %s: %s', (rel.get('seeders'), minimum_seeders, rel['name']))
continue
# If a single release comes through the "wait for", let through all
diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py
index 4c7aa3bf..aeeb3542 100755
--- a/couchpotato/core/plugins/renamer.py
+++ b/couchpotato/core/plugins/renamer.py
@@ -10,7 +10,8 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss, sp
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
- getImdb, link, symlink, tryInt, splitString, fnEscape, isSubFolder, getIdentifier
+ getImdb, link, symlink, tryInt, splitString, fnEscape, isSubFolder, \
+ getIdentifier, randomString, getFreeSpace, getSize
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
@@ -219,6 +220,12 @@ class Renamer(Plugin):
nfo_name = self.conf('nfo_name')
separator = self.conf('separator')
+ cd_keys = ['','']
+ if not any(x in folder_name for x in cd_keys) and not any(x in file_name for x in cd_keys):
+ log.error('Missing `cd` or `cd_nr` in the renamer. This will cause multi-file releases of being renamed to the same file.'
+ 'Force adding it')
+ file_name = '%s %s' % ('', file_name)
+
# Tag release folder as failed_rename in case no groups were found. This prevents check_snatched from removing the release from the downloader.
if not groups and self.statusInfoComplete(release_download):
self.tagRelease(release_download = release_download, tag = 'failed_rename')
@@ -266,13 +273,14 @@ class Renamer(Plugin):
category_label = category['label']
if category['destination'] and len(category['destination']) > 0 and category['destination'] != 'None':
- destination = category['destination']
+ destination = sp(category['destination'])
log.debug('Setting category destination for "%s": %s' % (media_title, destination))
else:
log.debug('No category destination found for "%s"' % media_title)
except:
log.error('Failed getting category label: %s', traceback.format_exc())
+
# Find subtitle for renaming
group['before_rename'] = []
fireEvent('renamer.before', group)
@@ -369,6 +377,9 @@ class Renamer(Plugin):
if separator:
final_file_name = final_file_name.replace(' ', separator)
+ final_folder_name = ss(final_folder_name)
+ final_file_name = ss(final_file_name)
+
# Move DVD files (no structure renaming)
if group['is_dvd'] and file_type is 'movie':
found = False
@@ -537,6 +548,13 @@ class Renamer(Plugin):
(not keep_original or self.fileIsAdded(current_file, group)):
remove_files.append(current_file)
+ total_space, available_space = getFreeSpace(destination)
+ renaming_size = getSize(rename_files.keys())
+ if renaming_size > available_space:
+ log.error('Not enough space left, need %s MB but only %s MB available', (renaming_size, available_space))
+ self.tagRelease(group = group, tag = 'not_enough_space')
+ continue
+
# Remove files
delete_folders = []
for src in remove_files:
@@ -552,9 +570,9 @@ class Renamer(Plugin):
os.remove(src)
parent_dir = os.path.dirname(src)
- if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and \
+ if parent_dir not in delete_folders and os.path.isdir(parent_dir) and \
not isSubFolder(destination, parent_dir) and not isSubFolder(media_folder, parent_dir) and \
- not isSubFolder(parent_dir, base_folder):
+ isSubFolder(parent_dir, base_folder):
delete_folders.append(parent_dir)
@@ -563,6 +581,7 @@ class Renamer(Plugin):
self.tagRelease(group = group, tag = 'failed_remove')
# Delete leftover folder from older releases
+ delete_folders = sorted(delete_folders, key = len, reverse = True)
for delete_folder in delete_folders:
try:
self.deleteEmptyFolder(delete_folder, show_error = False)
@@ -575,7 +594,10 @@ class Renamer(Plugin):
for src in rename_files:
if rename_files[src]:
dst = rename_files[src]
- log.info('Renaming "%s" to "%s"', (src, dst))
+
+ if dst in group['renamed_files']:
+ log.error('File "%s" already renamed once, adding random string at the end to prevent data loss', dst)
+ dst = '%s.random-%s' % (dst, randomString())
# Create dir
self.makeDir(os.path.dirname(dst))
@@ -617,8 +639,9 @@ class Renamer(Plugin):
group_folder = sp(os.path.join(base_folder, os.path.relpath(group['parentdir'], base_folder).split(os.path.sep)[0]))
try:
- log.info('Deleting folder: %s', group_folder)
- self.deleteEmptyFolder(group_folder)
+ if self.conf('cleanup') or self.conf('move_leftover'):
+ log.info('Deleting folder: %s', group_folder)
+ self.deleteEmptyFolder(group_folder)
except:
log.error('Failed removing %s: %s', (group_folder, traceback.format_exc()))
@@ -774,22 +797,32 @@ Remove it if you want it to be renamed (again, or at least let it try again)
dest = sp(dest)
try:
+ if os.path.exists(dest):
+ raise Exception('Destination "%s" already exists' % dest)
+
move_type = self.conf('file_action')
if use_default:
move_type = self.conf('default_file_action')
if move_type not in ['copy', 'link']:
try:
+ log.info('Moving "%s" to "%s"', (old, dest))
shutil.move(old, dest)
except:
- if os.path.exists(dest):
+ exists = os.path.exists(dest)
+ if exists and os.path.getsize(old) == os.path.getsize(dest):
log.error('Successfully moved file "%s", but something went wrong: %s', (dest, traceback.format_exc()))
os.unlink(old)
else:
+ # remove faultly copied file
+ if exists:
+ os.unlink(dest)
raise
elif move_type == 'copy':
+ log.info('Copying "%s" to "%s"', (old, dest))
shutil.copy(old, dest)
else:
+ log.info('Linking "%s" to "%s"', (old, dest))
# First try to hardlink
try:
log.debug('Hardlinking file "%s" to "%s"...', (old, dest))
@@ -799,9 +832,10 @@ Remove it if you want it to be renamed (again, or at least let it try again)
log.debug('Couldn\'t hardlink file "%s" to "%s". Symlinking instead. Error: %s.', (old, dest, traceback.format_exc()))
shutil.copy(old, dest)
try:
- symlink(dest, old + '.link')
+ old_link = '%s.link' % sp(old)
+ symlink(dest, old_link)
os.unlink(old)
- os.rename(old + '.link', 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()))
@@ -1192,7 +1226,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
except Exception as 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 os.path.isfile(move_to) and os.path.getsize(leftoverfile) == os.path.getsize(move_to):
if cleanup:
log.info('Deleting left over file %s instead...', leftoverfile)
os.unlink(leftoverfile)
diff --git a/couchpotato/runner.py b/couchpotato/runner.py
index 1c6f5779..36fc366f 100644
--- a/couchpotato/runner.py
+++ b/couchpotato/runner.py
@@ -9,6 +9,7 @@ import traceback
import warnings
import re
import tarfile
+import shutil
from CodernityDB.database_super_thread_safe import SuperThreadSafeDatabase
from argparse import ArgumentParser
@@ -108,14 +109,19 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
if not os.path.isdir(backup_path): os.makedirs(backup_path)
for root, dirs, files in os.walk(backup_path):
- for backup_file in sorted(files):
- ints = re.findall('\d+', backup_file)
+ # Only consider files being a direct child of the backup_path
+ if root == backup_path:
+ for backup_file in sorted(files):
+ ints = re.findall('\d+', backup_file)
- # Delete non zip files
- if len(ints) != 1:
- os.remove(os.path.join(backup_path, backup_file))
- else:
- existing_backups.append((int(ints[0]), backup_file))
+ # Delete non zip files
+ if len(ints) != 1:
+ os.remove(os.path.join(root, backup_file))
+ else:
+ existing_backups.append((int(ints[0]), backup_file))
+ else:
+ # Delete stray directories.
+ shutil.rmtree(root)
# Remove all but the last 5
for eb in existing_backups[:-backup_count]:
@@ -145,12 +151,15 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
if not os.path.exists(python_cache):
os.mkdir(python_cache)
+ session = requests.Session()
+ session.max_redirects = 5
+
# Register environment settings
Env.set('app_dir', sp(base_path))
Env.set('data_dir', sp(data_dir))
Env.set('log_path', sp(os.path.join(log_dir, 'CouchPotato.log')))
Env.set('db', db)
- Env.set('http_opener', requests.Session())
+ Env.set('http_opener', session)
Env.set('cache_dir', cache_dir)
Env.set('cache', FileSystemCache(python_cache))
Env.set('console_log', options.console_log)
diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js
index b9f72aba..c8e0a5e4 100644
--- a/couchpotato/static/scripts/page/settings.js
+++ b/couchpotato/static/scripts/page/settings.js
@@ -634,6 +634,7 @@ Option.Directory = new Class({
browser: null,
save_on_change: false,
use_cache: false,
+ current_dir: '',
create: function(){
var self = this;
@@ -645,8 +646,17 @@ Option.Directory = new Class({
'click': self.showBrowser.bind(self)
}
}).adopt(
- self.input = new Element('span', {
- 'text': self.getSettingValue()
+ self.input = new Element('input', {
+ 'value': self.getSettingValue(),
+ 'events': {
+ 'change': self.filterDirectory.bind(self),
+ 'keydown': function(e){
+ if(e.key == 'enter' || e.key == 'tab')
+ (e).stop();
+ },
+ 'keyup': self.filterDirectory.bind(self),
+ 'paste': self.filterDirectory.bind(self)
+ }
})
)
);
@@ -654,10 +664,55 @@ Option.Directory = new Class({
self.cached = {};
},
+ filterDirectory: function(e){
+ var self = this,
+ value = self.getValue(),
+ path_sep = Api.getOption('path_sep'),
+ active_selector = 'li:not(.blur):not(.empty)';
+
+ if(e.key == 'enter' || e.key == 'tab'){
+ (e).stop();
+
+ var first = self.dir_list.getElement(active_selector);
+ if(first){
+ self.selectDirectory(first.get('data-value'));
+ }
+ }
+ else {
+
+ // New folder
+ if(value.substr(-1) == path_sep){
+ if(self.current_dir != value)
+ self.selectDirectory(value)
+ }
+ else {
+ var pd = self.getParentDir(value);
+ if(self.current_dir != pd)
+ self.getDirs(pd);
+
+ var folder_filter = value.split(path_sep).getLast()
+ self.dir_list.getElements('li').each(function(li){
+ var valid = li.get('text').substr(0, folder_filter.length).toLowerCase() != folder_filter.toLowerCase()
+ li[valid ? 'addClass' : 'removeClass']('blur')
+ });
+
+ var first = self.dir_list.getElement(active_selector);
+ if(first){
+ if(!self.dir_list_scroll)
+ self.dir_list_scroll = new Fx.Scroll(self.dir_list, {
+ 'transition': 'quint:in:out'
+ });
+
+ self.dir_list_scroll.toElement(first);
+ }
+ }
+ }
+ },
+
selectDirectory: function(dir){
var self = this;
- self.input.set('text', dir);
+ self.input.set('value', dir);
self.getDirs()
},
@@ -668,9 +723,28 @@ Option.Directory = new Class({
self.selectDirectory(self.getParentDir())
},
+ caretAtEnd: function(){
+ var self = this;
+
+ self.input.focus();
+
+ if (typeof self.input.selectionStart == "number") {
+ self.input.selectionStart = self.input.selectionEnd = self.input.get('value').length;
+ } else if (typeof el.createTextRange != "undefined") {
+ self.input.focus();
+ var range = self.input.createTextRange();
+ range.collapse(false);
+ range.select();
+ }
+ },
+
showBrowser: function(){
var self = this;
+ // Move caret to back of the input
+ if(!self.browser || self.browser && !self.browser.isVisible())
+ self.caretAtEnd()
+
if(!self.browser){
self.browser = new Element('div.directory_list').adopt(
new Element('div.pointer'),
@@ -686,7 +760,9 @@ Option.Directory = new Class({
}).adopt(
self.show_hidden = new Element('input[type=checkbox].inlay', {
'events': {
- 'change': self.getDirs.bind(self)
+ 'change': function(){
+ self.getDirs()
+ }
}
})
)
@@ -707,7 +783,7 @@ Option.Directory = new Class({
'text': 'Clear',
'events': {
'click': function(e){
- self.input.set('text', '');
+ self.input.set('value', '');
self.hideBrowser(e, true);
}
}
@@ -735,7 +811,7 @@ Option.Directory = new Class({
new Form.Check(self.show_hidden);
}
- self.initial_directory = self.input.get('text');
+ self.initial_directory = self.input.get('value');
self.getDirs();
self.browser.show();
@@ -749,7 +825,7 @@ Option.Directory = new Class({
if(save)
self.save();
else
- self.input.set('text', self.initial_directory);
+ self.input.set('value', self.initial_directory);
self.browser.hide();
self.el.removeEvents('outerClick')
@@ -757,21 +833,21 @@ Option.Directory = new Class({
},
fillBrowser: function(json){
- var self = this;
+ var self = this,
+ v = self.getValue();
self.data = json;
- var v = self.getValue();
- var previous_dir = self.getParentDir();
+ var previous_dir = json.parent;
if(v == '')
- self.input.set('text', json.home);
+ self.input.set('value', json.home);
- if(previous_dir != v && previous_dir.length >= 1 && !json.is_root){
+ if(previous_dir.length >= 1 && !json.is_root){
var prev_dirname = self.getCurrentDirname(previous_dir);
if(previous_dir == json.home)
- prev_dirname = 'Home';
+ prev_dirname = 'Home Folder';
else if(previous_dir == '/' && json.platform == 'nt')
prev_dirname = 'Computer';
@@ -801,12 +877,13 @@ Option.Directory = new Class({
new Element('li.empty', {
'text': 'Selected folder is empty'
}).inject(self.dir_list)
+
+ self.caretAtEnd();
},
- getDirs: function(){
- var self = this;
-
- var c = self.getValue();
+ getDirs: function(dir){
+ var self = this,
+ c = dir || self.getValue();
if(self.cached[c] && self.use_cache){
self.fillBrowser()
@@ -817,7 +894,10 @@ Option.Directory = new Class({
'path': c,
'show_hidden': +self.show_hidden.checked
},
- 'onComplete': self.fillBrowser.bind(self)
+ 'onComplete': function(json){
+ self.current_dir = c;
+ self.fillBrowser(json);
+ }
})
}
},
@@ -831,8 +911,8 @@ Option.Directory = new Class({
var v = dir || self.getValue();
var sep = Api.getOption('path_sep');
var dirs = v.split(sep);
- if(dirs.pop() == '')
- dirs.pop();
+ if(dirs.pop() == '')
+ dirs.pop();
return dirs.join(sep) + sep
},
@@ -845,7 +925,7 @@ Option.Directory = new Class({
getValue: function(){
var self = this;
- return self.input.get('text');
+ return self.input.get('value');
}
});
diff --git a/couchpotato/static/style/settings.css b/couchpotato/static/style/settings.css
index 7fb1df29..e84d3814 100644
--- a/couchpotato/static/style/settings.css
+++ b/couchpotato/static/style/settings.css
@@ -302,15 +302,19 @@
font-family: 'Elusive-Icons';
color: #f5e39c;
}
- .page form .directory > span {
+ .page form .directory > input {
height: 25px;
display: inline-block;
float: right;
text-align: right;
white-space: nowrap;
cursor: pointer;
+ background: none;
+ border: 0;
+ color: #FFF;
+ width: 100%;
}
- .page form .directory span:empty:before {
+ .page form .directory input:empty:before {
content: 'No folder selected';
font-style: italic;
opacity: .3;
@@ -353,6 +357,11 @@
white-space: nowrap;
text-overflow: ellipsis;
}
+
+ .page .directory_list li.blur {
+ opacity: .3;
+ }
+
.page .directory_list li:last-child {
border-bottom: 1px solid rgba(255,255,255,0.1);
}
diff --git a/libs/requests/sessions.py b/libs/requests/sessions.py
index 508b0ef2..02c9fb2a 100644
--- a/libs/requests/sessions.py
+++ b/libs/requests/sessions.py
@@ -532,7 +532,11 @@ class Session(SessionRedirectMixin):
if not isinstance(request, PreparedRequest):
raise ValueError('You can only send PreparedRequests.')
+ redirect_count = 0
while request.url in self.redirect_cache:
+ redirect_count += 1
+ if redirect_count > self.max_redirects:
+ raise TooManyRedirects
request.url = self.redirect_cache.get(request.url)
# Set up variables needed for resolve_redirects and dispatching of hooks