Browse Source

Merge branch 'refs/heads/develop'

pull/1515/merge
Ruud 12 years ago
parent
commit
4db1b57c70
  1. 1
      couchpotato/__init__.py
  2. 2
      couchpotato/core/event.py
  3. 25
      couchpotato/core/migration/versions/001_Releases_last_edit.py
  4. 3
      couchpotato/core/notifications/email/main.py
  5. 48
      couchpotato/core/notifications/pushalot/__init__.py
  6. 37
      couchpotato/core/notifications/pushalot/main.py
  7. 28
      couchpotato/core/plugins/dashboard/main.py
  8. 6
      couchpotato/core/plugins/library/main.py
  9. 39
      couchpotato/core/plugins/movie/main.py
  10. 29
      couchpotato/core/plugins/movie/static/list.js
  11. 699
      couchpotato/core/plugins/movie/static/movie.actions.js
  12. 23
      couchpotato/core/plugins/movie/static/movie.css
  13. 393
      couchpotato/core/plugins/movie/static/movie.js
  14. 3
      couchpotato/core/plugins/movie/static/search.css
  15. 2
      couchpotato/core/plugins/movie/static/search.js
  16. 6
      couchpotato/core/plugins/renamer/main.py
  17. 14
      couchpotato/core/plugins/searcher/main.py
  18. 2
      couchpotato/core/plugins/wizard/static/wizard.js
  19. 3
      couchpotato/core/settings/model.py
  20. BIN
      couchpotato/static/images/icon.readd.png
  21. 61
      couchpotato/static/scripts/page/home.js
  22. 2
      couchpotato/static/scripts/page/manage.js
  23. 281
      couchpotato/static/scripts/page/wanted.js
  24. 3
      couchpotato/static/style/main.css
  25. 53
      init/ubuntu
  26. 5
      init/ubuntu.default

1
couchpotato/__init__.py

@ -78,6 +78,7 @@ def page_not_found(error):
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url) r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
return redirect(r) return redirect(r)
else: else:
if not Env.get('dev'):
time.sleep(0.1) time.sleep(0.1)
return 'Wrong API key used', 404 return 'Wrong API key used', 404

2
couchpotato/core/event.py

@ -104,6 +104,8 @@ def fireEvent(name, *args, **kwargs):
# Merge # Merge
if options['merge'] and len(results) > 0: if options['merge'] and len(results) > 0:
results.reverse() # Priority 1 is higher then 100
# Dict # Dict
if isinstance(results[0], dict): if isinstance(results[0], dict):
merged = {} merged = {}

25
couchpotato/core/migration/versions/001_Releases_last_edit.py

@ -0,0 +1,25 @@
from migrate.changeset.schema import create_column
from sqlalchemy.schema import MetaData, Column, Table, Index
from sqlalchemy.types import Integer
meta = MetaData()
def upgrade(migrate_engine):
meta.bind = migrate_engine
# Change release, add last_edit and index
last_edit_column = Column('last_edit', Integer)
release = Table('release', meta, last_edit_column)
create_column(last_edit_column, release)
Index('ix_release_last_edit', release.c.last_edit).create()
# Change movie last_edit
last_edit_column = Column('last_edit', Integer)
movie = Table('movie', meta, last_edit_column)
Index('ix_movie_last_edit', movie.c.last_edit).create()
def downgrade(migrate_engine):
pass

3
couchpotato/core/notifications/email/main.py

@ -1,4 +1,5 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from email.mime.text import MIMEText from email.mime.text import MIMEText
@ -39,7 +40,7 @@ class Email(Notification):
# Send the e-mail # Send the e-mail
log.debug("Sending the email") log.debug("Sending the email")
mailserver.sendmail(from_address, to_address, message.as_string()) mailserver.sendmail(from_address, splitString(to_address), message.as_string())
# Close the SMTP connection # Close the SMTP connection
mailserver.quit() mailserver.quit()

48
couchpotato/core/notifications/pushalot/__init__.py

