From ef4470bd789da13878d1a8db328b08374f501f63 Mon Sep 17 00:00:00 2001
From: JackDandy <JackDandy@users.noreply.github.com>
Date: Wed, 5 Nov 2014 05:36:16 +0000
Subject: [PATCH] Add send2trash, General Config/Send to trash, and catch show
 dir not found exception.

Add send2trash, a small package that sends files to the Trash (or Recycle Bin) natively and on all platforms. If send2trash is found not compatible, the user can use the default delete and manually delete failed send2trash files.
Add General Config/Misc/Send to trash for actions that involve removing shows and log rotation.
Add handling for the exception raised while deleting a show and show folder no longer exists.
---
 CHANGES.md                                    |   4 +-
 gui/slick/css/style.css                       |   2 +-
 .../interfaces/default/config_general.tmpl    |  15 ++
 lib/send2trash/__init__.py                    |  19 +++
 lib/send2trash/compat.py                      |  13 ++
 lib/send2trash/plat_gio.py                    |  14 ++
 lib/send2trash/plat_osx.py                    |  48 ++++++
 lib/send2trash/plat_other.py                  | 160 ++++++++++++++++++
 lib/send2trash/plat_win.py                    |  59 +++++++
 sickbeard/__init__.py                         |   9 +-
 sickbeard/logger.py                           |  12 +-
 sickbeard/tv.py                               |  38 ++++-
 sickbeard/webserve.py                         |   9 +-
 13 files changed, 389 insertions(+), 13 deletions(-)
 create mode 100644 lib/send2trash/__init__.py
 create mode 100644 lib/send2trash/compat.py
 create mode 100644 lib/send2trash/plat_gio.py
 create mode 100644 lib/send2trash/plat_osx.py
 create mode 100644 lib/send2trash/plat_other.py
 create mode 100644 lib/send2trash/plat_win.py

diff --git a/CHANGES.md b/CHANGES.md
index cc252b85..d4b36d95 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,4 @@
-### 0.x.x (2014-11-03 xx:xx:xx UTC)
+### 0.x.x (2014-11-05 xx:xx:xx UTC)
 
 * Add Bootstrap for UI features
 * Change UI to resize fluidly on different display sizes, fixes the issue where top menu items would disappear on smaller screens.
@@ -37,6 +37,8 @@
 * Removed requirement for http login for API when an API key is provided
 * Change API now uses Timezone setting at General Config/Interface/User Interface/ at relevant endpoints
 * Fixes changing root dirs on the mass edit page
+* Add the ability to use trash (or Recycle Bin) for selected actions on General Config/Misc/Send to trash
+* Add handling for when deleting a show and the show folder no longer exists
 
 [develop changelog]
 * Improve display of progress bars in the Downloads columns of the show list page
