diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 01dc0f1..f561941 100755 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -1,6 +1,7 @@ -from datetime import timedelta +import os import time import traceback +from datetime import timedelta from string import ascii_lowercase from CodernityDB.database import RecordNotFound, RecordDeleted @@ -68,6 +69,7 @@ class MediaPlugin(MediaBase): 'params': { 'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'}, 'delete_from': {'desc': 'Delete media from this page', 'type': 'string: all (default), wanted, manage'}, + 'with_files': {'desc': 'Delete the files as well', 'type': 'bool (true or false)'}, } }) @@ -416,12 +418,62 @@ class MediaPlugin(MediaBase): tempChar = lambda *args, **kwargs : self.charView(type = media_type, **kwargs) addApiView('%s.available_chars' % media_type, tempChar) - def delete(self, media_id, delete_from = None): + def deleteFiles(self, instance): + directories = dict() + + # Walk through all files in the Couch database + for name, paths in instance['files'].iteritems(): + log.info('Removing %s', name) + for path in paths: + # Add the directories and filename prefixes to a list so we can + # remove the directories and related files as well + directory = os.path.dirname(path) + if directory not in directories: + directories[directory] = set() + + directories[directory].add(os.path.splitext(path)[0]) + + if os.path.isfile(path): + try: + os.remove(path) + log.info('Removed %s', path) + except: + log.error('Unable to remove %s', path) + + # Walk through the directories and file prefixes for removal if + # possible + for directory, prefixes in directories.iteritems(): + if os.path.isdir(directory): + # If the files in the directory have the same name as the + # expected files (except for extensions and stuff), remove them + files = os.listdir(directory) + removed = 0 + for file_ in files: + for prefix in prefixes: + if file_.startswith(prefix): + try: + os.remove(file_) + removed += 1 + log.info('Removed %s', file_) + except: + log.error('Unable to remove %s', file_) + + try: + if len(files) == removed: + os.rmdir(directory) + log.info('Removed %s', directory) + else: + log.info('Not removing %s, %d files in directory', + (directory, len(files) - removed)) + except Exception: + log.error('Unable to remove %s', directory) + def delete(self, media_id, delete_from = None, with_files = False): try: db = get_db() media = db.get('id', media_id) + if media: deleted = False @@ -430,8 +482,14 @@ class MediaPlugin(MediaBase): if delete_from == 'all': # Delete connected releases for release in media_releases: + if with_files: + self.deleteFiles(release) + db.delete(release) + if with_files: + self.deleteFiles(media) + db.delete(media) deleted = True else: @@ -450,8 +508,13 @@ class MediaPlugin(MediaBase): if release.get('status') == 'done' or media.get('status') == 'done': db.delete(release) total_deleted += 1 + if with_files: + self.deleteFiles(release) if (total_releases == total_deleted) or (total_releases == 0 and not new_media_status) or (not new_media_status and delete_from == 'late'): + if with_files: + self.deleteFiles(media) + db.delete(media) deleted = True elif new_media_status: @@ -460,7 +523,7 @@ class MediaPlugin(MediaBase): # Remove profile (no use for in manage) if new_media_status == 'done': media['profile_id'] = None - + db.update(media) fireEvent('media.untag', media['_id'], 'recent', single = True) @@ -478,7 +541,7 @@ class MediaPlugin(MediaBase): ids = splitString(id) for media_id in ids: - self.delete(media_id, delete_from = kwargs.get('delete_from', 'all')) + self.delete(media_id, delete_from = kwargs.get('delete_from', 'all'), with_files = kwargs.get('with_files')) return { 'success': True, diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index a00829e..e84bf9c 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -841,9 +841,10 @@ MA.Delete = new Class({ var self = this; return new Element('a.delete', { 'text': 'Delete', - 'title': 'Remove the movie from this CP list', + 'title': 'Remove the movie from this CP list, and optionally delete files from disk', 'events': { - 'click': self.showConfirm.bind(self) + 'click': self.movie.list.options.identifier === 'manage' ? + self.showConfirmWithFiles.bind(self) : self.showConfirm.bind(self) } }); }, @@ -859,7 +860,7 @@ MA.Delete = new Class({ 'click': function(e){ e.target.set('text', 'Deleting...'); - self.del(); + self.del(false); } } }, { @@ -869,7 +870,57 @@ MA.Delete = new Class({ }, - del: function(){ + showConfirmWithFiles: function(e){ + var self = this; + (e).stopPropagation(); + + self.question = new Question('Are you sure you want to delete ' + self.getTitle() + '?', '', [{ + 'text': 'Yes, delete', + 'class': 'delete', + 'events': { + 'click': function(e){ + e.target.set('text', 'Deleting...'); + + self.del(false); + } + } + }, { + 'text': 'Yes, and also delete files from disk', + 'class': 'delete', + 'events': { + 'click': self.showConfirmWithFilesReally.bind(self) + } + }, { + 'text': 'Cancel', + 'cancel': true + }]); + + }, + + showConfirmWithFilesReally: function(e){ + var self = this; + (e).stopPropagation(); + + if(self.question) + self.question.close(); + + self.question = new Question('Are you sure you want to delete all media files for ' + self.getTitle() + '?', '', [{ + 'text': 'Yes, really delete all files for '+self.getTitle(), + 'class': 'delete', + 'events': { + 'click': function(e){ + e.target.set('text', 'Deleting from cp and disk...'); + + self.del(true); + } + } + }, { + 'text': 'Cancel', + 'cancel': true + }]); + }, + + del: function(withFiles){ var self = this; var movie = $(self.movie); @@ -877,7 +928,8 @@ MA.Delete = new Class({ Api.request('media.delete', { 'data': { 'id': self.movie.get('_id'), - 'delete_from': self.movie.list.options.identifier + 'delete_from': self.movie.list.options.identifier, + 'with_files': !!withFiles }, 'onComplete': function(){ if(self.question) diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js index 457aaa9..e821c0a 100644 --- a/couchpotato/core/plugins/profile/static/profile.js +++ b/couchpotato/core/plugins/profile/static/profile.js @@ -140,7 +140,7 @@ var Profile = new Class({ }; Array.each(self.type_container.getElements('.type'), function(type){ - if(!type.hasClass('deleted') && type.getElement('select').get('value') != -1 && type.getElement('select').get('value') != "") + if(!type.hasClass('deleted') && type.getElement('select').get('value') != -1 && type.getElement('select').get('value') !== "") data.types.include({ 'quality': type.getElement('select').get('value'), 'finish': +type.getElement('input.finish[type=checkbox]').checked, @@ -258,7 +258,7 @@ Profile.Type = new Class({ self.create(); self.addEvent('change', function(){ - var has_quality = !(self.qualities.get('value') == '-1' || self.qualities.get('value') == ''); + var has_quality = !(self.qualities.get('value') == '-1' || self.qualities.get('value') === ''); self.el[!has_quality ? 'addClass' : 'removeClass']('is_empty'); self.el[has_quality && Quality.getQuality(self.qualities.get('value')).allow_3d ? 'addClass': 'removeClass']('allow_3d'); self.deleted = !has_quality; diff --git a/couchpotato/static/style/main.scss b/couchpotato/static/style/main.scss index 82e93b5..1cc0e85 100644 --- a/couchpotato/static/style/main.scss +++ b/couchpotato/static/style/main.scss @@ -745,7 +745,7 @@ input[type=text], textarea { .inner { width: 100%; - max-width: 500px; + max-width: 600px; } h3 {