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')) }},