diff --git a/gui/slick/css/style.css b/gui/slick/css/style.css
index fdea91e0..17cb500b 100644
--- a/gui/slick/css/style.css
+++ b/gui/slick/css/style.css
@@ -3009,7 +3009,7 @@ span.token-input-delete-token {
 
 .red-text {color:#d33}
 .clear-left {clear:left}
-
+.nextline-block {display:block}
 /* =======================================================================
 jquery.confirm.css
 ========================================================================== */
diff --git a/gui/slick/interfaces/default/config_general.tmpl b/gui/slick/interfaces/default/config_general.tmpl
index 3c73da17..23b23f39 100644
--- a/gui/slick/interfaces/default/config_general.tmpl
+++ b/gui/slick/interfaces/default/config_general.tmpl
@@ -72,6 +72,21 @@
 							</label>
 						</div>
 
+						<div class="field-pair">
+							<span class="component-title">Send to trash for actions</span>
+							<span class="component-desc">
+								<label for="trash_remove_show" class="nextline-block">
+									<input type="checkbox" name="trash_remove_show" id="trash_remove_show" #if $sickbeard.TRASH_REMOVE_SHOW then 'checked="checked"' else ''#/>
+									<p>when using show "Remove" and delete files</p>
+								</label>
+								<label for="trash_rotate_logs" class="nextline-block">
+									<input type="checkbox" name="trash_rotate_logs" id="trash_rotate_logs" #if $sickbeard.TRASH_ROTATE_LOGS then 'checked="checked"' else ''#/>
+									<p>on scheduled deletes of the oldest log files</p>
+								</label>
+								<div class="clear-left"><p>selected actions use trash (recycle bin) instead of the default permanent delete</p></div>
+							</span>
+						</div>
+
 						<div class="field-pair">
 							<label for="log_dir">
 								<span class="component-title">Log file folder location</span>
diff --git a/lib/send2trash/__init__.py b/lib/send2trash/__init__.py
new file mode 100644
index 00000000..8a059a05
--- /dev/null
+++ b/lib/send2trash/__init__.py
@@ -0,0 +1,19 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+import sys
+
+if sys.platform == 'darwin':
+    from .plat_osx import send2trash
+elif sys.platform == 'win32':
+    from .plat_win import send2trash
+else:
+    try:
+        # If we can use gio, let's use it
+        from .plat_gio import send2trash
+    except ImportError:
+        # Oh well, let's fallback to our own Freedesktop trash implementation
+        from .plat_other import send2trash
diff --git a/lib/send2trash/compat.py b/lib/send2trash/compat.py
new file mode 100644
index 00000000..0f3a4897
--- /dev/null
+++ b/lib/send2trash/compat.py
@@ -0,0 +1,13 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+import sys
+if sys.version < '3':
+    text_type = unicode
+    binary_type = str
+else:
+    text_type = str
+    binary_type = bytes
diff --git a/lib/send2trash/plat_gio.py b/lib/send2trash/plat_gio.py
new file mode 100644
index 00000000..9282d90a
--- /dev/null
+++ b/lib/send2trash/plat_gio.py
@@ -0,0 +1,14 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+from gi.repository import GObject, Gio
+
+def send2trash(path):
+    try:
+        f = Gio.File.new_for_path(path)
+        f.trash(cancellable=None)
+    except GObject.GError as e:
+        raise OSError(e.message)
diff --git a/lib/send2trash/plat_osx.py b/lib/send2trash/plat_osx.py
new file mode 100644
index 00000000..fa2c2c85
--- /dev/null
+++ b/lib/send2trash/plat_osx.py
@@ -0,0 +1,48 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+from __future__ import unicode_literals
+
+from ctypes import cdll, byref, Structure, c_char, c_char_p
+from ctypes.util import find_library
+
+from .compat import binary_type
+
+Foundation = cdll.LoadLibrary(find_library('Foundation'))
+CoreServices = cdll.LoadLibrary(find_library('CoreServices'))
+
+GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString
+GetMacOSStatusCommentString.restype = c_char_p
+FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions
+FSMoveObjectToTrashSync = CoreServices.FSMoveObjectToTrashSync
+
+kFSPathMakeRefDefaultOptions = 0
+kFSPathMakeRefDoNotFollowLeafSymlink = 0x01
+
+kFSFileOperationDefaultOptions = 0
+kFSFileOperationOverwrite = 0x01
+kFSFileOperationSkipSourcePermissionErrors = 0x02
+kFSFileOperationDoNotMoveAcrossVolumes = 0x04
+kFSFileOperationSkipPreflight = 0x08
+
+class FSRef(Structure):
+    _fields_ = [('hidden', c_char * 80)]
+
+def check_op_result(op_result):
+    if op_result:
+        msg = GetMacOSStatusCommentString(op_result).decode('utf-8')
+        raise OSError(msg)
+
+def send2trash(path):
+    if not isinstance(path, binary_type):
+        path = path.encode('utf-8')
+    fp = FSRef()
+    opts = kFSPathMakeRefDoNotFollowLeafSymlink
+    op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None)
+    check_op_result(op_result)
+    opts = kFSFileOperationDefaultOptions
+    op_result = FSMoveObjectToTrashSync(byref(fp), None, opts)
+    check_op_result(op_result)
diff --git a/lib/send2trash/plat_other.py b/lib/send2trash/plat_other.py
new file mode 100644
index 00000000..59000eca
--- /dev/null
+++ b/lib/send2trash/plat_other.py
@@ -0,0 +1,160 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+# This is a reimplementation of plat_other.py with reference to the
+# freedesktop.org trash specification:
+#   [1] http://www.freedesktop.org/wiki/Specifications/trash-spec
+#   [2] http://www.ramendik.ru/docs/trashspec.html
+# See also:
+#   [3] http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
+#
+# For external volumes this implementation will raise an exception if it can't
+# find or create the user's trash directory.
+
+from __future__ import unicode_literals
+
+import sys
+import os
+import os.path as op
+from datetime import datetime
+import stat
+try:
+    from urllib.parse import quote
+except ImportError:
+    # Python 2
+    from urllib import quote
+
+FILES_DIR = 'files'
+INFO_DIR = 'info'
+INFO_SUFFIX = '.trashinfo'
+
+# Default of ~/.local/share [3]
+XDG_DATA_HOME = op.expanduser(os.environ.get('XDG_DATA_HOME', '~/.local/share'))
+HOMETRASH = op.join(XDG_DATA_HOME, 'Trash')
+
+uid = os.getuid()
+TOPDIR_TRASH = '.Trash'
+TOPDIR_FALLBACK = '.Trash-' + str(uid)
+
+def is_parent(parent, path):
+    path = op.realpath(path) # In case it's a symlink
+    parent = op.realpath(parent)
+    return path.startswith(parent)
+
+def format_date(date):
+    return date.strftime("%Y-%m-%dT%H:%M:%S")
+
+def info_for(src, topdir):
+    # ...it MUST not include a ".."" directory, and for files not "under" that
+    # directory, absolute pathnames must be used. [2]
+    if topdir is None or not is_parent(topdir, src):
+        src = op.abspath(src)
+    else:
+        src = op.relpath(src, topdir)
+
+    info  = "[Trash Info]\n"
+    info += "Path=" + quote(src) + "\n"
+    info += "DeletionDate=" + format_date(datetime.now()) + "\n"
+    return info
+
+def check_create(dir):
+    # use 0700 for paths [3]
+    if not op.exists(dir):
+        os.makedirs(dir, 0o700)
+
+def trash_move(src, dst, topdir=None):
+    filename = op.basename(src)
+    filespath = op.join(dst, FILES_DIR)
+    infopath = op.join(dst, INFO_DIR)
+    base_name, ext = op.splitext(filename)
+
+    counter = 0
+    destname = filename
+    while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)):
+        counter += 1
+        destname = '%s %s%s' % (base_name, counter, ext)
+    
+    check_create(filespath)
+    check_create(infopath)
+    
+    os.rename(src, op.join(filespath, destname))
+    f = open(op.join(infopath, destname + INFO_SUFFIX), 'w')
+    f.write(info_for(src, topdir))
+    f.close()
+
+def find_mount_point(path):
+    # Even if something's wrong, "/" is a mount point, so the loop will exit.
+    # Use realpath in case it's a symlink
+    path = op.realpath(path) # Required to avoid infinite loop
+    while not op.ismount(path):
+        path = op.split(path)[0]
+    return path
+
+def find_ext_volume_global_trash(volume_root):
+    # from [2] Trash directories (1) check for a .Trash dir with the right
+    # permissions set.
+    trash_dir = op.join(volume_root, TOPDIR_TRASH)
+    if not op.exists(trash_dir):
+        return None
+    
+    mode = os.lstat(trash_dir).st_mode
+    # vol/.Trash must be a directory, cannot be a symlink, and must have the
+    # sticky bit set.
+    if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX):
+        return None
+
+    trash_dir = op.join(trash_dir, str(uid))
+    try:
+        check_create(trash_dir)
+    except OSError:
+        return None
+    return trash_dir
+
+def find_ext_volume_fallback_trash(volume_root):
+    # from [2] Trash directories (1) create a .Trash-$uid dir.
+    trash_dir = op.join(volume_root, TOPDIR_FALLBACK)
+    # Try to make the directory, if we can't the OSError exception will escape
+    # be thrown out of send2trash.
+    check_create(trash_dir)
+    return trash_dir
+
+def find_ext_volume_trash(volume_root):
+    trash_dir = find_ext_volume_global_trash(volume_root)
+    if trash_dir is None:
+        trash_dir = find_ext_volume_fallback_trash(volume_root)
+    return trash_dir
+
+# Pull this out so it's easy to stub (to avoid stubbing lstat itself)
+def get_dev(path):
+    return os.lstat(path).st_dev
+
+def send2trash(path):
+    if not isinstance(path, str):
+        path = str(path, sys.getfilesystemencoding())
+    if not op.exists(path):
+        raise OSError("File not found: %s" % path)
+    # ...should check whether the user has the necessary permissions to delete
+    # it, before starting the trashing operation itself. [2]
+    if not os.access(path, os.W_OK):
+        raise OSError("Permission denied: %s" % path)
+    # if the file to be trashed is on the same device as HOMETRASH we
+    # want to move it there.
+    path_dev = get_dev(path)
+    
+    # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
+    # home directory, and these paths will be created further on if needed.
+    trash_dev = get_dev(op.expanduser('~'))
+
+    if path_dev == trash_dev:
+        topdir = XDG_DATA_HOME
+        dest_trash = HOMETRASH
+    else:
+        topdir = find_mount_point(path)
+        trash_dev = get_dev(topdir)
+        if trash_dev != path_dev:
+            raise OSError("Couldn't find mount point for %s" % path)
+        dest_trash = find_ext_volume_trash(topdir)
+    trash_move(path, dest_trash, topdir)
diff --git a/lib/send2trash/plat_win.py b/lib/send2trash/plat_win.py
new file mode 100644
index 00000000..3a55b9d3
--- /dev/null
+++ b/lib/send2trash/plat_win.py
@@ -0,0 +1,59 @@
+# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
+
+# This software is licensed under the "BSD" License as described in the "LICENSE" file, 
+# which should be included with this package. The terms are also available at 
+# http://www.hardcoded.net/licenses/bsd_license
+
+from __future__ import unicode_literals
+
+from ctypes import windll, Structure, byref, c_uint
+from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL
+import os.path as op
+
+from .compat import text_type
+
+shell32 = windll.shell32
+SHFileOperationW = shell32.SHFileOperationW
+
+class SHFILEOPSTRUCTW(Structure):
+    _fields_ = [
+        ("hwnd", HWND),
+        ("wFunc", UINT),
+        ("pFrom", LPCWSTR),
+        ("pTo", LPCWSTR),
+        ("fFlags", c_uint),
+        ("fAnyOperationsAborted", BOOL),
+        ("hNameMappings", c_uint),
+        ("lpszProgressTitle", LPCWSTR),
+        ]
+
+FO_MOVE = 1
+FO_COPY = 2
+FO_DELETE = 3
+FO_RENAME = 4
+
+FOF_MULTIDESTFILES = 1
+FOF_SILENT = 4
+FOF_NOCONFIRMATION = 16
+FOF_ALLOWUNDO = 64
+FOF_NOERRORUI = 1024
+
+def send2trash(path):
+    if not isinstance(path, text_type):
+        path = text_type(path, 'mbcs')
+    if not op.isabs(path):
+        path = op.abspath(path)
+    fileop = SHFILEOPSTRUCTW()
+    fileop.hwnd = 0
+    fileop.wFunc = FO_DELETE
+    fileop.pFrom = LPCWSTR(path + '\0')
+    fileop.pTo = None
+    fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT
+    fileop.fAnyOperationsAborted = 0
+    fileop.hNameMappings = 0
+    fileop.lpszProgressTitle = None
+    result = SHFileOperationW(byref(fileop))
+    if result:
+        msg = "Couldn't perform operation. Error code: %d" % result
+        raise OSError(msg)
+
diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py
index 40b69343..a4614460 100755
--- a/sickbeard/__init__.py
+++ b/sickbeard/__init__.py
@@ -147,6 +147,8 @@ CACHE_DIR = None
 ACTUAL_CACHE_DIR = None
 ROOT_DIRS = None
 UPDATE_SHOWS_ON_START = False