@ -0,0 +1,48 @@
from .main import Pushalot
def start():
return Pushalot()
config = [{
'name': 'pushalot',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'pushalot',
'description': 'for Windows Phone and Windows 8',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'auth_token',
'label': 'Auth Token',
},
{
'name': 'silent',
'label': 'Silent',
'default': 0,
'type': 'bool',
'description': 'Don\'t send Toast notifications. Only update Live Tile',
},
{
'name': 'important',
'label': 'High Priority',
'default': 0,
'type': 'bool',
'description': 'Send message with High priority.',
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]

37
couchpotato/core/notifications/pushalot/main.py

@ -0,0 +1,37 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import traceback
log = CPLog(__name__)
class Pushalot(Notification):
urls = {
'api': 'https://pushalot.com/api/sendmessage'
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = {
'AuthorizationToken': self.conf('auth_token'),
'Title': self.default_title,
'Body': toUnicode(message),
'LinkTitle': toUnicode("CouchPotato"),
'link': toUnicode("https://couchpota.to/"),
'IsImportant': self.conf('important'),
'IsSilent': self.conf('silent'),
}
headers = {
'Content-type': 'application/x-www-form-urlencoded'
}
try:
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
return True
except:
log.error('PushAlot failed: %s', traceback.format_exc())
return False

28
couchpotato/core/plugins/dashboard/main.py

@ -6,8 +6,9 @@ from couchpotato.core.helpers.variable import splitString, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie from couchpotato.core.settings.model import Movie
from sqlalchemy.sql.expression import or_ from sqlalchemy.orm import joinedload_all
import random import random
import time
log = CPLog(__name__) log = CPLog(__name__)
@ -41,7 +42,6 @@ class Dashboard(Plugin):
identifiers = [m.library.identifier for m in movies] identifiers = [m.library.identifier for m in movies]
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True) suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
print suggestions
return jsonified({ return jsonified({
'result': True, 'result': True,
@ -52,6 +52,7 @@ class Dashboard(Plugin):
params = getParams() params = getParams()
db = get_session() db = get_session()
now = time.time()
# Get profiles first, determine pre or post theater # Get profiles first, determine pre or post theater
profiles = fireEvent('profile.all', single = True) profiles = fireEvent('profile.all', single = True)
@ -72,10 +73,16 @@ class Dashboard(Plugin):
profile_pre[profile.get('id')] = contains profile_pre[profile.get('id')] = contains
# Get all active movies # Get all active movies
q = db.query(Movie) \ active_status = fireEvent('status.get', 'active', single = True)
.join(Movie.profile, Movie.library) \ subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery()
.filter(or_(*[Movie.status.has(identifier = s) for s in ['active']])) \
.group_by(Movie.id) q = db.query(Movie).join((subq, subq.c.id == Movie.id)) \
.options(joinedload_all('releases')) \
.options(joinedload_all('profile.types')) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files'))
# Add limit # Add limit
limit_offset = params.get('limit_offset') limit_offset = params.get('limit_offset')
@ -92,7 +99,7 @@ class Dashboard(Plugin):
movies = [] movies = []
for movie in all_movies: for movie in all_movies:
pp = profile_pre.get(movie.profile.id) pp = profile_pre.get(movie.profile.id)
eta = movie.library.info.get('release_date', {}) eta = movie.library.info.get('release_date', {}) or {}
coming_soon = False coming_soon = False
# Theater quality # Theater quality
@ -101,6 +108,7 @@ class Dashboard(Plugin):
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True): if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
coming_soon = True coming_soon = True
if coming_soon: if coming_soon:
temp = movie.to_dict({ temp = movie.to_dict({
'profile': {'types': {}}, 'profile': {'types': {}},
@ -108,6 +116,10 @@ class Dashboard(Plugin):
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
'files': {}, 'files': {},
}) })
# Don't list older movies
if ((not params.get('late') and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
movies.append(temp) movies.append(temp)
if len(movies) >= limit: if len(movies) >= limit:
@ -118,3 +130,5 @@ class Dashboard(Plugin):
'empty': len(movies) == 0, 'empty': len(movies) == 0,
'movies': movies, 'movies': movies,
}) })
getLateView = getSoonView

6
couchpotato/core/plugins/library/main.py

@ -38,7 +38,7 @@ class LibraryPlugin(Plugin):
title = LibraryTitle( title = LibraryTitle(
title = toUnicode(attrs.get('title')), title = toUnicode(attrs.get('title')),
simple_title = self.simplifyTitle(attrs.get('title')) simple_title = self.simplifyTitle(attrs.get('title')),
) )
l.titles.append(title) l.titles.append(title)
@ -96,6 +96,7 @@ class LibraryPlugin(Plugin):
titles = info.get('titles', []) titles = info.get('titles', [])
log.debug('Adding titles: %s', titles) log.debug('Adding titles: %s', titles)
counter = 0
for title in titles: for title in titles:
if not title: if not title:
continue continue
@ -103,9 +104,10 @@ class LibraryPlugin(Plugin):
t = LibraryTitle( t = LibraryTitle(
title = title, title = title,
simple_title = self.simplifyTitle(title), simple_title = self.simplifyTitle(title),
default = title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title) default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
) )
library.titles.append(t) library.titles.append(t)
counter += 1
db.commit() db.commit()

39
couchpotato/core/plugins/movie/main.py

@ -12,6 +12,7 @@ from couchpotato.environment import Env
from sqlalchemy.orm import joinedload_all from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_, desc from sqlalchemy.sql.expression import or_, asc, not_, desc
from string import ascii_lowercase from string import ascii_lowercase
import time
log = CPLog(__name__) log = CPLog(__name__)
@ -96,33 +97,34 @@ class MoviePlugin(Plugin):
addEvent('movie.list', self.list) addEvent('movie.list', self.list)
addEvent('movie.restatus', self.restatus) addEvent('movie.restatus', self.restatus)
# Clean releases that didn't have activity in the last week
addEvent('app.load', self.cleanReleases) addEvent('app.load', self.cleanReleases)
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanReleases, hours = 4)
def cleanReleases(self): def cleanReleases(self):
prop_name = 'cleaned_releases' log.debug('Removing releases from dashboard')
already_cleaned = Env.prop(prop_name, default = False)
if already_cleaned:
return True
log.info('Removing releases from library movies')
db = get_session() now = time.time()
week = 262080
movies = db.query(Movie).all()
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
snatched_status = fireEvent('status.get', 'snatched', single = True) snatched_status = fireEvent('status.get', 'snatched', single = True)
db = get_session()
# get movies last_edit more than a week ago
movies = db.query(Movie) \
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
.all()
#
for movie in movies: for movie in movies:
if movie.status_id == done_status.get('id'):
for rel in movie.releases: for rel in movie.releases:
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]: if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
fireEvent('release.delete', id = rel.id, single = True) fireEvent('release.delete', id = rel.id, single = True)
Env.prop(prop_name, True)
def getView(self): def getView(self):
movie_id = getParam('id') movie_id = getParam('id')
@ -366,7 +368,9 @@ class MoviePlugin(Plugin):
# Status # Status
status_active = fireEvent('status.add', 'active', single = True) status_active = fireEvent('status.add', 'active', single = True)
status_snatched = fireEvent('status.add', 'snatched', single = True) snatched_status = fireEvent('status.add', 'snatched', single = True)
ignored_status = fireEvent('status.add', 'ignored', single = True)
downloaded_status = fireEvent('status.add', 'downloaded', single = True)
default_profile = fireEvent('profile.default', single = True) default_profile = fireEvent('profile.default', single = True)
@ -390,10 +394,14 @@ class MoviePlugin(Plugin):
fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete) fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
search_after = False search_after = False
elif force_readd: elif force_readd:
# Clean snatched history # Clean snatched history
for release in m.releases: for release in m.releases:
if release.status_id == status_snatched.get('id'): if release.status_id in [downloaded_status.get('id'), snatched_status.get('id')]:
release.delete() if params.get('ignore_previous', False):
release.status_id = ignored_status.get('id')
else:
fireEvent('release.delete', release.id, single = True)
m.profile_id = params.get('profile_id', default_profile.get('id')) m.profile_id = params.get('profile_id', default_profile.get('id'))
else: else:
@ -402,6 +410,7 @@ class MoviePlugin(Plugin):
if force_readd: if force_readd:
m.status_id = status_id if status_id else status_active.get('id') m.status_id = status_id if status_id else status_active.get('id')
m.last_edit = int(time.time())
do_search = True do_search = True
db.commit() db.commit()

29
couchpotato/core/plugins/movie/static/list.js

