From 3e93983f6e82934e02b908c522ff001735328c27 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 15 Aug 2013 22:22:45 +0200 Subject: [PATCH 1/5] Move movie to new media type folder --- couchpotato/core/loader.py | 7 + couchpotato/core/media/__init__.py | 17 + couchpotato/core/media/movie/__init__.py | 0 couchpotato/core/media/movie/_base/__init__.py | 6 + couchpotato/core/media/movie/_base/main.py | 595 +++++++++++ couchpotato/core/media/movie/_base/static/list.js | 626 ++++++++++++ .../core/media/movie/_base/static/movie.actions.js | 822 +++++++++++++++ .../core/media/movie/_base/static/movie.css | 1044 ++++++++++++++++++++ couchpotato/core/media/movie/_base/static/movie.js | 298 ++++++ .../core/media/movie/_base/static/search.css | 275 ++++++ .../core/media/movie/_base/static/search.js | 414 ++++++++ couchpotato/core/plugins/movie/__init__.py | 6 - couchpotato/core/plugins/movie/main.py | 591 ----------- couchpotato/core/plugins/movie/static/list.js | 626 ------------ .../core/plugins/movie/static/movie.actions.js | 822 --------------- couchpotato/core/plugins/movie/static/movie.css | 1044 -------------------- couchpotato/core/plugins/movie/static/movie.js | 298 ------ couchpotato/core/plugins/movie/static/search.css | 275 ------ couchpotato/core/plugins/movie/static/search.js | 414 -------- 19 files changed, 4104 insertions(+), 4076 deletions(-) create mode 100644 couchpotato/core/media/__init__.py create mode 100644 couchpotato/core/media/movie/__init__.py create mode 100644 couchpotato/core/media/movie/_base/__init__.py create mode 100644 couchpotato/core/media/movie/_base/main.py create mode 100644 couchpotato/core/media/movie/_base/static/list.js create mode 100644 couchpotato/core/media/movie/_base/static/movie.actions.js create mode 100644 couchpotato/core/media/movie/_base/static/movie.css create mode 100644 couchpotato/core/media/movie/_base/static/movie.js create mode 100644 couchpotato/core/media/movie/_base/static/search.css create mode 100644 couchpotato/core/media/movie/_base/static/search.js delete mode 100644 couchpotato/core/plugins/movie/__init__.py delete mode 100644 couchpotato/core/plugins/movie/main.py delete mode 100644 couchpotato/core/plugins/movie/static/list.js delete mode 100644 couchpotato/core/plugins/movie/static/movie.actions.js delete mode 100644 couchpotato/core/plugins/movie/static/movie.css delete mode 100644 couchpotato/core/plugins/movie/static/movie.js delete mode 100644 couchpotato/core/plugins/movie/static/search.css delete mode 100644 couchpotato/core/plugins/movie/static/search.js diff --git a/couchpotato/core/loader.py b/couchpotato/core/loader.py index 0f49700..8aef89a 100644 --- a/couchpotato/core/loader.py +++ b/couchpotato/core/loader.py @@ -31,6 +31,13 @@ class Loader(object): if os.path.isdir(path) and provider[:2] != '__': self.paths[provider + '_provider'] = (25, 'couchpotato.core.providers.' + provider, path) + # Add media to loader + media_dir = os.path.join(root, 'couchpotato', 'core', 'media') + for media in os.listdir(media_dir): + path = os.path.join(media_dir, media) + if os.path.isdir(path) and media[:2] != '__': + self.paths[media + '_media'] = (25, 'couchpotato.core.media.' + media, path) + for plugin_type, plugin_tuple in self.paths.iteritems(): priority, module, dir_name = plugin_tuple diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py new file mode 100644 index 0000000..2d339e5 --- /dev/null +++ b/couchpotato/core/media/__init__.py @@ -0,0 +1,17 @@ +from couchpotato.core.event import addEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin + +log = CPLog(__name__) + + +class MediaBase(Plugin): + + identifier = None + + def __init__(self): + + addEvent('media.types', self.getType) + + def getType(self): + return self.identifier diff --git a/couchpotato/core/media/movie/__init__.py b/couchpotato/core/media/movie/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/couchpotato/core/media/movie/_base/__init__.py b/couchpotato/core/media/movie/_base/__init__.py new file mode 100644 index 0000000..4be3b12 --- /dev/null +++ b/couchpotato/core/media/movie/_base/__init__.py @@ -0,0 +1,6 @@ +from .main import MovieBase + +def start(): + return MovieBase() + +config = [] diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py new file mode 100644 index 0000000..fc537ef --- /dev/null +++ b/couchpotato/core/media/movie/_base/main.py @@ -0,0 +1,595 @@ +from couchpotato import get_session +from couchpotato.api import addApiView +from couchpotato.core.event import fireEvent, fireEventAsync, addEvent +from couchpotato.core.helpers.encoding import toUnicode, simplifyString +from couchpotato.core.helpers.variable import getImdb, splitString, tryInt +from couchpotato.core.logger import CPLog +from couchpotato.core.media import MediaBase +from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \ + Release +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__) + + +class MovieBase(MediaBase): + + identifier = 'movie' + + default_dict = { + 'profile': {'types': {'quality': {}}}, + 'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}}, + 'library': {'titles': {}, 'files':{}}, + 'files': {}, + 'status': {} + } + + def __init__(self): + super(MovieBase, self).__init__() + + addApiView('movie.search', self.search, docs = { + 'desc': 'Search the movie providers for a movie', + 'params': { + 'q': {'desc': 'The (partial) movie name you want to search for'}, + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'empty': bool, any movies returned or not, + 'movies': array, movies found, +}"""} + }) + addApiView('movie.list', self.listView, docs = { + 'desc': 'List movies in wanted list', + 'params': { + 'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'}, + 'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'}, + 'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'}, + 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'}, + 'search': {'desc': 'Search movie title'}, + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'empty': bool, any movies returned or not, + 'movies': array, movies found, +}"""} + }) + addApiView('movie.get', self.getView, docs = { + 'desc': 'Get a movie by id', + 'params': { + 'id': {'desc': 'The id of the movie'}, + } + }) + addApiView('movie.refresh', self.refresh, docs = { + 'desc': 'Refresh a movie by id', + 'params': { + 'id': {'desc': 'Movie ID(s) you want to refresh.', 'type': 'int (comma separated)'}, + } + }) + addApiView('movie.available_chars', self.charView) + addApiView('movie.add', self.addView, docs = { + 'desc': 'Add new movie to the wanted list', + 'params': { + 'identifier': {'desc': 'IMDB id of the movie your want to add.'}, + 'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'}, + 'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, + } + }) + addApiView('movie.edit', self.edit, docs = { + 'desc': 'Add new movie to the wanted list', + 'params': { + 'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'}, + 'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'}, + 'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, + } + }) + addApiView('movie.delete', self.deleteView, docs = { + 'desc': 'Delete a movie from the wanted list', + 'params': { + 'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'}, + 'delete_from': {'desc': 'Delete movie from this page', 'type': 'string: all (default), wanted, manage'}, + } + }) + + addEvent('movie.add', self.add) + addEvent('movie.delete', self.delete) + addEvent('movie.get', self.get) + 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): + + log.debug('Removing releases from dashboard') + + now = time.time() + week = 262080 + + done_status, available_status, snatched_status = \ + fireEvent('status.get', ['done', 'available', '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 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.expire_all() + + def getView(self, id = None, **kwargs): + + movie = self.get(id) if id else None + + return { + 'success': movie is not None, + 'movie': movie, + } + + def get(self, movie_id): + + db = get_session() + + imdb_id = getImdb(str(movie_id)) + + if(imdb_id): + m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first() + else: + m = db.query(Movie).filter_by(id = movie_id).first() + + results = None + if m: + results = m.to_dict(self.default_dict) + + db.expire_all() + return results + + def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None): + + db = get_session() + + # Make a list from string + if status and not isinstance(status, (list, tuple)): + status = [status] + if release_status and not isinstance(release_status, (list, tuple)): + release_status = [release_status] + + q = db.query(Movie) \ + .outerjoin(Movie.releases, Movie.library, Library.titles) \ + .filter(LibraryTitle.default == True) \ + .group_by(Movie.id) + + # Filter on movie status + if status and len(status) > 0: + q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status])) + + # Filter on release status + if release_status and len(release_status) > 0: + q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status])) + + filter_or = [] + if starts_with: + starts_with = toUnicode(starts_with.lower()) + if starts_with in ascii_lowercase: + filter_or.append(LibraryTitle.simple_title.startswith(starts_with)) + else: + ignore = [] + for letter in ascii_lowercase: + ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter))) + filter_or.append(not_(or_(*ignore))) + + if search: + filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%')) + + if filter_or: + q = q.filter(or_(*filter_or)) + + total_count = q.count() + + if order == 'release_order': + q = q.order_by(desc(Release.last_edit)) + else: + q = q.order_by(asc(LibraryTitle.simple_title)) + + q = q.subquery() + q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \ + .options(joinedload_all('releases.files')) \ + .options(joinedload_all('releases.info')) \ + .options(joinedload_all('profile.types')) \ + .options(joinedload_all('library.titles')) \ + .options(joinedload_all('library.files')) \ + .options(joinedload_all('status')) \ + .options(joinedload_all('files')) + + if limit_offset: + splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset + limit = splt[0] + offset = 0 if len(splt) is 1 else splt[1] + q2 = q2.limit(limit).offset(offset) + + results = q2.all() + movies = [] + for movie in results: + movies.append(movie.to_dict({ + 'profile': {'types': {}}, + 'releases': {'files':{}, 'info': {}}, + 'library': {'titles': {}, 'files':{}}, + 'files': {}, + })) + + db.expire_all() + return (total_count, movies) + + def availableChars(self, status = None, release_status = None): + + chars = '' + + db = get_session() + + # Make a list from string + if not isinstance(status, (list, tuple)): + status = [status] + if release_status and not isinstance(release_status, (list, tuple)): + release_status = [release_status] + + q = db.query(Movie) \ + .outerjoin(Movie.releases, Movie.library, Library.titles, Movie.status) \ + .options(joinedload_all('library.titles')) + + # Filter on movie status + if status and len(status) > 0: + q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status])) + + # Filter on release status + if release_status and len(release_status) > 0: + q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status])) + + results = q.all() + + for movie in results: + char = movie.library.titles[0].simple_title[0] + char = char if char in ascii_lowercase else '#' + if char not in chars: + chars += str(char) + + db.expire_all() + return ''.join(sorted(chars, key = str.lower)) + + def listView(self, **kwargs): + + status = splitString(kwargs.get('status', None)) + release_status = splitString(kwargs.get('release_status', None)) + limit_offset = kwargs.get('limit_offset', None) + starts_with = kwargs.get('starts_with', None) + search = kwargs.get('search', None) + order = kwargs.get('order', None) + + total_movies, movies = self.list( + status = status, + release_status = release_status, + limit_offset = limit_offset, + starts_with = starts_with, + search = search, + order = order + ) + + return { + 'success': True, + 'empty': len(movies) == 0, + 'total': total_movies, + 'movies': movies, + } + + def charView(self, **kwargs): + + status = splitString(kwargs.get('status', None)) + release_status = splitString(kwargs.get('release_status', None)) + chars = self.availableChars(status, release_status) + + return { + 'success': True, + 'empty': len(chars) == 0, + 'chars': chars, + } + + def refresh(self, id = '', **kwargs): + + db = get_session() + + for x in splitString(id): + movie = db.query(Movie).filter_by(id = x).first() + + if movie: + + # Get current selected title + default_title = '' + for title in movie.library.titles: + if title.default: default_title = title.title + + fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True) + fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x)) + + db.expire_all() + return { + 'success': True, + } + + def search(self, q = '', **kwargs): + + cache_key = u'%s/%s' % (__name__, simplifyString(q)) + movies = Env.get('cache').get(cache_key) + + if not movies: + + if getImdb(q): + movies = [fireEvent('movie.info', identifier = q, merge = True)] + else: + movies = fireEvent('movie.search', q = q, merge = True) + Env.get('cache').set(cache_key, movies) + + return { + 'success': True, + 'empty': len(movies) == 0 if movies else 0, + 'movies': movies, + } + + def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None): + + if not params.get('identifier'): + msg = 'Can\'t add movie without imdb identifier.' + log.error(msg) + fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg) + return False + else: + try: + is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True) + if not is_movie: + msg = 'Can\'t add movie, seems to be a TV show.' + log.error(msg) + fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg) + return False + except: + pass + + + library = fireEvent('library.add', single = True, attrs = params, update_after = update_library) + + # Status + status_active, snatched_status, ignored_status, done_status, downloaded_status = \ + fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True) + + default_profile = fireEvent('profile.default', single = True) + cat_id = params.get('category_id', None) + + db = get_session() + m = db.query(Movie).filter_by(library_id = library.get('id')).first() + added = True + do_search = False + if not m: + m = Movie( + library_id = library.get('id'), + profile_id = params.get('profile_id', default_profile.get('id')), + status_id = status_id if status_id else status_active.get('id'), + category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None, + ) + db.add(m) + db.commit() + + onComplete = None + if search_after: + onComplete = self.createOnComplete(m.id) + + 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 in [downloaded_status.get('id'), snatched_status.get('id'), done_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')) + m.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None + else: + log.debug('Movie already exists, not updating: %s', params) + added = False + + 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() + + # Remove releases + available_status = fireEvent('status.get', 'available', single = True) + for rel in m.releases: + if rel.status_id is available_status.get('id'): + db.delete(rel) + db.commit() + + movie_dict = m.to_dict(self.default_dict) + + if do_search and search_after: + onComplete = self.createOnComplete(m.id) + onComplete() + + if added: + fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')) + + db.expire_all() + return movie_dict + + + def addView(self, **kwargs): + + movie_dict = self.add(params = kwargs) + + return { + 'success': True, + 'added': True if movie_dict else False, + 'movie': movie_dict, + } + + def edit(self, id = '', **kwargs): + + db = get_session() + + available_status = fireEvent('status.get', 'available', single = True) + + ids = splitString(id) + for movie_id in ids: + + m = db.query(Movie).filter_by(id = movie_id).first() + if not m: + continue + + m.profile_id = kwargs.get('profile_id') + + cat_id = kwargs.get('category_id', None) + if cat_id is not None: + m.category_id = tryInt(cat_id) if tryInt(cat_id) > 0 else None + + # Remove releases + for rel in m.releases: + if rel.status_id is available_status.get('id'): + db.delete(rel) + db.commit() + + # Default title + if kwargs.get('default_title'): + for title in m.library.titles: + title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower() + + db.commit() + + fireEvent('movie.restatus', m.id) + + movie_dict = m.to_dict(self.default_dict) + fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id)) + + db.expire_all() + return { + 'success': True, + } + + def deleteView(self, id = '', **kwargs): + + ids = splitString(id) + for movie_id in ids: + self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all')) + + return { + 'success': True, + } + + def delete(self, movie_id, delete_from = None): + + db = get_session() + + movie = db.query(Movie).filter_by(id = movie_id).first() + if movie: + deleted = False + if delete_from == 'all': + db.delete(movie) + db.commit() + deleted = True + else: + done_status = fireEvent('status.get', 'done', single = True) + + total_releases = len(movie.releases) + total_deleted = 0 + new_movie_status = None + for release in movie.releases: + if delete_from in ['wanted', 'snatched']: + if release.status_id != done_status.get('id'): + db.delete(release) + total_deleted += 1 + new_movie_status = 'done' + elif delete_from == 'manage': + if release.status_id == done_status.get('id'): + db.delete(release) + total_deleted += 1 + new_movie_status = 'active' + db.commit() + + if total_releases == total_deleted: + db.delete(movie) + db.commit() + deleted = True + elif new_movie_status: + new_status = fireEvent('status.get', new_movie_status, single = True) + movie.profile_id = None + movie.status_id = new_status.get('id') + db.commit() + else: + fireEvent('movie.restatus', movie.id, single = True) + + if deleted: + fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict()) + + db.expire_all() + return True + + def restatus(self, movie_id): + + active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) + + db = get_session() + + m = db.query(Movie).filter_by(id = movie_id).first() + if not m or len(m.library.titles) == 0: + log.debug('Can\'t restatus movie, doesn\'t seem to exist.') + return False + + log.debug('Changing status for %s', (m.library.titles[0].title)) + if not m.profile: + m.status_id = done_status.get('id') + else: + move_to_wanted = True + + for t in m.profile.types: + for release in m.releases: + if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish): + move_to_wanted = False + + m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id') + + db.commit() + + return True + + def createOnComplete(self, movie_id): + + def onComplete(): + db = get_session() + movie = db.query(Movie).filter_by(id = movie_id).first() + fireEventAsync('movie.searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id)) + db.expire_all() + + return onComplete + + + def createNotifyFront(self, movie_id): + + def notifyFront(): + db = get_session() + movie = db.query(Movie).filter_by(id = movie_id).first() + fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict)) + db.expire_all() + + return notifyFront diff --git a/couchpotato/core/media/movie/_base/static/list.js b/couchpotato/core/media/movie/_base/static/list.js new file mode 100644 index 0000000..1b11fab --- /dev/null +++ b/couchpotato/core/media/movie/_base/static/list.js @@ -0,0 +1,626 @@ +var MovieList = new Class({ + + Implements: [Events, Options], + + options: { + navigation: true, + limit: 50, + load_more: true, + loader: true, + menu: [], + add_new: false, + force_view: false + }, + + movies: [], + movies_added: {}, + total_movies: 0, + letters: {}, + filter: null, + + initialize: function(options){ + var self = this; + self.setOptions(options); + + self.offset = 0; + self.filter = self.options.filter || { + 'starts_with': null, + 'search': null + } + + self.el = new Element('div.movies').adopt( + self.title = self.options.title ? new Element('h2', { + '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', { + 'events': { + 'click': self.loadMore.bind(self) + } + }) : null + ); + + if($(window).getSize().x <= 480 && !self.options.force_view) + self.changeView('list'); + else + self.changeView(self.getSavedView() || self.options.view || 'details'); + + self.getMovies(); + + App.addEvent('movie.added', self.movieAdded.bind(self)) + App.addEvent('movie.deleted', self.movieDeleted.bind(self)) + }, + + movieDeleted: function(notification){ + var self = this; + + if(self.movies_added[notification.data.id]){ + self.movies.each(function(movie){ + if(movie.get('id') == notification.data.id){ + movie.destroy(); + delete self.movies_added[notification.data.id]; + self.setCounter(self.counter_count-1); + } + }) + } + + self.checkIfEmpty(); + }, + + movieAdded: function(notification){ + var self = this; + + if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){ + window.scroll(0,0); + self.createMovie(notification.data, 'top'); + self.setCounter(self.counter_count+1); + + self.checkIfEmpty(); + } + }, + + create: function(){ + var self = this; + + // Create the alphabet nav + if(self.options.navigation) + self.createNavigation(); + + if(self.options.load_more) + self.scrollspy = new ScrollSpy({ + min: function(){ + var c = self.load_more.getCoordinates() + return c.top - window.document.getSize().y - 300 + }, + onEnter: self.loadMore.bind(self) + }); + + self.created = true; + }, + + addMovies: function(movies, total){ + var self = this; + + if(!self.created) self.create(); + + // do scrollspy + if(movies.length < self.options.limit && self.scrollspy){ + self.load_more.hide(); + self.scrollspy.stop(); + } + + Object.each(movies, function(movie){ + self.createMovie(movie); + }); + + self.total_movies += total; + self.setCounter(total); + + }, + + setCounter: function(count){ + var self = this; + + if(!self.navigation_counter) return; + + self.counter_count = count; + self.navigation_counter.set('text', (count || 0) + ' movies'); + + if (self.empty_message) { + self.empty_message.destroy(); + self.empty_message = null; + } + + if(self.total_movies && count == 0 && !self.empty_message){ + var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') + + (self.filter.starts_with ? ' in '+self.filter.starts_with+'' : ''); + + self.empty_message = new Element('.message', { + 'html': 'No movies found ' + message + '.
' + }).grab( + new Element('a', { + 'text': 'Reset filter', + 'events': { + 'click': function(){ + self.filter = { + 'starts_with': null, + 'search': null + }; + self.navigation_search_input.set('value', ''); + self.reset(); + self.activateLetter(); + self.getMovies(true); + self.last_search_value = ''; + } + } + }) + ).inject(self.movie_list); + + } + + }, + + createMovie: function(movie, inject_at){ + var self = this; + var m = new Movie(self, { + '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) + self.movies_added[movie.id] = true; + }, + + createNavigation: function(){ + var self = this; + var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + self.el.addClass('with_navigation') + + self.navigation = new Element('div.alph_nav').adopt( + self.mass_edit_form = new Element('div.mass_edit_form').adopt( + new Element('span.select').adopt( + self.mass_edit_select = new Element('input[type=checkbox].inlay', { + 'events': { + 'change': self.massEditToggleAll.bind(self) + } + }), + self.mass_edit_selected = new Element('span.count', {'text': 0}), + self.mass_edit_selected_label = new Element('span', {'text': 'selected'}) + ), + new Element('div.quality').adopt( + self.mass_edit_quality = new Element('select'), + new Element('a.button.orange', { + 'text': 'Change quality', + 'events': { + 'click': self.changeQualitySelected.bind(self) + } + }) + ), + new Element('div.delete').adopt( + new Element('span[text=or]'), + new Element('a.button.red', { + 'text': 'Delete', + 'events': { + 'click': self.deleteSelected.bind(self) + } + }) + ), + new Element('div.refresh').adopt( + new Element('span[text=or]'), + new Element('a.button.green', { + 'text': 'Refresh', + 'events': { + 'click': self.refreshSelected.bind(self) + } + }) + ) + ), + new Element('div.menus').adopt( + self.navigation_counter = new Element('span.counter[title=Total]'), + self.filter_menu = new Block.Menu(self, { + 'class': 'filter' + }), + self.navigation_actions = new Element('ul.actions', { + 'events': { + 'click:relay(li)': function(e, el){ + var a = 'active'; + self.navigation_actions.getElements('.'+a).removeClass(a); + self.changeView(el.get('data-view')); + this.addClass(a); + + el.inject(el.getParent(), 'top'); + el.getSiblings().hide() + setTimeout(function(){ + el.getSiblings().setStyle('display', null); + }, 100) + } + } + }), + self.navigation_menu = new Block.Menu(self, { + 'class': 'extra' + }) + ) + ).inject(self.el, 'top'); + + // Mass edit + self.mass_edit_select_class = new Form.Check(self.mass_edit_select); + Quality.getActiveProfiles().each(function(profile){ + new Element('option', { + 'value': profile.id ? profile.id : profile.data.id, + 'text': profile.label ? profile.label : profile.data.label + }).inject(self.mass_edit_quality) + }); + + self.filter_menu.addLink( + self.navigation_search_input = new Element('input', { + 'title': 'Search through ' + self.options.identifier, + 'placeholder': 'Search through ' + self.options.identifier, + 'events': { + 'keyup': self.search.bind(self), + 'change': self.search.bind(self) + } + }) + ).addClass('search'); + + self.filter_menu.addEvent('open', function(){ + self.navigation_search_input.focus(); + }); + + self.filter_menu.addLink( + self.navigation_alpha = new Element('ul.numbers', { + 'events': { + 'click:relay(li.available)': function(e, el){ + self.activateLetter(el.get('data-letter')) + self.getMovies(true) + } + } + }) + ); + + // Actions + ['mass_edit', 'details', 'list'].each(function(view){ + var current = self.current_view == view; + new Element('li', { + 'class': 'icon2 ' + view + (current ? ' active ' : ''), + 'data-view': view + }).inject(self.navigation_actions, current ? 'top' : 'bottom'); + }); + + // All + self.letters['all'] = new Element('li.letter_all.available.active', { + 'text': 'ALL', + }).inject(self.navigation_alpha); + + // Chars + chars.split('').each(function(c){ + self.letters[c] = new Element('li', { + 'text': c, + 'class': 'letter_'+c, + 'data-letter': c + }).inject(self.navigation_alpha); + }); + + // Get available chars and highlight + if(self.navigation.isDisplayed() || self.navigation.isVisible()) + Api.request('movie.available_chars', { + 'data': Object.merge({ + 'status': self.options.status + }, self.filter), + 'onSuccess': function(json){ + + json.chars.split('').each(function(c){ + self.letters[c.capitalize()].addClass('available') + }) + + } + }); + + // Add menu or hide + if (self.options.menu.length > 0) + self.options.menu.each(function(menu_item){ + self.navigation_menu.addLink(menu_item); + }) + else + self.navigation_menu.hide(); + + }, + + calculateSelected: function(){ + var self = this; + + var selected = 0, + movies = self.movies.length; + self.movies.each(function(movie){ + selected += movie.isSelected() ? 1 : 0 + }) + + var indeterminate = selected > 0 && selected < movies, + checked = selected == movies && selected > 0; + + self.mass_edit_select.set('indeterminate', indeterminate) + + self.mass_edit_select_class[checked ? 'check' : 'uncheck']() + self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate') + + self.mass_edit_selected.set('text', selected); + }, + + deleteSelected: function(){ + var self = this, + ids = self.getSelectedMovies(), + help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list'; + + var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{ + 'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'), + 'class': 'delete', + 'events': { + 'click': function(e){ + (e).preventDefault(); + this.set('text', 'Deleting..') + Api.request('movie.delete', { + 'data': { + 'id': ids.join(','), + 'delete_from': self.options.identifier + }, + 'onSuccess': function(){ + qObj.close(); + + var erase_movies = []; + self.movies.each(function(movie){ + if (movie.isSelected()){ + $(movie).destroy() + erase_movies.include(movie); + } + }); + + erase_movies.each(function(movie){ + self.movies.erase(movie); + movie.destroy(); + self.setCounter(self.counter_count-1); + }); + + self.calculateSelected(); + } + }); + + } + } + }, { + 'text': 'Cancel', + 'cancel': true + }]); + + }, + + changeQualitySelected: function(){ + var self = this; + var ids = self.getSelectedMovies() + + Api.request('movie.edit', { + 'data': { + 'id': ids.join(','), + 'profile_id': self.mass_edit_quality.get('value') + }, + 'onSuccess': self.search.bind(self) + }); + }, + + refreshSelected: function(){ + var self = this; + var ids = self.getSelectedMovies() + + Api.request('movie.refresh', { + 'data': { + 'id': ids.join(','), + } + }); + }, + + getSelectedMovies: function(){ + var self = this; + + var ids = [] + self.movies.each(function(movie){ + if (movie.isSelected()) + ids.include(movie.get('id')) + }); + + return ids + }, + + massEditToggleAll: function(){ + var self = this; + + var select = self.mass_edit_select.get('checked'); + + self.movies.each(function(movie){ + movie.select(select) + }); + + self.calculateSelected() + }, + + reset: function(){ + var self = this; + + self.movies = [] + 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(); + self.scrollspy.start(); + } + }, + + activateLetter: function(letter){ + var self = this; + + self.reset() + + self.letters[letter || 'all'].addClass('active'); + self.filter.starts_with = letter; + + }, + + changeView: function(new_view){ + var self = this; + + self.el + .removeClass(self.current_view+'_list') + .addClass(new_view+'_list') + + self.current_view = new_view; + Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000}); + }, + + getSavedView: function(){ + var self = this; + return Cookie.read(self.options.identifier+'_view2'); + }, + + search: function(){ + var self = this; + + if(self.search_timer) clearTimeout(self.search_timer); + self.search_timer = (function(){ + var search_value = self.navigation_search_input.get('value'); + if (search_value == self.last_search_value) return + + self.reset() + + self.activateLetter(); + self.filter.search = search_value; + + self.getMovies(true); + + self.last_search_value = search_value; + + }).delay(250); + + }, + + update: function(){ + var self = this; + + self.reset(); + self.getMovies(true); + }, + + getMovies: function(reset){ + var self = this; + + if(self.scrollspy){ + self.scrollspy.stop(); + self.load_more.set('text', 'loading...'); + } + + if(self.movies.length == 0 && self.options.loader){ + + self.loader_first = new Element('div.loading').adopt( + new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'}) + ).inject(self.el, 'top'); + + createSpinner(self.loader_first, { + radius: 4, + length: 4, + width: 1 + }); + + self.el.setStyle('min-height', 93); + + } + + Api.request(self.options.api_call || 'movie.list', { + 'data': Object.merge({ + 'status': self.options.status, + 'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null + }, self.filter), + 'onSuccess': function(json){ + + if(reset) + self.movie_list.empty(); + + if(self.loader_first){ + var lf = self.loader_first; + self.loader_first.addClass('hide') + self.loader_first = null; + setTimeout(function(){ + lf.destroy(); + }, 20000); + self.el.setStyle('min-height', null); + } + + self.store(json.movies); + self.addMovies(json.movies, json.total); + if(self.scrollspy) { + self.load_more.set('text', 'load more movies'); + self.scrollspy.start(); + } + + self.checkIfEmpty(); + self.fireEvent('loaded'); + } + }); + }, + + loadMore: function(){ + var self = this; + if(self.offset >= self.options.limit) + self.getMovies() + }, + + store: function(movies){ + var self = this; + + self.offset += movies.length; + + }, + + checkIfEmpty: function(){ + var self = this; + + 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.setStyle('display', [is_empty ? 'none' : '']) + + if(is_empty && self.options.on_empty_element){ + self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after'); + + if(self.navigation) + self.navigation.hide(); + + self.empty_element = self.options.on_empty_element; + } + else if(self.empty_element){ + self.empty_element.destroy(); + + if(self.navigation) + self.navigation.show(); + } + + }, + + toElement: function(){ + return this.el; + } + +}); \ No newline at end of file diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js new file mode 100644 index 0000000..ad02e80 --- /dev/null +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -0,0 +1,822 @@ +var MovieAction = new Class({ + + Implements: [Options], + + class_name: 'action icon2', + + initialize: function(movie, options){ + var self = this; + self.setOptions(options); + + 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') + }, + + getTitle: function(){ + var self = this; + + try { + return self.movie.getTitle(); + } + catch(e){ + try { + return self.movie.original_title ? self.movie.original_title : self.movie.titles[0]; + } + catch(e){ + return 'Unknown'; + } + } + }, + + get: function(key){ + var self = this; + try { + return self.movie.get(key) + } + catch(e){ + return self.movie[key] + } + }, + + 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('imdb') || self.movie.get('identifier'); + + self.el = new Element('a.imdb', { + 'title': 'Go to the IMDB page of ' + self.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.download', { + 'title': 'Show the releases that are available for ' + self.getTitle(), + 'events': { + 'click': self.show.bind(self) + } + }); + + if(self.movie.data.releases.length == 0) + self.el.hide() + else + self.showHelper(); + + }, + + createReleases: function(){ + var self = this; + + if(!self.options_container){ + self.options_container = new Element('div.options').grab( + self.release_container = new Element('div.releases.table') + ); + + // 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 + var item = 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.icon2', { + 'href': release.info['detail_url'], + 'target': '_blank' + }) : new Element('a'), + new Element('a.download.icon2', { + 'events': { + 'click': function(e){ + (e).preventDefault(); + if(!this.hasClass('completed')) + self.download(release); + } + } + }), + new Element('a.delete.icon2', { + 'events': { + 'click': function(e){ + (e).preventDefault(); + self.ignore(release); + } + } + }) + ).inject(self.release_container); + + release['el'] = item; + + 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 && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){ + + self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top'); + + 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 + ) + } + + } + + }, + + show: function(e){ + var self = this; + if(e) + (e).preventDefault(); + + self.createReleases(); + self.options_container.inject(self.movie, 'top'); + self.movie.slide('in', self.options_container); + }, + + showHelper: function(e){ + var self = this; + if(e) + (e).preventDefault(); + + self.createReleases(); + + if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){ + + self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container); + + self.trynext_container.adopt( + self.next_release ? [new Element('a.icon2.readd', { + 'text': self.last_release ? 'Download another release' : 'Download the best release', + 'events': { + 'click': self.tryNextRelease.bind(self) + } + }), + new Element('a.icon2.download', { + 'text': 'pick one yourself', + 'events': { + 'click': function(){ + self.movie.quality.fireEvent('click'); + } + } + })] : null, + new Element('a.icon2.completed', { + 'text': 'mark this movie done', + 'events': { + 'click': function(){ + Api.request('movie.delete', { + 'data': { + 'id': self.movie.get('id'), + 'delete_from': 'wanted' + }, + 'onComplete': function(){ + var movie = $(self.movie); + movie.set('tween', { + 'duration': 300, + 'onComplete': function(){ + self.movie.destroy() + } + }); + movie.tween('height', 0); + } + }); + } + } + }) + ) + } + + }, + + 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.icon2'); + + self.movie.busy(true); + + Api.request('release.download', { + 'data': { + 'id': release.id + }, + 'onComplete': function(json){ + self.movie.busy(false); + + 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 + }, + 'onComplete': function(){ + var el = release.el; + if(el.hasClass('failed') || el.hasClass('ignored')){ + el.removeClass('failed').removeClass('ignored'); + el.getElement('.release_status').set('text', 'available'); + } + else { + el.addClass('ignored'); + el.getElement('.release_status').set('text', 'ignored'); + } + } + }) + + }, + + tryNextRelease: function(movie_id){ + var self = this; + + self.createReleases(); + + 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.getTitle(), + 'events': { + 'click': self.watch.bind(self) + } + }); + + }, + + watch: function(offset){ + var self = this; + + var data_url = 'https://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.getTitle()), + 'year': self.get('year'), + 'offset': offset || 1 + }), + size = $(self.movie).getSize(), + height = self.options.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' + }), + self.category_select = new Element('select', { + 'name': 'category' + }).grab( + new Element('option', {'value': -1, 'text': 'None'}) + ), + 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); + }); + + + // Fill categories + var categories = CategoryList.getAll(); + + if(categories.length == 0) + self.category_select.hide(); + else { + self.category_select.show(); + categories.each(function(category){ + + var category_id = category.data.id; + + new Element('option', { + 'value': category_id, + 'text': category.data.label + }).inject(self.category_select); + + if(self.movie.category && self.movie.category.data && self.movie.category.data.id == category_id) + self.category_select.set('value', category_id); + + }); + } + + // Fill profiles + var profiles = Quality.getActiveProfiles(); + if(profiles.length == 1) + self.profile_select.hide(); + + profiles.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'), + 'category_id': self.category_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/media/movie/_base/static/movie.css b/couchpotato/core/media/movie/_base/static/movie.css new file mode 100644 index 0000000..adc4ebf --- /dev/null +++ b/couchpotato/core/media/movie/_base/static/movie.css @@ -0,0 +1,1044 @@ +.movies { + padding: 10px 0 20px; + position: relative; + z-index: 3; + width: 100%; +} + + .movies > div { + clear: both; + } + + .movies > div .message { + display: block; + padding: 20px; + font-size: 20px; + color: white; + text-align: center; + } + .movies > div .message a { + padding: 20px; + display: block; + } + + .movies.thumbs_list > div:not(.description) { + margin-right: -4px; + } + + .movies .loading { + display: block; + padding: 20px 0 0 0; + width: 100%; + z-index: 3; + transition: all .4s cubic-bezier(0.9,0,0.1,1); + height: 40px; + opacity: 1; + position: absolute; + text-align: center; + } + .movies .loading.hide { + height: 0; + padding: 0; + opacity: 0; + margin-top: -20px; + overflow: hidden; + } + + .movies .loading .spinner { + display: inline-block; + } + + .movies .loading .message { + margin: 0 20px; + } + + .movies h2 { + margin-bottom: 20px; + } + + @media all and (max-width: 480px) { + .movies h2 { + font-size: 25px; + margin-bottom: 10px; + } + } + + .movies > .description { + position: absolute; + top: 30px; + right: 0; + font-style: italic; + opacity: 0.8; + } + .movies:hover > .description { + opacity: 1; + } + + @media all and (max-width: 860px) { + .movies > .description { + display: none; + } + } + + .movies.thumbs_list { + padding: 20px 0 20px; + } + + .home .movies { + padding-top: 6px; + } + + .movies .movie { + position: relative; + margin: 10px 0; + padding-left: 20px; + overflow: hidden; + width: 100%; + height: 180px; + transition: all 0.6s cubic-bezier(0.9,0,0.1,1); + transition-property: width, height; + background: rgba(0,0,0,.2); + } + + .movies.mass_edit_list .movie { + padding-left: 22px; + background: none; + } + + .movies.details_list .movie { + padding-left: 120px; + } + + .movies.list_list .movie:not(.details_view), + .movies.mass_edit_list .movie { + height: 30px; + border-bottom: 1px solid rgba(255,255,255,.15); + } + + .movies.list_list .movie:last-child, + .movies.mass_edit_list .movie:last-child { + border: none; + } + + .movies.thumbs_list .movie { + width: 16.66667%; + height: auto; + display: inline-block; + margin: 0; + padding: 0; + vertical-align: top; + } + + @media all and (max-width: 800px) { + .movies.thumbs_list .movie { + width: 25%; + } + } + + .movies .movie .mask { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + } + + .movies.list_list .movie:not(.details_view), + .movies.mass_edit_list .movie { + margin: 0; + } + + .movies .data { + padding: 20px; + height: 100%; + width: 100%; + position: relative; + transition: all .6s cubic-bezier(0.9,0,0.1,1); + right: 0; + } + .movies.list_list .movie:not(.details_view) .data, + .movies.mass_edit_list .movie .data { + padding: 0 0 0 10px; + border: 0; + background: #4e5969; + } + .movies.mass_edit_list .movie .data { + padding-left: 8px; + } + + .movies.thumbs_list .data { + position: absolute; + left: 0; + top: 0; + width: 100%; + padding: 10px; + height: 100%; + background: none; + transition: none; + } + + .movies.thumbs_list .movie:hover .data { + background: rgba(0,0,0,0.9); + } + + .movies .data.hide_right { + right: -100%; + } + + .movies .movie .check { + display: none; + } + + .movies.mass_edit_list .movie .check { + position: absolute; + left: 0; + top: 0; + display: block; + margin: 7px 0 0 5px; + } + + .movies .poster { + position: absolute; + left: 0; + width: 120px; + line-height: 0; + overflow: hidden; + height: 100%; + transition: all .6s cubic-bezier(0.9,0,0.1,1); + background: rgba(0,0,0,.1); + } + .movies.thumbs_list .poster { + position: relative; + } + .movies.list_list .movie:not(.details_view) .poster, + .movies.mass_edit_list .poster { + width: 20px; + height: 30px; + } + .movies.mass_edit_list .poster { + display: none; + } + + .movies.thumbs_list .poster { + width: 100%; + height: 100%; + transition: none; + background: no-repeat center; + background-size: cover; + } + .movies.thumbs_list .no_thumbnail .empty_file { + width: 100%; + height: 100%; + } + + .movies .poster img, + .options .poster img { + width: 100%; + height: 100%; + } + .movies.thumbs_list .poster img { + height: auto; + width: 100%; + top: 0; + bottom: 0; + opacity: 0; + } + + .movies .info { + position: relative; + height: 100%; + width: 100%; + } + + .movies .info .title { + font-size: 28px; + font-weight: bold; + margin-bottom: 10px; + margin-top: 2px; + width: 100%; + padding-right: 80px; + transition: all 0.2s linear; + height: 35px; + top: -5px; + position: relative; + } + .movies.list_list .info .title, + .movies.mass_edit_list .info .title { + height: 100%; + top: 0; + margin: 0; + } + .touch_enabled .movies.list_list .info .title { + display: inline-block; + padding-right: 55px; + } + + .movies .info .title span { + display: inline-block; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + height: 100%; + line-height: 30px; + } + + .movies.thumbs_list .info .title span { + white-space: normal; + overflow: auto; + height: auto; + text-align: left; + } + + @media all and (max-width: 480px) { + .movies.thumbs_list .movie .info .title span, + .movies.thumbs_list .movie .info .year { + font-size: 15px; + line-height: 15px; + overflow: hidden; + } + } + + .movies.list_list .movie:not(.details_view) .info .title, + .movies.mass_edit_list .info .title { + font-size: 16px; + font-weight: normal; + width: auto; + } + + .movies.thumbs_list .movie:not(.no_thumbnail) .info { + display: none; + } + .movies.thumbs_list .movie:hover .info { + display: block; + } + + .movies.thumbs_list .info .title { + font-size: 21px; + word-wrap: break-word; + padding: 0; + height: 100%; + } + + .movies .info .year { + position: absolute; + color: #bbb; + right: 0; + top: 6px; + text-align: right; + transition: all 0.2s linear; + font-weight: normal; + } + .movies.list_list .movie:not(.details_view) .info .year, + .movies.mass_edit_list .info .year { + font-size: 1.25em; + right: 10px; + } + + .movies.thumbs_list .info .year { + font-size: 23px; + margin: 0; + bottom: 0; + left: 0; + top: auto; + right: auto; + color: #FFF; + } + + .touch_enabled .movies.list_list .movie .info .year { + font-size: 1em; + } + + .movies .info .description { + top: 30px; + clear: both; + bottom: 30px; + overflow: hidden; + position: absolute; + } + .movies .data:hover .description { + overflow: auto; + } + .movies.list_list .movie:not(.details_view) .info .description, + .movies.mass_edit_list .info .description, + .movies.thumbs_list .info .description { + display: none; + } + + .movies .data .quality { + position: absolute; + bottom: 2px; + display: block; + min-height: 20px; + } + + .movies.list_list .movie:hover .data .quality { + display: none; + } + + .touch_enabled .movies.list_list .movie .data .quality { + position: relative; + display: inline-block; + margin: 0; + top: -4px; + } + + @media all and (max-width: 480px) { + .movies .data .quality { + display: none; + } + } + + .movies .status_suggest .data .quality, + .movies.thumbs_list .data .quality { + display: none; + } + + .movies .data .quality span { + padding: 2px 3px; + font-weight: bold; + opacity: 0.5; + font-size: 10px; + height: 16px; + line-height: 12px; + vertical-align: middle; + display: inline-block; + text-transform: uppercase; + font-weight: normal; + margin: 0 4px 0 0; + border-radius: 2px; + background-color: rgba(255,255,255,0.1); + } + .movies.list_list .data .quality, + .movies.mass_edit_list .data .quality { + text-align: right; + right: 0; + margin-right: 60px; + z-index: 1; + top: 5px; + } + + .movies .data .quality .available, + .movies .data .quality .snatched { + opacity: 1; + cursor: pointer; + } + + .movies .data .quality .available { background-color: #578bc3; } + .movies .data .quality .failed { background-color: #a43d34; } + .movies .data .quality .snatched { background-color: #a2a232; } + .movies .data .quality .seeding { background-color: #0a6819; } + .movies .data .quality .done { + background-color: #369545; + opacity: 1; + } + .movies .data .quality .finish { + background-image: url('../images/sprite.png'); + background-repeat: no-repeat; + background-position: 0 2px; + padding-left: 14px; + background-size: 14px + } + + .movies .data .actions { + position: absolute; + bottom: 17px; + right: 20px; + line-height: 0; + top: 0; + display: block; + width: auto; + opacity: 0; + display: none; + } + @media all and (max-width: 480px) { + .movies .data .actions { + display: none !important; + } + } + + .movies .movie:hover .data .actions, + .touch_enabled .movies .movie .data .actions { + opacity: 1; + display: inline-block; + } + + .movies.details_list .data .actions { + top: auto; + bottom: 18px; + } + + .movies .movie:hover .actions { + opacity: 1; + display: inline-block; + } + .movies.thumbs_list .data .actions { + bottom: 12px; + right: 10px; + top: auto; + } + + .movies .movie:hover .action { opacity: 0.6; } + .movies .movie:hover .action:hover { opacity: 1; } + + .movies .data .action { + display: inline-block; + height: 22px; + min-width: 33px; + padding: 0 5px; + line-height: 26px; + text-align: center; + font-size: 13px; + color: #FFF; + margin-left: 1px; + } + .movies .data .action.trailer { color: #FFF; } + .movies .data .action.download { color: #b9dec0; } + .movies .data .action.edit { color: #c6b589; } + .movies .data .action.refresh { color: #cbeecc; } + .movies .data .action.delete { color: #e9b0b0; } + .movies .data .action.directory { color: #ffed92; } + .movies .data .action.readd { color: #c2fac5; } + + .movies.mass_edit_list .movie .data .actions { + display: none; + } + + .movies.list_list .movie:not(.details_view):hover .actions, + .movies.mass_edit_list .movie:hover .actions, + .touch_enabled .movies.list_list .movie:not(.details_view) .actions { + margin: 0; + background: #4e5969; + top: 2px; + bottom: 2px; + right: 5px; + z-index: 3; + } + + .movies .delete_container { + clear: both; + text-align: center; + font-size: 20px; + position: absolute; + padding: 80px 0 0; + left: 120px; + right: 0; + } + .movies .delete_container .or { + padding: 10px; + } + .movies .delete_container .delete { + background-color: #ff321c; + font-weight: normal; + } + .movies .delete_container .delete:hover { + color: #fff; + background-color: #d32917; + } + + .movies .options { + position: absolute; + right: 0; + left: 120px; + } + + .movies .options .form { + margin: 80px 0 0; + font-size: 20px; + text-align: center; + } + + .movies .options .form select { + margin-right: 20px; + } + + .movies .options .table { + height: 180px; + overflow: auto; + line-height: 2em; + } + .movies .options .table .item { + border-bottom: 1px solid rgba(255,255,255,0.1); + } + .movies .options .table .item.ignored span, + .movies .options .table .item.failed span { + text-decoration: line-through; + color: rgba(255,255,255,0.4); + } + .movies .options .table .item.ignored .delete:before, + .movies .options .table .item.failed .delete:before { + display: inline-block; + content: "\e04b"; + transform: scale(-1, 1); + } + + .movies .options .table .item:last-child { border: 0; } + .movies .options .table .item:nth-child(even) { + background: rgba(255,255,255,0.05); + } + .movies .options .table .item:not(.head):hover { + background: rgba(255,255,255,0.03); + } + + .movies .options .table .item > * { + display: inline-block; + padding: 0 5px; + width: 60px; + min-height: 24px; + white-space: nowrap; + text-overflow: ellipsis; + text-align: center; + vertical-align: top; + border-left: 1px solid rgba(255, 255, 255, 0.1); + } + .movies .options .table .item > *:first-child { + border: 0; + } + .movies .options .table .provider { + width: 120px; + text-overflow: ellipsis; + overflow: hidden; + } + .movies .options .table .name { + width: 340px; + overflow: hidden; + text-align: left; + padding: 0 10px; + } + .movies .options .table.files .name { width: 590px; } + .movies .options .table .type { width: 130px; } + .movies .options .table .is_available { width: 90px; } + .movies .options .table .age, + .movies .options .table .size { width: 40px; } + + .movies .options .table a { + width: 30px !important; + height: 20px; + opacity: 0.8; + line-height: 25px; + } + .movies .options .table a:hover { opacity: 1; } + .movies .options .table a.download { color: #a7fbaf; } + .movies .options .table a.delete { color: #fda3a3; } + .movies .options .table .ignored a.delete, + .movies .options .table .failed a.delete { color: #b5fda3; } + + .movies .options .table .head > * { + font-weight: bold; + font-size: 14px; + padding-top: 4px; + padding-bottom: 4px; + height: auto; + } + + .trailer_container { + width: 100%; + background: #000; + text-align: center; + transition: all .6s cubic-bezier(0.9,0,0.1,1); + overflow: hidden; + left: 0; + position: absolute; + z-index: 10; + } + .trailer_container.hide { + height: 0 !important; + } + + .hide_trailer { + position: absolute; + top: 0; + left: 50%; + margin-left: -50px; + width: 100px; + text-align: center; + padding: 3px 10px; + background: #4e5969; + transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s; + z-index: 11; + } + .hide_trailer.hide { + top: -30px; + } + + .movies .movie .try_container { + padding: 5px 10px; + text-align: center; + } + + .movies .movie .try_container a { + margin: 0 5px; + padding: 2px 5px; + } + + .movies .movie .releases .next_release { + border-left: 6px solid #2aa300; + } + + .movies .movie .releases .next_release > :first-child { + margin-left: -6px; + } + + .movies .movie .releases .last_release { + border-left: 6px solid #ffa200; + } + + .movies .movie .releases .last_release > :first-child { + margin-left: -6px; + } + .movies .movie .trynext { + display: inline; + position: absolute; + right: 180px; + z-index: 2; + opacity: 0; + background: #4e5969; + text-align: right; + height: 100%; + top: 0; + } + .touch_enabled .movies .movie .trynext { + display: none; + } + + @media all and (max-width: 480px) { + .movies .movie .trynext { + display: none; + } + } + .movies.mass_edit_list .trynext { display: none; } + .wanted .movies .movie .trynext { + padding-right: 30px; + } + .movies .movie:hover .trynext, + .touch_enabled .movies.details_list .movie .trynext { + opacity: 1; + } + + .movies.details_list .movie .trynext { + background: #47515f; + padding: 0; + right: 0; + height: 25px; + } + + .movies .movie .trynext a { + background-position: 5px center; + padding: 0 5px 0 25px; + margin-right: 10px; + color: #FFF; + height: 100%; + line-height: 27px; + font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; + } + .movies .movie .trynext a:before { + margin: 2px 0 0 -20px; + position: absolute; + font-family: 'Elusive-Icons'; + } + .movies.details_list .movie .trynext a { + line-height: 23px; + } + .movies .movie .trynext a:last-child { + margin: 0; + } + .movies .movie .trynext a:hover, + .touch_enabled .movies .movie .trynext a { + background-color: #369545; + } + + .movies .load_more { + display: block; + padding: 10px; + text-align: center; + font-size: 20px; + } + .movies .load_more.loading { + opacity: .5; + } + +.movies .alph_nav { + height: 44px; +} + + @media all and (max-width: 480px) { + .movies .alph_nav { + display: none; + } + } + + .movies .alph_nav .menus { + display: inline-block; + float: right; + } + +.movies .alph_nav .numbers, +.movies .alph_nav .counter, +.movies .alph_nav .actions { + list-style: none; + padding: 0 0 1px; + margin: 0; + user-select: none; +} + + .movies .alph_nav .counter { + display: inline-block; + text-align: right; + padding: 0 10px; + height: 100%; + line-height: 43px; + border-right: 1px solid rgba(255,255,255,.07); + } + + .movies .alph_nav .numbers li, + .movies .alph_nav .actions li { + display: inline-block; + vertical-align: top; + height: 100%; + line-height: 30px; + text-align: center; + border: 1px solid transparent; + transition: all 0.1s ease-in-out; + } + + .movies .alph_nav .numbers li { + width: 30px; + height: 30px; + opacity: 0.3; + } + .movies .alph_nav .numbers li.letter_all { + width: 60px; + } + + .movies .alph_nav li.available { + font-weight: bold; + cursor: pointer; + opacity: 1; + + } + .movies .alph_nav li.active.available, + .movies .alph_nav li.available:hover { + background: rgba(0,0,0,.1); + } + + .movies .alph_nav .search input { + padding: 6px 5px; + width: 100%; + height: 44px; + display: inline-block; + border: 0; + background: none; + color: #444; + font-size: 14px; + padding: 10px; + padding: 0 10px 0 30px; + border-bottom: 1px solid rgba(0,0,0,.08); + } + .movies .alph_nav .search input:focus { + background: rgba(0,0,0,.08); + } + + .movies .alph_nav .search input::-webkit-input-placeholder { + color: #444; + opacity: .6; + } + + .movies .alph_nav .search:before { + font-family: 'Elusive-Icons'; + content: "\e03e"; + position: absolute; + height: 20px; + line-height: 45px; + font-size: 12px; + margin: 0 0 0 10px; + opacity: .6; + color: #444; + } + + .movies .alph_nav .actions { + -moz-user-select: none; + width: 44px; + height: 44px; + display: inline-block; + vertical-align: top; + z-index: 200; + position: relative; + border: 1px solid rgba(255,255,255,.07); + border-width: 0 1px; + } + .movies .alph_nav .actions:hover { + box-shadow: 0 100px 20px -10px rgba(0,0,0,0.55); + } + .movies .alph_nav .actions li { + width: 100%; + height: 45px; + line-height: 40px; + position: relative; + z-index: 20; + display: none; + cursor: pointer; + } + .movies .alph_nav .actions:hover li:not(.active) { + display: block; + background: #FFF; + color: #444; + } + .movies .alph_nav .actions li:hover:not(.active) { + background: #ccc; + } + .movies .alph_nav .actions li.active { + display: block; + } + + .movies .alph_nav .actions li.mass_edit:before { + content: "\e070"; + } + + .movies .alph_nav .actions li.list:before { + content: "\e0d8"; + } + + .movies .alph_nav .actions li.details:before { + content: "\e022"; + } + + .movies .alph_nav .mass_edit_form { + clear: both; + text-align: center; + display: none; + overflow: hidden; + float: left; + height: 44px; + line-height: 44px; + } + .movies.mass_edit_list .mass_edit_form { + display: inline-block; + } + .movies.mass_edit_list .mass_edit_form .select { + font-size: 14px; + display: inline-block; + } + .movies.mass_edit_list .mass_edit_form .select .check { + display: inline-block; + vertical-align: middle; + margin: -4px 0 0 5px; + } + .movies.mass_edit_list .mass_edit_form .select span { + opacity: 0.7; + } + .movies.mass_edit_list .mass_edit_form .select .count { + font-weight: bold; + margin: 0 3px 0 10px; + } + + .movies .alph_nav .mass_edit_form .quality { + display: inline-block; + margin: 0 0 0 16px; + } + .movies .alph_nav .mass_edit_form .quality select { + width: 120px; + margin-right: 5px; + } + .movies .alph_nav .mass_edit_form .button { + padding: 3px 7px; + } + + .movies .alph_nav .mass_edit_form .refresh, + .movies .alph_nav .mass_edit_form .delete { + display: inline-block; + margin-left: 8px; + } + + .movies .alph_nav .mass_edit_form .refresh span, + .movies .alph_nav .mass_edit_form .delete span { + margin: 0 10px 0 0; + } + + .movies .alph_nav .more_menu > a { + background: none; + } + + .movies .alph_nav .more_menu.extra > a:before { + content: '...'; + font-size: 1.7em; + line-height: 23px; + text-align: center; + display: block; + } + + .movies .alph_nav .more_menu.filter { + } + + .movies .alph_nav .more_menu.filter > a:before { + content: "\e0e8"; + font-family: 'Elusive-Icons'; + line-height: 33px; + display: block; + text-align: center; + } + + .movies .alph_nav .more_menu.filter .wrapper { + right: 88px; + width: 300px; + } + +.movies .empty_wanted { + background-image: url('../images/emptylist.png'); + background-position: 80% 0; + height: 750px; + width: 100%; + max-width: 900px; + padding-top: 260px; +} + +.movies .empty_manage { + text-align: center; + font-size: 25px; + line-height: 150%; + padding: 40px 0; +} + + .movies .empty_manage .after_manage { + margin-top: 30px; + font-size: 16px; + } + + .movies .progress { + padding: 10px; + margin: 5px 0; + text-align: left; + } + + .movies .progress > div { + padding: 5px 10px; + font-size: 12px; + line-height: 12px; + text-align: left; + display: inline-block; + width: 49%; + background: rgba(255, 255, 255, 0.05); + margin: 2px 0.5%; + } + + .movies .progress > div .folder { + display: inline-block; + padding: 5px 20px 5px 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 85%; + direction: rtl; + vertical-align: middle; + } + + .movies .progress > div .percentage { + font-weight: bold; + display: inline-block; + text-transform: uppercase; + font-weight: normal; + font-size: 20px; + border-left: 1px solid rgba(255, 255, 255, .2); + width: 15%; + text-align: right; + vertical-align: middle; + } diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js new file mode 100644 index 0000000..f5b5a2d --- /dev/null +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -0,0 +1,298 @@ +var Movie = new Class({ + + Extends: BlockBase, + + action: {}, + + initialize: function(list, options, data){ + var self = this; + + self.data = data; + self.view = options.view || 'details'; + self.list = list; + + self.el = new Element('div.movie'); + + self.profile = Quality.getProfile(data.profile_id) || {}; + self.category = CategoryList.getCategory(data.category_id) || {}; + self.parent(self, options); + + self.addEvents(); + }, + + addEvents: function(){ + var self = this; + + App.addEvent('movie.update.'+self.data.id, function(notification){ + self.busy(false); + self.removeView(); + self.update.delay(2000, self, notification); + }); + + ['movie.busy', 'searcher.started'].each(function(listener){ + App.addEvent(listener+'.'+self.data.id, function(notification){ + if(notification.data) + self.busy(true) + }); + }) + + App.addEvent('searcher.ended.'+self.data.id, function(notification){ + if(notification.data) + self.busy(false) + }); + }, + + destroy: function(){ + var self = this; + + self.el.destroy(); + delete self.list.movies_added[self.get('id')]; + self.list.movies.erase(self) + + self.list.checkIfEmpty(); + + // Remove events + App.removeEvents('movie.update.'+self.data.id); + ['movie.busy', 'searcher.started'].each(function(listener){ + App.removeEvents(listener+'.'+self.data.id); + }) + }, + + busy: function(set_busy){ + var self = this; + + if(!set_busy){ + setTimeout(function(){ + if(self.spinner){ + self.mask.fade('out'); + setTimeout(function(){ + if(self.mask) + self.mask.destroy(); + if(self.spinner) + self.spinner.el.destroy(); + self.spinner = null; + self.mask = null; + }, 400); + } + }, 1000) + } + else if(!self.spinner) { + self.createMask(); + self.spinner = createSpinner(self.mask); + self.mask.fade('in'); + } + }, + + createMask: function(){ + var self = this; + self.mask = new Element('div.mask', { + 'styles': { + 'z-index': 4 + } + }).inject(self.el, 'top').fade('hide'); + }, + + positionMask: function(){ + var self = this, + s = self.el.getSize() + + return self.mask.setStyles({ + 'width': s.x, + 'height': s.y + }).position({ + 'relativeTo': self.el + }) + }, + + update: function(notification){ + var self = this; + + self.data = notification.data; + self.el.empty(); + self.removeView(); + + self.profile = Quality.getProfile(self.data.profile_id) || {}; + self.category = CategoryList.getCategory(self.data.category_id) || {}; + self.create(); + + self.busy(false); + }, + + create: function(){ + var self = this; + + var s = Status.get(self.get('status_id')); + self.el.addClass('status_'+s.identifier); + + self.el.adopt( + self.select_checkbox = new Element('input[type=checkbox].inlay', { + 'events': { + 'change': function(){ + self.fireEvent('select') + } + } + }), + self.thumbnail = File.Select.single('poster', self.data.library.files), + self.data_container = new Element('div.data.inlay.light').adopt( + self.info_container = new Element('div.info').adopt( + new Element('div.title').adopt( + self.title = new Element('span', { + 'text': self.getTitle() || 'n/a' + }), + self.year = new Element('div.year', { + 'text': self.data.library.year || 'n/a' + }) + ), + self.description = new Element('div.description', { + 'text': self.data.library.plot + }), + self.quality = new Element('div.quality', { + 'events': { + 'click': function(e){ + var releases = self.el.getElement('.actions .releases'); + if(releases.isVisible()) + releases.fireEvent('click', [e]) + } + } + }) + ), + self.actions = new Element('div.actions') + ) + ); + + if(self.thumbnail.empty) + self.el.addClass('no_thumbnail'); + + //self.changeView(self.view); + self.select_checkbox_class = new Form.Check(self.select_checkbox); + + // Add profile + if(self.profile.data) + self.profile.getTypes().each(function(type){ + + var q = self.addQuality(type.quality_id || type.get('quality_id')); + if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){ + q.addClass('finish'); + q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.') + } + + }); + + // Add releases + self.data.releases.each(function(release){ + + var q = self.quality.getElement('.q_id'+ release.quality_id), + status = Status.get(release.status_id); + + if(!q && (status.identifier == 'snatched' || status.identifier == 'done')) + var q = self.addQuality(release.quality_id) + + if (status && q && !q.hasClass(status.identifier)){ + q.addClass(status.identifier); + q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label) + } + + }); + + Object.each(self.options.actions, function(action, key){ + self.action[key.toLowerCase()] = action = new self.options.actions[key](self) + if(action.el) + self.actions.adopt(action) + }); + + }, + + addQuality: function(quality_id){ + var self = this; + + var q = Quality.getQuality(quality_id); + return new Element('span', { + 'text': q.label, + 'class': 'q_'+q.identifier + ' q_id' + q.id, + 'title': '' + }).inject(self.quality); + + }, + + getTitle: function(){ + var self = this; + + var titles = self.data.library.titles; + + var title = titles.filter(function(title){ + return title['default'] + }).pop() + + if(title) + return self.getUnprefixedTitle(title.title) + else if(titles.length > 0) + return self.getUnprefixedTitle(titles[0].title) + + return 'Unknown movie' + }, + + getUnprefixedTitle: function(t){ + if(t.substr(0, 4).toLowerCase() == 'the ') + t = t.substr(4) + ', The'; + return t; + }, + + slide: function(direction, el){ + var self = this; + + if(direction == 'in'){ + self.temp_view = self.view; + self.changeView('details'); + + self.el.addEvent('outerClick', function(){ + self.removeView(); + self.slide('out') + }) + el.show(); + self.data_container.addClass('hide_right'); + } + else { + self.el.removeEvents('outerClick') + + setTimeout(function(){ + self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide(); + }, 600); + + self.data_container.removeClass('hide_right'); + } + }, + + changeView: function(new_view){ + var self = this; + + self.el + .removeClass(self.view+'_view') + .addClass(new_view+'_view') + + self.view = new_view; + }, + + removeView: function(){ + var self = this; + + self.el.removeClass(self.view+'_view') + }, + + get: function(attr){ + return this.data[attr] || this.data.library[attr] + }, + + select: function(bool){ + var self = this; + self.select_checkbox_class[bool ? 'check' : 'uncheck']() + }, + + isSelected: function(){ + return this.select_checkbox.get('checked'); + }, + + toElement: function(){ + return this.el; + } + +}); \ No newline at end of file diff --git a/couchpotato/core/media/movie/_base/static/search.css b/couchpotato/core/media/movie/_base/static/search.css new file mode 100644 index 0000000..dc74734 --- /dev/null +++ b/couchpotato/core/media/movie/_base/static/search.css @@ -0,0 +1,275 @@ +.search_form { + display: inline-block; + vertical-align: middle; + position: absolute; + right: 105px; + top: 0; + text-align: right; + height: 100%; + border-bottom: 4px solid transparent; + transition: all .4s cubic-bezier(0.9,0,0.1,1); + position: absolute; + z-index: 20; + border: 1px solid transparent; + border-width: 0 0 4px; +} + .search_form:hover { + border-color: #047792; + } + + @media all and (max-width: 480px) { + .search_form { + right: 44px; + } + } + + .search_form.focused, + .search_form.shown { + border-color: #04bce6; + } + + .search_form .input { + height: 100%; + overflow: hidden; + width: 45px; + transition: all .4s cubic-bezier(0.9,0,0.1,1); + } + + .search_form.focused .input, + .search_form.shown .input { + width: 380px; + background: #4e5969; + } + + .search_form .input input { + border-radius: 0; + display: block; + border: 0; + background: none; + color: #FFF; + font-size: 25px; + height: 100%; + padding: 10px; + width: 100%; + opacity: 0; + padding: 0 40px 0 10px; + transition: all .4s ease-in-out .2s; + } + .search_form.focused .input input, + .search_form.shown .input input { + opacity: 1; + } + + @media all and (max-width: 480px) { + .search_form .input input { + font-size: 15px; + } + + .search_form.focused .input, + .search_form.shown .input { + width: 277px; + } + } + + .search_form .input a { + position: absolute; + top: 0; + right: 0; + width: 44px; + height: 100%; + cursor: pointer; + vertical-align: middle; + text-align: center; + line-height: 66px; + font-size: 15px; + color: #FFF; + } + + .search_form .input a:after { + content: "\e03e"; + } + + .search_form.shown.filled .input a:after { + content: "\e04e"; + } + + @media all and (max-width: 480px) { + .search_form .input a { + line-height: 44px; + } + } + + .search_form .results_container { + text-align: left; + position: absolute; + background: #5c697b; + margin: 4px 0 0; + width: 470px; + min-height: 50px; + box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55); + display: none; + } + @media all and (max-width: 480px) { + .search_form .results_container { + width: 320px; + } + } + .search_form.focused.filled .results_container, + .search_form.shown.filled .results_container { + display: block; + } + + .search_form .results { + max-height: 570px; + overflow-x: hidden; + } + + .movie_result { + overflow: hidden; + height: 50px; + position: relative; + } + + .movie_result .options { + position: absolute; + height: 100%; + top: 0; + left: 30px; + right: 0; + padding: 13px; + border: 1px solid transparent; + border-width: 1px 0; + border-radius: 0; + box-shadow: inset 0 1px 8px rgba(0,0,0,0.25); + } + .movie_result .options > .in_library_wanted { + margin-top: -7px; + } + + .movie_result .options > div { + border: 0; + } + + .movie_result .options .thumbnail { + vertical-align: middle; + } + + .movie_result .options select { + vertical-align: middle; + display: inline-block; + margin-right: 10px; + } + .movie_result .options select[name=title] { width: 170px; } + .movie_result .options select[name=profile] { width: 90px; } + .movie_result .options select[name=category] { width: 80px; } + + @media all and (max-width: 480px) { + + .movie_result .options select[name=title] { width: 90px; } + .movie_result .options select[name=profile] { width: 50px; } + .movie_result .options select[name=category] { width: 50px; } + + } + + .movie_result .options .button { + vertical-align: middle; + display: inline-block; + } + + .movie_result .options .message { + height: 100%; + font-size: 20px; + color: #fff; + line-height: 20px; + } + + .movie_result .data { + position: absolute; + height: 100%; + top: 0; + left: 30px; + right: 0; + background: #5c697b; + cursor: pointer; + border-top: 1px solid rgba(255,255,255, 0.08); + transition: all .4s cubic-bezier(0.9,0,0.1,1); + } + .movie_result .data.open { + left: 100% !important; + } + + .movie_result:last-child .data { border-bottom: 0; } + + .movie_result .in_wanted, .movie_result .in_library { + position: absolute; + bottom: 2px; + left: 14px; + font-size: 11px; + } + + .movie_result .thumbnail { + width: 34px; + min-height: 100%; + display: block; + margin: 0; + vertical-align: top; + } + + .movie_result .info { + position: absolute; + top: 20%; + left: 15px; + right: 7px; + vertical-align: middle; + } + + .movie_result .info h2 { + margin: 0; + font-weight: normal; + font-size: 20px; + padding: 0; + } + + .search_form .info h2 { + position: absolute; + width: 100%; + } + + .movie_result .info h2 .title { + display: block; + margin: 0; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .search_form .info h2 .title { + position: absolute; + width: 88%; + } + + .movie_result .info h2 .year { + padding: 0 5px; + text-align: center; + position: absolute; + width: 12%; + right: 0; + } + + @media all and (max-width: 480px) { + + .search_form .info h2 .year { + font-size: 12px; + margin-top: 7px; + } + + } + +.search_form .mask, +.movie_result .mask { + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; +} \ No newline at end of file diff --git a/couchpotato/core/media/movie/_base/static/search.js b/couchpotato/core/media/movie/_base/static/search.js new file mode 100644 index 0000000..376e61c --- /dev/null +++ b/couchpotato/core/media/movie/_base/static/search.js @@ -0,0 +1,414 @@ +Block.Search = new Class({ + + Extends: BlockBase, + + cache: {}, + + create: function(){ + var self = this; + + var focus_timer = 0; + self.el = new Element('div.search_form').adopt( + new Element('div.input').adopt( + self.input = new Element('input', { + 'placeholder': 'Search & add a new movie', + 'events': { + 'keyup': self.keyup.bind(self), + 'focus': function(){ + if(focus_timer) clearTimeout(focus_timer); + self.el.addClass('focused') + if(this.get('value')) + self.hideResults(false) + }, + 'blur': function(){ + focus_timer = (function(){ + self.el.removeClass('focused') + }).delay(100); + } + } + }), + new Element('a.icon2', { + 'events': { + 'click': self.clear.bind(self), + 'touchend': self.clear.bind(self) + } + }) + ), + self.result_container = new Element('div.results_container', { + 'tween': { + 'duration': 200 + }, + 'events': { + 'mousewheel': function(e){ + (e).stopPropagation(); + } + } + }).adopt( + self.results = new Element('div.results') + ) + ); + + self.mask = new Element('div.mask').inject(self.result_container).fade('hide'); + + }, + + clear: function(e){ + var self = this; + (e).preventDefault(); + + if(self.last_q === ''){ + self.input.blur() + self.last_q = null; + } + else { + + self.last_q = ''; + self.input.set('value', ''); + self.input.focus() + + self.movies = [] + self.results.empty() + self.el.removeClass('filled') + + } + }, + + hideResults: function(bool){ + var self = this; + + if(self.hidden == bool) return; + + self.el[bool ? 'removeClass' : 'addClass']('shown'); + + if(bool){ + History.removeEvent('change', self.hideResults.bind(self, !bool)); + self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool)); + } + else { + History.addEvent('change', self.hideResults.bind(self, !bool)); + self.el.addEvent('outerClick', self.hideResults.bind(self, !bool)); + } + + self.hidden = bool; + }, + + keyup: function(e){ + var self = this; + + self.el[self.q() ? 'addClass' : 'removeClass']('filled') + + if(self.q() != self.last_q){ + if(self.api_request && self.api_request.isRunning()) + self.api_request.cancel(); + + if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer) + self.autocomplete_timer = self.autocomplete.delay(300, self) + } + + }, + + autocomplete: function(){ + var self = this; + + if(!self.q()){ + self.hideResults(true) + return + } + + self.list() + }, + + list: function(){ + var self = this, + q = self.q(), + cache = self.cache[q]; + + self.hideResults(false); + + if(!cache){ + self.mask.fade('in'); + + if(!self.spinner) + self.spinner = createSpinner(self.mask); + + self.api_request = Api.request('movie.search', { + 'data': { + 'q': q + }, + 'onComplete': self.fill.bind(self, q) + }) + } + else + self.fill(q, cache) + + self.last_q = q; + + }, + + fill: function(q, json){ + var self = this; + + self.cache[q] = json + + self.movies = {} + self.results.empty() + + Object.each(json.movies, function(movie){ + + var m = new Block.Search.Item(movie); + $(m).inject(self.results) + self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m + + if(q == movie.imdb) + m.showOptions() + + }); + + // Calculate result heights + var w = window.getSize(), + rc = self.result_container.getCoordinates(); + + self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px') + self.mask.fade('out') + + }, + + loading: function(bool){ + this.el[bool ? 'addClass' : 'removeClass']('loading') + }, + + q: function(){ + return this.input.get('value').trim(); + } + +}); + +Block.Search.Item = new Class({ + + Implements: [Options, Events], + + initialize: function(info, options){ + var self = this; + self.setOptions(options); + + self.info = info; + self.alternative_titles = []; + + self.create(); + }, + + create: function(){ + var self = this, + info = self.info; + + self.el = new Element('div.movie_result', { + 'id': info.imdb + }).adopt( + self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', { + 'src': info.images.poster[0], + 'height': null, + 'width': null + }) : null, + self.options_el = new Element('div.options.inlay'), + self.data_container = new Element('div.data', { + 'events': { + 'click': self.showOptions.bind(self) + } + }).adopt( + self.info_container = new Element('div.info').adopt( + new Element('h2').adopt( + self.title = new Element('span.title', { + 'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown' + }), + self.year = info.year ? new Element('span.year', { + 'text': info.year + }) : null + ) + ) + ) + ) + + if(info.titles) + info.titles.each(function(title){ + self.alternativeTitle({ + 'title': title + }); + }) + }, + + alternativeTitle: function(alternative){ + var self = this; + + self.alternative_titles.include(alternative); + }, + + getTitle: function(){ + var self = this; + try { + return self.info.original_title ? self.info.original_title : self.info.titles[0]; + } + catch(e){ + return 'Unknown'; + } + }, + + get: function(key){ + return this.info[key] + }, + + showOptions: function(){ + var self = this; + + self.createOptions(); + + self.data_container.addClass('open'); + self.el.addEvent('outerClick', self.closeOptions.bind(self)) + + }, + + closeOptions: function(){ + var self = this; + + self.data_container.removeClass('open'); + self.el.removeEvents('outerClick') + }, + + add: function(e){ + var self = this; + + if(e) + (e).preventDefault(); + + self.loadingMask(); + + Api.request('movie.add', { + 'data': { + 'identifier': self.info.imdb, + 'title': self.title_select.get('value'), + 'profile_id': self.profile_select.get('value'), + 'category_id': self.category_select.get('value') + }, + 'onComplete': function(json){ + self.options_el.empty(); + self.options_el.adopt( + new Element('div.message', { + 'text': json.added ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs' + }) + ); + self.mask.fade('out'); + + self.fireEvent('added'); + }, + 'onFailure': function(){ + self.options_el.empty(); + self.options_el.adopt( + new Element('div.message', { + 'text': 'Something went wrong, check the logs for more info.' + }) + ); + self.mask.fade('out'); + } + }); + }, + + createOptions: function(){ + var self = this, + info = self.info; + + if(!self.options_el.hasClass('set')){ + + if(self.info.in_library){ + var in_library = []; + self.info.in_library.releases.each(function(release){ + in_library.include(release.quality.label) + }); + } + + self.options_el.grab( + new Element('div', { + 'class': self.info.in_wanted && self.info.in_wanted.profile || in_library ? 'in_library_wanted' : '' + }).adopt( + self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', { + 'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label + }) : (in_library ? new Element('span.in_library', { + 'text': 'Already in library: ' + in_library.join(', ') + }) : null), + self.title_select = new Element('select', { + 'name': 'title' + }), + self.profile_select = new Element('select', { + 'name': 'profile' + }), + self.category_select = new Element('select', { + 'name': 'category' + }).grab( + new Element('option', {'value': -1, 'text': 'None'}) + ), + self.add_button = new Element('a.button', { + 'text': 'Add', + 'events': { + 'click': self.add.bind(self) + } + }) + ) + ); + + Array.each(self.alternative_titles, function(alt){ + new Element('option', { + 'text': alt.title + }).inject(self.title_select) + }) + + + // Fill categories + var categories = CategoryList.getAll(); + + if(categories.length == 0) + self.category_select.hide(); + else { + self.category_select.show(); + categories.each(function(category){ + new Element('option', { + 'value': category.data.id, + 'text': category.data.label + }).inject(self.category_select); + }); + } + + // Fill profiles + var profiles = Quality.getActiveProfiles(); + if(profiles.length == 1) + self.profile_select.hide(); + + profiles.each(function(profile){ + new Element('option', { + 'value': profile.id ? profile.id : profile.data.id, + 'text': profile.label ? profile.label : profile.data.label + }).inject(self.profile_select) + }); + + self.options_el.addClass('set'); + + if(categories.length == 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 && + !(self.info.in_wanted && self.info.in_wanted.profile || in_library)) + self.add(); + + } + + }, + + loadingMask: function(){ + var self = this; + + self.mask = new Element('div.mask').inject(self.el).fade('hide') + + createSpinner(self.mask) + self.mask.fade('in') + + }, + + toElement: function(){ + return this.el + } + +}); diff --git a/couchpotato/core/plugins/movie/__init__.py b/couchpotato/core/plugins/movie/__init__.py deleted file mode 100644 index 4df29ad..0000000 --- a/couchpotato/core/plugins/movie/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .main import MoviePlugin - -def start(): - return MoviePlugin() - -config = [] diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py deleted file mode 100644 index 516ce85..0000000 --- a/couchpotato/core/plugins/movie/main.py +++ /dev/null @@ -1,591 +0,0 @@ -from couchpotato import get_session -from couchpotato.api import addApiView -from couchpotato.core.event import fireEvent, fireEventAsync, addEvent -from couchpotato.core.helpers.encoding import toUnicode, simplifyString -from couchpotato.core.helpers.variable import getImdb, splitString, tryInt -from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \ - Release -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__) - - -class MoviePlugin(Plugin): - - default_dict = { - 'profile': {'types': {'quality': {}}}, - 'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}}, - 'library': {'titles': {}, 'files':{}}, - 'files': {}, - 'status': {} - } - - def __init__(self): - addApiView('movie.search', self.search, docs = { - 'desc': 'Search the movie providers for a movie', - 'params': { - 'q': {'desc': 'The (partial) movie name you want to search for'}, - }, - 'return': {'type': 'object', 'example': """{ - 'success': True, - 'empty': bool, any movies returned or not, - 'movies': array, movies found, -}"""} - }) - addApiView('movie.list', self.listView, docs = { - 'desc': 'List movies in wanted list', - 'params': { - 'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'}, - 'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'}, - 'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'}, - 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'}, - 'search': {'desc': 'Search movie title'}, - }, - 'return': {'type': 'object', 'example': """{ - 'success': True, - 'empty': bool, any movies returned or not, - 'movies': array, movies found, -}"""} - }) - addApiView('movie.get', self.getView, docs = { - 'desc': 'Get a movie by id', - 'params': { - 'id': {'desc': 'The id of the movie'}, - } - }) - addApiView('movie.refresh', self.refresh, docs = { - 'desc': 'Refresh a movie by id', - 'params': { - 'id': {'desc': 'Movie ID(s) you want to refresh.', 'type': 'int (comma separated)'}, - } - }) - addApiView('movie.available_chars', self.charView) - addApiView('movie.add', self.addView, docs = { - 'desc': 'Add new movie to the wanted list', - 'params': { - 'identifier': {'desc': 'IMDB id of the movie your want to add.'}, - 'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'}, - 'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, - } - }) - addApiView('movie.edit', self.edit, docs = { - 'desc': 'Add new movie to the wanted list', - 'params': { - 'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'}, - 'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'}, - 'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, - } - }) - addApiView('movie.delete', self.deleteView, docs = { - 'desc': 'Delete a movie from the wanted list', - 'params': { - 'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'}, - 'delete_from': {'desc': 'Delete movie from this page', 'type': 'string: all (default), wanted, manage'}, - } - }) - - addEvent('movie.add', self.add) - addEvent('movie.delete', self.delete) - addEvent('movie.get', self.get) - 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): - - log.debug('Removing releases from dashboard') - - now = time.time() - week = 262080 - - done_status, available_status, snatched_status = \ - fireEvent('status.get', ['done', 'available', '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 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.expire_all() - - def getView(self, id = None, **kwargs): - - movie = self.get(id) if id else None - - return { - 'success': movie is not None, - 'movie': movie, - } - - def get(self, movie_id): - - db = get_session() - - imdb_id = getImdb(str(movie_id)) - - if(imdb_id): - m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first() - else: - m = db.query(Movie).filter_by(id = movie_id).first() - - results = None - if m: - results = m.to_dict(self.default_dict) - - db.expire_all() - return results - - def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None): - - db = get_session() - - # Make a list from string - if status and not isinstance(status, (list, tuple)): - status = [status] - if release_status and not isinstance(release_status, (list, tuple)): - release_status = [release_status] - - q = db.query(Movie) \ - .outerjoin(Movie.releases, Movie.library, Library.titles) \ - .filter(LibraryTitle.default == True) \ - .group_by(Movie.id) - - # Filter on movie status - if status and len(status) > 0: - q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status])) - - # Filter on release status - if release_status and len(release_status) > 0: - q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status])) - - filter_or = [] - if starts_with: - starts_with = toUnicode(starts_with.lower()) - if starts_with in ascii_lowercase: - filter_or.append(LibraryTitle.simple_title.startswith(starts_with)) - else: - ignore = [] - for letter in ascii_lowercase: - ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter))) - filter_or.append(not_(or_(*ignore))) - - if search: - filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%')) - - if filter_or: - q = q.filter(or_(*filter_or)) - - total_count = q.count() - - if order == 'release_order': - q = q.order_by(desc(Release.last_edit)) - else: - q = q.order_by(asc(LibraryTitle.simple_title)) - - q = q.subquery() - q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \ - .options(joinedload_all('releases.files')) \ - .options(joinedload_all('releases.info')) \ - .options(joinedload_all('profile.types')) \ - .options(joinedload_all('library.titles')) \ - .options(joinedload_all('library.files')) \ - .options(joinedload_all('status')) \ - .options(joinedload_all('files')) - - if limit_offset: - splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset - limit = splt[0] - offset = 0 if len(splt) is 1 else splt[1] - q2 = q2.limit(limit).offset(offset) - - results = q2.all() - movies = [] - for movie in results: - movies.append(movie.to_dict({ - 'profile': {'types': {}}, - 'releases': {'files':{}, 'info': {}}, - 'library': {'titles': {}, 'files':{}}, - 'files': {}, - })) - - db.expire_all() - return (total_count, movies) - - def availableChars(self, status = None, release_status = None): - - chars = '' - - db = get_session() - - # Make a list from string - if not isinstance(status, (list, tuple)): - status = [status] - if release_status and not isinstance(release_status, (list, tuple)): - release_status = [release_status] - - q = db.query(Movie) \ - .outerjoin(Movie.releases, Movie.library, Library.titles, Movie.status) \ - .options(joinedload_all('library.titles')) - - # Filter on movie status - if status and len(status) > 0: - q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status])) - - # Filter on release status - if release_status and len(release_status) > 0: - q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status])) - - results = q.all() - - for movie in results: - char = movie.library.titles[0].simple_title[0] - char = char if char in ascii_lowercase else '#' - if char not in chars: - chars += str(char) - - db.expire_all() - return ''.join(sorted(chars, key = str.lower)) - - def listView(self, **kwargs): - - status = splitString(kwargs.get('status', None)) - release_status = splitString(kwargs.get('release_status', None)) - limit_offset = kwargs.get('limit_offset', None) - starts_with = kwargs.get('starts_with', None) - search = kwargs.get('search', None) - order = kwargs.get('order', None) - - total_movies, movies = self.list( - status = status, - release_status = release_status, - limit_offset = limit_offset, - starts_with = starts_with, - search = search, - order = order - ) - - return { - 'success': True, - 'empty': len(movies) == 0, - 'total': total_movies, - 'movies': movies, - } - - def charView(self, **kwargs): - - status = splitString(kwargs.get('status', None)) - release_status = splitString(kwargs.get('release_status', None)) - chars = self.availableChars(status, release_status) - - return { - 'success': True, - 'empty': len(chars) == 0, - 'chars': chars, - } - - def refresh(self, id = '', **kwargs): - - db = get_session() - - for x in splitString(id): - movie = db.query(Movie).filter_by(id = x).first() - - if movie: - - # Get current selected title - default_title = '' - for title in movie.library.titles: - if title.default: default_title = title.title - - fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True) - fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x)) - - db.expire_all() - return { - 'success': True, - } - - def search(self, q = '', **kwargs): - - cache_key = u'%s/%s' % (__name__, simplifyString(q)) - movies = Env.get('cache').get(cache_key) - - if not movies: - - if getImdb(q): - movies = [fireEvent('movie.info', identifier = q, merge = True)] - else: - movies = fireEvent('movie.search', q = q, merge = True) - Env.get('cache').set(cache_key, movies) - - return { - 'success': True, - 'empty': len(movies) == 0 if movies else 0, - 'movies': movies, - } - - def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None): - - if not params.get('identifier'): - msg = 'Can\'t add movie without imdb identifier.' - log.error(msg) - fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg) - return False - else: - try: - is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True) - if not is_movie: - msg = 'Can\'t add movie, seems to be a TV show.' - log.error(msg) - fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg) - return False - except: - pass - - - library = fireEvent('library.add', single = True, attrs = params, update_after = update_library) - - # Status - status_active, snatched_status, ignored_status, done_status, downloaded_status = \ - fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True) - - default_profile = fireEvent('profile.default', single = True) - cat_id = params.get('category_id', None) - - db = get_session() - m = db.query(Movie).filter_by(library_id = library.get('id')).first() - added = True - do_search = False - if not m: - m = Movie( - library_id = library.get('id'), - profile_id = params.get('profile_id', default_profile.get('id')), - status_id = status_id if status_id else status_active.get('id'), - category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None, - ) - db.add(m) - db.commit() - - onComplete = None - if search_after: - onComplete = self.createOnComplete(m.id) - - 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 in [downloaded_status.get('id'), snatched_status.get('id'), done_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')) - m.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None - else: - log.debug('Movie already exists, not updating: %s', params) - added = False - - 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() - - # Remove releases - available_status = fireEvent('status.get', 'available', single = True) - for rel in m.releases: - if rel.status_id is available_status.get('id'): - db.delete(rel) - db.commit() - - movie_dict = m.to_dict(self.default_dict) - - if do_search and search_after: - onComplete = self.createOnComplete(m.id) - onComplete() - - if added: - fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')) - - db.expire_all() - return movie_dict - - - def addView(self, **kwargs): - - movie_dict = self.add(params = kwargs) - - return { - 'success': True, - 'added': True if movie_dict else False, - 'movie': movie_dict, - } - - def edit(self, id = '', **kwargs): - - db = get_session() - - available_status = fireEvent('status.get', 'available', single = True) - - ids = splitString(id) - for movie_id in ids: - - m = db.query(Movie).filter_by(id = movie_id).first() - if not m: - continue - - m.profile_id = kwargs.get('profile_id') - - cat_id = kwargs.get('category_id', None) - if cat_id is not None: - m.category_id = tryInt(cat_id) if tryInt(cat_id) > 0 else None - - # Remove releases - for rel in m.releases: - if rel.status_id is available_status.get('id'): - db.delete(rel) - db.commit() - - # Default title - if kwargs.get('default_title'): - for title in m.library.titles: - title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower() - - db.commit() - - fireEvent('movie.restatus', m.id) - - movie_dict = m.to_dict(self.default_dict) - fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id)) - - db.expire_all() - return { - 'success': True, - } - - def deleteView(self, id = '', **kwargs): - - ids = splitString(id) - for movie_id in ids: - self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all')) - - return { - 'success': True, - } - - def delete(self, movie_id, delete_from = None): - - db = get_session() - - movie = db.query(Movie).filter_by(id = movie_id).first() - if movie: - deleted = False - if delete_from == 'all': - db.delete(movie) - db.commit() - deleted = True - else: - done_status = fireEvent('status.get', 'done', single = True) - - total_releases = len(movie.releases) - total_deleted = 0 - new_movie_status = None - for release in movie.releases: - if delete_from in ['wanted', 'snatched']: - if release.status_id != done_status.get('id'): - db.delete(release) - total_deleted += 1 - new_movie_status = 'done' - elif delete_from == 'manage': - if release.status_id == done_status.get('id'): - db.delete(release) - total_deleted += 1 - new_movie_status = 'active' - db.commit() - - if total_releases == total_deleted: - db.delete(movie) - db.commit() - deleted = True - elif new_movie_status: - new_status = fireEvent('status.get', new_movie_status, single = True) - movie.profile_id = None - movie.status_id = new_status.get('id') - db.commit() - else: - fireEvent('movie.restatus', movie.id, single = True) - - if deleted: - fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict()) - - db.expire_all() - return True - - def restatus(self, movie_id): - - active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) - - db = get_session() - - m = db.query(Movie).filter_by(id = movie_id).first() - if not m or len(m.library.titles) == 0: - log.debug('Can\'t restatus movie, doesn\'t seem to exist.') - return False - - log.debug('Changing status for %s', (m.library.titles[0].title)) - if not m.profile: - m.status_id = done_status.get('id') - else: - move_to_wanted = True - - for t in m.profile.types: - for release in m.releases: - if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish): - move_to_wanted = False - - m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id') - - db.commit() - - return True - - def createOnComplete(self, movie_id): - - def onComplete(): - db = get_session() - movie = db.query(Movie).filter_by(id = movie_id).first() - fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id)) - db.expire_all() - - return onComplete - - - def createNotifyFront(self, movie_id): - - def notifyFront(): - db = get_session() - movie = db.query(Movie).filter_by(id = movie_id).first() - fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict)) - db.expire_all() - - return notifyFront diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js deleted file mode 100644 index 1b11fab..0000000 --- a/couchpotato/core/plugins/movie/static/list.js +++ /dev/null @@ -1,626 +0,0 @@ -var MovieList = new Class({ - - Implements: [Events, Options], - - options: { - navigation: true, - limit: 50, - load_more: true, - loader: true, - menu: [], - add_new: false, - force_view: false - }, - - movies: [], - movies_added: {}, - total_movies: 0, - letters: {}, - filter: null, - - initialize: function(options){ - var self = this; - self.setOptions(options); - - self.offset = 0; - self.filter = self.options.filter || { - 'starts_with': null, - 'search': null - } - - self.el = new Element('div.movies').adopt( - self.title = self.options.title ? new Element('h2', { - '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', { - 'events': { - 'click': self.loadMore.bind(self) - } - }) : null - ); - - if($(window).getSize().x <= 480 && !self.options.force_view) - self.changeView('list'); - else - self.changeView(self.getSavedView() || self.options.view || 'details'); - - self.getMovies(); - - App.addEvent('movie.added', self.movieAdded.bind(self)) - App.addEvent('movie.deleted', self.movieDeleted.bind(self)) - }, - - movieDeleted: function(notification){ - var self = this; - - if(self.movies_added[notification.data.id]){ - self.movies.each(function(movie){ - if(movie.get('id') == notification.data.id){ - movie.destroy(); - delete self.movies_added[notification.data.id]; - self.setCounter(self.counter_count-1); - } - }) - } - - self.checkIfEmpty(); - }, - - movieAdded: function(notification){ - var self = this; - - if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){ - window.scroll(0,0); - self.createMovie(notification.data, 'top'); - self.setCounter(self.counter_count+1); - - self.checkIfEmpty(); - } - }, - - create: function(){ - var self = this; - - // Create the alphabet nav - if(self.options.navigation) - self.createNavigation(); - - if(self.options.load_more) - self.scrollspy = new ScrollSpy({ - min: function(){ - var c = self.load_more.getCoordinates() - return c.top - window.document.getSize().y - 300 - }, - onEnter: self.loadMore.bind(self) - }); - - self.created = true; - }, - - addMovies: function(movies, total){ - var self = this; - - if(!self.created) self.create(); - - // do scrollspy - if(movies.length < self.options.limit && self.scrollspy){ - self.load_more.hide(); - self.scrollspy.stop(); - } - - Object.each(movies, function(movie){ - self.createMovie(movie); - }); - - self.total_movies += total; - self.setCounter(total); - - }, - - setCounter: function(count){ - var self = this; - - if(!self.navigation_counter) return; - - self.counter_count = count; - self.navigation_counter.set('text', (count || 0) + ' movies'); - - if (self.empty_message) { - self.empty_message.destroy(); - self.empty_message = null; - } - - if(self.total_movies && count == 0 && !self.empty_message){ - var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') + - (self.filter.starts_with ? ' in '+self.filter.starts_with+'' : ''); - - self.empty_message = new Element('.message', { - 'html': 'No movies found ' + message + '.
' - }).grab( - new Element('a', { - 'text': 'Reset filter', - 'events': { - 'click': function(){ - self.filter = { - 'starts_with': null, - 'search': null - }; - self.navigation_search_input.set('value', ''); - self.reset(); - self.activateLetter(); - self.getMovies(true); - self.last_search_value = ''; - } - } - }) - ).inject(self.movie_list); - - } - - }, - - createMovie: function(movie, inject_at){ - var self = this; - var m = new Movie(self, { - '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) - self.movies_added[movie.id] = true; - }, - - createNavigation: function(){ - var self = this; - var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - - self.el.addClass('with_navigation') - - self.navigation = new Element('div.alph_nav').adopt( - self.mass_edit_form = new Element('div.mass_edit_form').adopt( - new Element('span.select').adopt( - self.mass_edit_select = new Element('input[type=checkbox].inlay', { - 'events': { - 'change': self.massEditToggleAll.bind(self) - } - }), - self.mass_edit_selected = new Element('span.count', {'text': 0}), - self.mass_edit_selected_label = new Element('span', {'text': 'selected'}) - ), - new Element('div.quality').adopt( - self.mass_edit_quality = new Element('select'), - new Element('a.button.orange', { - 'text': 'Change quality', - 'events': { - 'click': self.changeQualitySelected.bind(self) - } - }) - ), - new Element('div.delete').adopt( - new Element('span[text=or]'), - new Element('a.button.red', { - 'text': 'Delete', - 'events': { - 'click': self.deleteSelected.bind(self) - } - }) - ), - new Element('div.refresh').adopt( - new Element('span[text=or]'), - new Element('a.button.green', { - 'text': 'Refresh', - 'events': { - 'click': self.refreshSelected.bind(self) - } - }) - ) - ), - new Element('div.menus').adopt( - self.navigation_counter = new Element('span.counter[title=Total]'), - self.filter_menu = new Block.Menu(self, { - 'class': 'filter' - }), - self.navigation_actions = new Element('ul.actions', { - 'events': { - 'click:relay(li)': function(e, el){ - var a = 'active'; - self.navigation_actions.getElements('.'+a).removeClass(a); - self.changeView(el.get('data-view')); - this.addClass(a); - - el.inject(el.getParent(), 'top'); - el.getSiblings().hide() - setTimeout(function(){ - el.getSiblings().setStyle('display', null); - }, 100) - } - } - }), - self.navigation_menu = new Block.Menu(self, { - 'class': 'extra' - }) - ) - ).inject(self.el, 'top'); - - // Mass edit - self.mass_edit_select_class = new Form.Check(self.mass_edit_select); - Quality.getActiveProfiles().each(function(profile){ - new Element('option', { - 'value': profile.id ? profile.id : profile.data.id, - 'text': profile.label ? profile.label : profile.data.label - }).inject(self.mass_edit_quality) - }); - - self.filter_menu.addLink( - self.navigation_search_input = new Element('input', { - 'title': 'Search through ' + self.options.identifier, - 'placeholder': 'Search through ' + self.options.identifier, - 'events': { - 'keyup': self.search.bind(self), - 'change': self.search.bind(self) - } - }) - ).addClass('search'); - - self.filter_menu.addEvent('open', function(){ - self.navigation_search_input.focus(); - }); - - self.filter_menu.addLink( - self.navigation_alpha = new Element('ul.numbers', { - 'events': { - 'click:relay(li.available)': function(e, el){ - self.activateLetter(el.get('data-letter')) - self.getMovies(true) - } - } - }) - ); - - // Actions - ['mass_edit', 'details', 'list'].each(function(view){ - var current = self.current_view == view; - new Element('li', { - 'class': 'icon2 ' + view + (current ? ' active ' : ''), - 'data-view': view - }).inject(self.navigation_actions, current ? 'top' : 'bottom'); - }); - - // All - self.letters['all'] = new Element('li.letter_all.available.active', { - 'text': 'ALL', - }).inject(self.navigation_alpha); - - // Chars - chars.split('').each(function(c){ - self.letters[c] = new Element('li', { - 'text': c, - 'class': 'letter_'+c, - 'data-letter': c - }).inject(self.navigation_alpha); - }); - - // Get available chars and highlight - if(self.navigation.isDisplayed() || self.navigation.isVisible()) - Api.request('movie.available_chars', { - 'data': Object.merge({ - 'status': self.options.status - }, self.filter), - 'onSuccess': function(json){ - - json.chars.split('').each(function(c){ - self.letters[c.capitalize()].addClass('available') - }) - - } - }); - - // Add menu or hide - if (self.options.menu.length > 0) - self.options.menu.each(function(menu_item){ - self.navigation_menu.addLink(menu_item); - }) - else - self.navigation_menu.hide(); - - }, - - calculateSelected: function(){ - var self = this; - - var selected = 0, - movies = self.movies.length; - self.movies.each(function(movie){ - selected += movie.isSelected() ? 1 : 0 - }) - - var indeterminate = selected > 0 && selected < movies, - checked = selected == movies && selected > 0; - - self.mass_edit_select.set('indeterminate', indeterminate) - - self.mass_edit_select_class[checked ? 'check' : 'uncheck']() - self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate') - - self.mass_edit_selected.set('text', selected); - }, - - deleteSelected: function(){ - var self = this, - ids = self.getSelectedMovies(), - help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list'; - - var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{ - 'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'), - 'class': 'delete', - 'events': { - 'click': function(e){ - (e).preventDefault(); - this.set('text', 'Deleting..') - Api.request('movie.delete', { - 'data': { - 'id': ids.join(','), - 'delete_from': self.options.identifier - }, - 'onSuccess': function(){ - qObj.close(); - - var erase_movies = []; - self.movies.each(function(movie){ - if (movie.isSelected()){ - $(movie).destroy() - erase_movies.include(movie); - } - }); - - erase_movies.each(function(movie){ - self.movies.erase(movie); - movie.destroy(); - self.setCounter(self.counter_count-1); - }); - - self.calculateSelected(); - } - }); - - } - } - }, { - 'text': 'Cancel', - 'cancel': true - }]); - - }, - - changeQualitySelected: function(){ - var self = this; - var ids = self.getSelectedMovies() - - Api.request('movie.edit', { - 'data': { - 'id': ids.join(','), - 'profile_id': self.mass_edit_quality.get('value') - }, - 'onSuccess': self.search.bind(self) - }); - }, - - refreshSelected: function(){ - var self = this; - var ids = self.getSelectedMovies() - - Api.request('movie.refresh', { - 'data': { - 'id': ids.join(','), - } - }); - }, - - getSelectedMovies: function(){ - var self = this; - - var ids = [] - self.movies.each(function(movie){ - if (movie.isSelected()) - ids.include(movie.get('id')) - }); - - return ids - }, - - massEditToggleAll: function(){ - var self = this; - - var select = self.mass_edit_select.get('checked'); - - self.movies.each(function(movie){ - movie.select(select) - }); - - self.calculateSelected() - }, - - reset: function(){ - var self = this; - - self.movies = [] - 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(); - self.scrollspy.start(); - } - }, - - activateLetter: function(letter){ - var self = this; - - self.reset() - - self.letters[letter || 'all'].addClass('active'); - self.filter.starts_with = letter; - - }, - - changeView: function(new_view){ - var self = this; - - self.el - .removeClass(self.current_view+'_list') - .addClass(new_view+'_list') - - self.current_view = new_view; - Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000}); - }, - - getSavedView: function(){ - var self = this; - return Cookie.read(self.options.identifier+'_view2'); - }, - - search: function(){ - var self = this; - - if(self.search_timer) clearTimeout(self.search_timer); - self.search_timer = (function(){ - var search_value = self.navigation_search_input.get('value'); - if (search_value == self.last_search_value) return - - self.reset() - - self.activateLetter(); - self.filter.search = search_value; - - self.getMovies(true); - - self.last_search_value = search_value; - - }).delay(250); - - }, - - update: function(){ - var self = this; - - self.reset(); - self.getMovies(true); - }, - - getMovies: function(reset){ - var self = this; - - if(self.scrollspy){ - self.scrollspy.stop(); - self.load_more.set('text', 'loading...'); - } - - if(self.movies.length == 0 && self.options.loader){ - - self.loader_first = new Element('div.loading').adopt( - new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'}) - ).inject(self.el, 'top'); - - createSpinner(self.loader_first, { - radius: 4, - length: 4, - width: 1 - }); - - self.el.setStyle('min-height', 93); - - } - - Api.request(self.options.api_call || 'movie.list', { - 'data': Object.merge({ - 'status': self.options.status, - 'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null - }, self.filter), - 'onSuccess': function(json){ - - if(reset) - self.movie_list.empty(); - - if(self.loader_first){ - var lf = self.loader_first; - self.loader_first.addClass('hide') - self.loader_first = null; - setTimeout(function(){ - lf.destroy(); - }, 20000); - self.el.setStyle('min-height', null); - } - - self.store(json.movies); - self.addMovies(json.movies, json.total); - if(self.scrollspy) { - self.load_more.set('text', 'load more movies'); - self.scrollspy.start(); - } - - self.checkIfEmpty(); - self.fireEvent('loaded'); - } - }); - }, - - loadMore: function(){ - var self = this; - if(self.offset >= self.options.limit) - self.getMovies() - }, - - store: function(movies){ - var self = this; - - self.offset += movies.length; - - }, - - checkIfEmpty: function(){ - var self = this; - - 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.setStyle('display', [is_empty ? 'none' : '']) - - if(is_empty && self.options.on_empty_element){ - self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after'); - - if(self.navigation) - self.navigation.hide(); - - self.empty_element = self.options.on_empty_element; - } - else if(self.empty_element){ - self.empty_element.destroy(); - - if(self.navigation) - self.navigation.show(); - } - - }, - - toElement: function(){ - return this.el; - } - -}); \ No newline at end of file diff --git a/couchpotato/core/plugins/movie/static/movie.actions.js b/couchpotato/core/plugins/movie/static/movie.actions.js deleted file mode 100644 index ad02e80..0000000 --- a/couchpotato/core/plugins/movie/static/movie.actions.js +++ /dev/null @@ -1,822 +0,0 @@ -var MovieAction = new Class({ - - Implements: [Options], - - class_name: 'action icon2', - - initialize: function(movie, options){ - var self = this; - self.setOptions(options); - - 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') - }, - - getTitle: function(){ - var self = this; - - try { - return self.movie.getTitle(); - } - catch(e){ - try { - return self.movie.original_title ? self.movie.original_title : self.movie.titles[0]; - } - catch(e){ - return 'Unknown'; - } - } - }, - - get: function(key){ - var self = this; - try { - return self.movie.get(key) - } - catch(e){ - return self.movie[key] - } - }, - - 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('imdb') || self.movie.get('identifier'); - - self.el = new Element('a.imdb', { - 'title': 'Go to the IMDB page of ' + self.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.download', { - 'title': 'Show the releases that are available for ' + self.getTitle(), - 'events': { - 'click': self.show.bind(self) - } - }); - - if(self.movie.data.releases.length == 0) - self.el.hide() - else - self.showHelper(); - - }, - - createReleases: function(){ - var self = this; - - if(!self.options_container){ - self.options_container = new Element('div.options').grab( - self.release_container = new Element('div.releases.table') - ); - - // 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 - var item = 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.icon2', { - 'href': release.info['detail_url'], - 'target': '_blank' - }) : new Element('a'), - new Element('a.download.icon2', { - 'events': { - 'click': function(e){ - (e).preventDefault(); - if(!this.hasClass('completed')) - self.download(release); - } - } - }), - new Element('a.delete.icon2', { - 'events': { - 'click': function(e){ - (e).preventDefault(); - self.ignore(release); - } - } - }) - ).inject(self.release_container); - - release['el'] = item; - - 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 && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){ - - self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top'); - - 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 - ) - } - - } - - }, - - show: function(e){ - var self = this; - if(e) - (e).preventDefault(); - - self.createReleases(); - self.options_container.inject(self.movie, 'top'); - self.movie.slide('in', self.options_container); - }, - - showHelper: function(e){ - var self = this; - if(e) - (e).preventDefault(); - - self.createReleases(); - - if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){ - - self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container); - - self.trynext_container.adopt( - self.next_release ? [new Element('a.icon2.readd', { - 'text': self.last_release ? 'Download another release' : 'Download the best release', - 'events': { - 'click': self.tryNextRelease.bind(self) - } - }), - new Element('a.icon2.download', { - 'text': 'pick one yourself', - 'events': { - 'click': function(){ - self.movie.quality.fireEvent('click'); - } - } - })] : null, - new Element('a.icon2.completed', { - 'text': 'mark this movie done', - 'events': { - 'click': function(){ - Api.request('movie.delete', { - 'data': { - 'id': self.movie.get('id'), - 'delete_from': 'wanted' - }, - 'onComplete': function(){ - var movie = $(self.movie); - movie.set('tween', { - 'duration': 300, - 'onComplete': function(){ - self.movie.destroy() - } - }); - movie.tween('height', 0); - } - }); - } - } - }) - ) - } - - }, - - 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.icon2'); - - self.movie.busy(true); - - Api.request('release.download', { - 'data': { - 'id': release.id - }, - 'onComplete': function(json){ - self.movie.busy(false); - - 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 - }, - 'onComplete': function(){ - var el = release.el; - if(el.hasClass('failed') || el.hasClass('ignored')){ - el.removeClass('failed').removeClass('ignored'); - el.getElement('.release_status').set('text', 'available'); - } - else { - el.addClass('ignored'); - el.getElement('.release_status').set('text', 'ignored'); - } - } - }) - - }, - - tryNextRelease: function(movie_id){ - var self = this; - - self.createReleases(); - - 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.getTitle(), - 'events': { - 'click': self.watch.bind(self) - } - }); - - }, - - watch: function(offset){ - var self = this; - - var data_url = 'https://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.getTitle()), - 'year': self.get('year'), - 'offset': offset || 1 - }), - size = $(self.movie).getSize(), - height = self.options.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' - }), - self.category_select = new Element('select', { - 'name': 'category' - }).grab( - new Element('option', {'value': -1, 'text': 'None'}) - ), - 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); - }); - - - // Fill categories - var categories = CategoryList.getAll(); - - if(categories.length == 0) - self.category_select.hide(); - else { - self.category_select.show(); - categories.each(function(category){ - - var category_id = category.data.id; - - new Element('option', { - 'value': category_id, - 'text': category.data.label - }).inject(self.category_select); - - if(self.movie.category && self.movie.category.data && self.movie.category.data.id == category_id) - self.category_select.set('value', category_id); - - }); - } - - // Fill profiles - var profiles = Quality.getActiveProfiles(); - if(profiles.length == 1) - self.profile_select.hide(); - - profiles.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'), - 'category_id': self.category_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 deleted file mode 100644 index adc4ebf..0000000 --- a/couchpotato/core/plugins/movie/static/movie.css +++ /dev/null @@ -1,1044 +0,0 @@ -.movies { - padding: 10px 0 20px; - position: relative; - z-index: 3; - width: 100%; -} - - .movies > div { - clear: both; - } - - .movies > div .message { - display: block; - padding: 20px; - font-size: 20px; - color: white; - text-align: center; - } - .movies > div .message a { - padding: 20px; - display: block; - } - - .movies.thumbs_list > div:not(.description) { - margin-right: -4px; - } - - .movies .loading { - display: block; - padding: 20px 0 0 0; - width: 100%; - z-index: 3; - transition: all .4s cubic-bezier(0.9,0,0.1,1); - height: 40px; - opacity: 1; - position: absolute; - text-align: center; - } - .movies .loading.hide { - height: 0; - padding: 0; - opacity: 0; - margin-top: -20px; - overflow: hidden; - } - - .movies .loading .spinner { - display: inline-block; - } - - .movies .loading .message { - margin: 0 20px; - } - - .movies h2 { - margin-bottom: 20px; - } - - @media all and (max-width: 480px) { - .movies h2 { - font-size: 25px; - margin-bottom: 10px; - } - } - - .movies > .description { - position: absolute; - top: 30px; - right: 0; - font-style: italic; - opacity: 0.8; - } - .movies:hover > .description { - opacity: 1; - } - - @media all and (max-width: 860px) { - .movies > .description { - display: none; - } - } - - .movies.thumbs_list { - padding: 20px 0 20px; - } - - .home .movies { - padding-top: 6px; - } - - .movies .movie { - position: relative; - margin: 10px 0; - padding-left: 20px; - overflow: hidden; - width: 100%; - height: 180px; - transition: all 0.6s cubic-bezier(0.9,0,0.1,1); - transition-property: width, height; - background: rgba(0,0,0,.2); - } - - .movies.mass_edit_list .movie { - padding-left: 22px; - background: none; - } - - .movies.details_list .movie { - padding-left: 120px; - } - - .movies.list_list .movie:not(.details_view), - .movies.mass_edit_list .movie { - height: 30px; - border-bottom: 1px solid rgba(255,255,255,.15); - } - - .movies.list_list .movie:last-child, - .movies.mass_edit_list .movie:last-child { - border: none; - } - - .movies.thumbs_list .movie { - width: 16.66667%; - height: auto; - display: inline-block; - margin: 0; - padding: 0; - vertical-align: top; - } - - @media all and (max-width: 800px) { - .movies.thumbs_list .movie { - width: 25%; - } - } - - .movies .movie .mask { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - } - - .movies.list_list .movie:not(.details_view), - .movies.mass_edit_list .movie { - margin: 0; - } - - .movies .data { - padding: 20px; - height: 100%; - width: 100%; - position: relative; - transition: all .6s cubic-bezier(0.9,0,0.1,1); - right: 0; - } - .movies.list_list .movie:not(.details_view) .data, - .movies.mass_edit_list .movie .data { - padding: 0 0 0 10px; - border: 0; - background: #4e5969; - } - .movies.mass_edit_list .movie .data { - padding-left: 8px; - } - - .movies.thumbs_list .data { - position: absolute; - left: 0; - top: 0; - width: 100%; - padding: 10px; - height: 100%; - background: none; - transition: none; - } - - .movies.thumbs_list .movie:hover .data { - background: rgba(0,0,0,0.9); - } - - .movies .data.hide_right { - right: -100%; - } - - .movies .movie .check { - display: none; - } - - .movies.mass_edit_list .movie .check { - position: absolute; - left: 0; - top: 0; - display: block; - margin: 7px 0 0 5px; - } - - .movies .poster { - position: absolute; - left: 0; - width: 120px; - line-height: 0; - overflow: hidden; - height: 100%; - transition: all .6s cubic-bezier(0.9,0,0.1,1); - background: rgba(0,0,0,.1); - } - .movies.thumbs_list .poster { - position: relative; - } - .movies.list_list .movie:not(.details_view) .poster, - .movies.mass_edit_list .poster { - width: 20px; - height: 30px; - } - .movies.mass_edit_list .poster { - display: none; - } - - .movies.thumbs_list .poster { - width: 100%; - height: 100%; - transition: none; - background: no-repeat center; - background-size: cover; - } - .movies.thumbs_list .no_thumbnail .empty_file { - width: 100%; - height: 100%; - } - - .movies .poster img, - .options .poster img { - width: 100%; - height: 100%; - } - .movies.thumbs_list .poster img { - height: auto; - width: 100%; - top: 0; - bottom: 0; - opacity: 0; - } - - .movies .info { - position: relative; - height: 100%; - width: 100%; - } - - .movies .info .title { - font-size: 28px; - font-weight: bold; - margin-bottom: 10px; - margin-top: 2px; - width: 100%; - padding-right: 80px; - transition: all 0.2s linear; - height: 35px; - top: -5px; - position: relative; - } - .movies.list_list .info .title, - .movies.mass_edit_list .info .title { - height: 100%; - top: 0; - margin: 0; - } - .touch_enabled .movies.list_list .info .title { - display: inline-block; - padding-right: 55px; - } - - .movies .info .title span { - display: inline-block; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - width: 100%; - height: 100%; - line-height: 30px; - } - - .movies.thumbs_list .info .title span { - white-space: normal; - overflow: auto; - height: auto; - text-align: left; - } - - @media all and (max-width: 480px) { - .movies.thumbs_list .movie .info .title span, - .movies.thumbs_list .movie .info .year { - font-size: 15px; - line-height: 15px; - overflow: hidden; - } - } - - .movies.list_list .movie:not(.details_view) .info .title, - .movies.mass_edit_list .info .title { - font-size: 16px; - font-weight: normal; - width: auto; - } - - .movies.thumbs_list .movie:not(.no_thumbnail) .info { - display: none; - } - .movies.thumbs_list .movie:hover .info { - display: block; - } - - .movies.thumbs_list .info .title { - font-size: 21px; - word-wrap: break-word; - padding: 0; - height: 100%; - } - - .movies .info .year { - position: absolute; - color: #bbb; - right: 0; - top: 6px; - text-align: right; - transition: all 0.2s linear; - font-weight: normal; - } - .movies.list_list .movie:not(.details_view) .info .year, - .movies.mass_edit_list .info .year { - font-size: 1.25em; - right: 10px; - } - - .movies.thumbs_list .info .year { - font-size: 23px; - margin: 0; - bottom: 0; - left: 0; - top: auto; - right: auto; - color: #FFF; - } - - .touch_enabled .movies.list_list .movie .info .year { - font-size: 1em; - } - - .movies .info .description { - top: 30px; - clear: both; - bottom: 30px; - overflow: hidden; - position: absolute; - } - .movies .data:hover .description { - overflow: auto; - } - .movies.list_list .movie:not(.details_view) .info .description, - .movies.mass_edit_list .info .description, - .movies.thumbs_list .info .description { - display: none; - } - - .movies .data .quality { - position: absolute; - bottom: 2px; - display: block; - min-height: 20px; - } - - .movies.list_list .movie:hover .data .quality { - display: none; - } - - .touch_enabled .movies.list_list .movie .data .quality { - position: relative; - display: inline-block; - margin: 0; - top: -4px; - } - - @media all and (max-width: 480px) { - .movies .data .quality { - display: none; - } - } - - .movies .status_suggest .data .quality, - .movies.thumbs_list .data .quality { - display: none; - } - - .movies .data .quality span { - padding: 2px 3px; - font-weight: bold; - opacity: 0.5; - font-size: 10px; - height: 16px; - line-height: 12px; - vertical-align: middle; - display: inline-block; - text-transform: uppercase; - font-weight: normal; - margin: 0 4px 0 0; - border-radius: 2px; - background-color: rgba(255,255,255,0.1); - } - .movies.list_list .data .quality, - .movies.mass_edit_list .data .quality { - text-align: right; - right: 0; - margin-right: 60px; - z-index: 1; - top: 5px; - } - - .movies .data .quality .available, - .movies .data .quality .snatched { - opacity: 1; - cursor: pointer; - } - - .movies .data .quality .available { background-color: #578bc3; } - .movies .data .quality .failed { background-color: #a43d34; } - .movies .data .quality .snatched { background-color: #a2a232; } - .movies .data .quality .seeding { background-color: #0a6819; } - .movies .data .quality .done { - background-color: #369545; - opacity: 1; - } - .movies .data .quality .finish { - background-image: url('../images/sprite.png'); - background-repeat: no-repeat; - background-position: 0 2px; - padding-left: 14px; - background-size: 14px - } - - .movies .data .actions { - position: absolute; - bottom: 17px; - right: 20px; - line-height: 0; - top: 0; - display: block; - width: auto; - opacity: 0; - display: none; - } - @media all and (max-width: 480px) { - .movies .data .actions { - display: none !important; - } - } - - .movies .movie:hover .data .actions, - .touch_enabled .movies .movie .data .actions { - opacity: 1; - display: inline-block; - } - - .movies.details_list .data .actions { - top: auto; - bottom: 18px; - } - - .movies .movie:hover .actions { - opacity: 1; - display: inline-block; - } - .movies.thumbs_list .data .actions { - bottom: 12px; - right: 10px; - top: auto; - } - - .movies .movie:hover .action { opacity: 0.6; } - .movies .movie:hover .action:hover { opacity: 1; } - - .movies .data .action { - display: inline-block; - height: 22px; - min-width: 33px; - padding: 0 5px; - line-height: 26px; - text-align: center; - font-size: 13px; - color: #FFF; - margin-left: 1px; - } - .movies .data .action.trailer { color: #FFF; } - .movies .data .action.download { color: #b9dec0; } - .movies .data .action.edit { color: #c6b589; } - .movies .data .action.refresh { color: #cbeecc; } - .movies .data .action.delete { color: #e9b0b0; } - .movies .data .action.directory { color: #ffed92; } - .movies .data .action.readd { color: #c2fac5; } - - .movies.mass_edit_list .movie .data .actions { - display: none; - } - - .movies.list_list .movie:not(.details_view):hover .actions, - .movies.mass_edit_list .movie:hover .actions, - .touch_enabled .movies.list_list .movie:not(.details_view) .actions { - margin: 0; - background: #4e5969; - top: 2px; - bottom: 2px; - right: 5px; - z-index: 3; - } - - .movies .delete_container { - clear: both; - text-align: center; - font-size: 20px; - position: absolute; - padding: 80px 0 0; - left: 120px; - right: 0; - } - .movies .delete_container .or { - padding: 10px; - } - .movies .delete_container .delete { - background-color: #ff321c; - font-weight: normal; - } - .movies .delete_container .delete:hover { - color: #fff; - background-color: #d32917; - } - - .movies .options { - position: absolute; - right: 0; - left: 120px; - } - - .movies .options .form { - margin: 80px 0 0; - font-size: 20px; - text-align: center; - } - - .movies .options .form select { - margin-right: 20px; - } - - .movies .options .table { - height: 180px; - overflow: auto; - line-height: 2em; - } - .movies .options .table .item { - border-bottom: 1px solid rgba(255,255,255,0.1); - } - .movies .options .table .item.ignored span, - .movies .options .table .item.failed span { - text-decoration: line-through; - color: rgba(255,255,255,0.4); - } - .movies .options .table .item.ignored .delete:before, - .movies .options .table .item.failed .delete:before { - display: inline-block; - content: "\e04b"; - transform: scale(-1, 1); - } - - .movies .options .table .item:last-child { border: 0; } - .movies .options .table .item:nth-child(even) { - background: rgba(255,255,255,0.05); - } - .movies .options .table .item:not(.head):hover { - background: rgba(255,255,255,0.03); - } - - .movies .options .table .item > * { - display: inline-block; - padding: 0 5px; - width: 60px; - min-height: 24px; - white-space: nowrap; - text-overflow: ellipsis; - text-align: center; - vertical-align: top; - border-left: 1px solid rgba(255, 255, 255, 0.1); - } - .movies .options .table .item > *:first-child { - border: 0; - } - .movies .options .table .provider { - width: 120px; - text-overflow: ellipsis; - overflow: hidden; - } - .movies .options .table .name { - width: 340px; - overflow: hidden; - text-align: left; - padding: 0 10px; - } - .movies .options .table.files .name { width: 590px; } - .movies .options .table .type { width: 130px; } - .movies .options .table .is_available { width: 90px; } - .movies .options .table .age, - .movies .options .table .size { width: 40px; } - - .movies .options .table a { - width: 30px !important; - height: 20px; - opacity: 0.8; - line-height: 25px; - } - .movies .options .table a:hover { opacity: 1; } - .movies .options .table a.download { color: #a7fbaf; } - .movies .options .table a.delete { color: #fda3a3; } - .movies .options .table .ignored a.delete, - .movies .options .table .failed a.delete { color: #b5fda3; } - - .movies .options .table .head > * { - font-weight: bold; - font-size: 14px; - padding-top: 4px; - padding-bottom: 4px; - height: auto; - } - - .trailer_container { - width: 100%; - background: #000; - text-align: center; - transition: all .6s cubic-bezier(0.9,0,0.1,1); - overflow: hidden; - left: 0; - position: absolute; - z-index: 10; - } - .trailer_container.hide { - height: 0 !important; - } - - .hide_trailer { - position: absolute; - top: 0; - left: 50%; - margin-left: -50px; - width: 100px; - text-align: center; - padding: 3px 10px; - background: #4e5969; - transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s; - z-index: 11; - } - .hide_trailer.hide { - top: -30px; - } - - .movies .movie .try_container { - padding: 5px 10px; - text-align: center; - } - - .movies .movie .try_container a { - margin: 0 5px; - padding: 2px 5px; - } - - .movies .movie .releases .next_release { - border-left: 6px solid #2aa300; - } - - .movies .movie .releases .next_release > :first-child { - margin-left: -6px; - } - - .movies .movie .releases .last_release { - border-left: 6px solid #ffa200; - } - - .movies .movie .releases .last_release > :first-child { - margin-left: -6px; - } - .movies .movie .trynext { - display: inline; - position: absolute; - right: 180px; - z-index: 2; - opacity: 0; - background: #4e5969; - text-align: right; - height: 100%; - top: 0; - } - .touch_enabled .movies .movie .trynext { - display: none; - } - - @media all and (max-width: 480px) { - .movies .movie .trynext { - display: none; - } - } - .movies.mass_edit_list .trynext { display: none; } - .wanted .movies .movie .trynext { - padding-right: 30px; - } - .movies .movie:hover .trynext, - .touch_enabled .movies.details_list .movie .trynext { - opacity: 1; - } - - .movies.details_list .movie .trynext { - background: #47515f; - padding: 0; - right: 0; - height: 25px; - } - - .movies .movie .trynext a { - background-position: 5px center; - padding: 0 5px 0 25px; - margin-right: 10px; - color: #FFF; - height: 100%; - line-height: 27px; - font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; - } - .movies .movie .trynext a:before { - margin: 2px 0 0 -20px; - position: absolute; - font-family: 'Elusive-Icons'; - } - .movies.details_list .movie .trynext a { - line-height: 23px; - } - .movies .movie .trynext a:last-child { - margin: 0; - } - .movies .movie .trynext a:hover, - .touch_enabled .movies .movie .trynext a { - background-color: #369545; - } - - .movies .load_more { - display: block; - padding: 10px; - text-align: center; - font-size: 20px; - } - .movies .load_more.loading { - opacity: .5; - } - -.movies .alph_nav { - height: 44px; -} - - @media all and (max-width: 480px) { - .movies .alph_nav { - display: none; - } - } - - .movies .alph_nav .menus { - display: inline-block; - float: right; - } - -.movies .alph_nav .numbers, -.movies .alph_nav .counter, -.movies .alph_nav .actions { - list-style: none; - padding: 0 0 1px; - margin: 0; - user-select: none; -} - - .movies .alph_nav .counter { - display: inline-block; - text-align: right; - padding: 0 10px; - height: 100%; - line-height: 43px; - border-right: 1px solid rgba(255,255,255,.07); - } - - .movies .alph_nav .numbers li, - .movies .alph_nav .actions li { - display: inline-block; - vertical-align: top; - height: 100%; - line-height: 30px; - text-align: center; - border: 1px solid transparent; - transition: all 0.1s ease-in-out; - } - - .movies .alph_nav .numbers li { - width: 30px; - height: 30px; - opacity: 0.3; - } - .movies .alph_nav .numbers li.letter_all { - width: 60px; - } - - .movies .alph_nav li.available { - font-weight: bold; - cursor: pointer; - opacity: 1; - - } - .movies .alph_nav li.active.available, - .movies .alph_nav li.available:hover { - background: rgba(0,0,0,.1); - } - - .movies .alph_nav .search input { - padding: 6px 5px; - width: 100%; - height: 44px; - display: inline-block; - border: 0; - background: none; - color: #444; - font-size: 14px; - padding: 10px; - padding: 0 10px 0 30px; - border-bottom: 1px solid rgba(0,0,0,.08); - } - .movies .alph_nav .search input:focus { - background: rgba(0,0,0,.08); - } - - .movies .alph_nav .search input::-webkit-input-placeholder { - color: #444; - opacity: .6; - } - - .movies .alph_nav .search:before { - font-family: 'Elusive-Icons'; - content: "\e03e"; - position: absolute; - height: 20px; - line-height: 45px; - font-size: 12px; - margin: 0 0 0 10px; - opacity: .6; - color: #444; - } - - .movies .alph_nav .actions { - -moz-user-select: none; - width: 44px; - height: 44px; - display: inline-block; - vertical-align: top; - z-index: 200; - position: relative; - border: 1px solid rgba(255,255,255,.07); - border-width: 0 1px; - } - .movies .alph_nav .actions:hover { - box-shadow: 0 100px 20px -10px rgba(0,0,0,0.55); - } - .movies .alph_nav .actions li { - width: 100%; - height: 45px; - line-height: 40px; - position: relative; - z-index: 20; - display: none; - cursor: pointer; - } - .movies .alph_nav .actions:hover li:not(.active) { - display: block; - background: #FFF; - color: #444; - } - .movies .alph_nav .actions li:hover:not(.active) { - background: #ccc; - } - .movies .alph_nav .actions li.active { - display: block; - } - - .movies .alph_nav .actions li.mass_edit:before { - content: "\e070"; - } - - .movies .alph_nav .actions li.list:before { - content: "\e0d8"; - } - - .movies .alph_nav .actions li.details:before { - content: "\e022"; - } - - .movies .alph_nav .mass_edit_form { - clear: both; - text-align: center; - display: none; - overflow: hidden; - float: left; - height: 44px; - line-height: 44px; - } - .movies.mass_edit_list .mass_edit_form { - display: inline-block; - } - .movies.mass_edit_list .mass_edit_form .select { - font-size: 14px; - display: inline-block; - } - .movies.mass_edit_list .mass_edit_form .select .check { - display: inline-block; - vertical-align: middle; - margin: -4px 0 0 5px; - } - .movies.mass_edit_list .mass_edit_form .select span { - opacity: 0.7; - } - .movies.mass_edit_list .mass_edit_form .select .count { - font-weight: bold; - margin: 0 3px 0 10px; - } - - .movies .alph_nav .mass_edit_form .quality { - display: inline-block; - margin: 0 0 0 16px; - } - .movies .alph_nav .mass_edit_form .quality select { - width: 120px; - margin-right: 5px; - } - .movies .alph_nav .mass_edit_form .button { - padding: 3px 7px; - } - - .movies .alph_nav .mass_edit_form .refresh, - .movies .alph_nav .mass_edit_form .delete { - display: inline-block; - margin-left: 8px; - } - - .movies .alph_nav .mass_edit_form .refresh span, - .movies .alph_nav .mass_edit_form .delete span { - margin: 0 10px 0 0; - } - - .movies .alph_nav .more_menu > a { - background: none; - } - - .movies .alph_nav .more_menu.extra > a:before { - content: '...'; - font-size: 1.7em; - line-height: 23px; - text-align: center; - display: block; - } - - .movies .alph_nav .more_menu.filter { - } - - .movies .alph_nav .more_menu.filter > a:before { - content: "\e0e8"; - font-family: 'Elusive-Icons'; - line-height: 33px; - display: block; - text-align: center; - } - - .movies .alph_nav .more_menu.filter .wrapper { - right: 88px; - width: 300px; - } - -.movies .empty_wanted { - background-image: url('../images/emptylist.png'); - background-position: 80% 0; - height: 750px; - width: 100%; - max-width: 900px; - padding-top: 260px; -} - -.movies .empty_manage { - text-align: center; - font-size: 25px; - line-height: 150%; - padding: 40px 0; -} - - .movies .empty_manage .after_manage { - margin-top: 30px; - font-size: 16px; - } - - .movies .progress { - padding: 10px; - margin: 5px 0; - text-align: left; - } - - .movies .progress > div { - padding: 5px 10px; - font-size: 12px; - line-height: 12px; - text-align: left; - display: inline-block; - width: 49%; - background: rgba(255, 255, 255, 0.05); - margin: 2px 0.5%; - } - - .movies .progress > div .folder { - display: inline-block; - padding: 5px 20px 5px 0; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - width: 85%; - direction: rtl; - vertical-align: middle; - } - - .movies .progress > div .percentage { - font-weight: bold; - display: inline-block; - text-transform: uppercase; - font-weight: normal; - font-size: 20px; - border-left: 1px solid rgba(255, 255, 255, .2); - width: 15%; - text-align: right; - vertical-align: middle; - } diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js deleted file mode 100644 index f5b5a2d..0000000 --- a/couchpotato/core/plugins/movie/static/movie.js +++ /dev/null @@ -1,298 +0,0 @@ -var Movie = new Class({ - - Extends: BlockBase, - - action: {}, - - initialize: function(list, options, data){ - var self = this; - - self.data = data; - self.view = options.view || 'details'; - self.list = list; - - self.el = new Element('div.movie'); - - self.profile = Quality.getProfile(data.profile_id) || {}; - self.category = CategoryList.getCategory(data.category_id) || {}; - self.parent(self, options); - - self.addEvents(); - }, - - addEvents: function(){ - var self = this; - - App.addEvent('movie.update.'+self.data.id, function(notification){ - self.busy(false); - self.removeView(); - self.update.delay(2000, self, notification); - }); - - ['movie.busy', 'searcher.started'].each(function(listener){ - App.addEvent(listener+'.'+self.data.id, function(notification){ - if(notification.data) - self.busy(true) - }); - }) - - App.addEvent('searcher.ended.'+self.data.id, function(notification){ - if(notification.data) - self.busy(false) - }); - }, - - destroy: function(){ - var self = this; - - self.el.destroy(); - delete self.list.movies_added[self.get('id')]; - self.list.movies.erase(self) - - self.list.checkIfEmpty(); - - // Remove events - App.removeEvents('movie.update.'+self.data.id); - ['movie.busy', 'searcher.started'].each(function(listener){ - App.removeEvents(listener+'.'+self.data.id); - }) - }, - - busy: function(set_busy){ - var self = this; - - if(!set_busy){ - setTimeout(function(){ - if(self.spinner){ - self.mask.fade('out'); - setTimeout(function(){ - if(self.mask) - self.mask.destroy(); - if(self.spinner) - self.spinner.el.destroy(); - self.spinner = null; - self.mask = null; - }, 400); - } - }, 1000) - } - else if(!self.spinner) { - self.createMask(); - self.spinner = createSpinner(self.mask); - self.mask.fade('in'); - } - }, - - createMask: function(){ - var self = this; - self.mask = new Element('div.mask', { - 'styles': { - 'z-index': 4 - } - }).inject(self.el, 'top').fade('hide'); - }, - - positionMask: function(){ - var self = this, - s = self.el.getSize() - - return self.mask.setStyles({ - 'width': s.x, - 'height': s.y - }).position({ - 'relativeTo': self.el - }) - }, - - update: function(notification){ - var self = this; - - self.data = notification.data; - self.el.empty(); - self.removeView(); - - self.profile = Quality.getProfile(self.data.profile_id) || {}; - self.category = CategoryList.getCategory(self.data.category_id) || {}; - self.create(); - - self.busy(false); - }, - - create: function(){ - var self = this; - - var s = Status.get(self.get('status_id')); - self.el.addClass('status_'+s.identifier); - - self.el.adopt( - self.select_checkbox = new Element('input[type=checkbox].inlay', { - 'events': { - 'change': function(){ - self.fireEvent('select') - } - } - }), - self.thumbnail = File.Select.single('poster', self.data.library.files), - self.data_container = new Element('div.data.inlay.light').adopt( - self.info_container = new Element('div.info').adopt( - new Element('div.title').adopt( - self.title = new Element('span', { - 'text': self.getTitle() || 'n/a' - }), - self.year = new Element('div.year', { - 'text': self.data.library.year || 'n/a' - }) - ), - self.description = new Element('div.description', { - 'text': self.data.library.plot - }), - self.quality = new Element('div.quality', { - 'events': { - 'click': function(e){ - var releases = self.el.getElement('.actions .releases'); - if(releases.isVisible()) - releases.fireEvent('click', [e]) - } - } - }) - ), - self.actions = new Element('div.actions') - ) - ); - - if(self.thumbnail.empty) - self.el.addClass('no_thumbnail'); - - //self.changeView(self.view); - self.select_checkbox_class = new Form.Check(self.select_checkbox); - - // Add profile - if(self.profile.data) - self.profile.getTypes().each(function(type){ - - var q = self.addQuality(type.quality_id || type.get('quality_id')); - if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){ - q.addClass('finish'); - q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.') - } - - }); - - // Add releases - self.data.releases.each(function(release){ - - var q = self.quality.getElement('.q_id'+ release.quality_id), - status = Status.get(release.status_id); - - if(!q && (status.identifier == 'snatched' || status.identifier == 'done')) - var q = self.addQuality(release.quality_id) - - if (status && q && !q.hasClass(status.identifier)){ - q.addClass(status.identifier); - q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label) - } - - }); - - Object.each(self.options.actions, function(action, key){ - self.action[key.toLowerCase()] = action = new self.options.actions[key](self) - if(action.el) - self.actions.adopt(action) - }); - - }, - - addQuality: function(quality_id){ - var self = this; - - var q = Quality.getQuality(quality_id); - return new Element('span', { - 'text': q.label, - 'class': 'q_'+q.identifier + ' q_id' + q.id, - 'title': '' - }).inject(self.quality); - - }, - - getTitle: function(){ - var self = this; - - var titles = self.data.library.titles; - - var title = titles.filter(function(title){ - return title['default'] - }).pop() - - if(title) - return self.getUnprefixedTitle(title.title) - else if(titles.length > 0) - return self.getUnprefixedTitle(titles[0].title) - - return 'Unknown movie' - }, - - getUnprefixedTitle: function(t){ - if(t.substr(0, 4).toLowerCase() == 'the ') - t = t.substr(4) + ', The'; - return t; - }, - - slide: function(direction, el){ - var self = this; - - if(direction == 'in'){ - self.temp_view = self.view; - self.changeView('details'); - - self.el.addEvent('outerClick', function(){ - self.removeView(); - self.slide('out') - }) - el.show(); - self.data_container.addClass('hide_right'); - } - else { - self.el.removeEvents('outerClick') - - setTimeout(function(){ - self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide(); - }, 600); - - self.data_container.removeClass('hide_right'); - } - }, - - changeView: function(new_view){ - var self = this; - - self.el - .removeClass(self.view+'_view') - .addClass(new_view+'_view') - - self.view = new_view; - }, - - removeView: function(){ - var self = this; - - self.el.removeClass(self.view+'_view') - }, - - get: function(attr){ - return this.data[attr] || this.data.library[attr] - }, - - select: function(bool){ - var self = this; - self.select_checkbox_class[bool ? 'check' : 'uncheck']() - }, - - isSelected: function(){ - return this.select_checkbox.get('checked'); - }, - - toElement: function(){ - return this.el; - } - -}); \ No newline at end of file diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css deleted file mode 100644 index dc74734..0000000 --- a/couchpotato/core/plugins/movie/static/search.css +++ /dev/null @@ -1,275 +0,0 @@ -.search_form { - display: inline-block; - vertical-align: middle; - position: absolute; - right: 105px; - top: 0; - text-align: right; - height: 100%; - border-bottom: 4px solid transparent; - transition: all .4s cubic-bezier(0.9,0,0.1,1); - position: absolute; - z-index: 20; - border: 1px solid transparent; - border-width: 0 0 4px; -} - .search_form:hover { - border-color: #047792; - } - - @media all and (max-width: 480px) { - .search_form { - right: 44px; - } - } - - .search_form.focused, - .search_form.shown { - border-color: #04bce6; - } - - .search_form .input { - height: 100%; - overflow: hidden; - width: 45px; - transition: all .4s cubic-bezier(0.9,0,0.1,1); - } - - .search_form.focused .input, - .search_form.shown .input { - width: 380px; - background: #4e5969; - } - - .search_form .input input { - border-radius: 0; - display: block; - border: 0; - background: none; - color: #FFF; - font-size: 25px; - height: 100%; - padding: 10px; - width: 100%; - opacity: 0; - padding: 0 40px 0 10px; - transition: all .4s ease-in-out .2s; - } - .search_form.focused .input input, - .search_form.shown .input input { - opacity: 1; - } - - @media all and (max-width: 480px) { - .search_form .input input { - font-size: 15px; - } - - .search_form.focused .input, - .search_form.shown .input { - width: 277px; - } - } - - .search_form .input a { - position: absolute; - top: 0; - right: 0; - width: 44px; - height: 100%; - cursor: pointer; - vertical-align: middle; - text-align: center; - line-height: 66px; - font-size: 15px; - color: #FFF; - } - - .search_form .input a:after { - content: "\e03e"; - } - - .search_form.shown.filled .input a:after { - content: "\e04e"; - } - - @media all and (max-width: 480px) { - .search_form .input a { - line-height: 44px; - } - } - - .search_form .results_container { - text-align: left; - position: absolute; - background: #5c697b; - margin: 4px 0 0; - width: 470px; - min-height: 50px; - box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55); - display: none; - } - @media all and (max-width: 480px) { - .search_form .results_container { - width: 320px; - } - } - .search_form.focused.filled .results_container, - .search_form.shown.filled .results_container { - display: block; - } - - .search_form .results { - max-height: 570px; - overflow-x: hidden; - } - - .movie_result { - overflow: hidden; - height: 50px; - position: relative; - } - - .movie_result .options { - position: absolute; - height: 100%; - top: 0; - left: 30px; - right: 0; - padding: 13px; - border: 1px solid transparent; - border-width: 1px 0; - border-radius: 0; - box-shadow: inset 0 1px 8px rgba(0,0,0,0.25); - } - .movie_result .options > .in_library_wanted { - margin-top: -7px; - } - - .movie_result .options > div { - border: 0; - } - - .movie_result .options .thumbnail { - vertical-align: middle; - } - - .movie_result .options select { - vertical-align: middle; - display: inline-block; - margin-right: 10px; - } - .movie_result .options select[name=title] { width: 170px; } - .movie_result .options select[name=profile] { width: 90px; } - .movie_result .options select[name=category] { width: 80px; } - - @media all and (max-width: 480px) { - - .movie_result .options select[name=title] { width: 90px; } - .movie_result .options select[name=profile] { width: 50px; } - .movie_result .options select[name=category] { width: 50px; } - - } - - .movie_result .options .button { - vertical-align: middle; - display: inline-block; - } - - .movie_result .options .message { - height: 100%; - font-size: 20px; - color: #fff; - line-height: 20px; - } - - .movie_result .data { - position: absolute; - height: 100%; - top: 0; - left: 30px; - right: 0; - background: #5c697b; - cursor: pointer; - border-top: 1px solid rgba(255,255,255, 0.08); - transition: all .4s cubic-bezier(0.9,0,0.1,1); - } - .movie_result .data.open { - left: 100% !important; - } - - .movie_result:last-child .data { border-bottom: 0; } - - .movie_result .in_wanted, .movie_result .in_library { - position: absolute; - bottom: 2px; - left: 14px; - font-size: 11px; - } - - .movie_result .thumbnail { - width: 34px; - min-height: 100%; - display: block; - margin: 0; - vertical-align: top; - } - - .movie_result .info { - position: absolute; - top: 20%; - left: 15px; - right: 7px; - vertical-align: middle; - } - - .movie_result .info h2 { - margin: 0; - font-weight: normal; - font-size: 20px; - padding: 0; - } - - .search_form .info h2 { - position: absolute; - width: 100%; - } - - .movie_result .info h2 .title { - display: block; - margin: 0; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - - .search_form .info h2 .title { - position: absolute; - width: 88%; - } - - .movie_result .info h2 .year { - padding: 0 5px; - text-align: center; - position: absolute; - width: 12%; - right: 0; - } - - @media all and (max-width: 480px) { - - .search_form .info h2 .year { - font-size: 12px; - margin-top: 7px; - } - - } - -.search_form .mask, -.movie_result .mask { - position: absolute; - height: 100%; - width: 100%; - left: 0; - top: 0; -} \ No newline at end of file diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js deleted file mode 100644 index 376e61c..0000000 --- a/couchpotato/core/plugins/movie/static/search.js +++ /dev/null @@ -1,414 +0,0 @@ -Block.Search = new Class({ - - Extends: BlockBase, - - cache: {}, - - create: function(){ - var self = this; - - var focus_timer = 0; - self.el = new Element('div.search_form').adopt( - new Element('div.input').adopt( - self.input = new Element('input', { - 'placeholder': 'Search & add a new movie', - 'events': { - 'keyup': self.keyup.bind(self), - 'focus': function(){ - if(focus_timer) clearTimeout(focus_timer); - self.el.addClass('focused') - if(this.get('value')) - self.hideResults(false) - }, - 'blur': function(){ - focus_timer = (function(){ - self.el.removeClass('focused') - }).delay(100); - } - } - }), - new Element('a.icon2', { - 'events': { - 'click': self.clear.bind(self), - 'touchend': self.clear.bind(self) - } - }) - ), - self.result_container = new Element('div.results_container', { - 'tween': { - 'duration': 200 - }, - 'events': { - 'mousewheel': function(e){ - (e).stopPropagation(); - } - } - }).adopt( - self.results = new Element('div.results') - ) - ); - - self.mask = new Element('div.mask').inject(self.result_container).fade('hide'); - - }, - - clear: function(e){ - var self = this; - (e).preventDefault(); - - if(self.last_q === ''){ - self.input.blur() - self.last_q = null; - } - else { - - self.last_q = ''; - self.input.set('value', ''); - self.input.focus() - - self.movies = [] - self.results.empty() - self.el.removeClass('filled') - - } - }, - - hideResults: function(bool){ - var self = this; - - if(self.hidden == bool) return; - - self.el[bool ? 'removeClass' : 'addClass']('shown'); - - if(bool){ - History.removeEvent('change', self.hideResults.bind(self, !bool)); - self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool)); - } - else { - History.addEvent('change', self.hideResults.bind(self, !bool)); - self.el.addEvent('outerClick', self.hideResults.bind(self, !bool)); - } - - self.hidden = bool; - }, - - keyup: function(e){ - var self = this; - - self.el[self.q() ? 'addClass' : 'removeClass']('filled') - - if(self.q() != self.last_q){ - if(self.api_request && self.api_request.isRunning()) - self.api_request.cancel(); - - if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer) - self.autocomplete_timer = self.autocomplete.delay(300, self) - } - - }, - - autocomplete: function(){ - var self = this; - - if(!self.q()){ - self.hideResults(true) - return - } - - self.list() - }, - - list: function(){ - var self = this, - q = self.q(), - cache = self.cache[q]; - - self.hideResults(false); - - if(!cache){ - self.mask.fade('in'); - - if(!self.spinner) - self.spinner = createSpinner(self.mask); - - self.api_request = Api.request('movie.search', { - 'data': { - 'q': q - }, - 'onComplete': self.fill.bind(self, q) - }) - } - else - self.fill(q, cache) - - self.last_q = q; - - }, - - fill: function(q, json){ - var self = this; - - self.cache[q] = json - - self.movies = {} - self.results.empty() - - Object.each(json.movies, function(movie){ - - var m = new Block.Search.Item(movie); - $(m).inject(self.results) - self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m - - if(q == movie.imdb) - m.showOptions() - - }); - - // Calculate result heights - var w = window.getSize(), - rc = self.result_container.getCoordinates(); - - self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px') - self.mask.fade('out') - - }, - - loading: function(bool){ - this.el[bool ? 'addClass' : 'removeClass']('loading') - }, - - q: function(){ - return this.input.get('value').trim(); - } - -}); - -Block.Search.Item = new Class({ - - Implements: [Options, Events], - - initialize: function(info, options){ - var self = this; - self.setOptions(options); - - self.info = info; - self.alternative_titles = []; - - self.create(); - }, - - create: function(){ - var self = this, - info = self.info; - - self.el = new Element('div.movie_result', { - 'id': info.imdb - }).adopt( - self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', { - 'src': info.images.poster[0], - 'height': null, - 'width': null - }) : null, - self.options_el = new Element('div.options.inlay'), - self.data_container = new Element('div.data', { - 'events': { - 'click': self.showOptions.bind(self) - } - }).adopt( - self.info_container = new Element('div.info').adopt( - new Element('h2').adopt( - self.title = new Element('span.title', { - 'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown' - }), - self.year = info.year ? new Element('span.year', { - 'text': info.year - }) : null - ) - ) - ) - ) - - if(info.titles) - info.titles.each(function(title){ - self.alternativeTitle({ - 'title': title - }); - }) - }, - - alternativeTitle: function(alternative){ - var self = this; - - self.alternative_titles.include(alternative); - }, - - getTitle: function(){ - var self = this; - try { - return self.info.original_title ? self.info.original_title : self.info.titles[0]; - } - catch(e){ - return 'Unknown'; - } - }, - - get: function(key){ - return this.info[key] - }, - - showOptions: function(){ - var self = this; - - self.createOptions(); - - self.data_container.addClass('open'); - self.el.addEvent('outerClick', self.closeOptions.bind(self)) - - }, - - closeOptions: function(){ - var self = this; - - self.data_container.removeClass('open'); - self.el.removeEvents('outerClick') - }, - - add: function(e){ - var self = this; - - if(e) - (e).preventDefault(); - - self.loadingMask(); - - Api.request('movie.add', { - 'data': { - 'identifier': self.info.imdb, - 'title': self.title_select.get('value'), - 'profile_id': self.profile_select.get('value'), - 'category_id': self.category_select.get('value') - }, - 'onComplete': function(json){ - self.options_el.empty(); - self.options_el.adopt( - new Element('div.message', { - 'text': json.added ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs' - }) - ); - self.mask.fade('out'); - - self.fireEvent('added'); - }, - 'onFailure': function(){ - self.options_el.empty(); - self.options_el.adopt( - new Element('div.message', { - 'text': 'Something went wrong, check the logs for more info.' - }) - ); - self.mask.fade('out'); - } - }); - }, - - createOptions: function(){ - var self = this, - info = self.info; - - if(!self.options_el.hasClass('set')){ - - if(self.info.in_library){ - var in_library = []; - self.info.in_library.releases.each(function(release){ - in_library.include(release.quality.label) - }); - } - - self.options_el.grab( - new Element('div', { - 'class': self.info.in_wanted && self.info.in_wanted.profile || in_library ? 'in_library_wanted' : '' - }).adopt( - self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', { - 'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label - }) : (in_library ? new Element('span.in_library', { - 'text': 'Already in library: ' + in_library.join(', ') - }) : null), - self.title_select = new Element('select', { - 'name': 'title' - }), - self.profile_select = new Element('select', { - 'name': 'profile' - }), - self.category_select = new Element('select', { - 'name': 'category' - }).grab( - new Element('option', {'value': -1, 'text': 'None'}) - ), - self.add_button = new Element('a.button', { - 'text': 'Add', - 'events': { - 'click': self.add.bind(self) - } - }) - ) - ); - - Array.each(self.alternative_titles, function(alt){ - new Element('option', { - 'text': alt.title - }).inject(self.title_select) - }) - - - // Fill categories - var categories = CategoryList.getAll(); - - if(categories.length == 0) - self.category_select.hide(); - else { - self.category_select.show(); - categories.each(function(category){ - new Element('option', { - 'value': category.data.id, - 'text': category.data.label - }).inject(self.category_select); - }); - } - - // Fill profiles - var profiles = Quality.getActiveProfiles(); - if(profiles.length == 1) - self.profile_select.hide(); - - profiles.each(function(profile){ - new Element('option', { - 'value': profile.id ? profile.id : profile.data.id, - 'text': profile.label ? profile.label : profile.data.label - }).inject(self.profile_select) - }); - - self.options_el.addClass('set'); - - if(categories.length == 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 && - !(self.info.in_wanted && self.info.in_wanted.profile || in_library)) - self.add(); - - } - - }, - - loadingMask: function(){ - var self = this; - - self.mask = new Element('div.mask').inject(self.el).fade('hide') - - createSpinner(self.mask) - self.mask.fade('in') - - }, - - toElement: function(){ - return this.el - } - -}); From 78ab419cd895ded2ba143fe3d1a1481959a7fbd7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 15 Aug 2013 23:38:14 +0200 Subject: [PATCH 2/5] Move movie plugin to media folder --- couchpotato/core/media/_base/__init__.py | 0 couchpotato/core/media/_base/searcher/__init__.py | 76 +++ couchpotato/core/media/_base/searcher/main.py | 207 +++++++ couchpotato/core/media/movie/_base/static/movie.js | 6 +- couchpotato/core/media/movie/searcher/__init__.py | 60 ++ couchpotato/core/media/movie/searcher/main.py | 500 ++++++++++++++++ couchpotato/core/plugins/automation/main.py | 2 +- couchpotato/core/plugins/dashboard/main.py | 4 +- couchpotato/core/plugins/renamer/main.py | 2 +- couchpotato/core/plugins/searcher/__init__.py | 124 ---- couchpotato/core/plugins/searcher/main.py | 644 --------------------- couchpotato/core/providers/base.py | 2 +- couchpotato/static/scripts/page/wanted.js | 4 +- 13 files changed, 853 insertions(+), 778 deletions(-) create mode 100644 couchpotato/core/media/_base/__init__.py create mode 100644 couchpotato/core/media/_base/searcher/__init__.py create mode 100644 couchpotato/core/media/_base/searcher/main.py create mode 100644 couchpotato/core/media/movie/searcher/__init__.py create mode 100644 couchpotato/core/media/movie/searcher/main.py delete mode 100644 couchpotato/core/plugins/searcher/__init__.py delete mode 100644 couchpotato/core/plugins/searcher/main.py diff --git a/couchpotato/core/media/_base/__init__.py b/couchpotato/core/media/_base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/couchpotato/core/media/_base/searcher/__init__.py b/couchpotato/core/media/_base/searcher/__init__.py new file mode 100644 index 0000000..f3d764d --- /dev/null +++ b/couchpotato/core/media/_base/searcher/__init__.py @@ -0,0 +1,76 @@ +from .main import Searcher +import random + +def start(): + return Searcher() + +config = [{ + 'name': 'searcher', + 'order': 20, + 'groups': [ + { + 'tab': 'searcher', + 'name': 'searcher', + 'label': 'Basics', + 'description': 'General search options', + 'options': [ + { + 'name': 'preferred_method', + 'label': 'First search', + 'description': 'Which of the methods do you prefer', + 'default': 'both', + 'type': 'dropdown', + 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')], + }, + ], + }, { + 'tab': 'searcher', + 'subtab': 'category', + 'subtab_label': 'Categories', + 'name': 'filter', + 'label': 'Global filters', + 'description': 'Prefer, ignore & required words in release names', + 'options': [ + { + 'name': 'preferred_words', + 'label': 'Preferred', + 'default': '', + 'placeholder': 'Example: CtrlHD, Amiable, Wiki', + 'description': 'Words that give the releases a higher score.' + }, + { + 'name': 'required_words', + 'label': 'Required', + 'default': '', + 'placeholder': 'Example: DTS, AC3 & English', + 'description': 'Release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"' + }, + { + 'name': 'ignored_words', + 'label': 'Ignored', + 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs', + 'description': 'Ignores releases that match any of these sets. (Works like explained above)' + }, + ], + }, + ], +}, { + 'name': 'nzb', + 'groups': [ + { + 'tab': 'searcher', + 'name': 'searcher', + 'label': 'NZB', + 'wizard': True, + 'options': [ + { + 'name': 'retention', + 'label': 'Usenet Retention', + 'default': 1500, + 'type': 'int', + 'unit': 'days' + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py new file mode 100644 index 0000000..772a8ac --- /dev/null +++ b/couchpotato/core/media/_base/searcher/main.py @@ -0,0 +1,207 @@ +from couchpotato import get_session +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.encoding import simplifyString, toUnicode +from couchpotato.core.helpers.variable import md5, getTitle +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin +from couchpotato.core.settings.model import Movie, Release, ReleaseInfo +from couchpotato.environment import Env +from inspect import ismethod, isfunction +import datetime +import re +import time +import traceback + +log = CPLog(__name__) + + +class Searcher(Plugin): + + def __init__(self): + addEvent('searcher.get_types', self.getSearchTypes) + addEvent('searcher.contains_other_quality', self.containsOtherQuality) + addEvent('searcher.correct_year', self.correctYear) + addEvent('searcher.correct_name', self.correctName) + addEvent('searcher.download', self.download) + + def download(self, data, movie, manual = False): + + # Test to see if any downloaders are enabled for this type + downloader_enabled = fireEvent('download.enabled', manual, data, single = True) + + if downloader_enabled: + + snatched_status = fireEvent('status.get', 'snatched', single = True) + + # Download movie to temp + filedata = None + if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))): + filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) + if filedata == 'try_next': + return filedata + + download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True) + log.debug('Downloader result: %s', download_result) + + if download_result: + try: + # Mark release as snatched + db = get_session() + rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() + if rls: + 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') + + # Save download-id info if returned + if isinstance(download_result, dict): + for key in download_result: + rls_info = ReleaseInfo( + identifier = 'download_%s' % key, + value = toUnicode(download_result.get(key)) + ) + rls.info.append(rls_info) + db.commit() + + log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label) + snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) + log.info(snatch_message) + fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict()) + + # 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()) + + return True + + log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('type', ''))) + + return False + + def getSearchTypes(self): + + download_types = fireEvent('download.enabled_types', merge = True) + provider_types = fireEvent('provider.enabled_types', merge = True) + + if download_types and len(list(set(provider_types) & set(download_types))) == 0: + log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_types)) + return [] + + for useless_provider in list(set(provider_types) - set(download_types)): + log.debug('Provider for "%s" enabled, but no downloader.', useless_provider) + + search_types = download_types + + if len(search_types) == 0: + log.error('There aren\'t any downloaders enabled. Please pick one in settings.') + return [] + + return search_types + + def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}): + + name = nzb['name'] + size = nzb.get('size', 0) + nzb_words = re.split('\W+', simplifyString(name)) + + qualities = fireEvent('quality.all', single = True) + + found = {} + for quality in qualities: + # Main in words + if quality['identifier'] in nzb_words: + found[quality['identifier']] = True + + # Alt in words + if list(set(nzb_words) & set(quality['alternative'])): + found[quality['identifier']] = True + + # Try guessing via quality tags + guess = fireEvent('quality.guess', [nzb.get('name')], single = True) + if guess: + found[guess['identifier']] = True + + # Hack for older movies that don't contain quality tag + year_name = fireEvent('scanner.name_year', name, single = True) + if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): + if size > 3000: # Assume dvdr + log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size)) + found['dvdr'] = True + else: # Assume dvdrip + log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size)) + found['dvdrip'] = True + + # Allow other qualities + for allowed in preferred_quality.get('allow'): + if found.get(allowed): + del found[allowed] + + return not (found.get(preferred_quality['identifier']) and len(found) == 1) + + def correctYear(self, haystack, year, year_range): + + if not isinstance(haystack, (list, tuple, set)): + haystack = [haystack] + + for string in haystack: + + year_name = fireEvent('scanner.name_year', string, single = True) + + if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)): + log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year)) + return True + + log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year)) + return False + + def correctName(self, check_name, movie_name): + + check_names = [check_name] + + # Match names between " + try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0)) + except: pass + + # Match longest name between [] + try: check_names.append(max(check_name.split('['), key = len)) + except: pass + + for check_name in list(set(check_names)): + check_movie = fireEvent('scanner.name_year', check_name, single = True) + + try: + check_words = filter(None, re.split('\W+', check_movie.get('name', ''))) + movie_words = filter(None, re.split('\W+', simplifyString(movie_name))) + + if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0: + return True + except: + pass + + return False + +class SearchSetupError(Exception): + pass diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index f5b5a2d..20956a0 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -29,14 +29,14 @@ var Movie = new Class({ self.update.delay(2000, self, notification); }); - ['movie.busy', 'searcher.started'].each(function(listener){ + ['movie.busy', 'movie.searcher.started'].each(function(listener){ App.addEvent(listener+'.'+self.data.id, function(notification){ if(notification.data) self.busy(true) }); }) - App.addEvent('searcher.ended.'+self.data.id, function(notification){ + App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){ if(notification.data) self.busy(false) }); @@ -53,7 +53,7 @@ var Movie = new Class({ // Remove events App.removeEvents('movie.update.'+self.data.id); - ['movie.busy', 'searcher.started'].each(function(listener){ + ['movie.busy', 'movie.searcher.started'].each(function(listener){ App.removeEvents(listener+'.'+self.data.id); }) }, diff --git a/couchpotato/core/media/movie/searcher/__init__.py b/couchpotato/core/media/movie/searcher/__init__.py new file mode 100644 index 0000000..2962caa --- /dev/null +++ b/couchpotato/core/media/movie/searcher/__init__.py @@ -0,0 +1,60 @@ +from .main import Searcher +import random + +def start(): + return Searcher() + +config = [{ + 'name': 'searcher', + 'order': 20, + 'groups': [ + { + 'tab': 'searcher', + 'name': 'movie_searcher', + 'label': 'Movie search', + 'description': 'Search options for movies', + 'advanced': True, + 'options': [ + { + 'name': 'always_search', + 'default': False, + 'type': 'bool', + 'label': 'Always search', + 'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.', + }, + { + 'name': 'run_on_launch', + 'label': 'Run on launch', + 'advanced': True, + 'default': 0, + 'type': 'bool', + 'description': 'Force run the searcher after (re)start.', + }, + { + 'name': 'cron_day', + 'label': 'Day', + 'advanced': True, + 'default': '*', + 'type': 'string', + 'description': '*: Every day, */2: Every 2 days, 1: Every first of the month. See APScheduler for details.', + }, + { + 'name': 'cron_hour', + 'label': 'Hour', + 'advanced': True, + 'default': random.randint(0, 23), + 'type': 'string', + 'description': '*: Every hour, */8: Every 8 hours, 3: At 3, midnight.', + }, + { + 'name': 'cron_minute', + 'label': 'Minute', + 'advanced': True, + 'default': random.randint(0, 59), + 'type': 'string', + 'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour." + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py new file mode 100644 index 0000000..dbfd679 --- /dev/null +++ b/couchpotato/core/media/movie/searcher/main.py @@ -0,0 +1,500 @@ +from couchpotato import get_session +from couchpotato.api import addApiView +from couchpotato.core.event import addEvent, fireEvent, fireEventAsync +from couchpotato.core.helpers.encoding import simplifyString, toUnicode +from couchpotato.core.helpers.variable import md5, getTitle, splitString, \ + possibleTitles, getImdb +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin +from couchpotato.core.settings.model import Movie, Release, ReleaseInfo +from couchpotato.environment import Env +from datetime import date +from sqlalchemy.exc import InterfaceError +import datetime +import random +import re +import time +import traceback + +log = CPLog(__name__) + + +class Searcher(Plugin): + + in_progress = False + + def __init__(self): + addEvent('movie.searcher.all', self.searchAll) + addEvent('movie.searcher.single', self.single) + addEvent('movie.searcher.correct_movie', self.correctMovie) + addEvent('movie.searcher.try_next_release', self.tryNextRelease) + addEvent('movie.searcher.could_be_released', self.couldBeReleased) + + addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = { + 'desc': 'Marks the snatched results as ignored and try the next best release', + 'params': { + 'id': {'desc': 'The id of the movie'}, + }, + }) + + addApiView('movie.searcher.full_search', self.searchAllView, docs = { + 'desc': 'Starts a full search for all wanted movies', + }) + + addApiView('movie.searcher.progress', self.getProgress, docs = { + 'desc': 'Get the progress of current full search', + 'return': {'type': 'object', 'example': """{ + 'progress': False || object, total & to_go, +}"""}, + }) + + if self.conf('run_on_launch'): + addEvent('app.load', self.searchAll) + + addEvent('app.load', self.setCrons) + addEvent('setting.save.searcher.cron_day.after', self.setCrons) + addEvent('setting.save.searcher.cron_hour.after', self.setCrons) + addEvent('setting.save.searcher.cron_minute.after', self.setCrons) + + def setCrons(self): + fireEvent('schedule.cron', 'movie.searcher.all', self.searchAll, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) + + def searchAllView(self, **kwargs): + + in_progress = self.in_progress + if not in_progress: + fireEventAsync('movie.searcher.all') + fireEvent('notify.frontend', type = 'movie.searcher.started', data = True, message = 'Full search started') + else: + fireEvent('notify.frontend', type = 'movie.searcher.already_started', data = True, message = 'Full search already in progress') + + return { + 'success': not in_progress + } + + def getProgress(self, **kwargs): + + return { + 'progress': self.in_progress + } + + def searchAll(self): + + if self.in_progress: + log.info('Search already in progress') + return + + self.in_progress = True + + db = get_session() + + movies = db.query(Movie).filter( + Movie.status.has(identifier = 'active') + ).all() + random.shuffle(movies) + + self.in_progress = { + 'total': len(movies), + 'to_go': len(movies), + } + + try: + search_types = fireEvent('searcher.get_types', single = True) + + for movie in movies: + movie_dict = movie.to_dict({ + 'category': {}, + 'profile': {'types': {'quality': {}}}, + 'releases': {'status': {}, 'quality': {}}, + 'library': {'titles': {}, 'files':{}}, + 'files': {} + }) + + try: + self.single(movie_dict, search_types) + except IndexError: + log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc())) + fireEvent('library.update', movie_dict['library']['identifier'], force = True) + except: + log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc())) + + self.in_progress['to_go'] -= 1 + + # Break if CP wants to shut down + if self.shuttingDown(): + break + + except SearchSetupError: + pass + + self.in_progress = False + + def single(self, movie, search_types = None): + + # Find out search type + try: + if not search_types: + search_types = fireEvent('searcher.get_types', single = True) + except SearchSetupError: + return + + done_status = fireEvent('status.get', 'done', single = True) + + if not movie['profile'] or movie['status_id'] == done_status.get('id'): + log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.') + return + + db = get_session() + + pre_releases = fireEvent('quality.pre_releases', single = True) + release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True) + available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True) + + found_releases = [] + too_early_to_search = [] + + default_title = getTitle(movie['library']) + if not default_title: + log.error('No proper info found for movie, removing it from library to cause it from having more issues.') + fireEvent('movie.delete', movie['id'], single = True) + return + + fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title) + + + ret = False + for quality_type in movie['profile']['types']: + if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']): + too_early_to_search.append(quality_type['quality']['identifier']) + continue + + has_better_quality = 0 + + # 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'), failed_status.get('id')]: + has_better_quality += 1 + + # Don't search for quality lower then already available. + if has_better_quality is 0: + + log.info('Search for %s in %s', (default_title, quality_type['quality']['label'])) + quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True) + + results = [] + for search_type in search_types: + type_results = fireEvent('%s.search' % search_type, movie, quality, merge = True) + if type_results: + results += type_results + + sorted_results = sorted(results, key = lambda k: k['score'], reverse = True) + if len(sorted_results) == 0: + log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label'])) + + download_preference = self.conf('preferred_method') + if download_preference != 'both': + sorted_results = sorted(sorted_results, key = lambda k: k['type'][:3], reverse = (download_preference == 'torrent')) + + # Check if movie isn't deleted while searching + if not db.query(Movie).filter_by(id = movie.get('id')).first(): + break + + # Add them to this movie releases list + for nzb in sorted_results: + + nzb_identifier = md5(nzb['url']) + found_releases.append(nzb_identifier) + + rls = db.query(Release).filter_by(identifier = nzb_identifier).first() + if not rls: + rls = Release( + identifier = nzb_identifier, + movie_id = movie.get('id'), + quality_id = quality_type.get('quality_id'), + status_id = available_status.get('id') + ) + db.add(rls) + else: + [db.delete(old_info) for old_info in rls.info] + rls.last_edit = int(time.time()) + + db.commit() + + for info in nzb: + try: + if not isinstance(nzb[info], (str, unicode, int, long, float)): + continue + + rls_info = ReleaseInfo( + identifier = info, + value = toUnicode(nzb[info]) + ) + rls.info.append(rls_info) + except InterfaceError: + log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc())) + + db.commit() + + nzb['status_id'] = rls.status_id + + + for nzb in sorted_results: + if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0): + log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name'])) + continue + + if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]: + log.info('Ignored: %s', nzb['name']) + continue + + if nzb['score'] <= 0: + log.info('Ignored, score to low: %s', nzb['name']) + continue + + downloaded = fireEvent('searcher.download', data = nzb, movie = movie, single = True) + if downloaded is True: + ret = True + break + elif downloaded != 'try_next': + break + + # Remove releases that aren't found anymore + for release in movie.get('releases', []): + if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases: + fireEvent('release.delete', release.get('id'), single = True) + + else: + log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title)) + fireEvent('movie.restatus', movie['id']) + break + + # Break if CP wants to shut down + if self.shuttingDown() or ret: + break + + if len(too_early_to_search) > 0: + log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) + + fireEvent('notify.frontend', type = 'movie.searcher.ended.%s' % movie['id'], data = True) + + return ret + + def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs): + + imdb_results = kwargs.get('imdb_results', False) + retention = Env.setting('retention', section = 'nzb') + + if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0): + log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name'])) + return False + + movie_name = getTitle(movie['library']) + movie_words = re.split('\W+', simplifyString(movie_name)) + nzb_name = simplifyString(nzb['name']) + nzb_words = re.split('\W+', nzb_name) + + # Make sure it has required words + required_words = splitString(self.conf('required_words').lower()) + try: required_words = list(set(required_words + splitString(movie['category']['required'].lower()))) + except: pass + + req_match = 0 + for req_set in required_words: + req = splitString(req_set, '&') + req_match += len(list(set(nzb_words) & set(req))) == len(req) + + if len(required_words) > 0 and req_match == 0: + log.info2('Wrong: Required word missing: %s', nzb['name']) + return False + + # Ignore releases + ignored_words = splitString(self.conf('ignored_words').lower()) + try: ignored_words = list(set(ignored_words + splitString(movie['category']['ignored'].lower()))) + except: pass + + ignored_match = 0 + for ignored_set in ignored_words: + ignored = splitString(ignored_set, '&') + ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored) + + if len(ignored_words) > 0 and ignored_match: + log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name'])) + return False + + # Ignore porn stuff + pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic'] + pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words)) + if pron_words: + log.info('Wrong: %s, probably pr0n', (nzb['name'])) + return False + + preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True) + + # Contains lower quality string + if fireEvent('searcher.contains_other_quality', nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single = True): + log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label'])) + return False + + + # File to small + if nzb['size'] and preferred_quality['size_min'] > nzb['size']: + log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min'])) + return False + + # File to large + if nzb['size'] and preferred_quality.get('size_max') < nzb['size']: + log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max'])) + return False + + + # Provider specific functions + get_more = nzb.get('get_more_info') + if get_more: + get_more(nzb) + + extra_check = nzb.get('extra_check') + if extra_check and not extra_check(nzb): + return False + + + if imdb_results: + return True + + # Check if nzb contains imdb link + if getImdb(nzb.get('description', '')) == movie['library']['identifier']: + return True + + for raw_title in movie['library']['titles']: + for movie_title in possibleTitles(raw_title['title']): + movie_words = re.split('\W+', simplifyString(movie_title)) + + if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True): + # if no IMDB link, at least check year range 1 + if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 1, single = True): + return True + + # if no IMDB link, at least check year + if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 0, single = True): + return True + + log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year'])) + return False + + def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}): + + name = nzb['name'] + size = nzb.get('size', 0) + nzb_words = re.split('\W+', simplifyString(name)) + + qualities = fireEvent('quality.all', single = True) + + found = {} + for quality in qualities: + # Main in words + if quality['identifier'] in nzb_words: + found[quality['identifier']] = True + + # Alt in words + if list(set(nzb_words) & set(quality['alternative'])): + found[quality['identifier']] = True + + # Try guessing via quality tags + guess = fireEvent('quality.guess', [nzb.get('name')], single = True) + if guess: + found[guess['identifier']] = True + + # Hack for older movies that don't contain quality tag + year_name = fireEvent('scanner.name_year', name, single = True) + if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): + if size > 3000: # Assume dvdr + log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size)) + found['dvdr'] = True + else: # Assume dvdrip + log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size)) + found['dvdrip'] = True + + # Allow other qualities + for allowed in preferred_quality.get('allow'): + if found.get(allowed): + del found[allowed] + + return not (found.get(preferred_quality['identifier']) and len(found) == 1) + + def checkIMDB(self, haystack, imdbId): + + for string in haystack: + if 'imdb.com/title/' + imdbId in string: + return True + + return False + + def couldBeReleased(self, is_pre_release, dates, year = None): + + now = int(time.time()) + now_year = date.today().year + + if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)): + return True + else: + + # For movies before 1972 + if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0: + return True + + if is_pre_release: + # Prerelease 1 week before theaters + if dates.get('theater') - 604800 < now: + return True + else: + # 12 weeks after theater release + if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now: + return True + + if dates.get('dvd') > 0: + + # 4 weeks before dvd release + if dates.get('dvd') - 2419200 < now: + return True + + # Dvd should be released + if dates.get('dvd') < now: + return True + + + return False + + def tryNextReleaseView(self, id = None, **kwargs): + + trynext = self.tryNextRelease(id) + + return { + 'success': trynext + } + + def tryNextRelease(self, movie_id, manual = False): + + snatched_status, ignored_status = fireEvent('status.get', ['snatched', 'ignored'], single = True) + + try: + db = get_session() + rels = db.query(Release).filter_by( + status_id = snatched_status.get('id'), + movie_id = movie_id + ).all() + + for rel in rels: + rel.status_id = ignored_status.get('id') + db.commit() + + movie_dict = fireEvent('movie.get', movie_id, single = True) + log.info('Trying next release for: %s', getTitle(movie_dict['library'])) + fireEvent('movie.searcher.single', movie_dict) + + return True + + except: + log.error('Failed searching for next release: %s', traceback.format_exc()) + return False + +class SearchSetupError(Exception): + pass diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py index 67bae1d..80e1285 100644 --- a/couchpotato/core/plugins/automation/main.py +++ b/couchpotato/core/plugins/automation/main.py @@ -36,4 +36,4 @@ class Automation(Plugin): for movie_id in movie_ids: movie_dict = fireEvent('movie.get', movie_id, single = True) - fireEvent('searcher.single', movie_dict) + fireEvent('movie.searcher.single', movie_dict) diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py index 939b415..df6f975 100644 --- a/couchpotato/core/plugins/dashboard/main.py +++ b/couchpotato/core/plugins/dashboard/main.py @@ -70,9 +70,9 @@ class Dashboard(Plugin): coming_soon = False # Theater quality - if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, movie.library.year, single = True): + if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, movie.library.year, single = True): coming_soon = True - if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, movie.library.year, single = True): + if pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, movie.library.year, single = True): coming_soon = True # Skip if movie is snatched/downloaded/available diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 30ce408..8d1a818 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -715,7 +715,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) db.commit() if self.conf('next_on_failed'): - fireEvent('searcher.try_next_release', movie_id = rel.movie_id) + fireEvent('movie.searcher.try_next_release', movie_id = rel.movie_id) elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) if self.statusInfoComplete(item): diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py deleted file mode 100644 index d925ae7..0000000 --- a/couchpotato/core/plugins/searcher/__init__.py +++ /dev/null @@ -1,124 +0,0 @@ -from .main import Searcher -import random - -def start(): - return Searcher() - -config = [{ - 'name': 'searcher', - 'order': 20, - 'groups': [ - { - 'tab': 'searcher', - 'name': 'searcher', - 'label': 'Search', - 'description': 'Options for the searchers', - 'options': [ - { - 'name': 'preferred_method', - 'label': 'First search', - 'description': 'Which of the methods do you prefer', - 'default': 'both', - 'type': 'dropdown', - 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')], - }, - { - 'name': 'always_search', - 'default': False, - 'advanced': True, - 'type': 'bool', - 'label': 'Always search', - 'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.', - }, - ], - }, { - 'tab': 'searcher', - 'subtab': 'category', - 'subtab_label': 'Categories', - 'name': 'filter', - 'label': 'Global filters', - 'description': 'Prefer, ignore & required words in release names', - 'options': [ - { - 'name': 'preferred_words', - 'label': 'Preferred', - 'default': '', - 'placeholder': 'Example: CtrlHD, Amiable, Wiki', - 'description': 'Words that give the releases a higher score.' - }, - { - 'name': 'required_words', - 'label': 'Required', - 'default': '', - 'placeholder': 'Example: DTS, AC3 & English', - 'description': 'Release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"' - }, - { - 'name': 'ignored_words', - 'label': 'Ignored', - 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs', - 'description': 'Ignores releases that match any of these sets. (Works like explained above)' - }, - ], - }, { - 'tab': 'searcher', - 'name': 'cronjob', - 'label': 'Cronjob', - 'advanced': True, - 'description': 'Cron settings for the searcher see: APScheduler for details.', - 'options': [ - { - 'name': 'run_on_launch', - 'label': 'Run on launch', - 'advanced': True, - 'default': 0, - 'type': 'bool', - 'description': 'Force run the searcher after (re)start.', - }, - { - 'name': 'cron_day', - 'label': 'Day', - 'advanced': True, - 'default': '*', - 'type': 'string', - 'description': '*: Every day, */2: Every 2 days, 1: Every first of the month.', - }, - { - 'name': 'cron_hour', - 'label': 'Hour', - 'advanced': True, - 'default': random.randint(0, 23), - 'type': 'string', - 'description': '*: Every hour, */8: Every 8 hours, 3: At 3, midnight.', - }, - { - 'name': 'cron_minute', - 'label': 'Minute', - 'advanced': True, - 'default': random.randint(0, 59), - 'type': 'string', - 'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour." - }, - ], - }, - ], -}, { - 'name': 'nzb', - 'groups': [ - { - 'tab': 'searcher', - 'name': 'searcher', - 'label': 'NZB', - 'wizard': True, - 'options': [ - { - 'name': 'retention', - 'label': 'Usenet Retention', - 'default': 1500, - 'type': 'int', - 'unit': 'days' - }, - ], - }, - ], -}] diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py deleted file mode 100644 index b55e720..0000000 --- a/couchpotato/core/plugins/searcher/main.py +++ /dev/null @@ -1,644 +0,0 @@ -from couchpotato import get_session -from couchpotato.api import addApiView -from couchpotato.core.event import addEvent, fireEvent, fireEventAsync -from couchpotato.core.helpers.encoding import simplifyString, toUnicode -from couchpotato.core.helpers.variable import md5, getTitle, splitString, \ - possibleTitles -from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Movie, Release, ReleaseInfo -from couchpotato.environment import Env -from datetime import date -from inspect import ismethod, isfunction -from sqlalchemy.exc import InterfaceError -import datetime -import random -import re -import time -import traceback - -log = CPLog(__name__) - - -class Searcher(Plugin): - - in_progress = False - - def __init__(self): - addEvent('searcher.all', self.allMovies) - addEvent('searcher.single', self.single) - addEvent('searcher.correct_movie', self.correctMovie) - addEvent('searcher.download', self.download) - addEvent('searcher.try_next_release', self.tryNextRelease) - addEvent('searcher.could_be_released', self.couldBeReleased) - - addApiView('searcher.try_next', self.tryNextReleaseView, docs = { - 'desc': 'Marks the snatched results as ignored and try the next best release', - 'params': { - 'id': {'desc': 'The id of the movie'}, - }, - }) - - addApiView('searcher.full_search', self.allMoviesView, docs = { - 'desc': 'Starts a full search for all wanted movies', - }) - - addApiView('searcher.progress', self.getProgress, docs = { - 'desc': 'Get the progress of current full search', - 'return': {'type': 'object', 'example': """{ - 'progress': False || object, total & to_go, -}"""}, - }) - - if self.conf('run_on_launch'): - addEvent('app.load', self.allMovies) - - addEvent('app.load', self.setCrons) - addEvent('setting.save.searcher.cron_day.after', self.setCrons) - addEvent('setting.save.searcher.cron_hour.after', self.setCrons) - addEvent('setting.save.searcher.cron_minute.after', self.setCrons) - - def setCrons(self): - fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) - - def allMoviesView(self, **kwargs): - - in_progress = self.in_progress - if not in_progress: - fireEventAsync('searcher.all') - fireEvent('notify.frontend', type = 'searcher.started', data = True, message = 'Full search started') - else: - fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress') - - return { - 'success': not in_progress - } - - def getProgress(self, **kwargs): - - return { - 'progress': self.in_progress - } - - def allMovies(self): - - if self.in_progress: - log.info('Search already in progress') - return - - self.in_progress = True - - db = get_session() - - movies = db.query(Movie).filter( - Movie.status.has(identifier = 'active') - ).all() - random.shuffle(movies) - - self.in_progress = { - 'total': len(movies), - 'to_go': len(movies), - } - - try: - search_types = self.getSearchTypes() - - for movie in movies: - movie_dict = movie.to_dict({ - 'category': {}, - 'profile': {'types': {'quality': {}}}, - 'releases': {'status': {}, 'quality': {}}, - 'library': {'titles': {}, 'files':{}}, - 'files': {} - }) - - try: - self.single(movie_dict, search_types) - except IndexError: - log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc())) - fireEvent('library.update', movie_dict['library']['identifier'], force = True) - except: - log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc())) - - self.in_progress['to_go'] -= 1 - - # Break if CP wants to shut down - if self.shuttingDown(): - break - - except SearchSetupError: - pass - - self.in_progress = False - - def single(self, movie, search_types = None): - - # Find out search type - try: - if not search_types: - search_types = self.getSearchTypes() - except SearchSetupError: - return - - done_status = fireEvent('status.get', 'done', single = True) - - if not movie['profile'] or movie['status_id'] == done_status.get('id'): - log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.') - return - - db = get_session() - - pre_releases = fireEvent('quality.pre_releases', single = True) - release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True) - available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True) - - found_releases = [] - too_early_to_search = [] - - default_title = getTitle(movie['library']) - if not default_title: - log.error('No proper info found for movie, removing it from library to cause it from having more issues.') - fireEvent('movie.delete', movie['id'], single = True) - return - - fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title) - - - ret = False - for quality_type in movie['profile']['types']: - if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']): - too_early_to_search.append(quality_type['quality']['identifier']) - continue - - has_better_quality = 0 - - # 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'), failed_status.get('id')]: - has_better_quality += 1 - - # Don't search for quality lower then already available. - if has_better_quality is 0: - - log.info('Search for %s in %s', (default_title, quality_type['quality']['label'])) - quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True) - - results = [] - for search_type in search_types: - type_results = fireEvent('%s.search' % search_type, movie, quality, merge = True) - if type_results: - results += type_results - - sorted_results = sorted(results, key = lambda k: k['score'], reverse = True) - if len(sorted_results) == 0: - log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label'])) - - download_preference = self.conf('preferred_method') - if download_preference != 'both': - sorted_results = sorted(sorted_results, key = lambda k: k['type'][:3], reverse = (download_preference == 'torrent')) - - # Check if movie isn't deleted while searching - if not db.query(Movie).filter_by(id = movie.get('id')).first(): - break - - # Add them to this movie releases list - for nzb in sorted_results: - - nzb_identifier = md5(nzb['url']) - found_releases.append(nzb_identifier) - - rls = db.query(Release).filter_by(identifier = nzb_identifier).first() - if not rls: - rls = Release( - identifier = nzb_identifier, - movie_id = movie.get('id'), - quality_id = quality_type.get('quality_id'), - status_id = available_status.get('id') - ) - db.add(rls) - else: - [db.delete(old_info) for old_info in rls.info] - rls.last_edit = int(time.time()) - - db.commit() - - for info in nzb: - try: - if not isinstance(nzb[info], (str, unicode, int, long, float)): - continue - - rls_info = ReleaseInfo( - identifier = info, - value = toUnicode(nzb[info]) - ) - rls.info.append(rls_info) - except InterfaceError: - log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc())) - - db.commit() - - nzb['status_id'] = rls.status_id - - - for nzb in sorted_results: - if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0): - log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name'])) - continue - - if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]: - log.info('Ignored: %s', nzb['name']) - continue - - if nzb['score'] <= 0: - log.info('Ignored, score to low: %s', nzb['name']) - continue - - downloaded = self.download(data = nzb, movie = movie) - if downloaded is True: - ret = True - break - elif downloaded != 'try_next': - break - - # Remove releases that aren't found anymore - for release in movie.get('releases', []): - if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases: - fireEvent('release.delete', release.get('id'), single = True) - - else: - log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title)) - fireEvent('movie.restatus', movie['id']) - break - - # Break if CP wants to shut down - if self.shuttingDown() or ret: - break - - if len(too_early_to_search) > 0: - log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) - - fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True) - - return ret - - def download(self, data, movie, manual = False): - - # Test to see if any downloaders are enabled for this type - downloader_enabled = fireEvent('download.enabled', manual, data, single = True) - - if downloader_enabled: - - snatched_status = fireEvent('status.get', 'snatched', single = True) - - # Download movie to temp - filedata = None - if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))): - filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) - if filedata == 'try_next': - return filedata - - download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True) - log.debug('Downloader result: %s', download_result) - - if download_result: - try: - # Mark release as snatched - db = get_session() - rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() - if rls: - 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') - - # Save download-id info if returned - if isinstance(download_result, dict): - for key in download_result: - rls_info = ReleaseInfo( - identifier = 'download_%s' % key, - value = toUnicode(download_result.get(key)) - ) - rls.info.append(rls_info) - db.commit() - - log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label) - snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) - log.info(snatch_message) - fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict()) - - # 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()) - - return True - - log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('type', ''))) - - return False - - def getSearchTypes(self): - - download_types = fireEvent('download.enabled_types', merge = True) - provider_types = fireEvent('provider.enabled_types', merge = True) - - if download_types and len(list(set(provider_types) & set(download_types))) == 0: - log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_types)) - raise NoProviders - - for useless_provider in list(set(provider_types) - set(download_types)): - log.debug('Provider for "%s" enabled, but no downloader.', useless_provider) - - search_types = download_types - - if len(search_types) == 0: - log.error('There aren\'t any downloaders enabled. Please pick one in settings.') - raise NoDownloaders - - return search_types - - def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs): - - imdb_results = kwargs.get('imdb_results', False) - retention = Env.setting('retention', section = 'nzb') - - if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0): - log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name'])) - return False - - movie_name = getTitle(movie['library']) - movie_words = re.split('\W+', simplifyString(movie_name)) - nzb_name = simplifyString(nzb['name']) - nzb_words = re.split('\W+', nzb_name) - - # Make sure it has required words - required_words = splitString(self.conf('required_words').lower()) - try: required_words = list(set(required_words + splitString(movie['category']['required'].lower()))) - except: pass - - req_match = 0 - for req_set in required_words: - req = splitString(req_set, '&') - req_match += len(list(set(nzb_words) & set(req))) == len(req) - - if len(required_words) > 0 and req_match == 0: - log.info2('Wrong: Required word missing: %s', nzb['name']) - return False - - # Ignore releases - ignored_words = splitString(self.conf('ignored_words').lower()) - try: ignored_words = list(set(ignored_words + splitString(movie['category']['ignored'].lower()))) - except: pass - - ignored_match = 0 - for ignored_set in ignored_words: - ignored = splitString(ignored_set, '&') - ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored) - - if len(ignored_words) > 0 and ignored_match: - log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name'])) - return False - - # Ignore porn stuff - pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic'] - pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words)) - if pron_words: - log.info('Wrong: %s, probably pr0n', (nzb['name'])) - return False - - preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True) - - # Contains lower quality string - if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality): - log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label'])) - return False - - - # File to small - if nzb['size'] and preferred_quality['size_min'] > nzb['size']: - log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min'])) - return False - - # File to large - if nzb['size'] and preferred_quality.get('size_max') < nzb['size']: - log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max'])) - return False - - - # Provider specific functions - get_more = nzb.get('get_more_info') - if get_more: - get_more(nzb) - - extra_check = nzb.get('extra_check') - if extra_check and not extra_check(nzb): - return False - - - if imdb_results: - return True - - # Check if nzb contains imdb link - if self.checkIMDB([nzb.get('description', '')], movie['library']['identifier']): - return True - - for raw_title in movie['library']['titles']: - for movie_title in possibleTitles(raw_title['title']): - movie_words = re.split('\W+', simplifyString(movie_title)) - - if self.correctName(nzb['name'], movie_title): - # if no IMDB link, at least check year range 1 - if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1): - return True - - # if no IMDB link, at least check year - if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0): - return True - - log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year'])) - return False - - def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}): - - name = nzb['name'] - size = nzb.get('size', 0) - nzb_words = re.split('\W+', simplifyString(name)) - - qualities = fireEvent('quality.all', single = True) - - found = {} - for quality in qualities: - # Main in words - if quality['identifier'] in nzb_words: - found[quality['identifier']] = True - - # Alt in words - if list(set(nzb_words) & set(quality['alternative'])): - found[quality['identifier']] = True - - # Try guessing via quality tags - guess = fireEvent('quality.guess', [nzb.get('name')], single = True) - if guess: - found[guess['identifier']] = True - - # Hack for older movies that don't contain quality tag - year_name = fireEvent('scanner.name_year', name, single = True) - if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): - if size > 3000: # Assume dvdr - log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size)) - found['dvdr'] = True - else: # Assume dvdrip - log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size)) - found['dvdrip'] = True - - # Allow other qualities - for allowed in preferred_quality.get('allow'): - if found.get(allowed): - del found[allowed] - - return not (found.get(preferred_quality['identifier']) and len(found) == 1) - - def checkIMDB(self, haystack, imdbId): - - for string in haystack: - if 'imdb.com/title/' + imdbId in string: - return True - - return False - - def correctYear(self, haystack, year, year_range): - - for string in haystack: - - year_name = fireEvent('scanner.name_year', string, single = True) - - if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)): - log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year)) - return True - - log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year)) - return False - - def correctName(self, check_name, movie_name): - - check_names = [check_name] - - # Match names between " - try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0)) - except: pass - - # Match longest name between [] - try: check_names.append(max(check_name.split('['), key = len)) - except: pass - - for check_name in list(set(check_names)): - check_movie = fireEvent('scanner.name_year', check_name, single = True) - - try: - check_words = filter(None, re.split('\W+', check_movie.get('name', ''))) - movie_words = filter(None, re.split('\W+', simplifyString(movie_name))) - - if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0: - return True - except: - pass - - return False - - def couldBeReleased(self, is_pre_release, dates, year = None): - - now = int(time.time()) - now_year = date.today().year - - if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)): - return True - else: - - # For movies before 1972 - if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0: - return True - - if is_pre_release: - # Prerelease 1 week before theaters - if dates.get('theater') - 604800 < now: - return True - else: - # 12 weeks after theater release - if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now: - return True - - if dates.get('dvd') > 0: - - # 4 weeks before dvd release - if dates.get('dvd') - 2419200 < now: - return True - - # Dvd should be released - if dates.get('dvd') < now: - return True - - - return False - - def tryNextReleaseView(self, id = None, **kwargs): - - trynext = self.tryNextRelease(id) - - return { - 'success': trynext - } - - def tryNextRelease(self, movie_id, manual = False): - - snatched_status, ignored_status = fireEvent('status.get', ['snatched', 'ignored'], single = True) - - try: - db = get_session() - rels = db.query(Release).filter_by( - status_id = snatched_status.get('id'), - movie_id = movie_id - ).all() - - for rel in rels: - rel.status_id = ignored_status.get('id') - db.commit() - - movie_dict = fireEvent('movie.get', movie_id, single = True) - log.info('Trying next release for: %s', getTitle(movie_dict['library'])) - fireEvent('searcher.single', movie_dict) - - return True - - except: - log.error('Failed searching for next release: %s', traceback.format_exc()) - return False - -class SearchSetupError(Exception): - pass - -class NoDownloaders(SearchSetupError): - pass - -class NoProviders(SearchSetupError): - pass diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index cb7b16d..d7ac7d1 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -257,7 +257,7 @@ class ResultList(list): new_result = self.fillResult(result) - is_correct_movie = fireEvent('searcher.correct_movie', + is_correct_movie = fireEvent('movie.searcher.correct_movie', nzb = new_result, movie = self.movie, quality = self.quality, imdb_results = self.kwargs.get('imdb_results', False), single = True) diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index eabd146..41bd4bc 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -40,7 +40,7 @@ Page.Wanted = new Class({ if(!self.search_in_progress){ - Api.request('searcher.full_search'); + Api.request('movie.searcher.full_search'); self.startProgressInterval(); } @@ -53,7 +53,7 @@ Page.Wanted = new Class({ var start_text = self.manual_search.get('text'); self.progress_interval = setInterval(function(){ if(self.search_progress && self.search_progress.running) return; - self.search_progress = Api.request('searcher.progress', { + self.search_progress = Api.request('movie.searcher.progress', { 'onComplete': function(json){ self.search_in_progress = true; if(!json.progress){ From f4d792079b91ba10f1ae1b4104cd286e123bdc9b Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 15 Aug 2013 23:46:55 +0200 Subject: [PATCH 3/5] Give moviesearcher a unique name --- couchpotato/core/media/movie/searcher/__init__.py | 4 ++-- couchpotato/core/media/movie/searcher/main.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/core/media/movie/searcher/__init__.py b/couchpotato/core/media/movie/searcher/__init__.py index 2962caa..791bff2 100644 --- a/couchpotato/core/media/movie/searcher/__init__.py +++ b/couchpotato/core/media/movie/searcher/__init__.py @@ -1,8 +1,8 @@ -from .main import Searcher +from .main import MovieSearcher import random def start(): - return Searcher() + return MovieSearcher() config = [{ 'name': 'searcher', diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index dbfd679..ced3bcc 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -19,7 +19,7 @@ import traceback log = CPLog(__name__) -class Searcher(Plugin): +class MovieSearcher(Plugin): in_progress = False From c8d79cde211f565e585d9327543940263a60e853 Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 15 Aug 2013 23:47:07 +0200 Subject: [PATCH 4/5] Add base for TV media type --- couchpotato/core/media/tv/__init__.py | 0 couchpotato/core/media/tv/_base/__init__.py | 6 ++++++ couchpotato/core/media/tv/_base/main.py | 13 +++++++++++++ couchpotato/core/media/tv/searcher/__init__.py | 7 +++++++ couchpotato/core/media/tv/searcher/main.py | 12 ++++++++++++ 5 files changed, 38 insertions(+) create mode 100644 couchpotato/core/media/tv/__init__.py create mode 100644 couchpotato/core/media/tv/_base/__init__.py create mode 100644 couchpotato/core/media/tv/_base/main.py create mode 100644 couchpotato/core/media/tv/searcher/__init__.py create mode 100644 couchpotato/core/media/tv/searcher/main.py diff --git a/couchpotato/core/media/tv/__init__.py b/couchpotato/core/media/tv/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/couchpotato/core/media/tv/_base/__init__.py b/couchpotato/core/media/tv/_base/__init__.py new file mode 100644 index 0000000..91d3708 --- /dev/null +++ b/couchpotato/core/media/tv/_base/__init__.py @@ -0,0 +1,6 @@ +from .main import TVBase + +def start(): + return TVBase() + +config = [] diff --git a/couchpotato/core/media/tv/_base/main.py b/couchpotato/core/media/tv/_base/main.py new file mode 100644 index 0000000..1436371 --- /dev/null +++ b/couchpotato/core/media/tv/_base/main.py @@ -0,0 +1,13 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.media import MediaBase + +log = CPLog(__name__) + + +class TVBase(MediaBase): + + identifier = 'tv' + + def __init__(self): + super(TVBase, self).__init__() + diff --git a/couchpotato/core/media/tv/searcher/__init__.py b/couchpotato/core/media/tv/searcher/__init__.py new file mode 100644 index 0000000..bf5dbc2 --- /dev/null +++ b/couchpotato/core/media/tv/searcher/__init__.py @@ -0,0 +1,7 @@ +from .main import TVSearcher +import random + +def start(): + return TVSearcher() + +config = [] diff --git a/couchpotato/core/media/tv/searcher/main.py b/couchpotato/core/media/tv/searcher/main.py new file mode 100644 index 0000000..07f8963 --- /dev/null +++ b/couchpotato/core/media/tv/searcher/main.py @@ -0,0 +1,12 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin + +log = CPLog(__name__) + + +class TVSearcher(Plugin): + + in_progress = False + + def __init__(self): + pass From 4d7c38d6dba5009dd17647faa06207edd3899d6d Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 15 Aug 2013 23:51:42 +0200 Subject: [PATCH 5/5] Cleanup console log --- couchpotato/core/plugins/category/static/category.js | 1 - 1 file changed, 1 deletion(-) diff --git a/couchpotato/core/plugins/category/static/category.js b/couchpotato/core/plugins/category/static/category.js index 38e433c..29a5a45 100644 --- a/couchpotato/core/plugins/category/static/category.js +++ b/couchpotato/core/plugins/category/static/category.js @@ -37,7 +37,6 @@ var CategoryListBase = new Class({ self.settings.addEvent('create', function(){ var renamer_group = self.settings.tabs.renamer.groups.renamer; - p(renamer_group.getElement('.renamer_to')) self.categories.each(function(category){ var input = new Option.Directory('section_name', 'option.name', category.get('destination'), {