+TRASH_REMOVE_SHOW = False
+TRASH_ROTATE_LOGS = False
 SORT_ARTICLE = False
 DEBUG = False
 
@@ -473,7 +475,7 @@ def initialize(consoleLogging=True):
             USE_TRAKT, TRAKT_USERNAME, TRAKT_PASSWORD, TRAKT_API, TRAKT_REMOVE_WATCHLIST, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_USE_RECOMMENDED, TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, \
             USE_PLEX, PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_NOTIFY_ONSUBTITLEDOWNLOAD, PLEX_UPDATE_LIBRARY, \
             PLEX_SERVER_HOST, PLEX_HOST, PLEX_USERNAME, PLEX_PASSWORD, DEFAULT_BACKLOG_FREQUENCY, MIN_BACKLOG_FREQUENCY, BACKLOG_STARTUP, SKIP_REMOVED_FILES, \
-            showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, UPDATE_SHOWS_ON_START, SORT_ARTICLE, showList, loadingShowList, \
+            showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, UPDATE_SHOWS_ON_START, TRASH_REMOVE_SHOW, TRASH_ROTATE_LOGS, SORT_ARTICLE, showList, loadingShowList, \
             NEWZNAB_DATA, NZBS, NZBS_UID, NZBS_HASH, INDEXER_DEFAULT, INDEXER_TIMEOUT, USENET_RETENTION, TORRENT_DIR, \
             QUALITY_DEFAULT, FLATTEN_FOLDERS_DEFAULT, SUBTITLES_DEFAULT, STATUS_DEFAULT, DAILYSEARCH_STARTUP, \
             GROWL_NOTIFY_ONSNATCH, GROWL_NOTIFY_ONDOWNLOAD, GROWL_NOTIFY_ONSUBTITLEDOWNLOAD, TWITTER_NOTIFY_ONSNATCH, TWITTER_NOTIFY_ONDOWNLOAD, TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD, \