@ -27,7 +27,12 @@ var MovieList = new Class({
self.el = new Element('div.movies').adopt( self.el = new Element('div.movies').adopt(
self.title = self.options.title ? new Element('h2', { self.title = self.options.title ? new Element('h2', {
'text': self.options.title 'text': self.options.title,
'styles': {'display': 'none'}
}) : null,
self.description = self.options.description ? new Element('div.description', {
'html': self.options.description,
'styles': {'display': 'none'}
}) : null, }) : null,
self.movie_list = new Element('div'), self.movie_list = new Element('div'),
self.load_more = self.options.load_more ? new Element('a.load_more', { self.load_more = self.options.load_more ? new Element('a.load_more', {
@ -37,7 +42,7 @@ var MovieList = new Class({
}) : null }) : null
); );
self.changeView(self.options.view || 'details'); self.changeView(self.getSavedView() || self.options.view || 'details');
self.getMovies(); self.getMovies();
@ -121,18 +126,14 @@ var MovieList = new Class({
createMovie: function(movie, inject_at){ createMovie: function(movie, inject_at){
var self = this; var self = this;
// Attach proper actions
var a = self.options.actions,
status = Status.get(movie.status_id),
actions = a ? a[status.identifier.capitalize()] || a.Wanted : {};
var m = new Movie(self, { var m = new Movie(self, {
'actions': actions, 'actions': self.options.actions,
'view': self.current_view, 'view': self.current_view,
'onSelect': self.calculateSelected.bind(self) 'onSelect': self.calculateSelected.bind(self)
}, movie); }, movie);
$(m).inject(self.movie_list, inject_at || 'bottom'); $(m).inject(self.movie_list, inject_at || 'bottom');
m.fireEvent('injected'); m.fireEvent('injected');
self.movies.include(m) self.movies.include(m)
@ -398,8 +399,11 @@ var MovieList = new Class({
var self = this; var self = this;
self.movies = [] self.movies = []
if(self.mass_edit_select)
self.calculateSelected() self.calculateSelected()
if(self.navigation_alpha)
self.navigation_alpha.getElements('.active').removeClass('active') self.navigation_alpha.getElements('.active').removeClass('active')
self.offset = 0; self.offset = 0;
if(self.scrollspy){ if(self.scrollspy){
self.load_more.show(); self.load_more.show();
@ -430,7 +434,7 @@ var MovieList = new Class({
getSavedView: function(){ getSavedView: function(){
var self = this; var self = this;
return Cookie.read(self.options.identifier+'_view') || 'thumbs'; return Cookie.read(self.options.identifier+'_view') || 'details';
}, },
search: function(){ search: function(){
@ -505,11 +509,14 @@ var MovieList = new Class({
checkIfEmpty: function(){ checkIfEmpty: function(){
var self = this; var self = this;
var is_empty = self.movies.length == 0 && self.total_movies == 0; var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
if(self.title) if(self.title)
self.title[is_empty ? 'hide' : 'show']() self.title[is_empty ? 'hide' : 'show']()
if(self.description)
self.description[is_empty ? 'hide' : 'show']()
if(is_empty && self.options.on_empty_element){ if(is_empty && self.options.on_empty_element){
self.el.grab(self.options.on_empty_element); self.el.grab(self.options.on_empty_element);

699
couchpotato/core/plugins/movie/static/movie.actions.js

@ -0,0 +1,699 @@
var MovieAction = new Class({
class_name: 'action icon',
initialize: function(movie){
var self = this;
self.movie = movie;
self.create();
if(self.el)
self.el.addClass(self.class_name)
},
create: function(){},
disable: function(){
this.el.addClass('disable')
},
enable: function(){
this.el.removeClass('disable')
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.movie, 'top').fade('hide');
//self.positionMask();
},
positionMask: function(){
var self = this,
movie = $(self.movie),
s = movie.getSize()
return;
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': movie
})
},
toElement: function(){
return this.el || null
}
});
var MA = {};
MA.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(),
'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank'
});
if(!self.id) self.disable();
}
});
MA.Release = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.releases.icon.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': {
'click': self.show.bind(self)
}
});
if(self.movie.data.releases.length == 0){
self.el.hide()
}
else {
var buttons_done = false;
self.movie.data.releases.sortBy('-info.score').each(function(release){
if(buttons_done) return;
var status = Status.get(release.status_id);
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
self.hide_on_click = false;
self.show();
buttons_done = true;
}
});
}
},
show: function(e){
var self = this;
if(e)
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.release_container = new Element('div.releases.table').adopt(
self.trynext_container = new Element('div.buttons.try_container')
)
).inject(self.movie, 'top');
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'Release name'}),
new Element('span.status', {'text': 'Status'}),
new Element('span.quality', {'text': 'Quality'}),
new Element('span.size', {'text': 'Size'}),
new Element('span.age', {'text': 'Age'}),
new Element('span.score', {'text': 'Score'}),
new Element('span.provider', {'text': 'Provider'})
).inject(self.release_container)
self.movie.data.releases.sortBy('-info.score').each(function(release){
var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id) || {},
info = release.info,
provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : '');
release.status = status;
var release_name = self.get(release, 'name');
if(release.files && release.files.length > 0){
try {
var movie_file = release.files.filter(function(file){
var type = File.Type.get(file.type_id);
return type && type.identifier == 'movie'
}).pick();
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
}
catch(e){}
}
// Create release
new Element('div', {
'class': 'item '+status.identifier,
'id': 'release_'+release.id
}).adopt(
new Element('span.name', {'text': release_name, 'title': release_name}),
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', { 'text': provider, 'title': provider }),
release.info['detail_url'] ? new Element('a.info.icon', {
'href': release.info['detail_url'],
'target': '_blank'
}) : null,
new Element('a.download.icon', {
'events': {
'click': function(e){
(e).preventDefault();
if(!this.hasClass('completed'))
self.download(release);
}
}
}),
new Element('a.delete.icon', {
'events': {
'click': function(e){
(e).preventDefault();
self.ignore(release);
this.getParent('.item').toggleClass('ignored')
}
}
})
).inject(self.release_container)
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
});
if(self.last_release){
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
}
if(self.next_release){
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
}
if(self.next_release || self.last_release){
self.trynext_container.adopt(
new Element('span.or', {
'text': 'This movie is snatched, if anything went wrong, download'
}),
self.last_release ? new Element('a.button.orange', {
'text': 'the same release again',
'events': {
'click': self.trySameRelease.bind(self)
}
}) : null,
self.next_release && self.last_release ? new Element('span.or', {
'text': ','
}) : null,
self.next_release ? [new Element('a.button.green', {
'text': self.last_release ? 'another release' : 'the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('span.or', {
'text': 'or pick one below'
})] : null
)
}
}
self.movie.slide('in', self.options_container);
},
get: function(release, type){
return release.info[type] || 'n/a'
},
download: function(release){
var self = this;
var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon');
icon.addClass('spinner');
Api.request('release.download', {
'data': {
'id': release.id
},
'onComplete': function(json){
icon.removeClass('spinner')
if(json.success)
icon.addClass('completed');
else
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
}
});
},
ignore: function(release){
var self = this;
Api.request('release.ignore', {
'data': {
'id': release.id
}
})
},
tryNextRelease: function(movie_id){
var self = this;
if(self.last_release)
self.ignore(self.last_release);
if(self.next_release)
self.download(self.next_release);
},
trySameRelease: function(movie_id){
var self = this;
if(self.last_release)
self.download(self.last_release);
}
});
MA.Trailer = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.el = new Element('a.trailer', {
'title': 'Watch the trailer of ' + self.movie.getTitle(),
'events': {
'click': self.watch.bind(self)
}
});
},
watch: function(offset){
var self = this;
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
var url = data_url.substitute({
'title': encodeURI(self.movie.getTitle()),
'year': self.movie.get('year'),
'offset': offset || 1
}),
size = $(self.movie).getSize(),
height = (size.x/16)*9,
id = 'trailer-'+randomString();
self.player_container = new Element('div[id='+id+']');
self.container = new Element('div.hide.trailer_container')
.adopt(self.player_container)
.inject($(self.movie), 'top');
self.container.setStyle('height', 0);
self.container.removeClass('hide');
self.close_button = new Element('a.hide.hide_trailer', {
'text': 'Hide trailer',
'events': {
'click': self.stop.bind(self)
}
}).inject(self.movie);
self.container.setStyle('height', height);
$(self.movie).setStyle('height', height);
new Request.JSONP({
'url': url,
'onComplete': function(json){
var video_url = json.feed.entry[0].id.$t.split('/'),
video_id = video_url[video_url.length-1];
self.player = new YT.Player(id, {
'height': height,
'width': size.x,
'videoId': video_id,
'playerVars': {
'autoplay': 1,
'showsearch': 0,
'wmode': 'transparent',
'iv_load_policy': 3
}
});
self.close_button.removeClass('hide');
var quality_set = false;
var change_quality = function(state){
if(!quality_set && (state.data == 1 || state.data || 2)){
try {
self.player.setPlaybackQuality('hd720');
quality_set = true;
}
catch(e){
}
}
}
self.player.addEventListener('onStateChange', change_quality);
}
}).send()
},
stop: function(){
var self = this;
self.player.stopVideo();
self.container.addClass('hide');
self.close_button.addClass('hide');
$(self.movie).setStyle('height', null);
setTimeout(function(){
self.container.destroy()
self.close_button.destroy();
}, 1800)
}
});
MA.Edit = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.edit', {
'title': 'Change movie information, like title and quality.',
'events': {
'click': self.editMovie.bind(self)
}
});
},
editMovie: function(e){
var self = this;
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
new Element('div.form').adopt(
self.title_select = new Element('select', {
'name': 'title'
}),
self.profile_select = new Element('select', {
'name': 'profile'
}),
new Element('a.button.edit', {
'text': 'Save & Search',
'events': {
'click': self.save.bind(self)
}
})
)
).inject(self.movie, 'top');
Array.each(self.movie.data.library.titles, function(alt){
new Element('option', {
'text': alt.title
}).inject(self.title_select);
if(alt['default'])
self.title_select.set('value', alt.title);
});
Quality.getActiveProfiles().each(function(profile){
var profile_id = profile.id ? profile.id : profile.data.id;
new Element('option', {
'value': profile_id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select);
if(self.movie.profile && self.movie.profile.data && self.movie.profile.data.id == profile_id)
self.profile_select.set('value', profile_id);
});
}
self.movie.slide('in', self.options_container);
},
save: function(e){
(e).preventDefault();
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),
'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'));
}
});
self.movie.slide('out');
}
})
MA.Refresh = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.refresh', {
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.doRefresh.bind(self)
}
});
},
doRefresh: function(e){
var self = this;
(e).preventDefault();
Api.request('movie.refresh', {
'data': {
'id': self.movie.get('id')
}
});
}
});
MA.Readd = new Class({
Extends: MovieAction,
create: function(){
var self = this;
var movie_done = Status.get(self.movie.data.status_id).identifier == 'done';
if(!movie_done)
var snatched = self.movie.data.releases.filter(function(release){
return release.status && (release.status.identifier == 'snatched' || release.status.identifier == 'downloaded' || release.status.identifier == 'done');
}).length;
if(movie_done || snatched && snatched > 0)
self.el = new Element('a.readd', {
'title': 'Readd the movie and mark all previous snatched/downloaded as ignored',
'events': {
'click': self.doReadd.bind(self)
}
});
},
doReadd: function(e){
var self = this;
(e).preventDefault();
Api.request('movie.add', {
'data': {
'identifier': self.movie.get('identifier'),
'ignore_previous': 1
}
});
}
});
MA.Delete = new Class({
Extends: MovieAction,
Implements: [Chain],
create: function(){
var self = this;
self.el = new Element('a.delete', {
'title': 'Remove the movie from this CP list',
'events': {
'click': self.showConfirm.bind(self)
}
});
},
showConfirm: function(e){
var self = this;
(e).preventDefault();
if(!self.delete_container){
self.delete_container = new Element('div.buttons.delete_container').adopt(
new Element('a.cancel', {
'text': 'Cancel',
'events': {
'click': self.hideConfirm.bind(self)
}
}),
new Element('span.or', {
'text': 'or'
}),
new Element('a.button.delete', {
'text': 'Delete ' + self.movie.title.get('text'),
'events': {
'click': self.del.bind(self)
}
})
).inject(self.movie, 'top');
}
self.movie.slide('in', self.delete_container);
},
hideConfirm: function(e){
var self = this;
(e).preventDefault();
self.movie.slide('out');
},
del: function(e){
(e).preventDefault();
var self = this;
var movie = $(self.movie);
self.chain(
function(){
self.callChain();
},
function(){
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id'),
'delete_from': self.movie.list.options.identifier
},
'onComplete': function(){
movie.set('tween', {
'duration': 300,
'onComplete': function(){
self.movie.destroy()
}
});
movie.tween('height', 0);
}
});
}
);
self.callChain();
}
});
MA.Files = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.directory', {
'title': 'Available files',
'events': {
'click': self.showFiles.bind(self)
}
});
},
showFiles: function(e){
var self = this;
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.files_container = new Element('div.files.table')
).inject(self.movie, 'top');
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'File'}),
new Element('span.type', {'text': 'Type'}),
new Element('span.is_available', {'text': 'Available'})
).inject(self.files_container)
Array.each(self.movie.data.releases, function(release){
var rel = new Element('div.release').inject(self.files_container);
Array.each(release.files, function(file){
new Element('div.file.item').adopt(
new Element('span.name', {'text': file.path}),
new Element('span.type', {'text': File.Type.get(file.type_id).name}),
new Element('span.available', {'text': file.available})
).inject(rel)
});
});
}
self.movie.slide('in', self.options_container);
},
});

