diff --git a/couchpotato/core/helpers/encoding.py b/couchpotato/core/helpers/encoding.py
index 2a493b2..1f1fe16 100644
--- a/couchpotato/core/helpers/encoding.py
+++ b/couchpotato/core/helpers/encoding.py
@@ -13,11 +13,10 @@ def toSafeString(original):
def simplifyString(original):
- string = toSafeString(original)
+ string = toSafeString(' '.join(re.split('\W+', original.lower())))
split = re.split('\W+', string.lower())
return toUnicode(' '.join(split))
-
def toUnicode(original, *args):
try:
if type(original) is unicode:
diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py
index b85861e..8ed09d0 100644
--- a/couchpotato/core/plugins/file/main.py
+++ b/couchpotato/core/plugins/file/main.py
@@ -8,7 +8,9 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env
from flask.helpers import send_from_directory
+from sqlalchemy.sql.expression import or_
import os.path
+import traceback
import urllib2
log = CPLog(__name__)
@@ -51,15 +53,15 @@ class FileManager(Plugin):
return dest
- except Exception, e:
- log.error('Unable to download file "%s": %s' % (url, e))
+ except Exception:
+ log.error('Unable to download file "%s": %s' % (url, traceback.format_exc()))
return False
def add(self, path = '', part = 1, type = (), available = 1, properties = {}):
db = get_session()
- f = db.query(File).filter_by(path = toUnicode(path)).first()
+ f = db.query(File).filter(or_(File.path == toUnicode(path), File.path == path)).first()
if not f:
f = File()
db.add(f)
@@ -78,7 +80,6 @@ class FileManager(Plugin):
def getType(self, type):
db = get_session()
-
type, identifier = type
ft = db.query(FileType).filter_by(identifier = identifier).first()
diff --git a/couchpotato/core/plugins/file/static/file.js b/couchpotato/core/plugins/file/static/file.js
index 48ef5be..68b1996 100644
--- a/couchpotato/core/plugins/file/static/file.js
+++ b/couchpotato/core/plugins/file/static/file.js
@@ -2,6 +2,11 @@ var File = new Class({
initialize: function(file){
var self = this;
+
+ if(!file){
+ self.el = new Element('div');
+ return
+ }
self.data = file;
self.type = File.Type.get(file.type_id);
diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py
index 6455972..28b9333 100644
--- a/couchpotato/core/plugins/library/main.py
+++ b/couchpotato/core/plugins/library/main.py
@@ -41,9 +41,7 @@ class LibraryPlugin(Plugin):
if update_after:
fireEventAsync('library.update', identifier = l.identifier, default_title = attrs.get('title', ''))
- library_dict = l.to_dict()
-
- return library_dict
+ return l.to_dict({'titles': {}, 'files':{}})
def update(self, identifier, default_title = '', force = False):
@@ -51,22 +49,27 @@ class LibraryPlugin(Plugin):
library = db.query(Library).filter_by(identifier = identifier).first()
done_status = fireEvent('status.get', 'done', single = True)
+ library_dict = library.to_dict({'titles': {}, 'files':{}})
+
if library.status_id == done_status.get('id') and not force:
- return
+ return library_dict
info = fireEvent('provider.movie.info', merge = True, identifier = identifier)
if not info or len(info) == 0:
log.error('Could not update, no movie info to work with: %s' % identifier)
- return
+ return library_dict
# Main info
library.plot = info.get('plot', '')
library.tagline = info.get('tagline', '')
library.year = info.get('year', 0)
library.status_id = done_status.get('id')
+ db.commit()
# Titles
[db.delete(title) for title in library.titles]
+ db.commit()
+
titles = info.get('titles', [])
log.debug('Adding titles: %s' % titles)
@@ -83,6 +86,9 @@ class LibraryPlugin(Plugin):
images = info.get('images', [])
for type in images:
for image in images[type]:
+ if not isinstance(image, str):
+ continue
+
file_path = fireEvent('file.download', url = image, single = True)
file = fireEvent('file.add', path = file_path, type = ('image', type[:-1]), single = True)
try:
@@ -94,3 +100,5 @@ class LibraryPlugin(Plugin):
#log.debug('Failed to attach to library: %s' % traceback.format_exc())
fireEvent('library.update.after')
+
+ return library.to_dict({'titles': {}, 'files':{}})
diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
index 8913abd..646c584 100644
--- a/couchpotato/core/plugins/movie/main.py
+++ b/couchpotato/core/plugins/movie/main.py
@@ -22,6 +22,9 @@ class MoviePlugin(Plugin):
path = self.registerStatic(__file__)
fireEvent('register_script', path + 'search.js')
fireEvent('register_style', path + 'search.css')
+ fireEvent('register_script', path + 'movie.js')
+ fireEvent('register_style', path + 'movie.css')
+ fireEvent('register_script', path + 'list.js')
def list(self):
@@ -35,7 +38,7 @@ class MoviePlugin(Plugin):
movies = []
for movie in results:
temp = movie.to_dict(deep = {
- 'releases': {'status': {}, 'quality': {}},
+ 'releases': {'status': {}, 'quality': {}, 'files':{}},
'library': {'titles': {}, 'files':{}},
'files': {}
})
@@ -62,7 +65,13 @@ class MoviePlugin(Plugin):
if movie:
#addEvent('library.update.after', )
- fireEventAsync('library.update', library = movie.library, default_title = default_title)
+ fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
+ fireEventAsync('searcher.single', movie.to_dict(deep = {
+ 'profile': {'types': {'quality': {}}},
+ 'releases': {'status': {}, 'quality': {}},
+ 'library': {'titles': {}, 'files':{}},
+ 'files': {}
+ }))
return jsonified({
'success': True,
@@ -98,7 +107,7 @@ class MoviePlugin(Plugin):
library = fireEvent('library.add', single = True, attrs = params)
status = fireEvent('status.add', 'active', single = True)
- m = db.query(Movie).filter_by(library_id = library.id).first()
+ m = db.query(Movie).filter_by(library_id = library.get('id')).first()
if not m:
m = Movie(
library_id = library.get('id'),
@@ -108,7 +117,7 @@ class MoviePlugin(Plugin):
m.status_id = status.get('id')
db.commit()
-
+
movie_dict = m.to_dict(deep = {
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}}
@@ -121,7 +130,22 @@ class MoviePlugin(Plugin):
})
def edit(self):
- pass
+
+ params = getParams()
+ db = get_session();
+
+ m = db.query(Movie).filter_by(id = params.get('id')).first()
+ m.profile_id = params.get('profile_id')
+
+ # Default title
+ for title in m.library.titles:
+ title.default = params.get('default_title').lower() == title.title.lower()
+
+ db.commit()
+
+ return jsonified({
+ 'success': True,
+ })
def delete(self):
diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
new file mode 100644
index 0000000..2facc39
--- /dev/null
+++ b/couchpotato/core/plugins/movie/static/list.js
@@ -0,0 +1,95 @@
+var MovieList = new Class({
+
+ Implements: [Options],
+
+ options: {
+ navigation: true
+ },
+
+ movies: [],
+
+ initialize: function(options){
+ var self = this;
+ self.setOptions(options);
+
+ self.el = new Element('div.movies');
+ self.getMovies();
+ },
+
+ create: function(){
+ var self = this;
+
+ // Create the alphabet nav
+ if(self.options.navigation)
+ self.createNavigation();
+
+ Object.each(self.movies, function(info){
+ var m = new Movie(self, {
+ 'actions': self.options.actions
+ }, info);
+ $(m).inject(self.el);
+ m.fireEvent('injected');
+ });
+
+ self.el.addEvents({
+ 'mouseenter:relay(.movie)': function(e, el){
+ el.addClass('hover')
+ },
+ 'mouseleave:relay(.movie)': function(e, el){
+ el.removeClass('hover')
+ }
+ });
+ },
+
+ createNavigation: function(){
+ var self = this;
+ var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ var selected = 'Z';
+
+ self.navigation = new Element('div.alph_nav').adopt(
+ self.alpha = new Element('ul.inlay'),
+ self.input = new Element('input.inlay'),
+ self.view = new Element('ul.inlay').adopt(
+ new Element('li.list'),
+ new Element('li.thumbnails'),
+ new Element('li.text')
+ )
+ ).inject(this.el, 'top')
+
+ chars.split('').each(function(c){
+ new Element('li', {
+ 'text': c,
+ 'class': c == selected ? 'selected' : ''
+ }).inject(self.alpha)
+ })
+
+ },
+
+ getMovies: function(status, onComplete){
+ var self = this
+
+ if(self.movies.length == 0)
+ Api.request('movie.list', {
+ 'data': {
+ 'status': self.options.status
+ },
+ 'onComplete': function(json){
+ self.store(json.movies);
+ self.create();
+ }
+ })
+ else
+ self.list()
+ },
+
+ store: function(movies){
+ var self = this;
+
+ self.movies = movies;
+ },
+
+ toElement: function(){
+ return this.el;
+ }
+
+});
\ No newline at end of file
diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css
new file mode 100644
index 0000000..70819a7
--- /dev/null
+++ b/couchpotato/core/plugins/movie/static/movie.css
@@ -0,0 +1,166 @@
+/* @override http://localhost:5000/static/movie_plugin/movie.css */
+
+.movies {
+ padding: 20px 0;
+}
+
+ .movies .movie {
+ overflow: hidden;
+ position: relative;
+
+ border-radius: 4px;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ margin: 10px 0;
+ }
+ .movies .movie:hover {
+ border-color: #ddd #fff #fff #ddd;
+ }
+ .movies .movie:hover .data {
+ }
+ .movies .data {
+ padding: 2%;
+ position: absolute;
+ width: 96.1%;
+ top: 0;
+ left: 0;
+
+ border-radius: 4px;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ }
+ .movies .data:after {
+ content: "";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+ }
+ .movies .data .poster, .options .poster {
+ overflow: hidden;
+ float: left;
+ max-width: 10%;
+ margin: 0 2% 0 0;
+ border-radius:3px;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ box-shadow: 0 0 10px rgba(0,0,0,0.35);
+ -moz-box-shadow: 0 0 10px rgba(0,0,0,0.35);
+ -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.35);
+ line-height: 0;
+ }
+
+ .movies .info {
+ float: right;
+ width: 88%;
+ }
+
+ .movies .info .title {
+ font-size: 30px;
+ font-weight: bold;
+ margin-bottom: 10px;
+ float: left;
+ width: 80%;
+ }
+
+ .movies .info .year {
+ font-size: 30px;
+ margin-bottom: 10px;
+ float: right;
+ color: #bbb;
+ width: 10%;
+ text-align: right;
+ }
+
+ .movies .info .rating {
+ font-size: 30px;
+ margin-bottom: 10px;
+ color: #444;
+ float: left;
+ width: 5%;
+ padding: 0 0 0 3%;
+ background: url('../images/rating.png') no-repeat left center;
+ }
+
+ .movies .info .description {
+ clear: both;
+ width: 95%;
+ }
+ .movies .data .actions {
+ position: absolute;
+ right: 15px;
+ bottom: 15px;
+ line-height: 0;
+ }
+ .movies .data:hover .action { opacity: 0.6; }
+ .movies .data:hover .action:hover { opacity: 1; }
+
+ .movies .data .action {
+ background: no-repeat center;
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ padding: 3px;
+ opacity: 0;
+ }
+ .movies .data .action.refresh { background-image: url('../images/reload.png'); }
+ .movies .data .action.delete { background-image: url('../images/delete.png'); }
+ .movies .data .action.edit { background-image: url('../images/edit.png'); }
+ .movies .data .action.imdb { background-image: url('../images/imdb.png'); }
+
+ .movies .delete_container {
+ clear: both;
+ text-align: center;
+ font-size: 20px;
+ position: relative;
+ }
+ .movies .delete_container .cancel {
+ }
+ .movies .delete_container .or {
+ padding: 10px;
+ }
+ .movies .delete_container .delete {
+ background-color: #ff321c;
+ font-weight: normal;
+ }
+ .movies .delete_container .delete:hover {
+ color: #fff;
+ background-color: #d32917;
+ }
+
+ .movies .options .form {
+ margin-top: -2%;
+ float: left;
+ font-size: 20px;
+ }
+
+ .movies .options .form select {
+ margin-right: 20px;
+ }
+
+ .movies .options {
+ padding: 2%;
+ }
+
+.movies .alph_nav ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+
+ display: inline-block;
+}
+
+ .movies .alph_nav li {
+ display: inline-block;
+ vertical-align: top;
+ width: 24px;
+ height: 24px;
+ line-height: 26px;
+ text-align: center;
+ cursor: pointer;
+ margin: 0 -1px 0 0;
+ }
+
+ .movies .alph_nav li:hover, .movies .alph_nav li.onlay {
+ font-weight: bold;
+ }
\ No newline at end of file
diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js
new file mode 100644
index 0000000..cabe5a4
--- /dev/null
+++ b/couchpotato/core/plugins/movie/static/movie.js
@@ -0,0 +1,176 @@
+var Movie = new Class({
+
+ Extends: BlockBase,
+
+ action: {},
+
+ initialize: function(self, options, data){
+ var self = this;
+
+ self.data = data;
+
+ self.profile = Quality.getProfile(data.profile_id);
+ self.parent(self, options);
+ self.addEvent('injected', self.afterInject.bind(self))
+ },
+
+ create: function(){
+ var self = this;
+
+ self.el = new Element('div.movie.inlay').adopt(
+ self.data_container = new Element('div.data.inlay.light', {
+ 'tween': {
+ duration: 400,
+ transition: 'quint:in:out'
+ }
+ }).adopt(
+ self.thumbnail = File.Select.single('poster', self.data.library.files),
+ self.info_container = new Element('div.info').adopt(
+ self.title = new Element('div.title', {
+ 'text': self.getTitle()
+ }),
+ self.year = new Element('div.year', {
+ 'text': self.data.library.year || 'Unknown'
+ }),
+ self.rating = new Element('div.rating', {
+ 'text': self.data.library.rating
+ }),
+ self.description = new Element('div.description', {
+ 'text': self.data.library.plot
+ }),
+ self.quality = new Element('div.quality', {
+ 'text': self.profile ? self.profile.get('label') : ''
+ })
+ ),
+ self.actions = new Element('div.actions')
+ )
+ );
+
+ Object.each(self.options.actions, function(action, key){
+ self.actions.adopt(
+ self.action[key.toLowerCase()] = new self.options.actions[key](self)
+ )
+ });
+
+ if(!self.data.library.rating)
+ self.rating.hide();
+
+ },
+
+ afterInject: function(){
+ var self = this;
+
+ var height = self.getHeight();
+ self.el.setStyle('height', height);
+ },
+
+ getTitle: function(){
+ var self = this;
+
+ var titles = self.data.library.titles;
+
+ var title = titles.filter(function(title){
+ return title['default']
+ }).pop()
+
+ if(title)
+ return title.title
+ else if(titles.length > 0)
+ return titles[0].title
+
+ return 'Unknown movie'
+ },
+
+ slide: function(direction){
+ var self = this;
+
+ if(direction == 'in'){
+ self.el.addEvent('outerClick', self.slide.bind(self, 'out'))
+ self.data_container.tween('left', 0, self.getWidth());
+ }
+ else {
+ self.el.removeEvents('outerClick')
+ self.data_container.tween('left', self.getWidth(), 0);
+ }
+ },
+
+ getHeight: function(){
+ var self = this;
+
+ if(!self.height)
+ self.height = self.data_container.getCoordinates().height;
+
+ return self.height;
+ },
+
+ getWidth: function(){
+ var self = this;
+
+ if(!self.width)
+ self.width = self.data_container.getCoordinates().width;
+
+ return self.width;
+ },
+
+ get: function(attr){
+ return this.data[attr] || this.data.library[attr]
+ }
+
+});
+
+var MovieAction = new Class({
+
+ class_name: 'action',
+
+ initialize: function(movie){
+ var self = this;
+ self.movie = movie;
+
+ self.create();
+ self.el.addClass(self.class_name)
+ },
+
+ create: function(){},
+
+ disable: function(){
+ this.el.addClass('disable')
+ },
+
+ enable: function(){
+ this.el.removeClass('disable')
+ },
+
+ toElement: function(){
+ return this.el
+ }
+
+});
+
+var IMDBAction = new Class({
+
+ Extends: MovieAction,
+ id: null,
+
+ create: function(){
+ var self = this;
+
+ self.id = self.movie.get('identifier');
+
+ self.el = new Element('a.imdb', {
+ 'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
+ 'events': {
+ 'click': self.gotoIMDB.bind(self)
+ }
+ });
+
+ if(!self.id) self.disable();
+ },
+
+ gotoIMDB: function(e){
+ var self = this;
+ (e).stop();
+
+ window.open('http://www.imdb.com/title/'+self.id+'/');
+ }
+
+})
\ No newline at end of file
diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css
index 0d9551e..e94a449 100644
--- a/couchpotato/core/plugins/movie/static/search.css
+++ b/couchpotato/core/plugins/movie/static/search.css
@@ -1,4 +1,4 @@
-/* @override http://localhost:5000/static/style/plugin/movie_add.css */
+/* @override http://localhost:5000/static/movie_plugin/search.css */
.search_form {
display: inline-block;
@@ -7,52 +7,46 @@
.search_form input {
padding-right: 25px;
- border: 1px solid #aaa;
padding: 4px;
margin: 0;
font-size: 14px;
width: 90%;
-
- border-radius: 3px;
- -webkit-border-radius: 3px;
- -moz-border-radius: 3px;
}
.search_form .input a {
- width: 12px;
+ width: 17px;
height: 20px;
display: inline-block;
margin: 0 0 -5px -20px;
top: 4px;
right: 5px;
- background: url('../../images/close_button.png') 0 center no-repeat;
+ background: url('../images/checks.png') right -36px no-repeat;
cursor: pointer;
}
- .search_form .input a:hover { background-position: -12px center; }
.search_form .results_container {
padding: 10px 0;
position: absolute;
- background: #fff;
- margin: 11px 0 0 -243px;
+ background: #5c697b;
+ margin: 6px 0 0 -246px;
width: 470px;
min-height: 140px;
-
- box-shadow: 0 0 30px rgba(0,0,0,0.2);
- -moz-box-shadow: 0 0 30px rgba(0,0,0,0.2);
- -webkit-box-shadow: 0 0 30px rgba(0,0,0,0.2);
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
+
+ box-shadow: 0 0 50px rgba(0,0,0,0.55);
+ -moz-box-shadow: 0 0 50px rgba(0,0,0,0.55);
+ -webkit-box-shadow: 0 0 50px rgba(0,0,0,0.55);
}
.search_form .spinner {
- background: #fff url('../../images/spinner.gif') no-repeat center 70px;
+ background: rgba(0,0,0,0.8) url('../images/spinner.gif') no-repeat center 70px;
}
.search_form .pointer {
border-right: 10px solid transparent;
border-left: 10px solid transparent;
- border-bottom: 10px solid #fff;
+ border-bottom: 10px solid #5c697b;
display: block;
position: absolute;
width: 0px;
@@ -62,16 +56,24 @@
.search_form .results .movie {
overflow: hidden;
- background: #666;
min-height: 140px;
}
.search_form .results .movie .options {
- height: 140px;
+ height: 139px;
+ border: 1px solid transparent;
+ border-width: 1px 0;
+ border-radius: 0;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
+ -moz-box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
+ -webkit-box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
}
.search_form .results .movie .options > div {
padding: 0 15px;
+ border: 0;
}
.search_form .results .movie .options .thumbnail {
@@ -105,11 +107,17 @@
position: relative;
min-height: 100px;
top: 0;
- margin: -140px 0 0 0;
- background: #fff;
+ margin: -143px 0 0 0;
min-height: 140px;
+ background: #5c697b;
+
+ border-bottom: 1px solid #333;
+ border-top: 1px solid rgba(255,255,255, 0.15);
}
+ .search_form .results .movie:first-child .data { border-top: 0; }
+ .search_form .results .movie:last-child .data { border-bottom: 0; }
+
.search_form .results .movie .thumbnail {
width: 17%;
display: inline-block;
@@ -129,7 +137,6 @@
display: inline-block;
vertical-align: top;
padding: 15px 0;
- background: #fff;
}
.search_form .results .movie .add +.info {
margin-left: 20%;
diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js
index ce3a29f..c3cfc46 100644
--- a/couchpotato/core/plugins/movie/static/search.js
+++ b/couchpotato/core/plugins/movie/static/search.js
@@ -9,7 +9,7 @@ Block.Search = new Class({
self.el = new Element('div.search_form').adopt(
new Element('div.input').adopt(
- self.input = new Element('input', {
+ self.input = new Element('input.inlay', {
'events': {
'keyup': self.keyup.bind(self),
'focus': self.hideResults.bind(self, false)
@@ -164,7 +164,7 @@ Block.Search.Item = new Class({
self.el = new Element('div.movie', {
'id': info.imdb
}).adopt(
- self.options = new Element('div.options'),
+ self.options = new Element('div.options.inlay'),
self.data_container = new Element('div.data', {
'tween': {
duration: 400,
diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py
index 48fb67a..7f8d9eb 100644
--- a/couchpotato/core/plugins/profile/main.py
+++ b/couchpotato/core/plugins/profile/main.py
@@ -1,6 +1,6 @@
from couchpotato import get_session
from couchpotato.api import addApiView
-from couchpotato.core.event import addEvent
+from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.request import jsonified, getParams, getParam
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
@@ -17,6 +17,10 @@ class ProfilePlugin(Plugin):
addApiView('profile.save', self.save)
addApiView('profile.delete', self.delete)
+ path = self.registerStatic(__file__)
+ fireEvent('register_script', path + 'profile.js')
+ fireEvent('register_style', path + 'profile.css')
+
def all(self):
db = get_session()
@@ -59,7 +63,7 @@ class ProfilePlugin(Plugin):
order += 1
db.commit()
-
+
profile_dict = p.to_dict(deep = {'types': {}})
return jsonified({
diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css
new file mode 100644
index 0000000..ab6ef98
--- /dev/null
+++ b/couchpotato/core/plugins/profile/static/profile.css
@@ -0,0 +1,18 @@
+.profile > .delete {
+ background-position: center;
+ height: 20px;
+ width: 20px;
+}
+
+.profile .types .type .handle {
+ background: url('../../images/handle.png') center;
+ display: inline-block;
+ height: 20px;
+ width: 20px;
+}
+
+.profile .types .type .delete {
+ background-position: center;
+ height: 20px;
+ width: 20px;
+}
\ No newline at end of file
diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js
new file mode 100644
index 0000000..080f971
--- /dev/null
+++ b/couchpotato/core/plugins/profile/static/profile.js
@@ -0,0 +1,259 @@
+var Profile = new Class({
+
+ data: {},
+ types: [],
+
+ initialize: function(data){
+ var self = this;
+
+ self.data = data;
+ self.types = [];
+
+ self.create();
+
+ self.el.addEvents({
+ 'change:relay(select, input[type=checkbox])': 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.profile').adopt(
+ self.header = new Element('h4', {'text': data.label}),
+ new Element('span.delete.icon', {
+ 'events': {
+ 'click': self.del.bind(self)
+ }
+ }),
+ new Element('div', {
+ 'class': 'ctrlHolder'
+ }).adopt(
+ new Element('label', {'text':'Name'}),
+ new Element('input.label.textInput.large', {
+ 'type':'text',
+ 'value': data.label,
+ 'events': {
+ 'keyup': function(){
+ self.header.set('text', this.get('value'))
+ }
+ }
+ })
+ ),
+ new Element('div.ctrlHolder').adopt(
+ new Element('label', {'text':'Wait'}),
+ new Element('input.wait_for.textInput.xsmall', {
+ 'type':'text',
+ 'value': data.types && data.types.length > 0 ? data.types[0].wait_for : 0
+ }),
+ new Element('span', {'text':' day(s) for better quality.'})
+ ),
+ new Element('div.ctrlHolder').adopt(
+ new Element('label', {'text': 'Qualities'}),
+ new Element('div.head').adopt(
+ new Element('span.quality_type', {'text': 'Search for'}),
+ new Element('span.finish', {'html': 'Finish'})
+ ),
+ self.type_container = new Element('ol.types'),
+ new Element('a.addType', {
+ 'text': 'Add another quality to search for.',
+ 'href': '#',
+ 'events': {
+ 'click': self.addType.bind(self)
+ }
+ })
+ )
+ );
+
+ self.makeSortable()
+
+ if(data.types)
+ Object.each(data.types, self.addType.bind(self))
+ },
+
+ save: function(delay){
+ var self = this;
+
+ if(self.save_timer) clearTimeout(self.save_timer);
+ self.save_timer = (function(){
+
+ var data = self.getData();
+ if(data.types.length < 2) return;
+
+ Api.request('profile.save', {
+ 'data': self.getData(),
+ 'useSpinner': true,
+ 'spinnerOptions': {
+ 'target': self.el
+ },
+ 'onComplete': function(json){
+ if(json.success){
+ self.data = json.profile
+ }
+ }
+ });
+ }).delay(delay, self)
+
+ },
+
+ getData: function(){
+ var self = this;
+
+ var data = {
+ 'id' : self.data.id,
+ 'label' : self.el.getElement('.label').get('value'),
+ 'wait_for' : self.el.getElement('.wait_for').get('value'),
+ 'types': []
+ }
+
+ Array.each(self.type_container.getElements('.type'), function(type){
+ if(!type.hasClass('deleted'))
+ data.types.include({
+ 'quality_id': type.getElement('select').get('value'),
+ 'finish': +type.getElement('input[type=checkbox]').checked
+ });
+ })
+
+ return data
+ },
+
+ addType: function(data){
+ var self = this;
+
+ var t = new Profile.Type(data);
+ $(t).inject(self.type_container);
+ self.sortable.addItems($(t));
+
+ self.types.include(t);
+
+ },
+
+ del: function(){
+ var self = this;
+
+ if(!confirm('Are you sure you want to delete this profile?')) return
+
+ Api.request('profile.delete', {
+ 'data': {
+ 'id': self.data.id
+ },
+ 'useSpinner': true,
+ 'spinnerOptions': {
+ 'target': self.el
+ },
+ 'onComplete': function(json){
+ if(json.success)
+ self.el.destroy();
+ else
+ alert(json.message)
+ }
+ });
+ },
+
+ makeSortable: function(){
+ var self = this;
+
+ self.sortable = new Sortables(self.type_container, {
+ 'revert': true,
+ //'clone': true,
+ 'handle': '.handle',
+ 'opacity': 0.5,
+ 'onComplete': self.save.bind(self, 300)
+ });
+ },
+
+ get: function(attr){
+ return this.data[attr]
+ },
+
+ isCore: function(){
+ return this.data.core
+ },
+
+ toElement: function(){
+ return this.el
+ }
+
+});
+
+Profile.Type = Class({
+
+ deleted: false,
+
+ initialize: function(data){
+ var self = this;
+
+ self.data = data;
+ self.create();
+
+ },
+
+ create: function(){
+ var self = this;
+ var data = self.data;
+
+ self.el = new Element('li.type').adopt(
+ new Element('span.quality_type').adopt(
+ self.fillQualities()
+ ),
+ new Element('span.finish').adopt(
+ self.finish = new Element('input', {
+ 'type':'checkbox',
+ 'class':'finish',
+ 'checked': data.finish
+ })
+ ),
+ new Element('span.delete.icon', {
+ 'events': {
+ 'click': self.del.bind(self)
+ }
+ }),
+ new Element('span.handle')
+ )
+
+ },
+
+ fillQualities: function(){
+ var self = this;
+
+ self.qualities = new Element('select');
+
+ Object.each(Quality.qualities, function(q){
+ new Element('option', {
+ 'text': q.label,
+ 'value': q.id
+ }).inject(self.qualities)
+ });
+
+ self.qualities.set('value', self.data.quality_id);
+
+ return self.qualities;
+
+ },
+
+ getData: function(){
+ var self = this;
+
+ return {
+ 'quality_id': self.qualities.get('value'),
+ 'finish': +self.finish.checked
+ }
+ },
+
+ del: function(){
+ var self = this;
+
+ self.el.addClass('deleted');
+ self.el.hide();
+ self.deleted = true;
+ },
+
+ toElement: function(){
+ return this.el;
+ }
+
+})
\ No newline at end of file
diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py
index e81369e..b537784 100644
--- a/couchpotato/core/plugins/quality/main.py
+++ b/couchpotato/core/plugins/quality/main.py
@@ -4,6 +4,8 @@ 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 Quality, Profile, ProfileType
+import os.path
+import re
log = CPLog(__name__)
@@ -11,10 +13,10 @@ log = CPLog(__name__)
class QualityPlugin(Plugin):
qualities = [
- {'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['1080p', 'bd25'], 'allow': [], 'ext':[], 'tags': ['x264', 'h264', 'blu ray']},
- {'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
- {'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
- {'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['mkv', 'avi']},
+ {'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['1080p', 'bd25'], 'allow': [], 'ext':[], 'tags': ['x264', 'h264', 'blu ray']},
+ {'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
+ {'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
+ {'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['dvdscr'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
@@ -28,6 +30,7 @@ class QualityPlugin(Plugin):
def __init__(self):
addEvent('quality.all', self.all)
addEvent('quality.single', self.single)
+ addEvent('quality.guess', self.guess)
addEvent('app.load', self.fill)
path = self.registerStatic(__file__)
@@ -50,9 +53,11 @@ class QualityPlugin(Plugin):
def single(self, identifier = ''):
db = get_session()
+ quality_dict = {}
quality = db.query(Quality).filter_by(identifier = identifier).first()
- quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
+ if quality:
+ quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
return quality_dict
@@ -110,3 +115,41 @@ class QualityPlugin(Plugin):
db.commit()
return True
+
+ def guess(self, files, extra = {}):
+ found = False
+
+ for file in files:
+ size = (os.path.getsize(file) / 1024 / 1024)
+ words = re.split('\W+', file.lower())
+ for quality in self.all():
+ correctSize = False
+
+ if size >= quality['size_min'] and size <= quality['size_max']:
+ correctSize = True
+
+ # Check tags
+ if type in words:
+ found = True
+
+ for alt in quality.get('alternative'):
+ if alt in words:
+ found = True
+
+ for tag in quality.get('tags', []):
+ if tag in words:
+ found = True
+
+ # Check extension + filesize
+ for ext in quality.get('ext'):
+ if ext in words and correctSize:
+ found = True
+
+ # Last check on resolution only
+ if quality.get('width', 480) == extra.get('resolution_width', 0):
+ found = True
+
+ if found:
+ return quality
+
+ return ''
diff --git a/couchpotato/core/plugins/quality/static/quality.css b/couchpotato/core/plugins/quality/static/quality.css
index c4abc1d..e69de29 100644
--- a/couchpotato/core/plugins/quality/static/quality.css
+++ b/couchpotato/core/plugins/quality/static/quality.css
@@ -1,21 +0,0 @@
-/* @override http://localhost:5000/static/style/plugin/quality.css */
-
-
-.profile > .delete {
- background-position: center;
- height: 20px;
- width: 20px;
-}
-
-.profile .types .type .handle {
- background: url('../../images/handle.png') center;
- display: inline-block;
- height: 20px;
- width: 20px;
-}
-
-.profile .types .type .delete {
- background-position: center;
- height: 20px;
- width: 20px;
-}
\ No newline at end of file
diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js
index 9dd82d4..1d9382f 100644
--- a/couchpotato/core/plugins/quality/static/quality.js
+++ b/couchpotato/core/plugins/quality/static/quality.js
@@ -33,7 +33,6 @@ var QualityBase = new Class({
self.content = tab.content;
self.createProfiles();
- self.createOrdering();
self.createSizes();
})
@@ -84,290 +83,35 @@ var QualityBase = new Class({
},
/**
- * Ordering
- */
- createOrdering: function(){
- var self = this;
-
- self.settings.createGroup({
- 'label': 'Order',
- 'description': 'Discriptions'
- }).inject(self.content)
-
- },
-
- /**
* Sizes
*/
createSizes: function(){
var self = this;
- self.settings.createGroup({
+ var group = self.settings.createGroup({
'label': 'Sizes',
'description': 'Discriptions',
'advanced': true
}).inject(self.content)
- }
-
-});
-window.Quality = new QualityBase();
-
-var Profile = new Class({
-
- data: {},
- types: [],
-
- initialize: function(data){
- var self = this;
-
- self.data = data;
- self.types = [];
-
- self.create();
-
- self.el.addEvents({
- 'change:relay(select, input[type=checkbox])': 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.profile').adopt(
- self.header = new Element('h4', {'text': data.label}),
- new Element('span.delete.icon', {
- 'events': {
- 'click': self.del.bind(self)
- }
- }),
- new Element('div', {
- 'class': 'ctrlHolder'
- }).adopt(
- new Element('label', {'text':'Name'}),
- new Element('input.label.textInput.large', {
- 'type':'text',
- 'value': data.label,
- 'events': {
- 'keyup': function(){
- self.header.set('text', this.get('value'))
- }
- }
- })
- ),
- new Element('div.ctrlHolder').adopt(
- new Element('label', {'text':'Wait'}),
- new Element('input.wait_for.textInput.xsmall', {
- 'type':'text',
- 'value': data.types && data.types.length > 0 ? data.types[0].wait_for : 0
- }),
- new Element('span', {'text':' day(s) for better quality.'})
- ),
- new Element('div.ctrlHolder').adopt(
- new Element('label', {'text': 'Qualities'}),
- new Element('div.head').adopt(
- new Element('span.quality_type', {'text': 'Search for'}),
- new Element('span.finish', {'html': 'Finish'})
- ),
- self.type_container = new Element('ol.types'),
- new Element('a.addType', {
- 'text': 'Add another quality to search for.',
- 'href': '#',
- 'events': {
- 'click': self.addType.bind(self)
- }
- })
- )
- );
-
- self.makeSortable()
-
- if(data.types)
- Object.each(data.types, self.addType.bind(self))
- },
-
- save: function(delay){
- var self = this;
-
- if(self.save_timer) clearTimeout(self.save_timer);
- self.save_timer = (function(){
-
- var data = self.getData();
- if(data.types.length < 2) return;
-
- Api.request('profile.save', {
- 'data': self.getData(),
- 'useSpinner': true,
- 'spinnerOptions': {
- 'target': self.el
- },
- 'onComplete': function(json){
- if(json.success){
- self.data = json.profile
- }
- }
- });
- }).delay(delay, self)
-
- },
-
- getData: function(){
- var self = this;
-
- var data = {
- 'id' : self.data.id,
- 'label' : self.el.getElement('.label').get('value'),
- 'wait_for' : self.el.getElement('.wait_for').get('value'),
- 'types': []
- }
-
- Array.each(self.type_container.getElements('.type'), function(type){
- if(!type.hasClass('deleted'))
- data.types.include({
- 'quality_id': type.getElement('select').get('value'),
- 'finish': +type.getElement('input[type=checkbox]').checked
- });
+
+ new Element('div.item.header').adopt(
+ new Element('span.label', {'text': 'Quality'}),
+ new Element('span.min', {'text': 'Min'}),
+ new Element('span.max', {'text': 'Max'})
+ ).inject(group)
+
+ Object.each(self.qualities, function(quality){
+ new Element('div.item').adopt(
+ new Element('span.label', {'text': quality.label}),
+ new Element('input.min', {'value': quality.size_min}),
+ new Element('input.max', {'value': quality.size_max})
+ ).inject(group)
})
-
- return data
- },
-
- addType: function(data){
- var self = this;
-
- var t = new Profile.Type(data);
- $(t).inject(self.type_container);
- self.sortable.addItems($(t));
-
- self.types.include(t);
-
- },
-
- del: function(){
- var self = this;
-
- if(!confirm('Are you sure you want to delete this profile?')) return
-
- Api.request('profile.delete', {
- 'data': {
- 'id': self.data.id
- },
- 'useSpinner': true,
- 'spinnerOptions': {
- 'target': self.el
- },
- 'onComplete': function(json){
- if(json.success)
- self.el.destroy();
- else
- alert(json.message)
- }
- });
- },
-
- makeSortable: function(){
- var self = this;
-
- self.sortable = new Sortables(self.type_container, {
- 'revert': true,
- //'clone': true,
- 'handle': '.handle',
- 'opacity': 0.5,
- 'onComplete': self.save.bind(self, 300)
- });
- },
-
- get: function(attr){
- return this.data[attr]
- },
-
- isCore: function(){
- return this.data.core
- },
-
- toElement: function(){
- return this.el
+
+ p(group)
+
}
});
-Profile.Type = Class({
-
- deleted: false,
-
- initialize: function(data){
- var self = this;
-
- self.data = data;
- self.create();
-
- },
-
- create: function(){
- var self = this;
- var data = self.data;
-
- self.el = new Element('li.type').adopt(
- new Element('span.quality_type').adopt(
- self.fillQualities()
- ),
- new Element('span.finish').adopt(
- self.finish = new Element('input', {
- 'type':'checkbox',
- 'class':'finish',
- 'checked': data.finish
- })
- ),
- new Element('span.delete.icon', {
- 'events': {
- 'click': self.del.bind(self)
- }
- }),
- new Element('span.handle')
- )
-
- },
-
- fillQualities: function(){
- var self = this;
-
- self.qualities = new Element('select');
-
- Object.each(Quality.qualities, function(q){
- new Element('option', {
- 'text': q.label,
- 'value': q.id
- }).inject(self.qualities)
- });
-
- self.qualities.set('value', self.data.quality_id);
-
- return self.qualities;
-
- },
-
- getData: function(){
- var self = this;
-
- return {
- 'quality_id': self.qualities.get('value'),
- 'finish': +self.finish.checked
- }
- },
-
- del: function(){
- var self = this;
-
- self.el.addClass('deleted');
- self.el.hide();
- self.deleted = true;
- },
-
- toElement: function(){
- return this.el;
- }
-
-})
+window.Quality = new QualityBase();
diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py
index 5d8aa22..07ad234 100644
--- a/couchpotato/core/plugins/renamer/__init__.py
+++ b/couchpotato/core/plugins/renamer/__init__.py
@@ -8,9 +8,8 @@ config = [{
'groups': [
{
'tab': 'renamer',
- 'name': 'tmdb',
- 'label': 'TheMovieDB',
- 'advanced': True,
+ 'name': 'renamer',
+ 'label': 'Folders',
'description': 'Move and rename your downloaded movies to your movie directory.',
'options': [
{
@@ -29,12 +28,52 @@ config = [{
'description': 'Folder where the movies will be moved to.',
},
{
+ 'name': 'folder_name',
+ 'label': 'Folder naming',
+ 'description': 'Name of the folder',
+ },
+ {
+ 'name': 'file_name',
+ 'label': 'File naming',
+ 'description': 'Name of the file',
+ },
+ {
+ 'advanced': True,
+ 'name': 'separator',
+ 'label': 'Separator',
+ 'description': 'Replace all the spaces with a character. Example: ".", "-". Leave empty to use spaces.',
+ },
+ {
+ 'advanced': True,
'name': 'run_every',
'label': 'Run every',
'default': 1,
'type': 'int',
'unit': 'min(s)',
'description': 'Search for new movies inside the folder every X minutes.',
+ },
+ ],
+ }, {
+ 'tab': 'renamer',
+ 'name': 'meta_renamer',
+ 'label': 'Advanced renaming',
+ 'description': 'Meta data file renaming. Use <filename> to use the above "File naming" settings, without the file extention.',
+ 'advanced': True,
+ 'options': [
+ {
+ 'name': 'trailer_name',
+ 'label': 'Trailer naming',
+ 'default': '-trailer.',
+ },
+ {
+ 'name': 'nfo_name',
+ 'label': 'NFO naming',
+ 'default': '.',
+ },
+ {
+ 'name': 'backdrop_name',
+ 'label': 'Backdrop naming',
+ 'default': '-backdrop.',
}
],
},
diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
index 30ee786..45f456a 100644
--- a/couchpotato/core/plugins/renamer/main.py
+++ b/couchpotato/core/plugins/renamer/main.py
@@ -1,6 +1,14 @@
+from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.helpers.encoding import toUnicode
+from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
+from couchpotato.core.settings.model import Library
+import os.path
+import re
+import shutil
+import traceback
log = CPLog(__name__)
@@ -8,12 +16,208 @@ log = CPLog(__name__)
class Renamer(Plugin):
def __init__(self):
- pass
addEvent('renamer.scan', self.scan)
- addEvent('app.load', self.scan)
+ #addEvent('app.load', self.scan)
#fireEvent('schedule.interval', 'renamer.scan', self.scan, minutes = self.conf('run_every'))
def scan(self):
- pass
+
+ groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True)
+
+ destination = self.conf('to')
+ folder_name = self.conf('folder_name')
+ file_name = self.conf('file_name')
+ trailer_name = self.conf('trailer_name')
+ backdrop_name = self.conf('fanart_name')
+ nfo_name = self.conf('nfo_name')
+ separator = self.conf('separator')
+
+ for group_identifier in groups:
+
+ group = groups[group_identifier]
+ rename_files = {}
+
+ # Add _UNKNOWN_ if no library is connected
+ if not group['library']:
+ if group['dirname']:
+ rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname'])
+ else: # Add it to filename
+ for file_type in group['files']:
+ for rename_me in group['files'][file_type]:
+ filename = os.path.basename(rename_me)
+ rename_files[rename_me] = rename_me.replace(filename, '_UNKNOWN_%s' % filename)
+
+ # Rename the files using the library data
+ else:
+ group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True)
+ library = group['library']
+
+ # Find subtitle for renaming
+ fireEvent('renamer.before', group)
+
+ # Remove weird chars from moviename
+ movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', group['library']['titles'][0]['title'])
+
+ # Put 'The' at the end
+ name_the = movie_name
+ if movie_name[:4].lower() == 'the ':
+ name_the = movie_name[4:] + ', The'
+
+ replacements = {
+ 'ext': 'mkv',
+ 'namethe': name_the.strip(),
+ 'thename': movie_name.strip(),
+ 'year': library['year'],
+ 'first': name_the[0].upper(),
+ 'dirname': group['dirname'],
+ 'quality': group['meta_data']['quality']['label'],
+ 'quality_type': group['meta_data']['quality_type'],
+ 'video': group['meta_data'].get('video'),
+ 'audio': group['meta_data'].get('audio'),
+ 'group': group['meta_data']['group'],
+ 'source': group['meta_data']['source'],
+ 'resolution_width': group['meta_data'].get('resolution_width'),
+ 'resolution_height': group['meta_data'].get('resolution_height'),
+ }
+
+ for file_type in group['files']:
+
+ # Move DVD files (no renaming)
+ if group['is_dvd'] and file_type is 'movie':
+ continue
+
+ # Move nfo depending on settings
+ if file_type is 'nfo' and not self.conf('rename_nfo'):
+ continue
+
+ # Subtitle extra
+ if file_type is 'subtitle_extra':
+ continue
+
+ # Move other files
+ multiple = len(group['files']['movie']) > 1
+ cd = 1 if multiple else 0
+
+ for file in sorted(list(group['files'][file_type])):
+
+ # Original filename
+ replacements['original'] = os.path.basename(file)
+
+ # Extension
+ replacements['ext'] = getExt(file)
+
+ # cd #
+ replacements['cd'] = ' cd%d' % cd if cd else ''
+ replacements['cd_nr'] = cd
+
+ # Naming
+ final_folder_name = self.doReplace(folder_name, replacements)
+ final_file_name = self.doReplace(file_name, replacements)
+ replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
+
+ # Meta naming
+ if file_type is 'trailer':
+ final_file_name = self.doReplace(trailer_name, replacements)
+ elif file_type is 'nfo':
+ final_file_name = self.doReplace(nfo_name, replacements)
+ elif file_type is 'backdrop':
+ final_file_name = self.doReplace(backdrop_name, replacements)
+
+ # Seperator replace
+ if separator:
+ final_file_name = final_file_name.replace(' ', separator)
+
+ # Main file
+ rename_files[file] = os.path.join(destination, final_folder_name, final_file_name)
+
+ # Check for extra subtitle files
+ if file_type is 'subtitle':
+
+ def test(s):
+ return file[:-len(replacements['ext'])] in s
+
+ for subtitle_extra in set(filter(test, group['files']['subtitle_extra'])):
+ replacements['ext'] = getExt(subtitle_extra)
+
+ final_folder_name = self.doReplace(folder_name, replacements)
+ final_file_name = self.doReplace(file_name, replacements)
+ rename_files[subtitle_extra] = os.path.join(destination, final_folder_name, final_file_name)
+
+ if multiple:
+ cd += 1
+
+ # Before renaming, remove the lower quality files
+ db = get_session()
+ library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
+ for movie in library.movies:
+ for release in movie.releases:
+ if release.quality.order < group['meta_data']['quality']['order']:
+ log.info('Removing older release for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
+ elif release.quality.order is group['meta_data']['quality']['order']:
+ log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label))
+ else:
+ log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
+
+ # Add _EXISTS_ to the parent dir
+ if group['dirname']:
+ for rename_me in rename_files: # Don't rename anything in this group
+ rename_files[rename_me] = None
+ rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_EXISTS_%s' % group['dirname'])
+ else: # Add it to filename
+ for rename_me in rename_files:
+ filename = os.path.basename(rename_me)
+ rename_files[rename_me] = rename_me.replace(filename, '_EXISTS_%s' % filename)
+
+ break
+
+ for file in release.files:
+ log.info('Removing "%s"' % file.path)
+
+ # Rename
+ for rename_me in rename_files:
+ if rename_files[rename_me]:
+ log.info('Renaming "%s" to "%s"' % (rename_me, rename_files[rename_me]))
+
+ path = os.path.dirname(rename_files[rename_me])
+ try:
+ if not os.path.isdir(path): os.makedirs(path)
+ except:
+ log.error('Failed creating dir %s: %s' % (path, traceback.format_exc()))
+ continue
+
+ #print rename_me, rename_files[rename_me]
+
+ # Search for trailers
+ fireEvent('renamer.after', group)
+
+ def moveFile(self, old, dest, suppress = True):
+ try:
+ shutil.move(old, dest)
+ except:
+ log.error("Couldn't move file '%s' to '%s': %s" % (old, dest, traceback.format_exc()))
+ return False
+
+ return True
+
+ def doReplace(self, string, replacements):
+ '''
+ replace confignames with the real thing
+ '''
+
+ replaced = toUnicode(string)
+ for x, r in replacements.iteritems():
+ if r is not None:
+ replaced = replaced.replace('<%s>' % toUnicode(x), toUnicode(r))
+ else:
+ #If information is not available, we don't want the tag in the filename
+ replaced = replaced.replace('<' + x + '>', '')
+
+ replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced)
+
+ sep = self.conf('separator')
+ return self.replaceDoubles(replaced).replace(' ', ' ' if not sep else sep)
+
+ def replaceDoubles(self, string):
+ return string.replace(' ', ' ').replace(' .', '.')
diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
index 2859ab1..caae1ce 100644
--- a/couchpotato/core/plugins/scanner/main.py
+++ b/couchpotato/core/plugins/scanner/main.py
@@ -1,10 +1,10 @@
from couchpotato import get_session
from couchpotato.core.event import fireEvent, addEvent
-from couchpotato.core.helpers.encoding import toUnicode
+from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import File, Library, Release, Movie
+from couchpotato.core.settings.model import File, Release, Movie
from couchpotato.environment import Env
from flask.helpers import json
from themoviedb.tmdb import opensubtitleHashFile
@@ -23,7 +23,7 @@ class Scanner(Plugin):
'trailer': 1048576, # 1MB
}
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
- ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films']
+ ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads']
extensions = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'],
'dvd': ['vts_*', 'vob'],
@@ -42,7 +42,7 @@ class Scanner(Plugin):
codecs = {
'audio': ['dts', 'ac3', 'ac3d', 'mp3'],
- 'video': ['x264', 'divx', 'xvid']
+ 'video': ['x264', 'h264', 'divx', 'xvid']
}
source_media = {
@@ -52,12 +52,16 @@ class Scanner(Plugin):
'hdtv': ['hdtv']
}
- clean = '(?i)[^\s](ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])[^\s]*'
+ clean = '[ _\,\.\(\)\[\]\-](french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
multipart_regex = [
'[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
'[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1
- '[ _\.-]+part[ _\.-]*([0-9a-d]+)', #*part1.mkv
- '[ _\.-]+dis[ck][ _\.-]*([0-9a-d]+)', #*disk1.mkv
+ '[ _\.-]+part[ _\.-]*([0-9a-d]+)', #*part1
+ '[ _\.-]+dis[ck][ _\.-]*([0-9a-d]+)', #*disk1
+ 'cd[ _\.-]*([0-9a-d]+)$', #cd1.ext
+ 'dvd[ _\.-]*([0-9a-d]+)$', #dvd1.ext
+ 'part[ _\.-]*([0-9a-d]+)$', #part1.mkv
+ 'dis[ck][ _\.-]*([0-9a-d]+)$', #disk1.mkv
'()[ _\.-]+([0-9]*[abcd]+)(\.....?)$',
'([a-z])([0-9]+)(\.....?)$',
'()([ab])(\.....?)$' #*a.mkv
@@ -65,42 +69,54 @@ class Scanner(Plugin):
def __init__(self):
- addEvent('app.load', self.scan)
+ #addEvent('app.load', self.scanLibrary)
- def scan(self, folder = '/Volumes/Media/Test/'):
+ addEvent('scanner.scan', self.scan)
- """
- Get all files
+ def scanLibrary(self):
- For each file larger then 350MB
- create movie "group", this is where all movie files will be grouped
- group multipart together
- check if its DVD (VIDEO_TS)
+ folder = '/Volumes/Media/Test/'
- # This should work for non-folder based structure
- for each moviegroup
+ groups = self.scan(folder = folder)
- for each file smaller then 350MB, allfiles.filter(moviename*)
+ # Open up the db
+ db = get_session()
- # Assuming the beginning of the filename is the same for this structure
- Movie is masterfile, moviename-cd1.ext -> moviename
- Find other files connected to moviename, moviename*.nfo, moviename*.sub, moviename*trailer.ext
+ # Mark all files as "offline" before a adding them to the database (again)
+ files_in_path = db.query(File).filter(File.path.like(toUnicode(folder) + u'%%'))
+ files_in_path.update({'available': 0}, synchronize_session = False)
+ db.commit()
- Remove found file from allfiles
+ update_after = []
+ for group in groups.itervalues():
- # This should work for folder based structure
- for each leftover file
- Loop over leftover files, use dirname as moviename
+ # Save to DB
+ if group['library']:
+ #library = db.query(Library).filter_by(id = library.get('id')).one()
+ # Add release
+ self.addRelease(group)
- For each found movie
+ # Add identifier for library update
+ update_after.append(group['library'].get('identifier'))
- determine filetype
+ for identifier in update_after:
+ fireEvent('library.update', identifier = identifier)
- Check if it's already in the db
+ # If cleanup option is enabled, remove offline files from database
+ if self.conf('cleanup_offline'):
+ files_in_path = db.query(File).filter(File.path.like(folder + '%%')).filter_by(available = 0)
+ [db.delete(x) for x in files_in_path]
+ db.commit()
+
+ db.remove()
- Add it to database
- """
+
+ def scan(self, folder = None):
+
+ if not folder or not os.path.isdir(folder):
+ log.error('Folder doesn\'t exists: %s' % folder)
+ return {}
# Get movie "master" files
movie_files = {}
@@ -153,22 +169,16 @@ class Scanner(Plugin):
# Remove the found files from the leftover stack
leftovers = leftovers - found_files
- # Open up the db
- db = get_session()
-
- # Mark all files as "offline" before a adding them to the database (again)
- files_in_path = db.query(File).filter(File.path.like(toUnicode(folder) + u'%%'))
- files_in_path.update({'available': 0}, synchronize_session = False)
- db.commit()
# Determine file types
- update_after = []
- for identifier, group in movie_files.iteritems():
+ for identifier in movie_files:
+ group = movie_files[identifier]
# Group extra (and easy) files first
images = self.getImages(group['unsorted_files'])
group['files'] = {
'subtitle': self.getSubtitles(group['unsorted_files']),
+ 'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
'nfo': self.getNfo(group['unsorted_files']),
'trailer': self.getTrailers(group['unsorted_files']),
'backdrop': images['backdrop'],
@@ -180,7 +190,22 @@ class Scanner(Plugin):
group['files']['movie'] = self.getDVDFiles(group['unsorted_files'])
else:
group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
- group['meta_data'] = self.getMetaData(group['files']['movie'])
+ group['meta_data'] = self.getMetaData(group)
+
+ # Get parent dir from movie files
+ for movie_file in group['files']['movie']:
+ group['parentdir'] = os.path.dirname(movie_file)
+ group['dirname'] = None
+
+ folders = group['parentdir'].replace(folder, '').split(os.path.sep)
+
+ # Try and get a proper dirname, so no "A", "Movie", "Download"
+ for folder in folders:
+ if folder.lower() in self.ignore_names or len(folder) < 2:
+ group['dirname'] = folder
+ break
+
+ break
# Leftover "sorted" files
for type in group['files']:
@@ -191,34 +216,16 @@ class Scanner(Plugin):
# Determine movie
group['library'] = self.determineMovie(group)
+ if not group['library']:
+ log.error('Unable to determin movie: %s' % group['identifiers'])
- # Save to DB
- if group['library']:
- #library = db.query(Library).filter_by(id = library.get('id')).one()
-
- # Add release
- release = self.addRelease(group)
- return
-
- # Add identifier for library update
- update_after.append(group['library'].get('identifier'))
-
- for identifier in update_after:
- fireEvent('library.update', identifier = identifier)
-
- # If cleanup option is enabled, remove offline files from database
- if self.conf('cleanup_offline'):
- files_in_path = db.query(File).filter(File.path.like(folder + '%%')).filter_by(available = 0)
- [db.delete(x) for x in files_in_path]
- db.commit()
-
- db.remove()
+ return movie_files
def addRelease(self, group):
db = get_session()
- identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data']['audio'], group['meta_data']['quality'])
+ identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier'])
# Add movie
done_status = fireEvent('status.get', 'done', single = True)
@@ -233,13 +240,13 @@ class Scanner(Plugin):
db.commit()
# Add release
- quality = fireEvent('quality.single', group['meta_data']['quality'], single = True)
release = db.query(Release).filter_by(identifier = identifier).first()
if not release:
+
release = Release(
identifier = identifier,
movie = movie,
- quality_id = quality.get('id'),
+ quality_id = group['meta_data']['quality'].get('id'),
status_id = done_status.get('id')
)
db.add(release)
@@ -259,18 +266,36 @@ class Scanner(Plugin):
db.remove()
- def getMetaData(self, files):
+ def getMetaData(self, group):
- return {
- 'audio': 'AC3',
- 'quality': '720p',
- 'quality_type': 'HD',
- 'resolution_width': 1280,
- 'resolution_height': 720
- }
+ data = {}
+ files = group['files']['movie']
for file in files:
- self.getMeta(file)
+ if os.path.getsize(file) < self.minimal_filesize['media']: continue # Ignore smaller files
+
+ meta = self.getMeta(file)
+
+ try:
+ data['video'] = self.getCodec(file, self.codecs['video'])
+ data['audio'] = meta['audio stream'][0]['compression']
+ data['resolution_width'] = meta['video stream'][0]['image width']
+ data['resolution_height'] = meta['video stream'][0]['image height']
+ except:
+ pass
+
+ if data.get('audio'): break
+
+ data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
+ if not data['quality']:
+ data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
+
+ data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 720 else 'SD'
+
+ data['group'] = self.getGroup(file[0])
+ data['source'] = self.getSourceMedia(file[0])
+
+ return data
def getMeta(self, filename):
lib_dir = os.path.join(Env.get('app_dir'), 'libs')
@@ -281,40 +306,58 @@ class Scanner(Plugin):
try:
meta = json.loads(z)
- log.info('Retrieved metainfo: %s' % meta)
return meta
- except Exception, e:
- print e
- log.error('Couldn\'t get metadata from file')
+ except Exception:
+ log.error('Couldn\'t get metadata from file: %s' % traceback.format_exc())
def determineMovie(self, group):
imdb_id = None
files = group['files']
- # Check and see if nfo contains the imdb-id
- try:
- for nfo_file in files['nfo']:
- imdb_id = self.getImdb(nfo_file)
- if imdb_id: break
- except:
- pass
- # Check if path is already in db
- db = get_session()
+ # Check for CP(imdb_id) string in the file paths
for file in files['movie']:
- f = db.query(File).filter_by(path = toUnicode(file)).first()
+ imdb_id = self.getCPImdb(file)
+ if imdb_id: break
+
+ # Check and see if nfo contains the imdb-id
+ if not imdb_id:
try:
- imdb_id = f.library[0].identifier
- break
+ for nfo_file in files['nfo']:
+ imdb_id = self.getImdb(nfo_file)
+ if imdb_id: break
except:
pass
- db.remove()
+
+ # Check if path is already in db
+ if not imdb_id:
+ db = get_session()
+ for file in files['movie']:
+ f = db.query(File).filter_by(path = toUnicode(file)).first()
+ try:
+ imdb_id = f.library[0].identifier
+ break
+ except:
+ pass
+ db.remove()
+
+ # Search based on OpenSubtitleHash
+ if not imdb_id and not group['is_dvd']:
+ for file in files['movie']:
+ movie = fireEvent('provider.movie.by_hash', file = file, merge = True)
+
+ if len(movie) > 0:
+ imdb_id = movie[0]['imdb']
+ if imdb_id: break
# Search based on identifiers
if not imdb_id:
for identifier in group['identifiers']:
+
if len(identifier) > 2:
+
movie = fireEvent('provider.movie.search', q = identifier, merge = True, limit = 1)
+
if len(movie) > 0:
imdb_id = movie[0]['imdb']
if imdb_id: break
@@ -329,7 +372,7 @@ class Scanner(Plugin):
}, update_after = False, single = True)
log.error('No imdb_id found for %s.' % group['identifiers'])
- return False
+ return {}
def saveFile(self, file, type = 'unknown', include_media_info = False):
@@ -342,6 +385,17 @@ class Scanner(Plugin):
# Check database and update/insert if necessary
return fireEvent('file.add', path = file, part = self.getPartNumber(file), type = self.file_types[type], properties = properties, single = True)
+ def getCPImdb(self, string):
+
+ try:
+ m = re.search('(cp\((?Ptt[0-9{7}]+)\))', string.lower())
+ id = m.group('id')
+ if id: return id
+ except AttributeError:
+ pass
+
+ return False
+
def getImdb(self, txt):
if os.path.isfile(txt):
@@ -375,6 +429,9 @@ class Scanner(Plugin):
def getSubtitles(self, files):
return set(filter(lambda s: getExt(s.lower()) in self.extensions['subtitle'], files))
+ def getSubtitlesExtras(self, files):
+ return set(filter(lambda s: getExt(s.lower()) in self.extensions['subtitle_extra'], files))
+
def getNfo(self, files):
return set(filter(lambda s: getExt(s.lower()) in self.extensions['nfo'], files))
@@ -447,20 +504,42 @@ class Scanner(Plugin):
return set(filter(lambda s:identifier in self.createFileIdentifier(s, folder), file_pile))
def createFileIdentifier(self, file_path, folder, exclude_filename = False):
+
identifier = file_path.replace(folder, '') # root folder
identifier = os.path.splitext(identifier)[0] # ext
+
if exclude_filename:
identifier = identifier[:len(identifier) - len(os.path.split(identifier)[-1])]
- identifier = self.removeMultipart(identifier) # multipart
- return identifier
+ # multipart
+ identifier = self.removeMultipart(identifier)
+
+ # groups, release tags, scenename cleaner, regex isn't correct
+ identifier = re.sub(self.clean, '::', simplifyString(identifier))
+
+ year = self.findYear(identifier)
+ if year:
+ identifier = '%s %s' % (identifier.split(year)[0].strip(), year)
+ else:
+ identifier = identifier.split('::')[0]
+
+ # Remove duplicates
+ out = []
+ for word in identifier.split():
+ if not word in out:
+ out.append(word)
+
+ identifier = ' '.join(out)
+
+ return simplifyString(identifier)
+
def removeMultipart(self, name):
for regex in self.multipart_regex:
try:
found = re.sub(regex, '', name)
if found != name:
- return found
+ name = found
except:
pass
return name
@@ -475,3 +554,33 @@ class Scanner(Plugin):
except:
pass
return name
+
+ def getCodec(self, filename, codecs):
+ codecs = map(re.escape, codecs)
+ try:
+ codec = re.search('[^A-Z0-9](?P' + '|'.join(codecs) + ')[^A-Z0-9]', filename, re.I)
+ return (codec and codec.group('codec')) or ''
+ except:
+ return ''
+
+ def getGroup(self, file):
+ try:
+ group = re.search('-(?P[A-Z0-9]+)$', file, re.I)
+ return group.group('group') or ''
+ except:
+ return ''
+
+ def getSourceMedia(self, file):
+ for media in self.source_media:
+ for alias in self.source_media[media]:
+ if alias in file.lower():
+ return media
+
+ return None
+
+ def findYear(self, text):
+ matches = re.search('(?P[0-9]{4})', text)
+ if matches:
+ return matches.group('year')
+
+ return ''
diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py
index fa77161..d2d8372 100644
--- a/couchpotato/core/plugins/searcher/main.py
+++ b/couchpotato/core/plugins/searcher/main.py
@@ -19,7 +19,7 @@ class Searcher(Plugin):
# Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.all, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
- addEvent('app.load', self.all)
+ #addEvent('app.load', self.all)
def all(self):
diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/movie/themoviedb/main.py
index a883797..8bf098e 100644
--- a/couchpotato/core/providers/movie/themoviedb/main.py
+++ b/couchpotato/core/providers/movie/themoviedb/main.py
@@ -15,12 +15,44 @@ class TheMovieDb(MovieProvider):
imageUrl = 'http://hwcdn.themoviedb.org'
def __init__(self):
+ addEvent('provider.movie.by_hash', self.byHash)
addEvent('provider.movie.search', self.search)
addEvent('provider.movie.info', self.getInfo)
# Use base wrapper
tmdb.Config.api_key = self.conf('api_key')
+ def byHash(self, file):
+ ''' Find movie by hash '''
+
+ if self.isDisabled():
+ return False
+
+ cache_key = 'tmdb.cache.%s' % simplifyString(file)
+ results = self.getCache(cache_key)
+
+ if not results:
+ log.debug('Searching for movie by hash: %s' % file)
+ try:
+ raw = tmdb.searchByHashingFile(file)
+
+ results = []
+ if raw:
+ try:
+ results = self.parseMovie(raw)
+ log.info('Found: %s' % results['titles'][0] + ' (' + str(results['year']) + ')')
+
+ self.setCache(cache_key, results)
+ return results
+ except SyntaxError, e:
+ log.error('Failed to parse XML response: %s' % e)
+ return False
+ except:
+ log.debug('No movies known by hash for: %s' % file)
+ pass
+
+ return results
+
def search(self, q, limit = 12):
''' Find movie by name '''
@@ -32,7 +64,7 @@ class TheMovieDb(MovieProvider):
results = self.getCache(cache_key)
if not results:
- log.debug('TheMovieDB - Searching for movie: %s' % q)
+ log.debug('Searching for movie: %s' % q)
raw = tmdb.search(search_string)
results = []
@@ -47,7 +79,8 @@ class TheMovieDb(MovieProvider):
if nr == limit:
break
- log.info('TheMovieDB - Found: %s' % [result['titles'][0] + ' (' + str(result['year']) + ')' for result in results])
+ log.info('Found: %s' % [result['titles'][0] + ' (' + str(result['year']) + ')' for result in results])
+
self.setCache(cache_key, results)
return results
except SyntaxError, e:
diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py
index b3a542e..2d10b0f 100644
--- a/couchpotato/core/settings/model.py
+++ b/couchpotato/core/settings/model.py
@@ -42,13 +42,14 @@ class Library(Entity):
tagline = Field(UnicodeText(255))
status = ManyToOne('Status')
- movie = OneToMany('Movie')
- titles = OneToMany('LibraryTitle', order_by = '-default')
+ movies = OneToMany('Movie')
+ titles = OneToMany('LibraryTitle')
files = ManyToMany('File')
class LibraryTitle(Entity):
""""""
+ using_options(order_by = '-default')
title = Field(Unicode)
default = Field(Boolean)
diff --git a/couchpotato/static/images/checks.png b/couchpotato/static/images/checks.png
new file mode 100644
index 0000000..3561d91
Binary files /dev/null and b/couchpotato/static/images/checks.png differ
diff --git a/couchpotato/static/images/close_button.png b/couchpotato/static/images/close_button.png
deleted file mode 100644
index d11e72b..0000000
Binary files a/couchpotato/static/images/close_button.png and /dev/null differ
diff --git a/couchpotato/static/scripts/library/form_replacement/Form.CheckGroup.js b/couchpotato/static/scripts/library/form_replacement/Form.CheckGroup.js
new file mode 100644
index 0000000..002fd6e
--- /dev/null
+++ b/couchpotato/static/scripts/library/form_replacement/Form.CheckGroup.js
@@ -0,0 +1,51 @@
+/*
+---
+name: Form.CheckGroup
+description: Class to represent a group of Form.Check wrapped checkboxes
+authors: Bryan J Swift (@bryanjswift)
+license: MIT-style license.
+requires: [Core/Class.Extras, Core/Element, Core/Element.Event, Form-Replacement/Form.Check]
+provides: Form.CheckGroup
+...
+*/
+if (typeof window.Form === 'undefined') { window.Form = {}; }
+
+Form.CheckGroup = new Class({
+ Implements: [Events,Options],
+ options: {
+ checkOptions: {},
+ initialValues: {}
+ },
+ checks: [],
+ initialize: function(group,options) {
+ if (!Form.Check) { throw 'required Class Form.Check not found'; }
+ this.setOptions(options);
+ group = $(group);
+ if (!group) { return this; }
+ var checkboxes = group.getElements('input[type=checkbox]');
+ checkboxes.each(this.addCheck,this);
+ },
+ addCheck: function(checkbox) {
+ var initialValues = this.options.initialValues[checkbox.get('name')];
+ var checkOptions = {};
+ checkOptions.checked = initialValues ? initialValues.contains(checkbox.get('value')) : checkbox.get('checked');
+ checkOptions.disabled = checkbox.get('disabled');
+ checkbox.store('Form.CheckGroup::data',this);
+ var check = checkbox.retrieve('Form.Check::data') || new Form.Check(checkbox, Object.append(checkOptions,this.options.checkOptions));
+ this.checks.push(check);
+ },
+ checkAll: function() {
+ this.checks.each(function(check) { if (!check.checked) { check.toggle(); } });
+ },
+ disable: function() {
+ this.checks.each(function(check) { check.disable(); });
+ this.fireEvent('disable',this);
+ },
+ enable: function() {
+ this.checks.each(function(check) { check.enable(); });
+ this.fireEvent('enable',this);
+ },
+ uncheckAll: function() {
+ this.checks.each(function(check) { if (check.checked) { check.toggle(); } });
+ }
+});
diff --git a/couchpotato/static/scripts/library/form_replacement/Form.RadioGroup.js b/couchpotato/static/scripts/library/form_replacement/Form.RadioGroup.js
new file mode 100644
index 0000000..f0000b5
--- /dev/null
+++ b/couchpotato/static/scripts/library/form_replacement/Form.RadioGroup.js
@@ -0,0 +1,59 @@
+/*
+---
+name: Form.RadioGroup
+description: Class to represent a group of Form.Radio buttons
+authors: Bryan J Swift (@bryanjswift)
+license: MIT-style license.
+requires: [Core/Class.Extras, Core/Element, Core/Element.Event, Form-Replacement/Form.Radio]
+provides: Form.RadioGroup
+...
+*/
+if (typeof window.Form === 'undefined') { window.Form = {}; }
+
+Form.RadioGroup = new Class({
+ Implements: [Events,Options],
+ options: {
+ radioOptions: {},
+ initialValues: {}
+ },
+ bound: {},
+ radios: [],
+ value: null,
+ initialize: function(group,options) {
+ if (!Form.Radio) { throw 'required Class Form.Radio not found'; }
+ this.setOptions(options);
+ this.bound = { select: this.select.bind(this) };
+ group = $(group);
+ if (!group) { return this; }
+ var radios = group.getElements('input[type=radio]');
+ radios.each(this.addCheck,this);
+ },
+ addCheck: function(radio,i) {
+ var initialValues = this.options.initialValues[radio.get('name')];
+ var radioOptions = {};
+ radioOptions.checked = initialValues ? initialValues.contains(radio.get('value')) : radio.get('checked');
+ radioOptions.disabled = radio.get('disabled');
+ var check = (radio.retrieve('Form.Radio::data')
+ || new Form.Radio(radio,Object.append(radioOptions,this.options.radioOptions)));
+ check.addEvent('onCheck',this.bound.select);
+ if (check.checked) { i ? this.changed(check) : this.value = check.value; }
+ radio.store('Form.RadioGroup::data',this);
+ this.radios.push(check);
+ },
+ changed: function(radio) {
+ this.value = radio.value;
+ this.fireEvent('onChange',this);
+ },
+ disable: function() {
+ this.radios.each(function(radio) { radio.disable(); });
+ },
+ enable: function() {
+ this.radios.each(function(radio) { radio.enable(); });
+ },
+ select: function(checkedRadio) {
+ this.radios.each(function(radio) {
+ if (radio.checked && radio.value !== checkedRadio.value) { radio.uncheck(); }
+ });
+ if (checkedRadio.value !== this.value) { this.changed(checkedRadio); }
+ }
+});
diff --git a/couchpotato/static/scripts/library/form_replacement/form_check.js b/couchpotato/static/scripts/library/form_replacement/form_check.js
new file mode 100644
index 0000000..3b29819
--- /dev/null
+++ b/couchpotato/static/scripts/library/form_replacement/form_check.js
@@ -0,0 +1,125 @@
+/*
+---
+name: Form.Check
+description: Class to represent a checkbox
+authors: Bryan J Swift (@bryanjswift)
+license: MIT-style license.
+requires: [Core/Class.Extras, Core/Element, Core/Element.Event]
+provides: Form.Check
+...
+*/
+if (typeof window.Form === 'undefined') { window.Form = {}; }
+
+Form.Check = new Class({
+ Implements: [Events, Options],
+ options: {
+ checked: false,
+ disabled: false
+ },
+ bound: {},
+ checked: false,
+ config: {
+ checkedClass: 'checked',
+ disabledClass: 'disabled',
+ elementClass: 'check',
+ highlightedClass: 'highlighted',
+ storage: 'Form.Check::data'
+ },
+ disabled: false,
+ element: null,
+ input: null,
+ label: null,
+ value: null,
+ initialize: function(input, options) {
+ this.setOptions(options);
+ this.bound = {
+ disable: this.disable.bind(this),
+ enable: this.enable.bind(this),
+ highlight: this.highlight.bind(this),
+ removeHighlight: this.removeHighlight.bind(this),
+ keyToggle: this.keyToggle.bind(this),
+ toggle: this.toggle.bind(this)
+ };
+ var bound = this.bound;
+ input = this.input = $(input);
+ var id = input.get('id');
+ this.label = document.getElement('label[for=' + id + ']');
+ this.element = new Element('div', {
+ 'class': input.get('class') + ' ' + this.config.elementClass,
+ id: id ? id + 'Check' : '',
+ events: {
+ click: bound.toggle,
+ mouseenter: bound.highlight,
+ mouseleave: bound.removeHighlight
+ }
+ });
+ this.input.addEvents({
+ keypress: bound.keyToggle,
+ keydown: bound.keyToggle,
+ keyup: bound.keyToggle
+ });
+ if (this.label) { this.label.addEvent('click', bound.toggle); }
+ this.element.wraps(input);
+ this.value = input.get('value');
+ if (this.input.checked) { this.check(); } else { this.uncheck(); }
+ if (this.input.disabled) { this.disable(); } else { this.enable(); }
+ input.store(this.config.storage, this).addEvents({
+ blur: bound.removeHighlight,
+ focus: bound.highlight
+ });
+ this.fireEvent('create', this);
+ },
+ check: function() {
+ this.element.addClass(this.config.checkedClass);
+ this.input.set('checked', 'checked').focus();
+ this.checked = true;
+ this.fireEvent('check', this);
+ },
+ disable: function() {
+ this.element.addClass(this.config.disabledClass);
+ this.input.set('disabled', 'disabled');
+ this.disabled = true;
+ this.fireEvent('disable', this);
+ },
+ enable: function() {
+ this.element.removeClass(this.config.disabledClass);
+ this.input.erase('disabled');
+ this.disabled = false;
+ this.fireEvent('enable', this);
+ },
+ highlight: function() {
+ this.element.addClass(this.config.highlightedClass);
+ this.fireEvent('highlight', this);
+ },
+ removeHighlight: function() {
+ this.element.removeClass(this.config.highlightedClass);
+ this.fireEvent('removeHighlight', this);
+ },
+ keyToggle: function(e) {
+ var evt = new Event(e);
+ if (evt.key === 'space') { this.toggle(e); }
+ },
+ toggle: function(e) {
+ var evt;
+ if (this.disabled) { return this; }
+ if (e) {
+ evt = new Event(e).stopPropagation();
+ if (evt.target.tagName.toLowerCase() !== 'a') {
+ evt.stop();
+ }
+ }
+ if (this.checked) {
+ this.uncheck();
+ } else {
+ this.check();
+ }
+ this.fireEvent('change', this);
+ return this;
+ },
+ uncheck: function() {
+ this.element.removeClass(this.config.checkedClass);
+ this.input.erase('checked');
+ this.checked = false;
+ this.fireEvent('uncheck', this);
+ }
+});
diff --git a/couchpotato/static/scripts/library/form_replacement/form_dropdown.js b/couchpotato/static/scripts/library/form_replacement/form_dropdown.js
new file mode 100644
index 0000000..0b01adf
--- /dev/null
+++ b/couchpotato/static/scripts/library/form_replacement/form_dropdown.js
@@ -0,0 +1,325 @@
+/*
+---
+name: Form.Dropdown
+description: Class to represent a select input
+authors: Bryan J Swift (@bryanjswift)
+license: MIT-style license.
+requires: [Core/Class.Extras, Core/Element, Core/Element.Event, Form-Replacement/Form.SelectOption]
+provides: Form.Dropdown
+...
+*/
+if (typeof window.Form === 'undefined') { window.Form = {}; }
+
+Form.Dropdown = new Class({
+ Implements: [Events,Options],
+ options: {
+ excludedValues: [],
+ initialValue: null,
+ mouseLeaveDelay: 350,
+ selectOptions: {},
+ typeDelay: 500
+ },
+ bound: {},
+ dropdownOptions: [],
+ element: null,
+ events: {},
+ highlighted: null,
+ input: null,
+ open: true,
+ selected: null,
+ selection: null,
+ typed: { lastKey: null, value: null, timer: null, pressed: null, shortlist: [], startkey: null },
+ value: null,
+ initialize: function(select,options) {
+ this.setOptions(options);
+ select = $(select);
+ this.bound = {
+ collapse: this.collapse.bind(this),
+ expand: this.expand.bind(this),
+ focus: this.focus.bind(this),
+ highlightOption: this.highlightOption.bind(this),
+ keydown: this.keydown.bind(this),
+ keypress: this.keypress.bind(this),
+ mouseenterDropdown: this.mouseenterDropdown.bind(this),
+ mouseleaveDropdown: this.mouseleaveDropdown.bind(this),
+ mousemove: this.mousemove.bind(this),
+ removeHighlightOption: this.removeHighlightOption.bind(this),
+ select: this.select.bind(this),
+ toggle: this.toggle.bind(this)
+ };
+ this.events = { mouseenter: this.bound.mouseenterDropdown, mouseleave: this.bound.mouseleaveDropdown };
+ this.value = this.options.initialValue;
+ this.initializeCreateElements(select);
+ var optionElements = select.getElements('option');
+ this.updateOptions(optionElements);
+ this.element.replaces(select);
+ document.addEvent('click', this.bound.collapse);
+ var eventName = Browser.ie || Browser.webkit ? 'keydown' : 'keypress';
+ var target = Browser.ie ? $(document.body) : window;
+ target.addEvent('keydown',this.bound.keydown).addEvent(eventName,this.bound.keypress);
+ },
+ initializeCreateElements: function(select) {
+ var id = select.get('id');
+ var dropdown = new Element('div', {
+ 'class': (select.get('class') + ' select').trim(),
+ 'id': (id && id !== '') ? id + 'Dropdown' : ''
+ });
+ var menu = new Element('div', {'class': 'menu'});
+ var list = new Element('div', {'class': 'list'});
+ var options = new Element('ul', {'class': 'options'});
+ dropdown.adopt(menu.adopt(list.adopt(options)));
+ var dropdownSelection = new Element('div', {
+ 'class': 'selection',
+ events: {click: this.bound.toggle}
+ });
+ var dropdownBackground = new Element('div', { 'class': 'dropdownBackground' });
+ var selection = new Element('span', { 'class': 'selectionDisplay' });
+ var input = new Element('input', {
+ type:'text',
+ id: id,
+ name: select.get('name'),
+ events: {
+ focus: this.bound.focus
+ }
+ });
+ dropdownSelection.adopt(dropdownBackground, selection, input);
+ dropdown.adopt(dropdownSelection);
+ this.element = dropdown;
+ this.selection = selection;
+ this.input = input;
+ return options;
+ },
+ collapse: function(e) {
+ this.open = false;
+ this.element.removeClass('active').removeClass('dropdown-active');
+ if (this.selected) { this.selected.removeHighlight(); }
+ this.element.removeEvents(this.events);
+ this.fireEvent('collapse', [this, e]);
+ },
+ deselect: function(option) {
+ option.deselect();
+ },
+ destroy: function() {
+ this.element = null;
+ this.selection = null;
+ this.input = null;
+ },
+ disable: function() {
+ this.collapse();
+ this.input.set('disabled', 'disabled').removeEvents({blur:this.bound.blur, focus:this.bound.focus});
+ this.selection.getParent().removeEvent('click', this.bound.toggle);
+ this.fireEvent('disable', this);
+ },
+ enable: function() {
+ this.input.erase('disabled').addEvents({blur:this.bound.blur, focus:this.bound.focus});
+ this.selection.getParent().addEvent('click', this.bound.toggle);
+ this.fireEvent('enable', this);
+ },
+ expand: function(e) {
+ clearTimeout(this.collapseInterval);
+ var evt = e ? new Event(e).stop() : null;
+ this.open = true;
+ this.input.focus();
+ this.element.addClass('active').addClass('dropdown-active');
+ if (this.selected) { this.selected.highlight(); }
+ this.element.addEvents(this.events);
+ this.fireEvent('expand', [this, e]);
+ },
+ focus: function(e) {
+ this.expand();
+ },
+ foundMatch: function(e) {
+ var typed = this.typed;
+ var shortlist = typed.shortlist;
+ var value = typed.value;
+ var i = 0;
+ var optionsLength = shortlist.length;
+ var excludedValues = this.options.excludedValues;
+ var found = false;
+ if (!optionsLength) { return; }
+ var option;
+ do {
+ option = shortlist[i];
+ if (option.text.toLowerCase().indexOf(value) === 0 && !excludedValues.contains(option.value)) {
+ found = true;
+ option.highlight(e);
+ typed.pressed = i + 1;
+ i = optionsLength;
+ }
+ i = i + 1;
+ } while(i < optionsLength);
+ return found;
+ },
+ highlightOption: function(option) {
+ if (this.highlighted) { this.highlighted.removeHighlight(); }
+ this.highlighted = option;
+ },
+ isOpen: function() {
+ return this.open;
+ },
+ keydown: function(e) {
+ if (!this.open) { return; }
+ this.dropdownOptions.each(function(option) { option.disable(); });
+ document.addEvent('mousemove', this.bound.mousemove);
+ },
+ keypress: function(e) {
+ if (!this.open) { return; }
+ (e).stop();
+
+ var code = e.code, key = e.key;
+
+ var typed = this.typed;
+ var match, i, options, option, optionsLength, found, first, excludedValues, shortlist;
+ switch(code) {
+ case 38: // up
+ case 37: // left
+ if (typed.pressed > 0) { typed.pressed = typed.pressed - 1; }
+ if (!this.highlighted) { this.dropdownOptions.getLast().highlight(e); break; }
+ match = this.highlighted.element.getPrevious();
+ match = match ? match.retrieve('Form.SelectOption::data') : this.dropdownOptions.getLast();
+ match.highlight(e);
+ break;
+ case 40: // down
+ case 39: // right
+ if (typed.shortlist.length > 0) { typed.pressed = typed.pressed + 1; }
+ if (!this.highlighted) { this.dropdownOptions[0].highlight(e); break; }
+ match = this.highlighted.element.getNext();
+ match = match ? match.retrieve('Form.SelectOption::data') : this.dropdownOptions[0];
+ match.highlight(e);
+ break;
+ case 13: // enter
+ e.stop();
+ case 9: // tab - skips the stop event but selects the item
+ this.highlighted.select();
+ break;
+ case 27: // esc
+ e.stop();
+ this.toggle();
+ break;
+ case 32: // space
+ default: // anything else
+ if (!(code >= 48 && code <= 122 && (code <= 57 || (code >= 65 && code <= 90) || code >=97) || code === 32)) {
+ break;
+ }
+ if (evt.control || evt.alt || evt.meta) { return; }
+ // alphanumeric or space
+ key = code === 32 ? ' ' : key;
+ clearTimeout(typed.timer);
+ options = this.dropdownOptions;
+ optionsLength = options.length;
+ excludedValues = this.options.excludedValues;
+ if (typed.timer === null) { // timer is expired
+ typed.shortlist = [];
+ if (key === typed.lastKey || key === typed.startkey) { // get next
+ typed.pressed = typed.pressed + 1;
+ typed.value = key;
+ } else { // get first
+ typed = this.resetTyped();
+ typed.value = key;
+ typed.startkey = key;
+ typed.pressed = 1;
+ }
+ typed.timer = this.resetTyped.delay(500, this);
+ } else {
+ if (key === typed.lastKey) { // check for match, if no match get next
+ typed.value = typed.value + key;
+ if (this.foundMatch(e)) { // got a match so break
+ typed.timer = this.resetTyped.delay(500, this);
+ break;
+ } else { // no match fall through
+ typed.shortlist = [];
+ typed.value = key;
+ typed.pressed = typed.pressed + 1;
+ typed.timer = null;
+ }
+ } else { // reset timer, get first match, set pressed to found position
+ typed.timer = this.resetTyped.delay(500, this);
+ typed.value = typed.value + key;
+ typed.startkey = typed.value.substring(0, 1);
+ typed.lastKey = key;
+ this.foundMatch(e);
+ break;
+ }
+ }
+ typed.lastKey = key;
+ shortlist = typed.shortlist;
+ i = 0;
+ found = 0;
+ do {
+ option = options[i];
+ if (option.text.toLowerCase().indexOf(key) === 0 && !excludedValues.contains(option.value)) {
+ if (found === 0) { first = option; }
+ found = found + 1;
+ if (found === typed.pressed) { option.highlight(e); }
+ shortlist.push(option);
+ }
+ i = i + 1;
+ } while(i < optionsLength);
+ if (typed.pressed > found) {
+ first.highlight(e);
+ typed.pressed = 1;
+ }
+ break;
+ }
+ },
+ mouseenterDropdown: function() {
+ clearTimeout(this.collapseInterval);
+ },
+ mouseleaveDropdown: function() {
+ this.collapseInterval = this.options.mouseLeaveDelay ? this.collapse.delay(this.options.mouseLeaveDelay,this) : null;
+ },
+ mousemove: function() {
+ this.dropdownOptions.each(function(option) { option.enable(); });
+ document.removeEvent('mousemove', this.bound.mousemove);
+ },
+ removeHighlightOption: function(option) {
+ this.highlighted = null;
+ },
+ reset: function() {
+ if (this.options.initialValue) {
+ this.dropdownOptions.each(function(o) {
+ if (o.value === this.options.initialValue) { o.select(); }
+ }, this);
+ } else {
+ this.dropdownOptions[0].select();
+ }
+ },
+ resetTyped: function() {
+ var typed = this.typed;
+ typed.value = null;
+ typed.timer = null;
+ return typed;
+ },
+ select: function(option, e) {
+ this.dropdownOptions.each(this.deselect);
+ this.selection.set('html', option.element.get('html'));
+ var oldValue = this.value;
+ this.value = option.value;
+ this.input.set('value', option.value);
+ this.selected = option;
+ this.fireEvent('select', [this, e]);
+ if (oldValue && oldValue !== this.value) { this.fireEvent('change', [this, e]); }
+ this.collapse(e);
+ },
+ toggle: function(e) {
+ if (this.open) { this.collapse(e); }
+ else { this.expand(e); }
+ },
+ updateOptions: function(optionElements) {
+ var optionsList = this.element.getElement('ul').empty(),
+ dropdownOptions = this.dropdownOptions.empty(),
+ selectOptions = this.options.selectOptions;
+ optionElements.each(function(opt) {
+ var option = new Form.SelectOption(opt, selectOptions);
+ option.addEvents({
+ 'onHighlight':this.bound.highlightOption,
+ 'onRemoveHighlight':this.bound.removeHighlightOption,
+ 'onSelect':this.bound.select
+ }).owner = this;
+ if (option.value === this.options.initialValue || opt.get('selected')) { this.select(option); }
+ dropdownOptions.push(option);
+ optionsList.adopt(option.element);
+ }, this);
+ if (!this.selected && optionElements[0]) { optionElements[0].retrieve('Form.SelectOption::data').select(); }
+ }
+});
diff --git a/couchpotato/static/scripts/library/form_replacement/form_radio.js b/couchpotato/static/scripts/library/form_replacement/form_radio.js
new file mode 100644
index 0000000..2fa15f7
--- /dev/null
+++ b/couchpotato/static/scripts/library/form_replacement/form_radio.js
@@ -0,0 +1,34 @@
+/*
+---
+name: Form.Radio
+description: Class to represent a radio button
+authors: Bryan J Swift (@bryanjswift)
+license: MIT-style license.
+requires: [Core/Class.Extras, Core/Element, Core/Element.Event, Form-Replacement/Form.Check]
+provides: Form.Radio
+...
+*/
+if (typeof window.Form === 'undefined') { window.Form = {}; }
+
+Form.Radio = new Class({
+ Extends: Form.Check,
+ config: {
+ elementClass: 'radio',
+ storage: 'Form.Radio::data'
+ },
+ initialize: function(input,options) {
+ this.parent(input,options);
+ },
+ toggle: function(e) {
+ if (this.element.hasClass('checked') || this.disabled) { return; }
+ var evt;
+ if (e) { evt = new Event(e).stop(); }
+ if (this.checked) {
+ this.uncheck();
+ } else {
+ this.check();
+ }
+ this.fireEvent(this.checked ? 'onCheck' : 'onUncheck',this);
+ this.fireEvent('onChange',this);
+ }
+});
\ No newline at end of file
diff --git a/couchpotato/static/scripts/library/form_replacement/form_selectoption.js b/couchpotato/static/scripts/library/form_replacement/form_selectoption.js
new file mode 100644
index 0000000..2b81140
--- /dev/null
+++ b/couchpotato/static/scripts/library/form_replacement/form_selectoption.js
@@ -0,0 +1,93 @@
+/*
+---
+name: Form.SelectOption
+description: Class to represent an option for Form.Dropdown
+authors: Bryan J Swift (@bryanjswift)
+license: MIT-style license.
+requires: [Core/Class.Extras, Core/Element, Core/Element.Event]
+provides: Form.SelectOption
+...
+*/
+if (typeof window.Form === 'undefined') { window.Form = {}; }
+
+Form.SelectOption = new Class({
+ Implements: [Events, Options],
+ options: {
+ optionTag: 'li',
+ selected: false
+ },
+ config: {
+ highlightedClass: 'highlighted',
+ optionClass: 'option',
+ selectedClass: 'selected'
+ },
+ element: null,
+ bound: {},
+ option: null,
+ selected: false,
+ text: null,
+ value: null,
+ initialize: function(option, options) {
+ this.setOptions(options);
+ option = $(option);
+ this.option = option;
+ this.bound = {
+ deselect: this.deselect.bind(this),
+ highlight: this.highlight.bind(this),
+ removeHighlight: this.removeHighlight.bind(this),
+ select: this.select.bind(this)
+ };
+ this.text = option.get('text');
+ this.value = option.get('value');
+ this.element = new Element(this.options.optionTag, {
+ 'class': (option.get('class') + ' ' + this.config.optionClass).trim(),
+ 'html': option.get('html'),
+ 'events': {
+ click: this.bound.select,
+ mouseenter: this.bound.highlight,
+ mouseleave: this.bound.removeHighlight
+ }
+ });
+ this.element.store('Form.SelectOption::data', this);
+ option.store('Form.SelectOption::data', this);
+ },
+ deselect: function(e) {
+ this.fireEvent('onDeselect', [this, e]);
+ this.element.removeClass(this.config.selectedClass).addEvent('click', this.bound.select);
+ this.selected = false;
+ },
+ destroy: function() {
+ this.element = null;
+ this.bound = null;
+ this.option = null;
+ },
+ disable: function() {
+ this.element.removeEvents({
+ mouseenter: this.bound.highlight,
+ mouseleave: this.bound.removeHighlight
+ });
+ this.fireEvent('onDisable', this);
+ },
+ enable: function() {
+ this.element.addEvents({
+ mouseenter: this.bound.highlight,
+ mouseleave: this.bound.removeHighlight
+ });
+ this.fireEvent('onEnable', this);
+ },
+ highlight: function(e) {
+ this.fireEvent('onHighlight', [this, e]);
+ this.element.addClass(this.config.highlightedClass);
+ return this;
+ },
+ removeHighlight: function(e) {
+ this.fireEvent('onRemoveHighlight', [this, e]);
+ this.element.removeClass(this.config.highlightedClass);
+ return this;
+ },
+ select: function(e) {
+ this.fireEvent('onSelect', [this, e]);
+ this.element.addClass(this.config.selectedClass).removeEvent('click', this.bound.select);
+ this.selected = true;
+ }
+});
diff --git a/couchpotato/static/scripts/page/manage.js b/couchpotato/static/scripts/page/manage.js
index 3525836..d3db0e0 100644
--- a/couchpotato/static/scripts/page/manage.js
+++ b/couchpotato/static/scripts/page/manage.js
@@ -3,6 +3,24 @@ Page.Manage = new Class({
Extends: PageBase,
name: 'manage',
- title: 'Do stuff to your existing movies!'
+ title: 'Do stuff to your existing movies!',
+
+ indexAction: function(param){
+ var self = this;
+
+ self.list = new MovieList({
+ 'status': 'done',
+ 'actions': Manage.Action
+ });
+ $(self.list).inject(self.el);
+
+ }
+
+});
+
+var Manage = {
+ 'Action': {
+ 'IMBD': IMDBAction
+ }
+}
-})
\ No newline at end of file
diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js
index 0066023..c1e9223 100644
--- a/couchpotato/static/scripts/page/settings.js
+++ b/couchpotato/static/scripts/page/settings.js
@@ -95,7 +95,7 @@ Page.Settings = new Class({
new Element('span', {
'text': 'Show advanced settings'
}),
- self.advanced_toggle = new Element('input[type=checkbox]', {
+ self.advanced_toggle = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.showAdvanced.bind(self)
}
@@ -103,6 +103,10 @@ Page.Settings = new Class({
)
)
);
+
+ new Form.Check(self.advanced_toggle, {
+ 'onChange': self.showAdvanced.bind(self)
+ })
// Create tabs
Object.each(self.tabs, function(tab, tab_name){
@@ -132,8 +136,6 @@ Page.Settings = new Class({
});
});
-
-
self.fireEvent('create');
self.openTab();
@@ -150,7 +152,7 @@ Page.Settings = new Class({
new Element('a', {
'href': '/'+self.name+'/'+tab_name+'/',
'text': label
- })
+ }).adopt()
).inject(self.tabs_container);
if(!self.tabs[tab_name])
@@ -335,7 +337,7 @@ Option.String = new Class({
self.el.adopt(
self.createLabel(),
- self.input = new Element('input', {
+ self.input = new Element('input.inlay', {
'type': 'text',
'name': self.postName(),
'value': self.getSettingValue()
@@ -365,6 +367,11 @@ Option.Dropdown = new Class({
})
self.input.set('value', self.getSettingValue());
+
+ var dd = new Form.Dropdown(self.input, {
+ 'onChange': self.changed.bind(self)
+ });
+ self.input = dd.input;
}
});
@@ -380,13 +387,17 @@ Option.Checkbox = new Class({
self.el.adopt(
self.createLabel().set('for', randomId),
- self.input = new Element('input', {
+ self.input = new Element('input.inlay', {
'name': self.postName(),
'type': 'checkbox',
'checked': self.getSettingValue(),
'id': randomId
})
- )
+ );
+
+ new Form.Check(self.input, {
+ 'onChange': self.changed.bind(self)
+ });
},
@@ -419,7 +430,7 @@ Option.Enabler = new Class({
var self = this;
self.el.adopt(
- self.input = new Element('input', {
+ self.input = new Element('input.inlay', {
'type': 'checkbox',
'checked': self.getSettingValue(),
'id': 'r-'+randomString(),
@@ -427,7 +438,16 @@ Option.Enabler = new Class({
'change': self.checkState.bind(self)
}
})
- )
+ );
+
+ new Form.Check(self.input, {
+ 'onChange': self.changed.bind(self)
+ });
+ },
+
+ changed: function(){
+ this.parent();
+ this.checkState();
},
checkState: function(){
@@ -457,14 +477,13 @@ Option.Directory = new Class({
type: 'span',
browser: '',
save_on_change: false,
- show_hidden: false,
create: function(){
var self = this;
self.el.adopt(
self.createLabel(),
- self.input = new Element('span', {
+ self.input = new Element('span.directory', {
'text': self.getSettingValue(),
'events': {
'click': self.showBrowser.bind(self)
@@ -495,12 +514,19 @@ Option.Directory = new Class({
if(!self.browser)
self.browser = new Element('div.directory_list').adopt(
- self.back_button = new Element('a.button.back', {
- 'text': '',
- 'events': {
- 'click': self.previousDirectory.bind(self)
- }
- }),
+ new Element('div.actions').adopt(
+ self.back_button = new Element('a.button.back', {
+ 'text': '',
+ 'events': {
+ 'click': self.previousDirectory.bind(self)
+ }
+ }),
+ new Element('label', {
+ 'text': 'Show hidden files'
+ }).adopt(
+ self.show_hidden = new Element('input[type=checkbox].inlay')
+ )
+ ),
self.dir_list = new Element('ul', {
'events': {
'click:relay(li)': self.selectDirectory.bind(self)
@@ -584,7 +610,7 @@ Option.Directory = new Class({
Api.request('directory.list', {
'data': {
'path': c,
- 'show_hidden': +self.show_hidden
+ 'show_hidden': +self.show_hidden.checked
},
'onComplete': self.fillBrowser.bind(self)
})
diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js
index ea177b1..5cb9f39 100644
--- a/couchpotato/static/scripts/page/wanted.js
+++ b/couchpotato/static/scripts/page/wanted.js
@@ -5,208 +5,26 @@ Page.Wanted = new Class({
name: 'wanted',
title: 'Gimmy gimmy gimmy!',
- movies: [],
-
indexAction: function(param){
var self = this;
- self.get()
- },
-
- list: function(){
- var self = this;
-
- if(!self.movie_container)
- self.movie_container = new Element('div.movies').inject(self.el);
-
- self.movie_container.empty();
- Object.each(self.movies, function(info){
- var m = new Movie(self, {}, info);
- $(m).inject(self.movie_container);
- m.fireEvent('injected');
+ self.list = new MovieList({
+ 'status': 'active',
+ 'actions': Wanted.Action
});
+ $(self.list).inject(self.el);
- self.movie_container.addEvents({
- 'mouseenter:relay(.movie)': function(e, el){
- el.addClass('hover')
- },
- 'mouseleave:relay(.movie)': function(e, el){
- el.removeClass('hover')
- }
- })
- },
-
- get: function(status, onComplete){
- var self = this
-
- if(self.movies.length == 0)
- Api.request('movie.list', {
- 'data': {},
- 'onComplete': function(json){
- self.store(json.movies);
- self.list();
- }
- })
- else
- self.list()
- },
-
- store: function(movies){
- var self = this;
-
- self.movies = movies;
}
});
-var Movie = new Class({
-
- Extends: BlockBase,
-
- initialize: function(self, options, data){
- var self = this;
-
- self.data = data;
-
- self.profile = Quality.getProfile(data.profile_id);
- self.parent(self, options);
- self.addEvent('injected', self.afterInject.bind(self))
- },
-
- create: function(){
- var self = this;
-
- self.el = new Element('div.movie').adopt(
- self.data_container = new Element('div.data', {
- 'tween': {
- duration: 400,
- transition: 'quint:in:out'
- }
- }).adopt(
- self.thumbnail = File.Select.single('poster', self.data.library.files),
- self.info_container = new Element('div.info').adopt(
- self.title = new Element('div.title', {
- 'text': self.getTitle()
- }),
- self.year = new Element('div.year', {
- 'text': self.data.library.year || 'Unknown'
- }),
- self.rating = new Element('div.rating', {
- 'text': self.data.library.rating
- }),
- self.description = new Element('div.description', {
- 'text': self.data.library.plot
- }),
- self.quality = new Element('div.quality', {
- 'text': self.profile.get('label')
- })
- ),
- self.actions = new Element('div.actions').adopt(
- self.action_imdb = new Movie.Action.IMDB(self),
- self.action_edit = new Movie.Action.Edit(self),
- self.action_refresh = new Movie.Action.Refresh(self),
- self.action_delete = new Movie.Action.Delete(self)
- )
- )
- );
-
- if(!self.data.library.rating)
- self.rating.hide();
-
- },
-
- afterInject: function(){
- var self = this;
-
- var height = self.getHeight();
- self.el.setStyle('height', height);
- },
-
- getTitle: function(){
- var self = this;
-
- var titles = self.data.library.titles;
-
- var title = titles.filter(function(title){
- return title['default']
- }).pop()
-
- if(title)
- return title.title
- else if(titles.length > 0)
- return titles[0].title
-
- return 'Unknown movie'
- },
-
- slide: function(direction){
- var self = this;
-
- if(direction == 'in'){
- self.el.addEvent('outerClick', self.slide.bind(self, 'out'))
- self.data_container.tween('left', 0, self.getWidth());
- }
- else {
- self.el.removeEvents('outerClick')
- self.data_container.tween('left', self.getWidth(), 0);
- }
- },
-
- getHeight: function(){
- var self = this;
-
- if(!self.height)
- self.height = self.data_container.getCoordinates().height;
-
- return self.height;
- },
-
- getWidth: function(){
- var self = this;
-
- if(!self.width)
- self.width = self.data_container.getCoordinates().width;
-
- return self.width;
- },
-
- get: function(attr){
- return this.data[attr] || this.data.library[attr]
- }
-
-});
-
-var MovieAction = new Class({
-
- class_name: 'action',
-
- initialize: function(movie){
- var self = this;
- self.movie = movie;
-
- self.create();
- self.el.addClass(self.class_name)
- },
-
- create: function(){},
-
- disable: function(){
- this.el.addClass('disable')
- },
-
- enable: function(){
- this.el.removeClass('disable')
- },
-
- toElement: function(){
- return this.el
+var Wanted = {
+ 'Action': {
+ 'IMBD': IMDBAction
}
+}
-})
-
-Movie.Action = {}
-
-Movie.Action.Edit = new Class({
+Wanted.Action.Edit = new Class({
Extends: MovieAction,
@@ -229,7 +47,11 @@ Movie.Action.Edit = new Class({
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
$(self.movie.thumbnail).clone(),
- new Element('div.form').adopt(
+ new Element('div.form', {
+ 'styles': {
+ 'line-height': self.movie.getHeight()
+ }
+ }).adopt(
self.title_select = new Element('select', {
'name': 'title'
}),
@@ -244,56 +66,49 @@ Movie.Action.Edit = new Class({
})
)
).inject(self.movie, 'top');
- }
+ Array.each(self.movie.data.library.titles, function(alt){
+ new Element('option', {
+ 'text': alt.title
+ }).inject(self.title_select)
+ });
+
+ Object.each(Quality.profiles, function(profile){
+ new Element('option', {
+ 'value': profile.id ? profile.id : profile.data.id,
+ 'text': profile.label ? profile.label : profile.data.label
+ }).inject(self.profile_select);
+ self.profile_select.set('value', self.movie.profile.get('id'));
+ });
+
+ }
self.movie.slide('in');
},
- save: function(){
+ save: function(e){
+ (e).stop();
var self = this;
Api.request('movie.edit', {
'data': {
+ 'id': self.movie.get('id'),
'default_title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value')
},
'useSpinner': true,
- 'spinnerTarget': self.movie
- })
- }
-
-})
-
-Movie.Action.IMDB = new Class({
-
- Extends: MovieAction,
- id: null,
-
- create: function(){
- var self = this;
-
- self.id = self.movie.get('identifier');
-
- self.el = new Element('a.imdb', {
- 'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
- 'events': {
- 'click': self.gotoIMDB.bind(self)
+ 'spinnerTarget': $(self.movie),
+ 'onComplete': function(){
+ self.movie.quality.set('text', self.profile_select.getSelected()[0].get('text'))
+ self.movie.title.set('text', self.title_select.getSelected()[0].get('text'))
}
});
- if(!self.id) self.disable();
- },
-
- gotoIMDB: function(e){
- var self = this;
- (e).stop();
-
- window.open('http://www.imdb.com/title/'+self.id+'/');
+ self.movie.slide('out');
}
})
-Movie.Action.Refresh = new Class({
+Wanted.Action.Refresh = new Class({
Extends: MovieAction,
@@ -322,7 +137,7 @@ Movie.Action.Refresh = new Class({
})
-Movie.Action.Delete = new Class({
+Wanted.Action.Delete = new Class({
Extends: MovieAction,
@@ -360,7 +175,7 @@ Movie.Action.Delete = new Class({
'text': 'or'
}),
new Element('a.button.delete', {
- 'text': 'Delete movie',
+ 'text': 'Delete ' + self.movie.title.get('text'),
'events': {
'click': self.del.bind(self)
}
@@ -411,4 +226,4 @@ Movie.Action.Delete = new Class({
}
-})
+})
\ No newline at end of file
diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css
index 0360e44..f5cf9be 100644
--- a/couchpotato/static/style/main.css
+++ b/couchpotato/static/style/main.css
@@ -1,17 +1,18 @@
/* @override http://localhost:5000/static/style/main.css */
html {
- color: #343434;
+ color: #fff;
font-size: 12px;
line-height: 1.5;
- font-family: Helvetica, Arial, Geneva, sans-serif;
+ font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
height: 100%;
+ text-shadow: 0 1px 0 #000;
}
body {
margin: 0;
padding: 0;
- background: #fff;
+ background: #4e5969;
overflow-y: scroll;
height: 100%;
}
@@ -40,11 +41,11 @@ a img {
a {
text-decoration:none;
- color: #6ea1d7;
+ color: #fff;
outline: 0;
cursor: pointer;
}
-a:hover { color: #4d66c4; }
+a:hover { color: #f3f3f3; }
.page {
display: none;
@@ -99,7 +100,7 @@ form {
}
.spinner{
- background: #f7f7f7 url('../images/spinner.gif') no-repeat center;
+ background: #4e5969 url('../images/spinner.gif') no-repeat center;
}
.button {
@@ -130,9 +131,8 @@ form {
/*** Navigation ***/
.header {
- background: #f7f7f7;
+ background: #4e5969;
padding:10px;
- border-bottom: 1px solid #f1f1f1;
height: 60px;
-moz-box-shadow: 0 0 30px rgba(0,0,0,0.1);
-webkit-box-shadow: 0 0 30px rgba(0,0,0,0.1);
@@ -153,7 +153,7 @@ form {
padding: 0;
}
.header .navigation li {
- color: #8b8b8b;
+ color: #fff;
display: inline-block;
font-size:20px;
font-weight: bold;
@@ -183,10 +183,151 @@ form {
}
.header .navigation li a:link, .header .navigation li a:visited {
- color: #2c2c2c;
+ color: #fff;
}
.header .navigation li a:hover, .header .navigation li a:active {
- color: #8b8b8b;
+ color: #b1d8dc;
+ }
+
+/*** Global Styles ***/
+.check {
+ display: inline-block;
+ vertical-align: middle;
+ height: 16px;
+ width: 16px;
+ cursor: pointer;
+}
+ .check.highlighted { background-color: #424c59; }
+ .check.checked {
+ background-image: url('../images/checks.png');
+ background-position: -2px 0;
+}
+.check input {
+ display: none !important;
+}
+
+.select {
+ cursor: pointer;
+ display: inline-block;
+}
+
+ .select .selection {
+ display: inline-block;
+ padding: 0 30px 0 20px;
+
+ border-radius:30px;
+ -moz-border-radius: 30px;
+ -webkit-border-radius: 30px;
+
+ box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20);
+ -moz-box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20);
+ -webkit-box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20);
+
+ background: url('../images/checks.png') no-repeat 94% -53px, -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ color-stop(0, #406db8),
+ color-stop(1, #5b9bd1)
+ );
+ background: url('../images/checks.png') no-repeat 94% -53px, -moz-linear-gradient(
+ center top,
+ #5b9bd1 0%,
+ #406db8 100%
+ );
+ }
+
+ .select .selection .selectionDisplay {
+ display: inline-block;
+ padding-right: 15px;
+ border-right: 1px solid rgba(0,0,0,0.2);
+
+ box-shadow: 1px 0 0 rgba(255,255,255,0.15);
+ -moz-box-shadow: 1px 0 0 rgba(255,255,255,0.15);
+ -webkit-box-shadow: 1px 0 0 rgba(255,255,255,0.15);
}
+ .select .menu {
+ clear: both;
+ overflow: hidden;
+ font-weight: bold;
+ }
+ .select .list {
+ display: none;
+ background: #282d34;
+ border: 1px solid #1f242b;
+ position: absolute;
+ margin: 28px 0 0 0;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.4);
+ -moz-box-shadow: 0 1px 2px rgba(0,0,0,0.4);
+ -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.4);
+ border-radius:3px;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+
+ z-index: 3;
+ }
+ .select.active .list {
+ display: block;
+ }
+ .select .list ul {
+ display: block;
+ width: 100% !important;
+ }
+ .select .list li {
+ padding: 2px 33px 2px 20px;
+ display: block;
+ }
+ .select .list li.highlighted {
+ background: rgba(255,255,255,0.1);
+ }
+
+ .select input { display: none; }
+
+.inlay {
+ color: #fff;
+ border: 0;
+ border-radius:3px;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ background: #282d34;
+ box-shadow: inset 0 1px 8px rgba(0,0,0,0.25), 0 1px 0px rgba(255,255,255,0.25);
+ -moz-box-shadow: inset 0 1px 8px rgba(0,0,0,0.25), 0 1px 0px rgba(255,255,255,0.25);
+ -webkit-box-shadow: inset 0 1px 8px rgba(0,0,0,0.25), 0 1px 0px rgba(255,255,255,0.25);
+}
+
+ .inlay.light {
+ background: #47515f;
+ outline: none;
+ box-shadow: inset 0 1px 8px rgba(0,0,0,0.05), 0 1px 0px rgba(255,255,255,0.15);
+ -moz-box-shadow: inset 0 1px 8px rgba(0,0,0,0.05), 0 1px 0px rgba(255,255,255,0.15);
+ -webkit-box-shadow: inset 0 1px 8px rgba(0,0,0,0.05), 0 1px 0px rgba(255,255,255,0.15);
+ }
+
+ .inlay:focus {
+ background: #3a4350;
+ outline: none;
+ }
+
+.onlay, .inlay .selected, .inlay > li:hover {
+ border-radius:3px;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border: 1px solid #252930;
+ box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2);
+ -moz-box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2);
+ -webkit-box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2);
+ background-image: -webkit-gradient(
+ linear,
+ left bottom,
+ left top,
+ color-stop(0, rgb(55,62,74)),
+ color-stop(1, rgb(73,83,98))
+ );
+ background-image: -moz-linear-gradient(
+ center bottom,
+ rgb(55,62,74) 0%,
+ rgb(73,83,98) 100%
+ );
+}
diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css
index 967b77e..7d7ec34 100644
--- a/couchpotato/static/style/page/settings.css
+++ b/couchpotato/static/style/page/settings.css
@@ -12,35 +12,40 @@
list-style: none;
padding: 40px 0;
margin: 0;
- min-height: 300px;
+ min-height: 470px;
+
+ background-image: -webkit-gradient(
+ linear,
+ right top,
+ 40% 4%,
+ color-stop(0, rgba(0,0,0, 0.3)),
+ color-stop(0.9, rgba(0,0,0, 0))
+ );
+ background-image: -moz-linear-gradient(
+ 30% 0% 16deg,
+ rgba(0,0,0,0) 0%,
+ rgba(0,0,0,0.3) 100%
+ );
}
.page.settings .tabs a {
display: block;
- padding: 10px 15px;
- border: 1px solid transparent;
- position: relative;
- z-index: 1;
- margin-right: -1px;
+ padding: 11px 15px;
}
.page.settings .tabs .active a {
- color: black;
- background: #fbfbfb;
- border-color: #f1f1f1;
- border-right-color: transparent;
+ background: #4e5969;
}
+
.page.settings .containers {
width: 75.8%;
float: left;
padding: 20px 2%;
min-height: 300px;
- background: #fbfbfb;
- border-left: 1px solid #f1f1f1;s
}
.page.settings .advanced {
display: none;
- color: #ce3b19;
+ color: #edc07f;
}
.page.settings.show_advanced .advanced { display: block; }
@@ -57,10 +62,13 @@
font-size: 25px;
padding: 0 9px 10px 30px;
margin: 0;
- border-bottom: 1px solid #f3f3f3;
+ border-bottom: 1px solid #333;
+
+ box-shadow: 0 1px 0px rgba(255,255,255, 0.15);
+ -moz-box-shadow: 0 1px 0px rgba(255,255,255, 0.15);
+ -webkit-box-shadow: 0 1px 0px rgba(255,255,255, 0.15);
}
.page.settings fieldset h2 .hint {
- color: #888;
font-size: 12px;
margin-left: 10px;
}
@@ -81,26 +89,44 @@
.page.settings .ctrlHolder {
line-height: 25px;
padding: 10px 10px 10px 30px;
- border-bottom: 1px solid #f3f3f3;
font-size: 14px;
+ border: 0;
}
.page.settings .ctrlHolder:last-child { border: none; }
- .page.settings .ctrlHolder:hover { background: rgba(211,234,254,0.1); }
- .page.settings .ctrlHolder.focused:hover { background: rgba(251,246,48,0.29); }
+ .page.settings .ctrlHolder:hover { background: rgba(255,255,255,0.05); }
+ .page.settings .ctrlHolder.focused { background: rgba(255,255,255,0.2); }
.page.settings .ctrlHolder .formHint {
float: right;
width: 47%;
margin: -18px 0;
padding: 0;
+ color: #fff;
}
+
+ .check {
+ display: inline-block;
+ vertical-align: middle;
+ height: 16px;
+ width: 16px;
+ cursor: pointer;
+ }
+ .check.highlighted { background-color: #424c59; }
+ .check.checked {
+ background-image: url('../../images/checks.png');
+ background-position: -2px 0;
+}
+ .check input {
+ display: none;
+ }
- .page.settings .ctrlHolder input[type=checkbox] + .formHint {
+ .page.settings .check + .formHint {
float: none;
width: auto;
display: inline-block;
margin-left: 1% !important;
- color: #222;
+ height: 24px;
+ vertical-align: middle;
}
.page.settings .ctrlHolder label {
@@ -111,8 +137,7 @@
}
.page.settings input[type=text], .page.settings input[type=password] {
- border: 1px solid #aaa;
- padding: 3px;
+ padding: 5px 3px;
margin: 0;
width: 30%;
@@ -135,5 +160,44 @@
}
.page.settings .advanced_toggle span { padding: 0 5px; }
.page.settings.show_advanced .advanced_toggle {
- color: #ce3b19;
- }
\ No newline at end of file
+ color: #edc07f;
+ }
+
+ .page.settings .directory {
+ display: inline-block;
+ padding: 0 4px;
+ font-size: 13px;
+ width: 29.7%;
+ }
+ .page.settings .directory_list {
+ position: absolute;
+ width: 300px;
+ margin: 0 0 0 16.5%;
+ background: #282d34;
+ border: 1px solid #1f242b;
+ position: absolute;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.4);
+ -moz-box-shadow: 0 1px 2px rgba(0,0,0,0.4);
+ -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.4);
+ border-radius:3px;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ }
+
+ .page.settings .directory_list ul {
+ width: 100%;
+ max-height: 200px;
+ overflow: auto;
+ }
+
+ .page.settings .directory_list li {
+ padding: 2px 10px;
+ }
+
+ .page.settings .directory_list .actions {
+ clear: both;
+ padding: 10px;
+ background: #414953;
+ }
+ .page.settings .directory_list .actions:first-child { border-bottom: 1px solid #1f242b; }
+ .page.settings .directory_list .actions:last-child { border-top: 1px solid #1f242b; }
\ No newline at end of file
diff --git a/couchpotato/static/style/page/wanted.css b/couchpotato/static/style/page/wanted.css
index 10f8df7..e69de29 100644
--- a/couchpotato/static/style/page/wanted.css
+++ b/couchpotato/static/style/page/wanted.css
@@ -1,134 +0,0 @@
-/* @override http://localhost:5000/static/style/page/wanted.css */
-
-.page.wanted .movies {
- padding: 20px 0;
-}
-
- .page.wanted .movie {
- overflow: hidden;
- position: relative;
- background: #999;
-
- border-radius: 4px;
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
- margin: 10px 0;
- border: 1px solid #fff;
- border-color: #eee #fff #fff #eee;
- background: #ddd;
- }
- .page.wanted .movie:hover {
- border-color: #ddd #fff #fff #ddd;
- }
- .page.wanted .movie:hover .data {
- background: #f5f5f5;
- }
- .page.wanted .data {
- padding: 2%;
- position: absolute;
- width: 96.1%;
- top: 0;
- left: 0;
- background: #f9f9f9;
- }
- .page.wanted .data:after {
- content: "";
- display: block;
- height: 0;
- clear: both;
- visibility: hidden;
- }
- .page.wanted .data .poster, .page.wanted .options .poster {
- overflow: hidden;
- float: left;
- max-width: 10%;
- margin: 0 2% 0 0;
- border-radius:3px;
- -moz-border-radius: 3px;
- -webkit-border-radius: 3px;
- -moz-box-shadow: 0 0 10px rgba(0,0,0,0.35);
- -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.35);
- line-height: 0;
- }
-
- .page.wanted .info {
- float: right;
- width: 88%;
- }
-
- .page.wanted .info .title {
- font-size: 30px;
- font-weight: bold;
- margin-bottom: 10px;
- float: left;
- width: 80%;
- }
-
- .page.wanted .info .year {
- font-size: 30px;
- margin-bottom: 10px;
- float: right;
- color: #bbb;
- width: 10%;
- text-align: right;
- }
-
- .page.wanted .info .rating {
- font-size: 30px;
- margin-bottom: 10px;
- color: #444;
- float: left;
- width: 5%;
- padding: 0 0 0 3%;
- background: url('../../images/rating.png') no-repeat left center;
- }
-
- .page.wanted .info .description {
- clear: both;
- width: 95%;
- }
- .page.wanted .data .actions {
- position: absolute;
- right: 15px;
- bottom: 15px;
- line-height: 0;
- }
- .page.wanted .data:hover .action { opacity: 0.6; }
- .page.wanted .data:hover .action:hover { opacity: 1; }
-
- .page.wanted .data .action {
- background: no-repeat center;
- display: inline-block;
- width: 20px;
- height: 20px;
- padding: 3px;
- opacity: 0;
- }
- .page.wanted .data .action.refresh { background-image: url('../../images/reload.png'); }
- .page.wanted .data .action.delete { background-image: url('../../images/delete.png'); }
- .page.wanted .data .action.edit { background-image: url('../../images/edit.png'); }
- .page.wanted .data .action.imdb { background-image: url('../../images/imdb.png'); }
-
- .page.wanted .delete_container {
- clear: both;
- text-align: center;
- font-size: 20px;
- position: relative;
- }
- .page.wanted .delete_container .cancel {
- }
- .page.wanted .delete_container .or {
- padding: 10px;
- }
- .page.wanted .delete_container .delete {
- background-color: #ff321c;
- font-weight: normal;
- }
- .page.wanted .delete_container .delete:hover {
- color: #fff;
- background-color: #d32917;
- }
-
- .page.wanted .options {
- padding: 2%;
- }
\ No newline at end of file
diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html
index d04fc67..ec64755 100644
--- a/couchpotato/templates/_desktop.html
+++ b/couchpotato/templates/_desktop.html
@@ -12,6 +12,11 @@
+
+
+
+
+
@@ -19,6 +24,13 @@
+ {% for url in fireEvent('clientscript.get_scripts', as_html = True, single = True) %}
+
+ {% endfor %}
+ {% for url in fireEvent('clientscript.get_styles', as_html = True, single = True) %}
+
+ {% endfor %}
+
@@ -26,11 +38,6 @@
- {% for url in fireEvent('clientscript.get_scripts', as_html = True, single = True) %}
- {% endfor %}
- {% for url in fireEvent('clientscript.get_styles', as_html = True, single = True) %}
- {% endfor %}
-