@@ -608,6 +610,9 @@ def initialize(consoleLogging=True):
             ANON_REDIRECT = ''
 
         UPDATE_SHOWS_ON_START = bool(check_setting_int(CFG, 'General', 'update_shows_on_start', 0))
+        TRASH_REMOVE_SHOW = bool(check_setting_int(CFG, 'General', 'trash_remove_show', 0))
+        TRASH_ROTATE_LOGS = bool(check_setting_int(CFG, 'General', 'trash_rotate_logs', 0))
+
         SORT_ARTICLE = bool(check_setting_int(CFG, 'General', 'sort_article', 0))
 
         USE_API = bool(check_setting_int(CFG, 'General', 'use_api', 0))
@@ -1426,6 +1431,8 @@ def save_config():
     new_config['General']['naming_anime'] = int(NAMING_ANIME)
     new_config['General']['launch_browser'] = int(LAUNCH_BROWSER)
     new_config['General']['update_shows_on_start'] = int(UPDATE_SHOWS_ON_START)
+    new_config['General']['trash_remove_show'] = int(TRASH_REMOVE_SHOW)
+    new_config['General']['trash_rotate_logs'] = int(TRASH_ROTATE_LOGS)
     new_config['General']['sort_article'] = int(SORT_ARTICLE)
     new_config['General']['proxy_setting'] = PROXY_SETTING
     new_config['General']['proxy_indexers'] = int(PROXY_INDEXERS)