23
couchpotato/core/plugins/movie/static/movie.css

@ -1,11 +1,25 @@
.movies { .movies {
padding: 60px 0 20px; padding: 60px 0 20px;
position: relative;
z-index: 3;
} }
.movies h2 { .movies h2 {
margin-bottom: 20px; margin-bottom: 20px;
} }
.movies > .description {
position: absolute;
top: 30px;
right: 0;
font-style: italic;
text-shadow: none;
opacity: 0.8;
}
.movies:hover > .description {
opacity: 1;
}
.movies.thumbs_list { .movies.thumbs_list {
padding: 20px 0 20px; padding: 20px 0 20px;
} }
@ -272,7 +286,7 @@
text-align: right; text-align: right;
right: 0; right: 0;
margin-right: 50px; margin-right: 50px;
z-index: 2; z-index: 1;
} }
.movies .data .quality .available, .movies .data .quality .available,
@ -303,6 +317,11 @@
line-height: 0; line-height: 0;
margin-top: -25px; margin-top: -25px;
} }
.movies.thumbs_list .data .actions {
bottom: 8px;
right: 10px;
}
.movies .data:hover .action { opacity: 0.6; } .movies .data:hover .action { opacity: 0.6; }
.movies .data:hover .action:hover { opacity: 1; } .movies .data:hover .action:hover { opacity: 1; }
.movies.mass_edit_list .data .actions { .movies.mass_edit_list .data .actions {
@ -505,7 +524,7 @@
.movies .alph_nav { .movies .alph_nav {
transition: box-shadow .4s linear; transition: box-shadow .4s linear;
position: fixed; position: fixed;
z-index: 2; z-index: 4;
top: 0; top: 0;
padding: 100px 60px 7px; padding: 100px 60px 7px;
width: 1080px; width: 1080px;

393
couchpotato/core/plugins/movie/static/movie.js

@ -80,7 +80,7 @@ var Movie = new Class({
var self = this; var self = this;
self.mask = new Element('div.mask', { self.mask = new Element('div.mask', {
'styles': { 'styles': {
'z-index': '1' 'z-index': 4
} }
}).inject(self.el, 'top').fade('hide'); }).inject(self.el, 'top').fade('hide');
}, },
@ -291,394 +291,3 @@ var Movie = new Class({
} }
}); });
var MovieAction = new Class({
class_name: 'action icon',
initialize: function(movie){
var self = this;
self.movie = movie;
self.create();
if(self.el)
self.el.addClass(self.class_name)
},
create: function(){},
disable: function(){
this.el.addClass('disable')
},
enable: function(){
this.el.removeClass('disable')
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.movie, 'top').fade('hide');
//self.positionMask();
},
positionMask: function(){
var self = this,
movie = $(self.movie),
s = movie.getSize()
return;
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': movie
})
},
toElement: function(){
return this.el || null
}
});
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(),
'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank'
});
if(!self.id) self.disable();
}
});
var ReleaseAction = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.releases.icon.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': {
'click': self.show.bind(self)
}
});
if(self.movie.data.releases.length == 0){
self.el.hide()
}
else {
var buttons_done = false;
self.movie.data.releases.sortBy('-info.score').each(function(release){
if(buttons_done) return;
var status = Status.get(release.status_id);
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
self.hide_on_click = false;
self.show();
buttons_done = true;
}
});
}
},
show: function(e){
var self = this;
if(e)
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.release_container = new Element('div.releases.table').adopt(
self.trynext_container = new Element('div.buttons.try_container')
)
).inject(self.movie, 'top');
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'Release name'}),
new Element('span.status', {'text': 'Status'}),
new Element('span.quality', {'text': 'Quality'}),
new Element('span.size', {'text': 'Size'}),
new Element('span.age', {'text': 'Age'}),
new Element('span.score', {'text': 'Score'}),
new Element('span.provider', {'text': 'Provider'})
).inject(self.release_container)
self.movie.data.releases.sortBy('-info.score').each(function(release){
var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id) || {},
info = release.info,
provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : '');
release.status = status;
// Create release
new Element('div', {
'class': 'item '+status.identifier,
'id': 'release_'+release.id
}).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', { 'text': provider, 'title': provider }),
release.info['detail_url'] ? new Element('a.info.icon', {
'href': release.info['detail_url'],
'target': '_blank'
}) : null,
new Element('a.download.icon', {
'events': {
'click': function(e){
(e).preventDefault();
if(!this.hasClass('completed'))
self.download(release);
}
}
}),
new Element('a.delete.icon', {
'events': {
'click': function(e){
(e).preventDefault();
self.ignore(release);
this.getParent('.item').toggleClass('ignored')
}
}
})
).inject(self.release_container)
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
});
if(self.last_release){
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
}
if(self.next_release){
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
}
if(self.next_release || self.last_release){
self.trynext_container.adopt(
new Element('span.or', {
'text': 'This movie is snatched, if anything went wrong, download'
}),
self.last_release ? new Element('a.button.orange', {
'text': 'the same release again',
'events': {
'click': self.trySameRelease.bind(self)
}
}) : null,
self.next_release && self.last_release ? new Element('span.or', {
'text': ','
}) : null,
self.next_release ? [new Element('a.button.green', {
'text': self.last_release ? 'another release' : 'the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('span.or', {
'text': 'or pick one below'
})] : null
)
}
}
self.movie.slide('in', self.options_container);
},
get: function(release, type){
return release.info[type] || 'n/a'
},
download: function(release){
var self = this;
var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon');
icon.addClass('spinner');
Api.request('release.download', {
'data': {
'id': release.id
},
'onComplete': function(json){
icon.removeClass('spinner')
if(json.success)
icon.addClass('completed');
else
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
}
});
},
ignore: function(release){
var self = this;
Api.request('release.ignore', {
'data': {
'id': release.id
}
})
},
tryNextRelease: function(movie_id){
var self = this;
if(self.last_release)
self.ignore(self.last_release);
if(self.next_release)
self.download(self.next_release);
},
trySameRelease: function(movie_id){
var self = this;
if(self.last_release)
self.download(self.last_release);
}
});
var TrailerAction = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.el = new Element('a.trailer', {
'title': 'Watch the trailer of ' + self.movie.getTitle(),
'events': {
'click': self.watch.bind(self)
}
});
},
watch: function(offset){
var self = this;
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
var url = data_url.substitute({
'title': encodeURI(self.movie.getTitle()),
'year': self.movie.get('year'),
'offset': offset || 1
}),
size = $(self.movie).getSize(),
height = (size.x/16)*9,
id = 'trailer-'+randomString();
self.player_container = new Element('div[id='+id+']');
self.container = new Element('div.hide.trailer_container')
.adopt(self.player_container)
.inject($(self.movie), 'top');
self.container.setStyle('height', 0);
self.container.removeClass('hide');
self.close_button = new Element('a.hide.hide_trailer', {
'text': 'Hide trailer',
'events': {
'click': self.stop.bind(self)
}
}).inject(self.movie);
self.container.setStyle('height', height);
$(self.movie).setStyle('height', height);
new Request.JSONP({
'url': url,
'onComplete': function(json){
var video_url = json.feed.entry[0].id.$t.split('/'),
video_id = video_url[video_url.length-1];
self.player = new YT.Player(id, {
'height': height,
'width': size.x,
'videoId': video_id,
'playerVars': {
'autoplay': 1,
'showsearch': 0,
'wmode': 'transparent',
'iv_load_policy': 3
}
});
self.close_button.removeClass('hide');
var quality_set = false;
var change_quality = function(state){
if(!quality_set && (state.data == 1 || state.data || 2)){
try {
self.player.setPlaybackQuality('hd720');
quality_set = true;
}
catch(e){
}
}
}
self.player.addEventListener('onStateChange', change_quality);
}
}).send()
},
stop: function(){
var self = this;
self.player.stopVideo();
self.container.addClass('hide');
self.close_button.addClass('hide');
$(self.movie).setStyle('height', null);
setTimeout(function(){
self.container.destroy()
self.close_button.destroy();
}, 1800)
}
});

