diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index 38b3617..7058fac 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -78,6 +78,7 @@ def page_not_found(error): r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url) return redirect(r) else: - time.sleep(0.1) + if not Env.get('dev'): + time.sleep(0.1) return 'Wrong API key used', 404 diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py index aa05ce0..bd704f5 100644 --- a/couchpotato/core/event.py +++ b/couchpotato/core/event.py @@ -104,6 +104,8 @@ def fireEvent(name, *args, **kwargs): # Merge if options['merge'] and len(results) > 0: + results.reverse() # Priority 1 is higher then 100 + # Dict if isinstance(results[0], dict): merged = {} diff --git a/couchpotato/core/migration/versions/001_Releases_last_edit.py b/couchpotato/core/migration/versions/001_Releases_last_edit.py new file mode 100644 index 0000000..d4b1208 --- /dev/null +++ b/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 diff --git a/couchpotato/core/notifications/email/main.py b/couchpotato/core/notifications/email/main.py index be61e94..014b585 100644 --- a/couchpotato/core/notifications/email/main.py +++ b/couchpotato/core/notifications/email/main.py @@ -1,4 +1,5 @@ from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import splitString from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification from email.mime.text import MIMEText @@ -39,7 +40,7 @@ class Email(Notification): # Send the e-mail 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 mailserver.quit() diff --git a/couchpotato/core/notifications/pushalot/__init__.py b/couchpotato/core/notifications/pushalot/__init__.py new file mode 100644 index 0000000..a2a297a --- /dev/null +++ b/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.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/pushalot/main.py b/couchpotato/core/notifications/pushalot/main.py new file mode 100644 index 0000000..3b11331 --- /dev/null +++ b/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 diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py index d5f9ef0..d14eb5f 100644 --- a/couchpotato/core/plugins/dashboard/main.py +++ b/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.plugins.base import Plugin from couchpotato.core.settings.model import Movie -from sqlalchemy.sql.expression import or_ +from sqlalchemy.orm import joinedload_all import random +import time log = CPLog(__name__) @@ -41,7 +42,6 @@ class Dashboard(Plugin): identifiers = [m.library.identifier for m in movies] suggestions = fireEvent('movie.suggest', movies = identifiers, single = True) - print suggestions return jsonified({ 'result': True, @@ -52,6 +52,7 @@ class Dashboard(Plugin): params = getParams() db = get_session() + now = time.time() # Get profiles first, determine pre or post theater profiles = fireEvent('profile.all', single = True) @@ -72,10 +73,16 @@ class Dashboard(Plugin): profile_pre[profile.get('id')] = contains # Get all active movies - q = db.query(Movie) \ - .join(Movie.profile, Movie.library) \ - .filter(or_(*[Movie.status.has(identifier = s) for s in ['active']])) \ - .group_by(Movie.id) + active_status = fireEvent('status.get', 'active', single = True) + subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery() + + 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 limit_offset = params.get('limit_offset') @@ -92,7 +99,7 @@ class Dashboard(Plugin): movies = [] for movie in all_movies: 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 # Theater quality @@ -101,6 +108,7 @@ class Dashboard(Plugin): if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True): coming_soon = True + if coming_soon: temp = movie.to_dict({ 'profile': {'types': {}}, @@ -108,7 +116,11 @@ class Dashboard(Plugin): 'library': {'titles': {}, 'files':{}}, 'files': {}, }) - movies.append(temp) + + # 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) if len(movies) >= limit: break @@ -118,3 +130,5 @@ class Dashboard(Plugin): 'empty': len(movies) == 0, 'movies': movies, }) + + getLateView = getSoonView diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py index aa1611d..b91ebe1 100644 --- a/couchpotato/core/plugins/library/main.py +++ b/couchpotato/core/plugins/library/main.py @@ -38,7 +38,7 @@ class LibraryPlugin(Plugin): title = LibraryTitle( title = toUnicode(attrs.get('title')), - simple_title = self.simplifyTitle(attrs.get('title')) + simple_title = self.simplifyTitle(attrs.get('title')), ) l.titles.append(title) @@ -96,6 +96,7 @@ class LibraryPlugin(Plugin): titles = info.get('titles', []) log.debug('Adding titles: %s', titles) + counter = 0 for title in titles: if not title: continue @@ -103,9 +104,10 @@ class LibraryPlugin(Plugin): t = LibraryTitle( title = 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) + counter += 1 db.commit() diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index c708ec4..5ec968c 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -12,6 +12,7 @@ from couchpotato.environment import Env from sqlalchemy.orm import joinedload_all from sqlalchemy.sql.expression import or_, asc, not_, desc from string import ascii_lowercase +import time log = CPLog(__name__) @@ -96,32 +97,33 @@ class MoviePlugin(Plugin): addEvent('movie.list', self.list) addEvent('movie.restatus', self.restatus) + # Clean releases that didn't have activity in the last week addEvent('app.load', self.cleanReleases) + fireEvent('schedule.interval', 'movie.clean_releases', self.cleanReleases, hours = 4) def cleanReleases(self): - prop_name = 'cleaned_releases' - already_cleaned = Env.prop(prop_name, default = False) - if already_cleaned: - return True + log.debug('Removing releases from dashboard') - log.info('Removing releases from library movies') - - db = get_session() - - movies = db.query(Movie).all() + now = time.time() + week = 262080 done_status = fireEvent('status.get', 'done', single = True) available_status = fireEvent('status.get', 'available', single = True) snatched_status = fireEvent('status.get', 'snatched', single = True) - for movie in movies: - if movie.status_id == done_status.get('id'): - for rel in movie.releases: - if rel.status_id in [available_status.get('id'), snatched_status.get('id')]: - fireEvent('release.delete', id = rel.id, 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() - Env.prop(prop_name, True) + # + for movie in movies: + for rel in movie.releases: + if rel.status_id in [available_status.get('id'), snatched_status.get('id')]: + fireEvent('release.delete', id = rel.id, single = True) def getView(self): @@ -366,7 +368,9 @@ class MoviePlugin(Plugin): # Status 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) @@ -390,10 +394,14 @@ class MoviePlugin(Plugin): fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete) search_after = False elif force_readd: + # Clean snatched history for release in m.releases: - if release.status_id == status_snatched.get('id'): - release.delete() + if release.status_id in [downloaded_status.get('id'), snatched_status.get('id')]: + 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')) else: @@ -402,6 +410,7 @@ class MoviePlugin(Plugin): if force_readd: m.status_id = status_id if status_id else status_active.get('id') + m.last_edit = int(time.time()) do_search = True db.commit() diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index 9e76fad..d8c7fa8 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -27,7 +27,12 @@ var MovieList = new Class({ self.el = new Element('div.movies').adopt( 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, self.movie_list = new Element('div'), self.load_more = self.options.load_more ? new Element('a.load_more', { @@ -37,7 +42,7 @@ var MovieList = new Class({ }) : null ); - self.changeView(self.options.view || 'details'); + self.changeView(self.getSavedView() || self.options.view || 'details'); self.getMovies(); @@ -121,18 +126,14 @@ var MovieList = new Class({ createMovie: function(movie, inject_at){ 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, { - 'actions': actions, + 'actions': self.options.actions, 'view': self.current_view, 'onSelect': self.calculateSelected.bind(self) }, movie); + $(m).inject(self.movie_list, inject_at || 'bottom'); + m.fireEvent('injected'); self.movies.include(m) @@ -398,8 +399,11 @@ var MovieList = new Class({ var self = this; self.movies = [] - self.calculateSelected() - self.navigation_alpha.getElements('.active').removeClass('active') + if(self.mass_edit_select) + self.calculateSelected() + if(self.navigation_alpha) + self.navigation_alpha.getElements('.active').removeClass('active') + self.offset = 0; if(self.scrollspy){ self.load_more.show(); @@ -430,7 +434,7 @@ var MovieList = new Class({ getSavedView: function(){ var self = this; - return Cookie.read(self.options.identifier+'_view') || 'thumbs'; + return Cookie.read(self.options.identifier+'_view') || 'details'; }, search: function(){ @@ -505,11 +509,14 @@ var MovieList = new Class({ checkIfEmpty: function(){ 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) self.title[is_empty ? 'hide' : 'show']() + if(self.description) + self.description[is_empty ? 'hide' : 'show']() + if(is_empty && self.options.on_empty_element){ self.el.grab(self.options.on_empty_element); diff --git a/couchpotato/core/plugins/movie/static/movie.actions.js b/couchpotato/core/plugins/movie/static/movie.actions.js new file mode 100644 index 0000000..a56e9ab --- /dev/null +++ b/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); + }, + +}); \ No newline at end of file diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 9d67e62..65fe520 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -1,11 +1,25 @@ .movies { padding: 60px 0 20px; + position: relative; + z-index: 3; } .movies h2 { 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 { padding: 20px 0 20px; } @@ -272,7 +286,7 @@ text-align: right; right: 0; margin-right: 50px; - z-index: 2; + z-index: 1; } .movies .data .quality .available, @@ -303,6 +317,11 @@ line-height: 0; margin-top: -25px; } + .movies.thumbs_list .data .actions { + bottom: 8px; + right: 10px; + } + .movies .data:hover .action { opacity: 0.6; } .movies .data:hover .action:hover { opacity: 1; } .movies.mass_edit_list .data .actions { @@ -505,7 +524,7 @@ .movies .alph_nav { transition: box-shadow .4s linear; position: fixed; - z-index: 2; + z-index: 4; top: 0; padding: 100px 60px 7px; width: 1080px; diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index 283bb41..ffdd14d 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -80,7 +80,7 @@ var Movie = new Class({ var self = this; self.mask = new Element('div.mask', { 'styles': { - 'z-index': '1' + 'z-index': 4 } }).inject(self.el, 'top').fade('hide'); }, @@ -290,395 +290,4 @@ var Movie = new Class({ return this.el; } -}); - -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) - } - - }); \ No newline at end of file diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css index 391e347..23d87b4 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -202,7 +202,8 @@ .movie_result .info h2 span:before { content: "("; } .movie_result .info h2 span:after { content: ")"; } -.search_form .mask { +.search_form .mask, +.movie_result .mask { border-radius: 3px; position: absolute; height: 100%; diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js index ba8b547..dd8e7b0 100644 --- a/couchpotato/core/plugins/movie/static/search.js +++ b/couchpotato/core/plugins/movie/static/search.js @@ -366,7 +366,7 @@ Block.Search.Item = new Class({ loadingMask: function(){ 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) self.mask.fade('in') diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 7df93b8..b706215 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -13,6 +13,7 @@ import errno import os import re import shutil +import time import traceback log = CPLog(__name__) @@ -275,6 +276,7 @@ class Renamer(Plugin): for profile_type in movie.profile.types: if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish: movie.status_id = done_status.get('id') + movie.last_edit = int(time.time()) db.commit() except Exception, e: 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') try: release.status_id = downloaded_status.get('id') + release.last_edit = int(time.time()) except Exception, e: log.error('Failed marking release as finished: %s %s', (e, traceback.format_exc())) + db.commit() # Remove leftover files @@ -556,6 +560,7 @@ class Renamer(Plugin): 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) rel.status_id = ignored_status.get('id') + rel.last_edit = int(time.time()) db.commit() continue @@ -580,6 +585,7 @@ class Renamer(Plugin): fireEvent('searcher.try_next_release', movie_id = rel.movie_id) else: rel.status_id = failed_status.get('id') + rel.last_edit = int(time.time()) db.commit() elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index c5ed138..0285917 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -165,7 +165,7 @@ class Searcher(Plugin): # See if better quality is available 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 # Don't search for quality lower then already available. @@ -209,6 +209,7 @@ class Searcher(Plugin): db.add(rls) else: [db.delete(old_info) for old_info in rls.info] + rls.last_edit = int(time.time()) db.commit() @@ -293,7 +294,10 @@ class Searcher(Plugin): db = get_session() rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() 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() log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label) @@ -301,26 +305,28 @@ class Searcher(Plugin): log.info(snatch_message) fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict()) - # If renamer isn't used, mark movie done - if not Env.setting('enabled', 'renamer'): - active_status = fireEvent('status.get', 'active', single = True) - done_status = fireEvent('status.get', 'done', single = True) - try: - if movie['status_id'] == active_status.get('id'): - for profile_type in movie['profile']['types']: - if rls and profile_type['quality_id'] == rls.quality.id and profile_type['finish']: - log.info('Renamer disabled, marking movie as finished: %s', log_movie) - - # Mark release done - rls.status_id = done_status.get('id') - db.commit() - - # Mark movie done - mvie = db.query(Movie).filter_by(id = movie['id']).first() - mvie.status_id = done_status.get('id') - db.commit() - except: - log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc()) + # If renamer isn't used, mark movie done + if not renamer_enabled: + active_status = fireEvent('status.get', 'active', single = True) + done_status = fireEvent('status.get', 'done', single = True) + try: + if movie['status_id'] == active_status.get('id'): + for profile_type in movie['profile']['types']: + if profile_type['quality_id'] == rls.quality.id and profile_type['finish']: + log.info('Renamer disabled, marking movie as finished: %s', log_movie) + + # Mark release done + rls.status_id = done_status.get('id') + rls.last_edit = int(time.time()) + db.commit() + + # Mark movie done + mvie = db.query(Movie).filter_by(id = movie['id']).first() + mvie.status_id = done_status.get('id') + mvie.last_edit = int(time.time()) + db.commit() + except: + log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc()) except: log.error('Failed marking movie finished: %s', traceback.format_exc()) diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js index eb41cb5..12ae477 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.js +++ b/couchpotato/core/plugins/wizard/static/wizard.js @@ -82,7 +82,7 @@ Page.Wizard = new Class({ 'target': self.el }, 'onComplete': function(){ - window.location = App.createUrl(); + window.location = App.createUrl('wanted'); } }); } diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 64117fb..141ee4f 100644 --- a/couchpotato/core/settings/model.py +++ b/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 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) status = ManyToOne('Status') @@ -95,6 +95,7 @@ class Release(Entity): """Logically groups all files that belong to a certain release, such as parts of a movie, subtitles.""" + last_edit = Field(Integer, default = lambda: int(time.time()), index = True) identifier = Field(String(100), index = True) movie = ManyToOne('Movie') diff --git a/couchpotato/static/images/icon.readd.png b/couchpotato/static/images/icon.readd.png new file mode 100644 index 0000000..dacb432 Binary files /dev/null and b/couchpotato/static/images/icon.readd.png differ diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index caf4d64..e3c1ae0 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -8,8 +8,14 @@ Page.Home = new Class({ indexAction: function(param){ var self = this; - if(self.soon_list) + if(self.soon_list){ + + // Reset lists + self.available_list.update(); + self.late_list.update(); + return + } // Snatched self.available_list = new MovieList({ @@ -17,48 +23,55 @@ Page.Home = new Class({ 'identifier': 'snatched', 'load_more': false, 'view': 'list', - 'actions': MovieActions, + 'actions': [MA.IMDB, MA.Trailer, MA.Files, MA.Release, MA.Edit, MA.Readd, MA.Refresh, MA.Delete], 'title': 'Snatched & Available', + 'on_empty_element': new Element('div'), 'filter': { 'release_status': 'snatched,available' } }); - // Downloaded - // 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 + // Coming Soon self.soon_list = new MovieList({ 'navigation': false, 'identifier': 'soon', - 'limit': 24, - 'title': 'Soon', + 'limit': 18, + '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': { 'random': true }, + 'actions': [MA.IMDB, MA.Refresh], 'load_more': false, 'view': 'thumbs', '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 Settings.', + '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.available_list), - $(self.soon_list) + $(self.soon_list), + $(self.late_list) ); // Suggest diff --git a/couchpotato/static/scripts/page/manage.js b/couchpotato/static/scripts/page/manage.js index ec293f8..aef1f3c 100644 --- a/couchpotato/static/scripts/page/manage.js +++ b/couchpotato/static/scripts/page/manage.js @@ -30,7 +30,7 @@ Page.Manage = new Class({ 'filter': { '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], 'on_empty_element': new Element('div.empty_manage').adopt( new Element('div', { diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index a9e0bc7..6e32997 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -22,7 +22,7 @@ Page.Wanted = new Class({ self.wanted = new MovieList({ 'identifier': 'wanted', 'status': 'active', - 'actions': MovieActions, + 'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Edit, MA.Refresh, MA.Readd, MA.Delete], 'add_new': true, 'menu': [self.manual_search], 'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted') @@ -71,283 +71,4 @@ 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 - }; - -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index ff29222..3ad502b 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -157,6 +157,7 @@ body > .spinner, .mask{ .icon.folder { background-image: url('../images/icon.folder.png'); } .icon.imdb { background-image: url('../images/icon.imdb.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.files { background-image: url('../images/icon.files.png'); } .icon.info { background-image: url('../images/icon.info.png'); } @@ -584,7 +585,7 @@ body > .spinner, .mask{ bottom: 0; padding: 2px; width: 240px; - z-index: 2; + z-index: 20; overflow: hidden; font-size: 14px; font-weight: bold; diff --git a/init/ubuntu b/init/ubuntu index 495efe6..376c001 100644 --- a/init/ubuntu +++ b/init/ubuntu @@ -12,51 +12,56 @@ # Description: starts instance of CouchPotato using start-stop-daemon ### END INIT INFO -############### EDIT ME ################## -# path to app -APP_PATH=/usr/local/sbin/CouchPotatoServer/ +# Check for existance of defaults file +# and utilze if available +if [ -f /etc/default/couchpotato ]; then + . /etc/default/couchpotato +else + echo "/etc/default/couchpotato not found using default settings."; +fi -# user -RUN_AS=YOUR_USERNAME_HERE +# Script name +NAME=couchpotato -# path to python bin -DAEMON=/usr/bin/python +# App name +DESC=CouchPotato -# Path to store PID file -PID_FILE=/var/run/couchpotato.pid +# Path to app root +CP_APP_PATH=${APP_PATH-/usr/local/sbin/CouchPotatoServer/} -# script name -NAME=couchpotato +# User to run CP as +CP_RUN_AS=${RUN_AS-root} -# app name -DESC=CouchPotato +# Path to python bin +CP_DAEMON=${DAEMON_PATH-/usr/bin/python} -# startup args -DAEMON_OPTS=" CouchPotato.py --daemon --pid_file=${PID_FILE}" +# Path to store 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 case "$1" in start) echo "Starting $DESC" - rm -rf $PID_FILE || return 1 - touch $PID_FILE - chown $RUN_AS $PID_FILE - start-stop-daemon -d $APP_PATH -c $RUN_AS --start --background --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS + rm -rf $CP_PID_FILE || return 1 + touch $CP_PID_FILE + chown $CP_RUN_AS $CP_PID_FILE + start-stop-daemon -d $CP_APP_PATH -c $CP_RUN_AS --start --background --pidfile $CP_PID_FILE --exec $CP_DAEMON -- $CP_DAEMON_OPTS ;; stop) 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) echo "Restarting $DESC" - start-stop-daemon --stop --pidfile $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 --stop --pidfile $CP_PID_FILE --retry 15 + 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 diff --git a/init/ubuntu.default b/init/ubuntu.default new file mode 100644 index 0000000..0d1e712 --- /dev/null +++ b/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 \ No newline at end of file