diff --git a/sickbeard/logger.py b/sickbeard/logger.py
index c4c51592..3b77a394 100644
--- a/sickbeard/logger.py
+++ b/sickbeard/logger.py
@@ -29,6 +29,11 @@ import sickbeard
 
 from sickbeard import classes
 
+try:
+    from lib.send2trash import send2trash
+except ImportError:
+    pass
+
 
 # number of log files to keep
 NUM_LOGS = 3
@@ -231,7 +236,12 @@ class SBRotatingLogHandler(object):
             cur_file_name = self._log_file_name(i)
             try:
                 if i >= NUM_LOGS:
-                    os.remove(cur_file_name)
+                    if sickbeard.TRASH_ROTATE_LOGS:
+                        new_name = '%s.%s' % (cur_file_name, int(time.time()))
+                        os.rename(cur_file_name, new_name)
+                        send2trash(new_name)
+                    else:
+                        os.remove(cur_file_name)
                 else:
                     os.rename(cur_file_name, self._log_file_name(i + 1))
             except OSError:
diff --git a/sickbeard/tv.py b/sickbeard/tv.py
index 3af0ece8..8076076d 100644
--- a/sickbeard/tv.py
+++ b/sickbeard/tv.py
@@ -35,6 +35,11 @@ from name_parser.parser import NameParser, InvalidNameException, InvalidShowExce
 
 from lib import subliminal
 
+try:
+    from lib.send2trash import send2trash
+except ImportError:
+    pass
+
 from lib.imdb import imdb
 
 from sickbeard import db
@@ -986,32 +991,51 @@ class TVShow(object):
         myDB = db.DBConnection()
         myDB.mass_action(sql_l)
 
+        action = ('delete', 'trash')[sickbeard.TRASH_REMOVE_SHOW]
+
         # remove self from show list
         sickbeard.showList = [x for x in sickbeard.showList if int(x.indexerid) != self.indexerid]
 
         # clear the cache
         image_cache_dir = ek.ek(os.path.join, sickbeard.CACHE_DIR, 'images')
         for cache_file in ek.ek(glob.glob, ek.ek(os.path.join, image_cache_dir, str(self.indexerid) + '.*')):