3
couchpotato/core/plugins/movie/static/search.css

@ -202,7 +202,8 @@
.movie_result .info h2 span:before { content: "("; } .movie_result .info h2 span:before { content: "("; }
.movie_result .info h2 span:after { content: ")"; } .movie_result .info h2 span:after { content: ")"; }
.search_form .mask { .search_form .mask,
.movie_result .mask {
border-radius: 3px; border-radius: 3px;
position: absolute; position: absolute;
height: 100%; height: 100%;

2
couchpotato/core/plugins/movie/static/search.js

@ -366,7 +366,7 @@ Block.Search.Item = new Class({
loadingMask: function(){ loadingMask: function(){
var self = this; var self = this;
self.mask = new Element('span.mask').inject(self.el).fade('hide') self.mask = new Element('div.mask').inject(self.el).fade('hide')
createSpinner(self.mask) createSpinner(self.mask)
self.mask.fade('in') self.mask.fade('in')

6
couchpotato/core/plugins/renamer/main.py

@ -13,6 +13,7 @@ import errno
import os import os
import re import re
import shutil import shutil
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@ -275,6 +276,7 @@ class Renamer(Plugin):
for profile_type in movie.profile.types: for profile_type in movie.profile.types:
if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish: if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish:
movie.status_id = done_status.get('id') movie.status_id = done_status.get('id')
movie.last_edit = int(time.time())
db.commit() db.commit()
except Exception, e: except Exception, e:
log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc())) log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
@ -316,8 +318,10 @@ class Renamer(Plugin):
log.debug('Marking release as downloaded') log.debug('Marking release as downloaded')
try: try:
release.status_id = downloaded_status.get('id') release.status_id = downloaded_status.get('id')
release.last_edit = int(time.time())
except Exception, e: except Exception, e:
log.error('Failed marking release as finished: %s %s', (e, traceback.format_exc())) log.error('Failed marking release as finished: %s %s', (e, traceback.format_exc()))
db.commit() db.commit()
# Remove leftover files # Remove leftover files
@ -556,6 +560,7 @@ class Renamer(Plugin):
if rel.movie.status_id == done_status.get('id'): if rel.movie.status_id == done_status.get('id'):
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title) log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
rel.status_id = ignored_status.get('id') rel.status_id = ignored_status.get('id')
rel.last_edit = int(time.time())
db.commit() db.commit()
continue continue
@ -580,6 +585,7 @@ class Renamer(Plugin):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id) fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
else: else:
rel.status_id = failed_status.get('id') rel.status_id = failed_status.get('id')
rel.last_edit = int(time.time())
db.commit() db.commit()
elif item['status'] == 'completed': elif item['status'] == 'completed':
log.info('Download of %s completed!', item['name']) log.info('Download of %s completed!', item['name'])

14
couchpotato/core/plugins/searcher/main.py

@ -165,7 +165,7 @@ class Searcher(Plugin):
# See if better quality is available # See if better quality is available
for release in movie['releases']: for release in movie['releases']:
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]: if release['quality']['order'] < quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]:
has_better_quality += 1 has_better_quality += 1
# Don't search for quality lower then already available. # Don't search for quality lower then already available.
@ -209,6 +209,7 @@ class Searcher(Plugin):
db.add(rls) db.add(rls)
else: else:
[db.delete(old_info) for old_info in rls.info] [db.delete(old_info) for old_info in rls.info]
rls.last_edit = int(time.time())
db.commit() db.commit()
@ -293,7 +294,10 @@ class Searcher(Plugin):
db = get_session() db = get_session()
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
if rls: if rls:
rls.status_id = snatched_status.get('id') renamer_enabled = Env.setting('enabled', 'renamer')
done_status = fireEvent('status.get', 'done', single = True)
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
db.commit() db.commit()
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label) log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
@ -302,22 +306,24 @@ class Searcher(Plugin):
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict()) fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
# If renamer isn't used, mark movie done # If renamer isn't used, mark movie done
if not Env.setting('enabled', 'renamer'): if not renamer_enabled:
active_status = fireEvent('status.get', 'active', single = True) active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
try: try:
if movie['status_id'] == active_status.get('id'): if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']: for profile_type in movie['profile']['types']:
if rls and profile_type['quality_id'] == rls.quality.id and profile_type['finish']: if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s', log_movie) log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# Mark release done # Mark release done
rls.status_id = done_status.get('id') rls.status_id = done_status.get('id')
rls.last_edit = int(time.time())
db.commit() db.commit()
# Mark movie done # Mark movie done
mvie = db.query(Movie).filter_by(id = movie['id']).first() mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = done_status.get('id') mvie.status_id = done_status.get('id')
mvie.last_edit = int(time.time())
db.commit() db.commit()
except: except:
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc()) log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())

