diff --git a/couchpotato/core/plugins/category/__init__.py b/couchpotato/core/plugins/category/__init__.py new file mode 100644 index 0000000..6dc41df --- /dev/null +++ b/couchpotato/core/plugins/category/__init__.py @@ -0,0 +1,6 @@ +from .main import CategoryPlugin + +def start(): + return CategoryPlugin() + +config = [] diff --git a/couchpotato/core/plugins/category/main.py b/couchpotato/core/plugins/category/main.py new file mode 100644 index 0000000..a91930d --- /dev/null +++ b/couchpotato/core/plugins/category/main.py @@ -0,0 +1,123 @@ +from couchpotato import get_session +from couchpotato.api import addApiView +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin +from couchpotato.core.settings.model import Movie, Category + +log = CPLog(__name__) + + +class CategoryPlugin(Plugin): + + to_dict = {'destination': {}} + + def __init__(self): + addEvent('category.all', self.all) + + addApiView('category.save', self.save) + addApiView('category.save_order', self.saveOrder) + addApiView('category.delete', self.delete) + addApiView('category.list', self.allView, docs = { + 'desc': 'List all available categories', + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'list': array, categories +}"""} + }) + + def allView(self, **kwargs): + + return { + 'success': True, + 'list': self.all() + } + + def all(self): + + db = get_session() + categories = db.query(Category).all() + + temp = [] + for category in categories: + temp.append(category.to_dict(self.to_dict)) + + db.expire_all() + return temp + + def save(self, **kwargs): + + db = get_session() + + c = db.query(Category).filter_by(id = kwargs.get('id')).first() + if not c: + c = Category() + db.add(c) + + c.order = kwargs.get('order', c.order if c.order else 0) + c.label = toUnicode(kwargs.get('label')) + c.path = toUnicode(kwargs.get('path')) + c.ignored = toUnicode(kwargs.get('ignored')) + c.preferred = toUnicode(kwargs.get('preferred')) + c.required = toUnicode(kwargs.get('required')) + + db.commit() + + category_dict = c.to_dict(self.to_dict) + + return { + 'success': True, + 'category': category_dict + } + + def saveOrder(self, **kwargs): + + db = get_session() + + order = 0 + for category_id in kwargs.get('ids', []): + c = db.query(Category).filter_by(id = category_id).first() + c.order = order + + order += 1 + + db.commit() + + return { + 'success': True + } + + def delete(self, id = None, **kwargs): + + db = get_session() + + success = False + message = '' + try: + c = db.query(Category).filter_by(id = id).first() + db.delete(c) + db.commit() + + # Force defaults on all empty category movies + self.removeFromMovie(id) + + success = True + except Exception, e: + message = log.error('Failed deleting category: %s', e) + + db.expire_all() + return { + 'success': success, + 'message': message + } + + def removeFromMovie(self, category_id): + + db = get_session() + movies = db.query(Movie).filter(Movie.category_id == category_id).all() + + if len(movies) > 0: + for movie in movies: + movie.category_id = None + db.commit() diff --git a/couchpotato/core/plugins/category/static/category.css b/couchpotato/core/plugins/category/static/category.css new file mode 100644 index 0000000..fda8ca6 --- /dev/null +++ b/couchpotato/core/plugins/category/static/category.css @@ -0,0 +1,84 @@ +.add_new_category { + padding: 20px; + display: block; + text-align: center; + font-size: 20px; + border-bottom: 1px solid rgba(255,255,255,0.2); +} + +.category { + border-bottom: 1px solid rgba(255,255,255,0.2); + position: relative; +} + + .category > .delete { + position: absolute; + padding: 16px; + right: 0; + cursor: pointer; + opacity: 0.6; + color: #fd5353; + } + .category > .delete:hover { + opacity: 1; + } + + .category .ctrlHolder:hover { + background: none; + } + + .category .formHint { + width: 250px !important; + vertical-align: top !important; + margin: 0 !important; + padding-left: 3px !important; + opacity: 0.1; + } + .category:hover .formHint { + opacity: 1; + } + +#category_ordering { + +} + + #category_ordering ul { + float: left; + margin: 0; + width: 275px; + padding: 0; + } + + #category_ordering li { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + border-bottom: 1px solid rgba(255,255,255,0.2); + padding: 0 5px; + } + #category_ordering li:last-child { border: 0; } + + #category_ordering li .check { + margin: 2px 10px 0 0; + vertical-align: top; + } + + #category_ordering li > span { + display: inline-block; + height: 20px; + vertical-align: top; + line-height: 20px; + } + + #category_ordering li .handle { + background: url('../../static/profile_plugin/handle.png') center; + width: 20px; + float: right; + } + + #category_ordering .formHint { + clear: none; + float: right; + width: 250px; + margin: 0; + } \ No newline at end of file diff --git a/couchpotato/core/plugins/category/static/category.js b/couchpotato/core/plugins/category/static/category.js new file mode 100644 index 0000000..092a74c --- /dev/null +++ b/couchpotato/core/plugins/category/static/category.js @@ -0,0 +1,295 @@ +var CategoryListBase = new Class({ + + initialize: function(){ + var self = this; + + App.addEvent('load', self.addSettings.bind(self)); + }, + + setup: function(categories){ + var self = this; + + self.categories = [] + Array.each(categories, self.createCategory.bind(self)); + + }, + + addSettings: function(){ + var self = this; + + self.settings = App.getPage('Settings') + self.settings.addEvent('create', function(){ + var tab = self.settings.createSubTab('category', { + 'label': 'Categories', + 'name': 'category', + 'subtab_label': 'Category & filtering' + }, self.settings.tabs.searcher ,'searcher'); + + self.tab = tab.tab; + self.content = tab.content; + + self.createList(); + self.createOrdering(); + + }) + + }, + + createList: function(){ + var self = this; + + var count = self.categories.length; + + self.settings.createGroup({ + 'label': 'Categories', + 'description': 'Create your own categories.' + }).inject(self.content).adopt( + self.category_container = new Element('div.container'), + new Element('a.add_new_category', { + 'text': count > 0 ? 'Create another category' : 'Click here to create a category.', + 'events': { + 'click': function(){ + var category = self.createCategory(); + $(category).inject(self.category_container) + } + } + }) + ); + + // Add categories, that aren't part of the core (for editing) + Array.each(self.categories, function(category){ + $(category).inject(self.category_container) + }); + + }, + + createCategory: function(data){ + var self = this; + + var data = data || {'id': randomString()} + var category = new Category(data) + self.categories.include(category) + + return category; + }, + + createOrdering: function(){ + var self = this; + + var category_list; + var group = self.settings.createGroup({ + 'label': 'Category order' + }).adopt( + new Element('.ctrlHolder#category_ordering').adopt( + new Element('label[text=Order]'), + category_list = new Element('ul'), + new Element('p.formHint', { + 'html': 'Change the order the categories are in the dropdown list.
First one will be default.' + }) + ) + ).inject(self.content) + + Array.each(self.categories, function(category){ + new Element('li', {'data-id': category.data.id}).adopt( + new Element('span.category_label', { + 'text': category.data.label + }), + new Element('span.handle') + ).inject(category_list); + + }); + + // Sortable + self.category_sortable = new Sortables(category_list, { + 'revert': true, + 'handle': '', + 'opacity': 0.5, + 'onComplete': self.saveOrdering.bind(self) + }); + + }, + + saveOrdering: function(){ + var self = this; + + var ids = []; + + self.category_sortable.list.getElements('li').each(function(el, nr){ + ids.include(el.get('data-id')); + }); + + Api.request('category.save_order', { + 'data': { + 'ids': ids + } + }); + + } + +}) + +window.CategoryList = new CategoryListBase(); + +var Category = new Class({ + + data: {}, + + initialize: function(data){ + var self = this; + + self.data = data; + self.types = []; + + self.create(); + + self.el.addEvents({ + 'change:relay(select)': self.save.bind(self, 0), + 'keyup:relay(input[type=text])': self.save.bind(self, [300]) + }); + + }, + + create: function(){ + var self = this; + + var data = self.data; + + self.el = new Element('div.category').adopt( + self.delete_button = new Element('span.delete.icon2', { + 'events': { + 'click': self.del.bind(self) + } + }), + new Element('.category_label.ctrlHolder').adopt( + new Element('label', {'text':'Name'}), + new Element('input.inlay', { + 'type':'text', + 'value': data.label, + 'placeholder': 'Label' + }) + ), + new Element('.category_preferred.ctrlHolder').adopt( + new Element('label', {'text':'Preferred'}), + new Element('input.inlay', { + 'type':'text', + 'value': data.preferred, + 'placeholder': 'Ignored' + }) + ), + new Element('.category_required.ctrlHolder').adopt( + new Element('label', {'text':'Required'}), + new Element('input.inlay', { + 'type':'text', + 'value': data.required, + 'placeholder': 'Required' + }) + ), + new Element('.category_ignored.ctrlHolder').adopt( + new Element('label', {'text':'Ignored'}), + new Element('input.inlay', { + 'type':'text', + 'value': data.ignored, + 'placeholder': 'Ignored' + }) + ) + ); + + self.makeSortable() + + }, + + save: function(delay){ + var self = this; + + if(self.save_timer) clearTimeout(self.save_timer); + self.save_timer = (function(){ + + var data = self.getData(); + + Api.request('category.save', { + 'data': self.getData(), + 'useSpinner': true, + 'spinnerOptions': { + 'target': self.el + }, + 'onComplete': function(json){ + if(json.success){ + self.data = json.category; + } + } + }); + + }).delay(delay, self) + + }, + + getData: function(){ + var self = this; + + var data = { + 'id' : self.data.id, + 'label' : self.el.getElement('.category_label input').get('value'), + 'required' : self.el.getElement('.category_required input').get('value'), + 'preferred' : self.el.getElement('.category_preferred input').get('value'), + 'ignored' : self.el.getElement('.category_ignored input').get('value') + } + + return data + }, + + del: function(){ + var self = this; + + var label = self.el.getElement('.category_label input').get('value'); + var qObj = new Question('Are you sure you want to delete "'+label+'"?', '', [{ + 'text': 'Delete "'+label+'"', + 'class': 'delete', + 'events': { + 'click': function(e){ + (e).preventDefault(); + Api.request('category.delete', { + 'data': { + 'id': self.data.id + }, + 'useSpinner': true, + 'spinnerOptions': { + 'target': self.el + }, + 'onComplete': function(json){ + if(json.success) { + qObj.close(); + self.el.destroy(); + } else { + alert(json.message); + } + } + }); + } + } + }, { + 'text': 'Cancel', + 'cancel': true + }]); + + }, + + makeSortable: function(){ + var self = this; + + self.sortable = new Sortables(self.category_container, { + 'revert': true, + 'handle': '.handle', + 'opacity': 0.5, + 'onComplete': self.save.bind(self, 300) + }); + }, + + get: function(attr){ + return this.data[attr] + }, + + toElement: function(){ + return this.el + } + +}); \ No newline at end of file diff --git a/couchpotato/core/plugins/category/static/handle.png b/couchpotato/core/plugins/category/static/handle.png new file mode 100644 index 0000000..adff5b2 Binary files /dev/null and b/couchpotato/core/plugins/category/static/handle.png differ diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js index bd2ff2a..84e80f8 100644 --- a/couchpotato/core/plugins/quality/static/quality.js +++ b/couchpotato/core/plugins/quality/static/quality.js @@ -41,7 +41,8 @@ var QualityBase = new Class({ self.settings.addEvent('create', function(){ var tab = self.settings.createSubTab('profile', { 'label': 'Quality', - 'name': 'profile' + 'name': 'profile', + 'subtab_label': 'Qualities' }, self.settings.tabs.searcher ,'searcher'); self.tab = tab.tab; diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py index bed90eb..7e68a07 100644 --- a/couchpotato/core/plugins/searcher/__init__.py +++ b/couchpotato/core/plugins/searcher/__init__.py @@ -15,25 +15,6 @@ config = [{ 'description': 'Options for the searchers', 'options': [ { - 'name': 'preferred_words', - 'label': 'Preferred words', - 'default': '', - 'description': 'These words will give the releases a higher score.' - }, - { - 'name': 'required_words', - 'label': 'Required words', - 'default': '', - 'placeholder': 'Example: DTS, AC3 & English', - 'description': 'A release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"' - }, - { - 'name': 'ignored_words', - 'label': 'Ignored words', - 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs', - 'description': 'Ignores releases that match any of these sets. (Works like explained above)' - }, - { 'name': 'preferred_method', 'label': 'First search', 'description': 'Which of the methods do you prefer', @@ -52,6 +33,34 @@ config = [{ ], }, { 'tab': 'searcher', + 'subtab': 'category', + 'subtab_label': 'Categories', + 'name': 'filter', + 'label': 'Global filters', + 'description': 'Prefer, ignore & required words in release names', + 'options': [ + { + 'name': 'preferred_words', + 'label': 'Preferred', + 'default': '', + 'description': 'Words that give the releases a higher score.' + }, + { + 'name': 'required_words', + 'label': 'Required', + 'default': '', + 'placeholder': 'Example: DTS, AC3 & English', + 'description': 'Release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"' + }, + { + 'name': 'ignored_words', + 'label': 'Ignored', + 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs', + 'description': 'Ignores releases that match any of these sets. (Works like explained above)' + }, + ], + }, { + 'tab': 'searcher', 'name': 'cronjob', 'label': 'Cronjob', 'advanced': True, @@ -97,13 +106,14 @@ config = [{ 'groups': [ { 'tab': 'searcher', - 'name': 'nzb', + 'name': 'searcher', 'label': 'NZB', 'wizard': True, 'options': [ { 'name': 'retention', - 'default': 1000, + 'label': 'Usenet Retention', + 'default': 1500, 'type': 'int', 'unit': 'days' }, diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 25e0883..860c2e1 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -213,15 +213,21 @@ class Category(Entity): label = Field(Unicode(50)) order = Field(Integer, default = 0, index = True) - core = Field(Boolean, default = False) - hide = Field(Boolean, default = False) - - movie = OneToMany('Movie') - path = Field(Unicode(255)) required = Field(Unicode(255)) preferred = Field(Unicode(255)) ignored = Field(Unicode(255)) - + + movie = OneToMany('Movie') + destination = ManyToOne('Destination') + + +class Destination(Entity): + """""" + + path = Field(Unicode(255)) + + category = OneToMany('Category') + class ProfileType(Entity): """""" @@ -288,13 +294,6 @@ class Notification(Entity): data = Field(JsonType) -class Folder(Entity): - """Renamer destination folders.""" - - path = Field(Unicode(255)) - label = Field(Unicode(255)) - - class Properties(Entity): identifier = Field(String(50), index = True) diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index 0d7ebe7..ef97506 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -161,7 +161,7 @@ Page.Settings = new Class({ // Create subtab if(group.subtab){ if (!self.tabs[group.tab].subtabs[group.subtab]) - self.createSubTab(group.subtab, {}, self.tabs[group.tab], group.tab); + self.createSubTab(group.subtab, group, self.tabs[group.tab], group.tab); var content_container = self.tabs[group.tab].subtabs[group.subtab].content } @@ -243,7 +243,7 @@ Page.Settings = new Class({ if(!parent_tab.subtabs_el) parent_tab.subtabs_el = new Element('ul.subtabs').inject(parent_tab.tab); - var label = tab.label || (tab.name || tab_name.replace('_', ' ')).capitalize() + var label = tab.subtab_label || tab_name.replace('_', ' ').capitalize() var tab_el = new Element('li.t_'+tab_name).adopt( new Element('a', { 'href': App.createUrl(self.name+'/'+parent_tab_name+'/'+tab_name), diff --git a/couchpotato/templates/index.html b/couchpotato/templates/index.html index 5f16ef4..500dc78 100644 --- a/couchpotato/templates/index.html +++ b/couchpotato/templates/index.html @@ -70,6 +70,8 @@ File.Type.setup({{ json_encode(fireEvent('file.types', single = True)) }}); + CategoryList.setup({{ json_encode(fireEvent('category.all', single = True)) }}); + App.setup({ 'base_url': {{ json_encode(Env.get('web_base')) }}, 'args': {{ json_encode(Env.get('args')) }},