-            logger.log(u"Deleting cache file " + cache_file)
-            os.remove(cache_file)
+            logger.log(u'Attempt to %s cache file %s' % (action, cache_file))
+            try:
+                if sickbeard.TRASH_REMOVE_SHOW:
+                    send2trash(cache_file)
+                else:
+                    os.remove(cache_file)
+
+            except OSError, e:
+                logger.log(u'Unable to %s %s: %s / %s' % (action, cache_file, repr(e), str(e)), logger.WARNING)
 
         # remove entire show folder
         if full:
             try:
-                logger.log(u"Deleting show folder " + self.location)
+                logger.log(u'Attempt to %s show folder %s' % (action, self._location))
                 # check first the read-only attribute
                 file_attribute = ek.ek(os.stat, self.location)[0]
                 if (not file_attribute & stat.S_IWRITE):
                     # File is read-only, so make it writeable
-                    logger.log('Read only mode on folder ' + self.location + ' Will try to make it writeable', logger.DEBUG)
+                    logger.log('Attempting to make writeable the read only folder %s' % self._location, logger.DEBUG)
                     try:
                         ek.ek(os.chmod, self.location, stat.S_IWRITE)
                     except:
-                        logger.log(u'Cannot change permissions of ' + self.location, logger.WARNING)
+                        logger.log(u'Unable to change permissions of %s' % self._location, logger.WARNING)
+
+                if sickbeard.TRASH_REMOVE_SHOW:
+                    send2trash(self.location)
+                else:
+                    ek.ek(shutil.rmtree, self.location)
+
+                logger.log(u'%s show folder %s' %
+                           (('Deleted', 'Trashed')[sickbeard.TRASH_REMOVE_SHOW],
+                            self._location))
 
-                ek.ek(shutil.rmtree, self.location)
+            except exceptions.ShowDirNotFoundException:
+                logger.log(u"Show folder does not exist, no need to %s %s" % (action, self._location), logger.WARNING)
             except OSError, e:
-                logger.log(u"Unable to delete " + self.location + ": " + repr(e) + " / " + str(e), logger.WARNING)
+                logger.log(u'Unable to %s %s: %s / %s' % (action, self._location, repr(e), str(e)), logger.WARNING)
 
     def populateCache(self):
         cache_inst = image_cache.ImageCache()
diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py
index 2db765eb..63722675 100644
--- a/sickbeard/webserve.py
+++ b/sickbeard/webserve.py
@@ -1514,7 +1514,7 @@ class ConfigGeneral(MainHandler):
 
 
     def saveGeneral(self, log_dir=None, web_port=None, web_log=None, encryption_version=None, web_ipv6=None,
-                    update_shows_on_start=None, update_frequency=None, launch_browser=None, web_username=None,
+                    update_shows_on_start=None, trash_remove_show=None, trash_rotate_logs=None, update_frequency=None, launch_browser=None, web_username=None,
                     use_api=None, api_key=None, indexer_default=None, timezone_display=None, cpu_preset=None,
                     web_password=None, version_notify=None, enable_https=None, https_cert=None, https_key=None,
                     handle_reverse_proxy=None, sort_article=None, auto_update=None, notify_on_update=None,
@@ -1533,6 +1533,8 @@ class ConfigGeneral(MainHandler):
         # sickbeard.LOG_DIR is set in config.change_LOG_DIR()
 
         sickbeard.UPDATE_SHOWS_ON_START = config.checkbox_to_value(update_shows_on_start)
+        sickbeard.TRASH_REMOVE_SHOW = config.checkbox_to_value(trash_remove_show)
+        sickbeard.TRASH_ROTATE_LOGS = config.checkbox_to_value(trash_rotate_logs)
         config.change_UPDATE_FREQUENCY(update_frequency)
         sickbeard.LAUNCH_BROWSER = config.checkbox_to_value(launch_browser)
         sickbeard.SORT_ARTICLE = config.checkbox_to_value(sort_article)
@@ -4065,7 +4067,10 @@ class Home(MainHandler):
 
         showObj.deleteShow(bool(full))
 
-        ui.notifications.message('<b>%s</b> has been deleted' % showObj.name)
+        ui.notifications.message('<b>%s</b> has been %s %s' %
+                                 (showObj.name,
+                                 ('deleted', 'trashed')[sickbeard.TRASH_REMOVE_SHOW],
+                                 ('(media untouched)', '(with all related media)')[bool(full)]))
         redirect("/home/")
 
 
-- 
GitLab