2
couchpotato/core/plugins/wizard/static/wizard.js

@ -82,7 +82,7 @@ Page.Wizard = new Class({
'target': self.el 'target': self.el
}, },
'onComplete': function(){ 'onComplete': function(){
window.location = App.createUrl(); window.location = App.createUrl('wanted');
} }
}); });
} }

3
couchpotato/core/settings/model.py

@ -45,7 +45,7 @@ class Movie(Entity):
The files belonging to the movie object are global for the whole movie The files belonging to the movie object are global for the whole movie
such as trailers, nfo, thumbnails""" such as trailers, nfo, thumbnails"""
last_edit = Field(Integer, default = lambda: int(time.time())) last_edit = Field(Integer, default = lambda: int(time.time()), index = True)
library = ManyToOne('Library', cascade = 'delete, delete-orphan', single_parent = True) library = ManyToOne('Library', cascade = 'delete, delete-orphan', single_parent = True)
status = ManyToOne('Status') status = ManyToOne('Status')
@ -95,6 +95,7 @@ class Release(Entity):
"""Logically groups all files that belong to a certain release, such as """Logically groups all files that belong to a certain release, such as
parts of a movie, subtitles.""" parts of a movie, subtitles."""
last_edit = Field(Integer, default = lambda: int(time.time()), index = True)
identifier = Field(String(100), index = True) identifier = Field(String(100), index = True)
movie = ManyToOne('Movie') movie = ManyToOne('Movie')

