Browse Source

Renamer, movie searcher, Library scanning

New theme for UI
and more..
pull/1/merge
Ruud 14 years ago
parent
commit
98aafeaec3
  1. 3
      couchpotato/core/helpers/encoding.py
  2. 9
      couchpotato/core/plugins/file/main.py
  3. 5
      couchpotato/core/plugins/file/static/file.js
  4. 18
      couchpotato/core/plugins/library/main.py
  5. 34
      couchpotato/core/plugins/movie/main.py
  6. 95
      couchpotato/core/plugins/movie/static/list.js
  7. 166
      couchpotato/core/plugins/movie/static/movie.css
  8. 176
      couchpotato/core/plugins/movie/static/movie.js
  9. 51
      couchpotato/core/plugins/movie/static/search.css
  10. 4
      couchpotato/core/plugins/movie/static/search.js
  11. 8
      couchpotato/core/plugins/profile/main.py
  12. 18
      couchpotato/core/plugins/profile/static/profile.css
  13. 259
      couchpotato/core/plugins/profile/static/profile.js
  14. 53
      couchpotato/core/plugins/quality/main.py
  15. 21
      couchpotato/core/plugins/quality/static/quality.css
  16. 292
      couchpotato/core/plugins/quality/static/quality.js
  17. 45
      couchpotato/core/plugins/renamer/__init__.py
  18. 210
      couchpotato/core/plugins/renamer/main.py
  19. 297
      couchpotato/core/plugins/scanner/main.py
  20. 2
      couchpotato/core/plugins/searcher/main.py
  21. 37
      couchpotato/core/providers/movie/themoviedb/main.py
  22. 5
      couchpotato/core/settings/model.py
  23. BIN
      couchpotato/static/images/checks.png
  24. BIN
      couchpotato/static/images/close_button.png
  25. 51
      couchpotato/static/scripts/library/form_replacement/Form.CheckGroup.js
  26. 59
      couchpotato/static/scripts/library/form_replacement/Form.RadioGroup.js
  27. 125
      couchpotato/static/scripts/library/form_replacement/form_check.js
  28. 325
      couchpotato/static/scripts/library/form_replacement/form_dropdown.js
  29. 34
      couchpotato/static/scripts/library/form_replacement/form_radio.js
  30. 93
      couchpotato/static/scripts/library/form_replacement/form_selectoption.js
  31. 22
      couchpotato/static/scripts/page/manage.js
  32. 62
      couchpotato/static/scripts/page/settings.js
  33. 267
      couchpotato/static/scripts/page/wanted.js
  34. 163
      couchpotato/static/style/main.css
  35. 112
      couchpotato/static/style/page/settings.css
  36. 134
      couchpotato/static/style/page/wanted.css
  37. 17
      couchpotato/templates/_desktop.html

3
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:

9
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()

5
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);

18
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':{}})

34
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):

95
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;
}
});

166
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;
}

176
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+'/');
}
})

51
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%;

4
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,

8
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({

18
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;
}

259
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': '<acronym title="Won\'t download anything else if it has found this quality.">Finish</acronym>'})
),
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;
}
})

53
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 ''

21
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;
}

292
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': '<acronym title="Won\'t download anything else if it has found this quality.">Finish</acronym>'})
),
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();

45
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 &lt;filename&gt; to use the above "File naming" settings, without the file extention.',
'advanced': True,
'options': [
{
'name': 'trailer_name',
'label': 'Trailer naming',
'default': '<filename>-trailer.<ext>',
},
{
'name': 'nfo_name',
'label': 'NFO naming',
'default': '<filename>.<ext>',
},
{
'name': 'backdrop_name',
'label': 'Backdrop naming',
'default': '<filename>-backdrop.<ext>',
}
],
},

210
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(' .', '.')

297
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\((?P<id>tt[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<codec>' + '|'.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<group>[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<year>[0-9]{4})', text)
if matches:
return matches.group('year')
return ''

2
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):

37
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:

5
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)

BIN
couchpotato/static/images/checks.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
couchpotato/static/images/close_button.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

51
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(); } });
}
});

59
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); }
}
});

125
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);
}
});

325
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(); }
}
});

34
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);
}
});

93
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;
}
});

22
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
}
}
})

62
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)
})

267
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({
}
})
})

163
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%
);
}

112
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;
}
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; }

134
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%;
}

17
couchpotato/templates/_desktop.html

@ -12,6 +12,11 @@
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/mootools_more.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/uniform.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_check.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_radio.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_dropdown.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/form_replacement/form_selectoption.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/couchpotato.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/history.js') }}"></script>
@ -19,6 +24,13 @@
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/block/navigation.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/block/footer.js') }}"></script>
{% for url in fireEvent('clientscript.get_scripts', as_html = True, single = True) %}
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>
{% endfor %}
{% for url in fireEvent('clientscript.get_styles', as_html = True, single = True) %}
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">
{% endfor %}
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/wanted.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/settings.js') }}"></script>
@ -26,11 +38,6 @@
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/soon.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/manage.js') }}"></script>
{% for url in fireEvent('clientscript.get_scripts', as_html = True, single = True) %}
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %}
{% for url in fireEvent('clientscript.get_styles', as_html = True, single = True) %}
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %}
<link href="{{ url_for('.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
<link rel="apple-touch-icon" href="{{ url_for('.static', filename='images/homescreen.png') }}" />

Loading…
Cancel
Save