BIN
couchpotato/static/images/icon.readd.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

61
couchpotato/static/scripts/page/home.js

@ -8,8 +8,14 @@ Page.Home = new Class({
indexAction: function(param){ indexAction: function(param){
var self = this; var self = this;
if(self.soon_list) if(self.soon_list){
// Reset lists
self.available_list.update();
self.late_list.update();
return return
}
// Snatched // Snatched
self.available_list = new MovieList({ self.available_list = new MovieList({
@ -17,48 +23,55 @@ Page.Home = new Class({
'identifier': 'snatched', 'identifier': 'snatched',
'load_more': false, 'load_more': false,
'view': 'list', 'view': 'list',
'actions': MovieActions, 'actions': [MA.IMDB, MA.Trailer, MA.Files, MA.Release, MA.Edit, MA.Readd, MA.Refresh, MA.Delete],
'title': 'Snatched & Available', 'title': 'Snatched & Available',
'on_empty_element': new Element('div'),
'filter': { 'filter': {
'release_status': 'snatched,available' 'release_status': 'snatched,available'
} }
}); });
// Downloaded // Coming Soon
// self.downloaded_list = new MovieList({
// 'navigation': false,
// 'identifier': 'downloaded',
// 'load_more': false,
// 'view': 'titles',
// 'filter': {
// 'release_status': 'done',
// 'order': 'release_order'
// }
// });
// self.el.adopt(
// new Element('h2', {
// 'text': 'Just downloaded'
// }),
// $(self.downloaded_list)
// );
// Comming Soon
self.soon_list = new MovieList({ self.soon_list = new MovieList({
'navigation': false, 'navigation': false,
'identifier': 'soon', 'identifier': 'soon',
'limit': 24, 'limit': 18,
'title': 'Soon', 'title': 'Available soon',
'description': 'These are being searches for and should be available soon as they will be released on DVD in the next few weeks.',
'on_empty_element': new Element('div').adopt(
new Element('h1', {'text': 'Available soon'}),
new Element('span', {'text': 'There are no movies available soon. Add some movies, so you have something to watch later.'})
),
'filter': { 'filter': {
'random': true 'random': true
}, },
'actions': [MA.IMDB, MA.Refresh],
'load_more': false, 'load_more': false,
'view': 'thumbs', 'view': 'thumbs',
'api_call': 'dashboard.soon' 'api_call': 'dashboard.soon'
}); });
// Still not available
self.late_list = new MovieList({
'navigation': false,
'identifier': 'late',
'limit': 50,
'title': 'Still not available',
'description': 'Try another quality profile or maybe add more providers in <a href="'+App.createUrl('settings/searcher/providers/')+'">Settings</a>.',
'on_empty_element': new Element('div'),
'filter': {
'late': true
},
'load_more': false,
'view': 'list',
'actions': [MA.IMDB, MA.Trailer, MA.Edit, MA.Refresh, MA.Delete],
'api_call': 'dashboard.soon'
});
self.el.adopt( self.el.adopt(
$(self.available_list), $(self.available_list),
$(self.soon_list) $(self.soon_list),
$(self.late_list)
); );
// Suggest // Suggest

2
couchpotato/static/scripts/page/manage.js

@ -30,7 +30,7 @@ Page.Manage = new Class({
'filter': { 'filter': {
'release_status': 'done' 'release_status': 'done'
}, },
'actions': MovieActions, 'actions': [MA.IMDB, MA.Trailer, MA.Files, MA.Readd, MA.Edit, MA.Delete],
'menu': [self.refresh_button, self.refresh_quick], 'menu': [self.refresh_button, self.refresh_quick],
'on_empty_element': new Element('div.empty_manage').adopt( 'on_empty_element': new Element('div.empty_manage').adopt(
new Element('div', { new Element('div', {

281
couchpotato/static/scripts/page/wanted.js

@ -22,7 +22,7 @@ Page.Wanted = new Class({
self.wanted = new MovieList({ self.wanted = new MovieList({
'identifier': 'wanted', 'identifier': 'wanted',
'status': 'active', 'status': 'active',
'actions': MovieActions, 'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Edit, MA.Refresh, MA.Readd, MA.Delete],
'add_new': true, 'add_new': true,
'menu': [self.manual_search], 'menu': [self.manual_search],
'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted') 'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted')
@ -72,282 +72,3 @@ Page.Wanted = new Class({
} }
}); });
var MovieActions = {};
window.addEvent('domready', function(){
MovieActions.Wanted = {
'IMDB': IMDBAction
,'Trailer': TrailerAction
,'Releases': ReleaseAction
,'Edit': new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.edit', {
'title': 'Change movie information, like title and quality.',
'events': {
'click': self.editMovie.bind(self)
}
});
},
editMovie: function(e){
var self = this;
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
new Element('div.form').adopt(
self.title_select = new Element('select', {
'name': 'title'
}),
self.profile_select = new Element('select', {
'name': 'profile'
}),
new Element('a.button.edit', {
'text': 'Save & Search',
'events': {
'click': self.save.bind(self)
}
})
)
).inject(self.movie, 'top');
Array.each(self.movie.data.library.titles, function(alt){
new Element('option', {
'text': alt.title
}).inject(self.title_select);
if(alt['default'])
self.title_select.set('value', alt.title);
});
Quality.getActiveProfiles().each(function(profile){
var profile_id = profile.id ? profile.id : profile.data.id;
new Element('option', {
'value': profile_id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select);
if(self.movie.profile && self.movie.profile.data && self.movie.profile.data.id == profile_id)
self.profile_select.set('value', profile_id);
});
}
self.movie.slide('in', self.options_container);
},
save: function(e){
(e).preventDefault();
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),
'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'));
}
});
self.movie.slide('out');
}
})
,'Refresh': new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.refresh', {
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.doRefresh.bind(self)
}
});
},
doRefresh: function(e){
var self = this;
(e).preventDefault();
Api.request('movie.refresh', {
'data': {
'id': self.movie.get('id')
}
});
}
})
,'Delete': new Class({
Extends: MovieAction,
Implements: [Chain],
create: function(){
var self = this;
self.el = new Element('a.delete', {
'title': 'Remove the movie from this CP list',
'events': {
'click': self.showConfirm.bind(self)
}
});
},
showConfirm: function(e){
var self = this;
(e).preventDefault();
if(!self.delete_container){
self.delete_container = new Element('div.buttons.delete_container').adopt(
new Element('a.cancel', {
'text': 'Cancel',
'events': {
'click': self.hideConfirm.bind(self)
}
}),
new Element('span.or', {
'text': 'or'
}),
new Element('a.button.delete', {
'text': 'Delete ' + self.movie.title.get('text'),
'events': {
'click': self.del.bind(self)
}
})
).inject(self.movie, 'top');
}
self.movie.slide('in', self.delete_container);
},
hideConfirm: function(e){
var self = this;
(e).preventDefault();
self.movie.slide('out');
},
del: function(e){
(e).preventDefault();
var self = this;
var movie = $(self.movie);
self.chain(
function(){
self.callChain();
},
function(){
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id'),
'delete_from': self.movie.list.options.identifier
},
'onComplete': function(){
movie.set('tween', {
'duration': 300,
'onComplete': function(){
self.movie.destroy()
}
});
movie.tween('height', 0);
}
});
}
);
self.callChain();
}
})
};
MovieActions.Snatched = {
'IMDB': IMDBAction
,'Delete': MovieActions.Wanted.Delete
};
MovieActions.Done = {
'IMDB': IMDBAction
,'Edit': MovieActions.Wanted.Edit
,'Trailer': TrailerAction
,'Files': new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.directory', {
'title': 'Available files',
'events': {
'click': self.showFiles.bind(self)
}
});
},
showFiles: function(e){
var self = this;
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.files_container = new Element('div.files.table')
).inject(self.movie, 'top');
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'File'}),
new Element('span.type', {'text': 'Type'}),
new Element('span.is_available', {'text': 'Available'})
).inject(self.files_container)
Array.each(self.movie.data.releases, function(release){
var rel = new Element('div.release').inject(self.files_container);
Array.each(release.files, function(file){
new Element('div.file.item').adopt(
new Element('span.name', {'text': file.path}),
new Element('span.type', {'text': File.Type.get(file.type_id).name}),
new Element('span.available', {'text': file.available})
).inject(rel)
});
});
}
self.movie.slide('in', self.options_container);
},
})
,'Delete': MovieActions.Wanted.Delete
};
})

3
couchpotato/static/style/main.css

@ -157,6 +157,7 @@ body > .spinner, .mask{
.icon.folder { background-image: url('../images/icon.folder.png'); } .icon.folder { background-image: url('../images/icon.folder.png'); }
.icon.imdb { background-image: url('../images/icon.imdb.png'); } .icon.imdb { background-image: url('../images/icon.imdb.png'); }
.icon.refresh { background-image: url('../images/icon.refresh.png'); } .icon.refresh { background-image: url('../images/icon.refresh.png'); }
.icon.readd { background-image: url('../images/icon.readd.png'); }
.icon.rating { background-image: url('../images/icon.rating.png'); } .icon.rating { background-image: url('../images/icon.rating.png'); }
.icon.files { background-image: url('../images/icon.files.png'); } .icon.files { background-image: url('../images/icon.files.png'); }
.icon.info { background-image: url('../images/icon.info.png'); } .icon.info { background-image: url('../images/icon.info.png'); }
@ -584,7 +585,7 @@ body > .spinner, .mask{
bottom: 0; bottom: 0;
padding: 2px; padding: 2px;
width: 240px; width: 240px;
z-index: 2; z-index: 20;
overflow: hidden; overflow: hidden;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;

53
init/ubuntu

@ -12,51 +12,56 @@
# Description: starts instance of CouchPotato using start-stop-daemon # Description: starts instance of CouchPotato using start-stop-daemon
### END INIT INFO ### END INIT INFO
############### EDIT ME ################## # Check for existance of defaults file
# path to app # and utilze if available
APP_PATH=/usr/local/sbin/CouchPotatoServer/ if [ -f /etc/default/couchpotato ]; then
. /etc/default/couchpotato
else
echo "/etc/default/couchpotato not found using default settings.";
fi
# user # Script name
RUN_AS=YOUR_USERNAME_HERE NAME=couchpotato
# path to python bin # App name
DAEMON=/usr/bin/python DESC=CouchPotato
# Path to store PID file # Path to app root
PID_FILE=/var/run/couchpotato.pid CP_APP_PATH=${APP_PATH-/usr/local/sbin/CouchPotatoServer/}
# script name # User to run CP as
NAME=couchpotato CP_RUN_AS=${RUN_AS-root}
# app name # Path to python bin
DESC=CouchPotato CP_DAEMON=${DAEMON_PATH-/usr/bin/python}
# startup args # Path to store PID file
DAEMON_OPTS=" CouchPotato.py --daemon --pid_file=${PID_FILE}" CP_PID_FILE=${PID_FILE-/var/run/couchpotato.pid}
############### END EDIT ME ################## # Other startup args
CP_DAEMON_OPTS=" CouchPotato.py --daemon --pid_file=${CP_PID_FILE}"
test -x $DAEMON || exit 0 test -x $CP_DAEMON || exit 0
set -e set -e
case "$1" in case "$1" in
start) start)
echo "Starting $DESC" echo "Starting $DESC"
rm -rf $PID_FILE || return 1 rm -rf $CP_PID_FILE || return 1
touch $PID_FILE touch $CP_PID_FILE
chown $RUN_AS $PID_FILE chown $CP_RUN_AS $CP_PID_FILE
start-stop-daemon -d $APP_PATH -c $RUN_AS --start --background --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS start-stop-daemon -d $CP_APP_PATH -c $CP_RUN_AS --start --background --pidfile $CP_PID_FILE --exec $CP_DAEMON -- $CP_DAEMON_OPTS
;; ;;
stop) stop)
echo "Stopping $DESC" echo "Stopping $DESC"
start-stop-daemon --stop --pidfile $PID_FILE --retry 15 start-stop-daemon --stop --pidfile $CP_PID_FILE --retry 15
;; ;;
restart|force-reload) restart|force-reload)
echo "Restarting $DESC" echo "Restarting $DESC"
start-stop-daemon --stop --pidfile $PID_FILE --retry 15 start-stop-daemon --stop --pidfile $CP_PID_FILE --retry 15
start-stop-daemon -d $APP_PATH -c $RUN_AS --start --background --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS start-stop-daemon -d $CP_APP_PATH -c $CP_RUN_AS --start --background --pidfile $CP_PID_FILE --exec $CP_DAEMON -- $CP_DAEMON_OPTS
;; ;;
*) *)
N=/etc/init.d/$NAME N=/etc/init.d/$NAME

5
init/ubuntu.default

@ -0,0 +1,5 @@
# COPY THIS FILE TO /etc/default/couchpotato
# OPTIONS: APP_PATH, RUN_AS, DAEMON_PATH, CP_PID_FILE
APP_PATH=
RUN_AS=root
Loading…
Cancel
Save