diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 9e24d914..b99aacc9 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -49,13 +49,13 @@ class Downloader(Provider): return [] - def _download(self, data = None, movie = None, manual = False, filedata = None): - if not movie: movie = {} + def _download(self, data = None, media = None, manual = False, filedata = None): + if not media: media = {} if not data: data = {} if self.isDisabled(manual, data): return - return self.download(data = data, movie = movie, filedata = filedata) + return self.download(data = data, media = media, filedata = filedata) def _getAllDownloadStatus(self): if self.isDisabled(manual = True, data = {}): diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py index 854860cd..93740515 100644 --- a/couchpotato/core/downloaders/blackhole/main.py +++ b/couchpotato/core/downloaders/blackhole/main.py @@ -12,8 +12,8 @@ class Blackhole(Downloader): protocol = ['nzb', 'torrent', 'torrent_magnet'] - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} directory = self.conf('directory') @@ -33,7 +33,7 @@ class Blackhole(Downloader): log.error('No nzb/torrent available: %s', data.get('url')) return False - file_name = self.createFileName(data, filedata, movie) + file_name = self.createFileName(data, filedata, media) full_path = os.path.join(directory, file_name) if self.conf('create_subdir'): diff --git a/couchpotato/core/downloaders/deluge/main.py b/couchpotato/core/downloaders/deluge/main.py index f3a1238f..454a501b 100644 --- a/couchpotato/core/downloaders/deluge/main.py +++ b/couchpotato/core/downloaders/deluge/main.py @@ -32,7 +32,10 @@ class Deluge(Downloader): return self.drpc - def download(self, data, movie, filedata = None): + def download(self, data = None, media = None, filedata = None): + if not media: media = {} + if not data: data = {} + log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol'))) if not self.connect(): @@ -73,7 +76,7 @@ class Deluge(Downloader): if data.get('protocol') == 'torrent_magnet': remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options) else: - filename = self.createFileName(data, filedata, movie) + filename = self.createFileName(data, filedata, media) remote_torrent = self.drpc.add_torrent_file(filename, filedata, options) if not remote_torrent: diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index f8506134..1cc2648b 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -19,8 +19,8 @@ class NZBGet(Downloader): url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc' - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} if not filedata: @@ -30,7 +30,7 @@ class NZBGet(Downloader): log.info('Sending "%s" to NZBGet.', data.get('name')) url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} - nzb_name = ss('%s.nzb' % self.createNzbName(data, movie)) + nzb_name = ss('%s.nzb' % self.createNzbName(data, media)) rpc = xmlrpclib.ServerProxy(url) try: diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py index f4e233be..9e160486 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/couchpotato/core/downloaders/nzbvortex/main.py @@ -23,13 +23,13 @@ class NZBVortex(Downloader): api_level = None session_id = None - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} # Send the nzb try: - nzb_filename = self.createFileName(data, filedata, movie) + nzb_filename = self.createFileName(data, filedata, media) self.call('nzb/add', params = {'file': (nzb_filename, filedata)}, multipart = True) raw_statuses = self.call('nzb') diff --git a/couchpotato/core/downloaders/pneumatic/main.py b/couchpotato/core/downloaders/pneumatic/main.py index 643350e1..0c2c46d1 100644 --- a/couchpotato/core/downloaders/pneumatic/main.py +++ b/couchpotato/core/downloaders/pneumatic/main.py @@ -12,8 +12,8 @@ class Pneumatic(Downloader): protocol = ['nzb'] strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s' - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} directory = self.conf('directory') @@ -25,7 +25,7 @@ class Pneumatic(Downloader): log.error('No nzb available!') return False - fullPath = os.path.join(directory, self.createFileName(data, filedata, movie)) + fullPath = os.path.join(directory, self.createFileName(data, filedata, media)) try: if not os.path.isfile(fullPath): @@ -33,7 +33,7 @@ class Pneumatic(Downloader): with open(fullPath, 'wb') as f: f.write(filedata) - nzb_name = self.createNzbName(data, movie) + nzb_name = self.createNzbName(data, media) strm_path = os.path.join(directory, nzb_name) strm_file = open(strm_path + '.strm', 'wb') diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py index 8381f0a2..0281c49f 100755 --- a/couchpotato/core/downloaders/rtorrent/main.py +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -77,7 +77,10 @@ class rTorrent(Downloader): return True - def download(self, data, movie, filedata = None): + def download(self, data = None, media = None, filedata = None): + if not media: media = {} + if not data: data = {} + log.debug('Sending "%s" to rTorrent.', (data.get('name'))) if not self.connect(): diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index aba21231..d934e6c9 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -16,8 +16,8 @@ class Sabnzbd(Downloader): protocol = ['nzb'] - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} log.info('Sending "%s" to SABnzbd.', data.get('name')) @@ -25,7 +25,7 @@ class Sabnzbd(Downloader): req_params = { 'cat': self.conf('category'), 'mode': 'addurl', - 'nzbname': self.createNzbName(data, movie), + 'nzbname': self.createNzbName(data, media), 'priority': self.conf('priority'), } @@ -36,7 +36,7 @@ class Sabnzbd(Downloader): return False # If it's a .rar, it adds the .rar extension, otherwise it stays .nzb - nzb_filename = self.createFileName(data, filedata, movie) + nzb_filename = self.createFileName(data, filedata, media) req_params['mode'] = 'addfile' else: req_params['name'] = data.get('url') diff --git a/couchpotato/core/downloaders/synology/main.py b/couchpotato/core/downloaders/synology/main.py index 0721085c..79b8e87a 100644 --- a/couchpotato/core/downloaders/synology/main.py +++ b/couchpotato/core/downloaders/synology/main.py @@ -13,8 +13,8 @@ class Synology(Downloader): protocol = ['nzb', 'torrent', 'torrent_magnet'] log = CPLog(__name__) - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} response = False diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 2eabb2e8..c5a55d4b 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -31,7 +31,9 @@ class Transmission(Downloader): return self.trpc - def download(self, data, movie, filedata = None): + def download(self, data = None, media = None, filedata = None): + if not media: media = {} + if not data: data = {} log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol'))) diff --git a/couchpotato/core/downloaders/utorrent/__init__.py b/couchpotato/core/downloaders/utorrent/__init__.py index 0c4c323c..d45e2e6c 100644 --- a/couchpotato/core/downloaders/utorrent/__init__.py +++ b/couchpotato/core/downloaders/utorrent/__init__.py @@ -36,11 +36,6 @@ config = [{ 'name': 'label', 'description': 'Label to add torrent as.', }, - { - 'name': 'directory', - 'type': 'directory', - 'description': 'Download to this directory. Keep empty for default uTorrent download directory.', - }, { 'name': 'remove_complete', 'label': 'Remove torrent', diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index e05d1043..18b607f0 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -36,8 +36,8 @@ class uTorrent(Downloader): return self.utorrent_api - def download(self, data = None, movie = None, filedata = None): - if not movie: movie = {} + def download(self, data = None, media = None, filedata = None): + if not media: media = {} if not data: data = {} log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('protocol'))) @@ -78,7 +78,7 @@ class uTorrent(Downloader): info = bdecode(filedata)["info"] torrent_hash = sha1(benc(info)).hexdigest().upper() - torrent_filename = self.createFileName(data, filedata, movie) + torrent_filename = self.createFileName(data, filedata, media) if data.get('seed_ratio'): torrent_params['seed_override'] = 1 @@ -92,17 +92,11 @@ class uTorrent(Downloader): if len(torrent_hash) == 32: torrent_hash = b16encode(b32decode(torrent_hash)) - # Set download directory - if self.conf('directory'): - directory = self.conf('directory') - else: - directory = False - # Send request to uTorrent if data.get('protocol') == 'torrent_magnet': - self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'), directory) + self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url')) else: - self.utorrent_api.add_torrent_file(torrent_filename, filedata, directory) + self.utorrent_api.add_torrent_file(torrent_filename, filedata) # Change settings of added torrent self.utorrent_api.set_torrent(torrent_hash, torrent_params) @@ -256,13 +250,13 @@ class uTorrentAPI(object): def add_torrent_uri(self, filename, torrent, add_folder = False): action = "action=add-url&s=%s" % urllib.quote(torrent) if add_folder: - action += "&path=%s" % urllib.quote(add_folder) + action += "&path=%s" % urllib.quote(filename) return self._request(action) def add_torrent_file(self, filename, filedata, add_folder = False): action = "action=add-file" if add_folder: - action += "&path=%s" % urllib.quote(add_folder) + action += "&path=%s" % urllib.quote(filename) return self._request(action, {"torrent_file": (ss(filename), filedata)}) def set_torrent(self, hash, params): diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 68ae5314..86d5a4bc 100644 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -1,10 +1,15 @@ from couchpotato import get_session from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent -from couchpotato.core.helpers.variable import splitString +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase -from couchpotato.core.settings.model import Media +from couchpotato.core.settings.model import Library, LibraryTitle, Release, \ + Media +from sqlalchemy.orm import joinedload_all +from sqlalchemy.sql.expression import or_, asc, not_, desc +from string import ascii_lowercase log = CPLog(__name__) @@ -20,7 +25,49 @@ class MediaPlugin(MediaBase): } }) - addEvent('app.load', self.addSingleRefresh) + addApiView('media.list', self.listView, docs = { + 'desc': 'List media', + 'params': { + 'type': {'type': 'string', 'desc': 'Media type to filter on.'}, + '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, + 'media': array, media found, +}"""} + }) + + addApiView('media.get', self.getView, docs = { + 'desc': 'Get media by id', + 'params': { + 'id': {'desc': 'The id of the media'}, + } + }) + + addApiView('media.delete', self.deleteView, docs = { + 'desc': 'Delete a media from the wanted list', + 'params': { + 'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'}, + 'delete_from': {'desc': 'Delete media from this page', 'type': 'string: all (default), wanted, manage'}, + } + }) + + addApiView('media.available_chars', self.charView) + + addEvent('app.load', self.addSingleRefreshView) + addEvent('app.load', self.addSingleListView) + addEvent('app.load', self.addSingleCharView) + addEvent('app.load', self.addSingleDeleteView) + + addEvent('media.get', self.get) + addEvent('media.list', self.list) + addEvent('media.delete', self.delete) + addEvent('media.restatus', self.restatus) def refresh(self, id = '', **kwargs): db = get_session() @@ -43,7 +90,369 @@ class MediaPlugin(MediaBase): 'success': True, } - def addSingleRefresh(self): + def addSingleRefreshView(self): for media_type in fireEvent('media.types', merge = True): addApiView('%s.refresh' % media_type, self.refresh) + + def get(self, media_id): + + db = get_session() + + imdb_id = getImdb(str(media_id)) + + if imdb_id: + m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first() + else: + m = db.query(Media).filter_by(id = media_id).first() + + results = None + if m: + results = m.to_dict(self.default_dict) + + db.expire_all() + return results + + def getView(self, id = None, **kwargs): + + media = self.get(id) if id else None + + return { + 'success': media is not None, + 'media': media, + } + + def list(self, types = None, 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] + if types and not isinstance(types, (list, tuple)): + types = [types] + + # query movie ids + q = db.query(Media) \ + .with_entities(Media.id) \ + .group_by(Media.id) + + # Filter on movie status + if status and len(status) > 0: + statuses = fireEvent('status.get', status, single = len(status) > 1) + statuses = [s.get('id') for s in statuses] + + q = q.filter(Media.status_id.in_(statuses)) + + # Filter on release status + if release_status and len(release_status) > 0: + q = q.join(Media.releases) + + statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) + statuses = [s.get('id') for s in statuses] + + q = q.filter(Release.status_id.in_(statuses)) + + # Filter on type + if types and len(types) > 0: + try: q = q.filter(Media.type.in_(types)) + except: pass + + # Only join when searching / ordering + if starts_with or search or order != 'release_order': + q = q.join(Media.library, Library.titles) \ + .filter(LibraryTitle.default == True) + + # Add search filters + 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 len(filter_or) > 0: + q = q.filter(or_(*filter_or)) + + total_count = q.count() + if total_count == 0: + return 0, [] + + if order == 'release_order': + q = q.order_by(desc(Release.last_edit)) + else: + q = q.order_by(asc(LibraryTitle.simple_title)) + + 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] + q = q.limit(limit).offset(offset) + + # Get all media_ids in sorted order + media_ids = [m.id for m in q.all()] + + # List release statuses + releases = db.query(Release) \ + .filter(Release.movie_id.in_(media_ids)) \ + .all() + + release_statuses = dict((m, set()) for m in media_ids) + releases_count = dict((m, 0) for m in media_ids) + for release in releases: + release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id)) + releases_count[release.movie_id] += 1 + + # Get main movie data + q2 = db.query(Media) \ + .options(joinedload_all('library.titles')) \ + .options(joinedload_all('library.files')) \ + .options(joinedload_all('status')) \ + .options(joinedload_all('files')) + + q2 = q2.filter(Media.id.in_(media_ids)) + + results = q2.all() + + # Create dict by movie id + movie_dict = {} + for movie in results: + movie_dict[movie.id] = movie + + # List movies based on media_ids order + movies = [] + for media_id in media_ids: + + releases = [] + for r in release_statuses.get(media_id): + x = splitString(r) + releases.append({'status_id': x[0], 'quality_id': x[1]}) + + # Merge releases with movie dict + movies.append(mergeDicts(movie_dict[media_id].to_dict({ + 'library': {'titles': {}, 'files':{}}, + 'files': {}, + }), { + 'releases': releases, + 'releases_count': releases_count.get(media_id), + })) + + db.expire_all() + return total_count, movies + + def listView(self, **kwargs): + + types = splitString(kwargs.get('types')) + status = splitString(kwargs.get('status')) + release_status = splitString(kwargs.get('release_status')) + limit_offset = kwargs.get('limit_offset') + starts_with = kwargs.get('starts_with') + search = kwargs.get('search') + order = kwargs.get('order') + + total_movies, movies = self.list( + types = types, + 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 addSingleListView(self): + + for media_type in fireEvent('media.types', merge = True): + def tempList(*args, **kwargs): + return self.listView(types = media_type, *args, **kwargs) + addApiView('%s.list' % media_type, tempList) + + def availableChars(self, types = None, status = None, release_status = None): + + types = types or [] + status = status or [] + release_status = release_status or [] + + 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] + if types and not isinstance(types, (list, tuple)): + types = [types] + + q = db.query(Media) + + # Filter on movie status + if status and len(status) > 0: + statuses = fireEvent('status.get', status, single = len(release_status) > 1) + statuses = [s.get('id') for s in statuses] + + q = q.filter(Media.status_id.in_(statuses)) + + # Filter on release status + if release_status and len(release_status) > 0: + + statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) + statuses = [s.get('id') for s in statuses] + + q = q.join(Media.releases) \ + .filter(Release.status_id.in_(statuses)) + + # Filter on type + if types and len(types) > 0: + try: q = q.filter(Media.type.in_(types)) + except: pass + + q = q.join(Library, LibraryTitle) \ + .with_entities(LibraryTitle.simple_title) \ + .filter(LibraryTitle.default == True) + + titles = q.all() + + chars = set() + for title in titles: + try: + char = title[0][0] + char = char if char in ascii_lowercase else '#' + chars.add(str(char)) + except: + log.error('Failed getting title for %s', title.libraries_id) + + if len(chars) == 25: + break + + db.expire_all() + return ''.join(sorted(chars)) + + def charView(self, **kwargs): + + type = splitString(kwargs.get('type', 'movie')) + status = splitString(kwargs.get('status', None)) + release_status = splitString(kwargs.get('release_status', None)) + chars = self.availableChars(type, status, release_status) + + return { + 'success': True, + 'empty': len(chars) == 0, + 'chars': chars, + } + + def addSingleCharView(self): + + for media_type in fireEvent('media.types', merge = True): + def tempChar(*args, **kwargs): + return self.charView(types = media_type, *args, **kwargs) + addApiView('%s.available_chars' % media_type, tempChar) + + def delete(self, media_id, delete_from = None): + + db = get_session() + + media = db.query(Media).filter_by(id = media_id).first() + if media: + deleted = False + if delete_from == 'all': + db.delete(media) + db.commit() + deleted = True + else: + done_status = fireEvent('status.get', 'done', single = True) + + total_releases = len(media.releases) + total_deleted = 0 + new_movie_status = None + for release in media.releases: + if delete_from in ['wanted', 'snatched', 'late']: + 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(media) + db.commit() + deleted = True + elif new_movie_status: + new_status = fireEvent('status.get', new_movie_status, single = True) + media.profile_id = None + media.status_id = new_status.get('id') + db.commit() + else: + fireEvent('media.restatus', media.id, single = True) + + if deleted: + fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict()) + + db.expire_all() + return True + + def deleteView(self, id = '', **kwargs): + + ids = splitString(id) + for media_id in ids: + self.delete(media_id, delete_from = kwargs.get('delete_from', 'all')) + + return { + 'success': True, + } + + def addSingleDeleteView(self): + + for media_type in fireEvent('media.types', merge = True): + def tempDelete(*args, **kwargs): + return self.deleteView(types = media_type, *args, **kwargs) + addApiView('%s.delete' % media_type, tempDelete) + + def restatus(self, media_id): + + active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) + + db = get_session() + + m = db.query(Media).filter_by(id = media_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 + diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 6745c5a2..aa8c5130 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -2,15 +2,10 @@ 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 -from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \ - mergeDicts +from couchpotato.core.helpers.variable import splitString, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.media.movie import MovieTypeBase -from couchpotato.core.settings.model import Library, LibraryTitle, Media, \ - Release -from sqlalchemy.orm import joinedload_all -from sqlalchemy.sql.expression import or_, asc, not_, desc -from string import ascii_lowercase +from couchpotato.core.settings.model import Media import time log = CPLog(__name__) @@ -26,28 +21,6 @@ class MovieBase(MovieTypeBase): super(MovieBase, self).__init__() self.initType() - 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.available_chars', self.charView) addApiView('movie.add', self.addView, docs = { 'desc': 'Add new movie to the wanted list', 'params': { @@ -65,255 +38,8 @@ class MovieBase(MovieTypeBase): '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) - - 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(Media).filter(Media.library.has(identifier = imdb_id)).first() - else: - m = db.query(Media).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] - - # query movie ids - q = db.query(Media) \ - .with_entities(Media.id) \ - .group_by(Media.id) - - # Filter on movie status - if status and len(status) > 0: - statuses = fireEvent('status.get', status, single = len(status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.filter(Media.status_id.in_(statuses)) - - # Filter on release status - if release_status and len(release_status) > 0: - q = q.join(Media.releases) - - statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.filter(Release.status_id.in_(statuses)) - - # Only join when searching / ordering - if starts_with or search or order != 'release_order': - q = q.join(Media.library, Library.titles) \ - .filter(LibraryTitle.default == True) - - # Add search filters - 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 len(filter_or) > 0: - q = q.filter(or_(*filter_or)) - - total_count = q.count() - if total_count == 0: - return 0, [] - - if order == 'release_order': - q = q.order_by(desc(Release.last_edit)) - else: - q = q.order_by(asc(LibraryTitle.simple_title)) - - 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] - q = q.limit(limit).offset(offset) - - # Get all movie_ids in sorted order - movie_ids = [m.id for m in q.all()] - - # List release statuses - releases = db.query(Release) \ - .filter(Release.media_id.in_(movie_ids)) \ - .all() - - release_statuses = dict((m, set()) for m in movie_ids) - releases_count = dict((m, 0) for m in movie_ids) - for release in releases: - release_statuses[release.media_id].add('%d,%d' % (release.status_id, release.quality_id)) - releases_count[release.media_id] += 1 - - # Get main movie data - q2 = db.query(Media) \ - .options(joinedload_all('library.titles')) \ - .options(joinedload_all('library.files')) \ - .options(joinedload_all('status')) \ - .options(joinedload_all('files')) - - q2 = q2.filter(Media.id.in_(movie_ids)) - - results = q2.all() - - # Create dict by movie id - movie_dict = {} - for movie in results: - movie_dict[movie.id] = movie - - # List movies based on movie_ids order - movies = [] - for movie_id in movie_ids: - - releases = [] - for r in release_statuses.get(movie_id): - x = splitString(r) - releases.append({'status_id': x[0], 'quality_id': x[1]}) - - # Merge releases with movie dict - movies.append(mergeDicts(movie_dict[movie_id].to_dict({ - 'library': {'titles': {}, 'files':{}}, - 'files': {}, - }), { - 'releases': releases, - 'releases_count': releases_count.get(movie_id), - })) - - db.expire_all() - return total_count, movies - - def availableChars(self, status = None, release_status = None): - - status = status or [] - release_status = release_status or [] - - 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(Media) - - # Filter on movie status - if status and len(status) > 0: - statuses = fireEvent('status.get', status, single = len(release_status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.filter(Media.status_id.in_(statuses)) - - # Filter on release status - if release_status and len(release_status) > 0: - - statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) - statuses = [s.get('id') for s in statuses] - - q = q.join(Media.releases) \ - .filter(Release.status_id.in_(statuses)) - - q = q.join(Library, LibraryTitle) \ - .with_entities(LibraryTitle.simple_title) \ - .filter(LibraryTitle.default == True) - - titles = q.all() - - chars = set() - for title in titles: - try: - char = title[0][0] - char = char if char in ascii_lowercase else '#' - chars.add(str(char)) - except: - log.error('Failed getting title for %s', title.libraries_id) - - if len(chars) == 25: - break - - db.expire_all() - return ''.join(sorted(chars)) - - def listView(self, **kwargs): - - status = splitString(kwargs.get('status')) - release_status = splitString(kwargs.get('release_status')) - limit_offset = kwargs.get('limit_offset') - starts_with = kwargs.get('starts_with') - search = kwargs.get('search') - order = kwargs.get('order') - - 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 add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None): if not params: params = {} @@ -447,7 +173,7 @@ class MovieBase(MovieTypeBase): db.commit() - fireEvent('movie.restatus', m.id) + fireEvent('media.restatus', m.id) movie_dict = m.to_dict(self.default_dict) fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id)) @@ -456,89 +182,3 @@ class MovieBase(MovieTypeBase): 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(Media).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', 'late']: - 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(Media).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 diff --git a/couchpotato/core/media/movie/_base/static/list.js b/couchpotato/core/media/movie/_base/static/list.js index db598b20..85dee2e5 100644 --- a/couchpotato/core/media/movie/_base/static/list.js +++ b/couchpotato/core/media/movie/_base/static/list.js @@ -281,7 +281,7 @@ var MovieList = new Class({ // Get available chars and highlight if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible())) - Api.request('movie.available_chars', { + Api.request('media.available_chars', { 'data': Object.merge({ 'status': self.options.status }, self.filter), @@ -372,7 +372,7 @@ var MovieList = new Class({ 'click': function(e){ (e).preventDefault(); this.set('text', 'Deleting..') - Api.request('movie.delete', { + Api.request('media.delete', { 'data': { 'id': ids.join(','), 'delete_from': self.options.identifier @@ -550,8 +550,9 @@ var MovieList = new Class({ } - Api.request(self.options.api_call || 'movie.list', { + Api.request(self.options.api_call || 'media.list', { 'data': Object.merge({ + 'type': 'movie', 'status': self.options.status, 'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null }, self.filter), diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index e3591f34..22a55c08 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -431,7 +431,7 @@ MA.Release = new Class({ markMovieDone: function(){ var self = this; - Api.request('movie.delete', { + Api.request('media.delete', { 'data': { 'id': self.movie.get('id'), 'delete_from': 'wanted' @@ -821,7 +821,7 @@ MA.Delete = new Class({ self.callChain(); }, function(){ - Api.request('movie.delete', { + Api.request('media.delete', { 'data': { 'id': self.movie.get('id'), 'delete_from': self.movie.list.options.identifier diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index bc258451..9c0ae015 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -78,9 +78,9 @@ var Movie = new Class({ self.list.checkIfEmpty(); // Remove events - self.global_events.each(function(handle, listener){ + Object.each(self.global_events, function(handle, listener){ App.off(listener, handle); - }) + }); }, busy: function(set_busy, timeout){ diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py index f80f63fe..02b037a4 100644 --- a/couchpotato/core/media/movie/searcher/main.py +++ b/couchpotato/core/media/movie/searcher/main.py @@ -145,7 +145,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): 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) + fireEvent('media.delete', movie['id'], single = True) return fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title) @@ -192,7 +192,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): else: log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title)) - fireEvent('movie.restatus', movie['id']) + fireEvent('media.restatus', movie['id']) break # Break if CP wants to shut down @@ -333,7 +333,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): rel.status_id = ignored_status.get('id') db.commit() - movie_dict = fireEvent('movie.get', movie_id, single = True) + movie_dict = fireEvent('media.get', movie_id, single = True) log.info('Trying next release for: %s', getTitle(movie_dict['library'])) fireEvent('movie.searcher.single', movie_dict, manual = manual) diff --git a/couchpotato/core/plugins/automation/__init__.py b/couchpotato/core/plugins/automation/__init__.py index 440232b2..a81719c4 100644 --- a/couchpotato/core/plugins/automation/__init__.py +++ b/couchpotato/core/plugins/automation/__init__.py @@ -41,7 +41,7 @@ config = [{ 'label': 'Required Genres', 'default': '', 'placeholder': 'Example: Action, Crime & Drama', - 'description': 'Ignore movies that don\'t contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"' + 'description': ('Ignore movies that don\'t contain at least one set of genres.', 'Sets are separated by "," and each word within a set must be separated with "&"') }, { 'name': 'ignored_genres', diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py index 92547cb0..2edcd3be 100644 --- a/couchpotato/core/plugins/automation/main.py +++ b/couchpotato/core/plugins/automation/main.py @@ -43,7 +43,7 @@ class Automation(Plugin): if self.shuttingDown(): break - movie_dict = fireEvent('movie.get', movie_id, single = True) + movie_dict = fireEvent('media.get', movie_id, single = True) fireEvent('movie.searcher.single', movie_dict) - return True \ No newline at end of file + return True diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 649e359d..2bc8fb9e 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -289,19 +289,19 @@ class Plugin(object): Env.get('cache').set(cache_key_md5, value, timeout) return value - def createNzbName(self, data, movie): - tag = self.cpTag(movie) + def createNzbName(self, data, media): + tag = self.cpTag(media) return '%s%s' % (toSafeString(toUnicode(data.get('name'))[:127 - len(tag)]), tag) - def createFileName(self, data, filedata, movie): - name = sp(os.path.join(self.createNzbName(data, movie))) + def createFileName(self, data, filedata, media): + name = sp(os.path.join(self.createNzbName(data, media))) if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '' not in filedata: return '%s.%s' % (name, 'rar') return '%s.%s' % (name, data.get('protocol')) - def cpTag(self, movie): + def cpTag(self, media): if Env.setting('enabled', 'renamer'): - return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else '' + return '.cp(' + media['library'].get('identifier') + ')' if media['library'].get('identifier') else '' return '' diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index 87207615..ebafb8fe 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -112,11 +112,11 @@ class Manage(Plugin): if self.conf('cleanup') and full and not self.shuttingDown(): # Get movies with done status - total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True) + total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', single = True) for done_movie in done_movies: if done_movie['library']['identifier'] not in added_identifiers: - fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all') + fireEvent('media.delete', movie_id = done_movie['id'], delete_from = 'all') else: releases = fireEvent('release.for_movie', id = done_movie.get('id'), single = True) @@ -202,7 +202,7 @@ class Manage(Plugin): self.in_progress[folder]['to_go'] -= 1 total = self.in_progress[folder]['total'] - movie_dict = fireEvent('movie.get', identifier, single = True) + movie_dict = fireEvent('media.get', identifier, single = True) fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library'])) diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 30ae8cd5..8ad79a31 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -142,7 +142,7 @@ class Release(Plugin): except: log.debug('Failed to attach "%s" to release: %s', (added_files, traceback.format_exc())) - fireEvent('movie.restatus', media.id) + fireEvent('media.restatus', media.id) return True @@ -269,7 +269,7 @@ class Release(Plugin): if filedata == 'try_next': return filedata - download_result = fireEvent('download', data = data, movie = media, manual = manual, filedata = filedata, single = True) + download_result = fireEvent('download', data = data, media = media, manual = manual, filedata = filedata, single = True) log.debug('Downloader result: %s', download_result) if download_result: diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index c8f6b37f..8b602cbd 100755 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -93,7 +93,7 @@ config = [{ 'default': 1, 'type': 'int', 'unit': 'min(s)', - 'description': 'Detect movie status every X minutes. Will start the renamer if movie is completed or handle failed download if these options are enabled', + 'description': ('Detect movie status every X minutes.', 'Will start the renamer if movie is completed or handle failed download if these options are enabled'), }, { 'advanced': True, @@ -122,13 +122,13 @@ config = [{ 'advanced': True, 'name': 'separator', 'label': 'File-Separator', - 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', + 'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'), }, { 'advanced': True, 'name': 'foldersep', 'label': 'Folder-Separator', - 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', + 'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'), }, { 'name': 'file_action', @@ -136,7 +136,7 @@ config = [{ 'default': 'link', 'type': 'dropdown', 'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')], - 'description': 'Link or Copy after downloading completed (and allow for seeding), or Move after seeding completed. Link first tries hard link, then sym link and falls back to Copy.', + 'description': ('Link, Copy or Move after download completed.', 'Link first tries hard link, then sym link and falls back to Copy. It is perfered to use link when downloading torrents as it will save you space, while still beeing able to seed.'), 'advanced': True, }, { diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index d95424d8..6ecaf79d 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -820,6 +820,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) try: for rel in rels: rel_dict = rel.to_dict({'info': {}}) + movie_dict = fireEvent('media.get', rel.movie_id, single = True) if not isinstance(rel_dict['info'], (dict)): log.error('Faulty release found without any info, ignoring.') diff --git a/couchpotato/core/plugins/subtitle/__init__.py b/couchpotato/core/plugins/subtitle/__init__.py index bbd40853..fcff4cdf 100644 --- a/couchpotato/core/plugins/subtitle/__init__.py +++ b/couchpotato/core/plugins/subtitle/__init__.py @@ -20,7 +20,7 @@ config = [{ }, { 'name': 'languages', - 'description': 'Comma separated, 2 letter country code. Example: en, nl. See the codes at on Wikipedia', + 'description': ('Comma separated, 2 letter country code.', 'Example: en, nl. See the codes at on Wikipedia'), }, # { # 'name': 'automatic', diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 3cbd382d..75febaa8 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -290,14 +290,14 @@ class ResultList(list): result_ids = None provider = None - movie = None + media = None quality = None - def __init__(self, provider, movie, quality, **kwargs): + def __init__(self, provider, media, quality, **kwargs): self.result_ids = [] self.provider = provider - self.movie = movie + self.media = media self.quality = quality self.kwargs = kwargs @@ -311,13 +311,13 @@ class ResultList(list): new_result = self.fillResult(result) - is_correct = fireEvent('searcher.correct_release', new_result, self.movie, self.quality, + is_correct = fireEvent('searcher.correct_release', new_result, self.media, self.quality, imdb_results = self.kwargs.get('imdb_results', False), single = True) if is_correct and new_result['id'] not in self.result_ids: is_correct_weight = float(is_correct) - new_result['score'] += fireEvent('score.calculate', new_result, self.movie, single = True) + new_result['score'] += fireEvent('score.calculate', new_result, self.media, single = True) old_score = new_result['score'] new_result['score'] = int(old_score * is_correct_weight) diff --git a/couchpotato/core/providers/info/_modifier/main.py b/couchpotato/core/providers/info/_modifier/main.py index 113ce4ca..1335c0b4 100644 --- a/couchpotato/core/providers/info/_modifier/main.py +++ b/couchpotato/core/providers/info/_modifier/main.py @@ -104,11 +104,11 @@ class Movie(ModifierBase): for movie in l.media: if movie.status_id == active_status['id']: - temp['in_wanted'] = fireEvent('movie.get', movie.id, single = True) + temp['in_wanted'] = fireEvent('media.get', movie.id, single = True) for release in movie.releases: if release.status_id == done_status['id']: - temp['in_library'] = fireEvent('movie.get', movie.id, single = True) + temp['in_library'] = fireEvent('media.get', movie.id, single = True) except: log.error('Tried getting more info on searched movies: %s', traceback.format_exc()) diff --git a/couchpotato/core/providers/torrent/torrentpotato/__init__.py b/couchpotato/core/providers/torrent/torrentpotato/__init__.py new file mode 100644 index 00000000..5054f98c --- /dev/null +++ b/couchpotato/core/providers/torrent/torrentpotato/__init__.py @@ -0,0 +1,66 @@ +from .main import TorrentPotato + +def start(): + return TorrentPotato() + +config = [{ + 'name': 'torrentpotato', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'TorrentPotato', + 'order': 10, + 'description': 'CouchPotato torrent provider. Checkout the wiki page about this provider for more info.', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'use', + 'default': '' + }, + { + 'name': 'host', + 'default': '', + 'description': 'The url path of your TorrentPotato provider.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'default': '0', + 'description': 'Starting score for each release found via this provider.', + }, + { + 'name': 'name', + 'label': 'Username', + 'default': '', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'default': '1', + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'default': '40', + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'pass_key', + 'default': ',', + 'label': 'Pass Key', + 'description': 'Can be found on your profile page', + 'type': 'combined', + 'combine': ['use', 'host', 'pass_key', 'name', 'seed_ratio', 'seed_time', 'extra_score'], + }, + ], + }, + ], +}] diff --git a/couchpotato/core/providers/torrent/torrentpotato/main.py b/couchpotato/core/providers/torrent/torrentpotato/main.py new file mode 100644 index 00000000..a76c0c8f --- /dev/null +++ b/couchpotato/core/providers/torrent/torrentpotato/main.py @@ -0,0 +1,129 @@ +from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode +from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.base import ResultList +from couchpotato.core.providers.torrent.base import TorrentProvider +from urlparse import urlparse +import re +import traceback + +log = CPLog(__name__) + + +class TorrentPotato(TorrentProvider): + + urls = {} + limits_reached = {} + + http_time_between_calls = 1 # Seconds + + def search(self, movie, quality): + hosts = self.getHosts() + + results = ResultList(self, movie, quality, imdb_results = True) + + for host in hosts: + if self.isDisabled(host): + continue + + self._searchOnHost(host, movie, quality, results) + + return results + + def _searchOnHost(self, host, movie, quality, results): + + arguments = tryUrlencode({ + 'user': host['name'], + 'passkey': host['pass_key'], + 'imdbid': movie['library']['identifier'] + }) + url = '%s?%s' % (host['host'], arguments) + + torrents = self.getJsonData(url, cache_timeout = 1800) + + if torrents: + try: + if torrents.get('error'): + log.error('%s: %s', (torrents.get('error'), host['host'])) + elif torrents.get('results'): + for torrent in torrents.get('results', []): + results.append({ + 'id': torrent.get('torrent_id'), + 'protocol': 'torrent' if re.match('^(http|https|ftp)://.*$', torrent.get('download_url')) else 'torrent_magnet', + 'provider_extra': urlparse(host['host']).hostname or host['host'], + 'name': toUnicode(torrent.get('release_name')), + 'url': torrent.get('download_url'), + 'detail_url': torrent.get('details_url'), + 'size': torrent.get('size'), + 'score': host['extra_score'], + 'seeders': torrent.get('seeders'), + 'leechers': torrent.get('leechers'), + 'seed_ratio': host['seed_ratio'], + 'seed_time': host['seed_time'], + }) + + except: + log.error('Failed getting results from %s: %s', (host['host'], traceback.format_exc())) + + def getHosts(self): + + uses = splitString(str(self.conf('use')), clean = False) + hosts = splitString(self.conf('host'), clean = False) + names = splitString(self.conf('name'), clean = False) + seed_times = splitString(self.conf('seed_time'), clean = False) + seed_ratios = splitString(self.conf('seed_ratio'), clean = False) + pass_keys = splitString(self.conf('pass_key'), clean = False) + extra_score = splitString(self.conf('extra_score'), clean = False) + + list = [] + for nr in range(len(hosts)): + + try: key = pass_keys[nr] + except: key = '' + + try: host = hosts[nr] + except: host = '' + + try: name = names[nr] + except: name = '' + + try: ratio = seed_ratios[nr] + except: ratio = '' + + try: seed_time = seed_times[nr] + except: seed_time = '' + + list.append({ + 'use': uses[nr], + 'host': host, + 'name': name, + 'seed_ratio': tryFloat(ratio), + 'seed_time': tryInt(seed_time), + 'pass_key': key, + 'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0 + }) + + return list + + def belongsTo(self, url, provider = None, host = None): + + hosts = self.getHosts() + + for host in hosts: + result = super(TorrentPotato, self).belongsTo(url, host = host['host'], provider = provider) + if result: + return result + + def isDisabled(self, host = None): + return not self.isEnabled(host) + + def isEnabled(self, host = None): + + # Return true if at least one is enabled and no host is given + if host is None: + for host in self.getHosts(): + if self.isEnabled(host): + return True + return False + + return TorrentProvider.isEnabled(self) and host['host'] and host['pass_key'] and int(host['use']) diff --git a/couchpotato/static/scripts/library/mootools_more.js b/couchpotato/static/scripts/library/mootools_more.js index d2d70369..d51c4c15 100644 --- a/couchpotato/static/scripts/library/mootools_more.js +++ b/couchpotato/static/scripts/library/mootools_more.js @@ -1,6 +1,6 @@ // MooTools: the javascript framework. -// Load this file's selection again by visiting: http://mootools.net/more/43db227db7a621ebb062ee621432ae3d -// Or build this file again with packager using: packager build More/Events.Pseudos More/Date More/Date.Extras More/Element.Forms More/Element.Position More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical +// Load this file's selection again by visiting: http://mootools.net/more/7a819726f7f5e85fc48bef295ff78dbe +// Or build this file again with packager using: packager build More/Events.Pseudos More/Date More/Date.Extras More/Element.Forms More/Element.Position More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical More/Tips /* --- @@ -3161,3 +3161,264 @@ Request.implement({ }); + +/* +--- + +script: Tips.js + +name: Tips + +description: Class for creating nice tips that follow the mouse cursor when hovering an element. + +license: MIT-style license + +authors: + - Valerio Proietti + - Christoph Pojer + - Luis Merino + +requires: + - Core/Options + - Core/Events + - Core/Element.Event + - Core/Element.Style + - Core/Element.Dimensions + - /MooTools.More + +provides: [Tips] + +... +*/ + +(function(){ + +var read = function(option, element){ + return (option) ? (typeOf(option) == 'function' ? option(element) : element.get(option)) : ''; +}; + +this.Tips = new Class({ + + Implements: [Events, Options], + + options: {/* + id: null, + onAttach: function(element){}, + onDetach: function(element){}, + onBound: function(coords){},*/ + onShow: function(){ + this.tip.setStyle('display', 'block'); + }, + onHide: function(){ + this.tip.setStyle('display', 'none'); + }, + title: 'title', + text: function(element){ + return element.get('rel') || element.get('href'); + }, + showDelay: 100, + hideDelay: 100, + className: 'tip-wrap', + offset: {x: 16, y: 16}, + windowPadding: {x:0, y:0}, + fixed: false, + waiAria: true + }, + + initialize: function(){ + var params = Array.link(arguments, { + options: Type.isObject, + elements: function(obj){ + return obj != null; + } + }); + this.setOptions(params.options); + if (params.elements) this.attach(params.elements); + this.container = new Element('div', {'class': 'tip'}); + + if (this.options.id){ + this.container.set('id', this.options.id); + if (this.options.waiAria) this.attachWaiAria(); + } + }, + + toElement: function(){ + if (this.tip) return this.tip; + + this.tip = new Element('div', { + 'class': this.options.className, + styles: { + position: 'absolute', + top: 0, + left: 0 + } + }).adopt( + new Element('div', {'class': 'tip-top'}), + this.container, + new Element('div', {'class': 'tip-bottom'}) + ); + + return this.tip; + }, + + attachWaiAria: function(){ + var id = this.options.id; + this.container.set('role', 'tooltip'); + + if (!this.waiAria){ + this.waiAria = { + show: function(element){ + if (id) element.set('aria-describedby', id); + this.container.set('aria-hidden', 'false'); + }, + hide: function(element){ + if (id) element.erase('aria-describedby'); + this.container.set('aria-hidden', 'true'); + } + }; + } + this.addEvents(this.waiAria); + }, + + detachWaiAria: function(){ + if (this.waiAria){ + this.container.erase('role'); + this.container.erase('aria-hidden'); + this.removeEvents(this.waiAria); + } + }, + + attach: function(elements){ + $$(elements).each(function(element){ + var title = read(this.options.title, element), + text = read(this.options.text, element); + + element.set('title', '').store('tip:native', title).retrieve('tip:title', title); + element.retrieve('tip:text', text); + this.fireEvent('attach', [element]); + + var events = ['enter', 'leave']; + if (!this.options.fixed) events.push('move'); + + events.each(function(value){ + var event = element.retrieve('tip:' + value); + if (!event) event = function(event){ + this['element' + value.capitalize()].apply(this, [event, element]); + }.bind(this); + + element.store('tip:' + value, event).addEvent('mouse' + value, event); + }, this); + }, this); + + return this; + }, + + detach: function(elements){ + $$(elements).each(function(element){ + ['enter', 'leave', 'move'].each(function(value){ + element.removeEvent('mouse' + value, element.retrieve('tip:' + value)).eliminate('tip:' + value); + }); + + this.fireEvent('detach', [element]); + + if (this.options.title == 'title'){ // This is necessary to check if we can revert the title + var original = element.retrieve('tip:native'); + if (original) element.set('title', original); + } + }, this); + + return this; + }, + + elementEnter: function(event, element){ + clearTimeout(this.timer); + this.timer = (function(){ + this.container.empty(); + + ['title', 'text'].each(function(value){ + var content = element.retrieve('tip:' + value); + var div = this['_' + value + 'Element'] = new Element('div', { + 'class': 'tip-' + value + }).inject(this.container); + if (content) this.fill(div, content); + }, this); + this.show(element); + this.position((this.options.fixed) ? {page: element.getPosition()} : event); + }).delay(this.options.showDelay, this); + }, + + elementLeave: function(event, element){ + clearTimeout(this.timer); + this.timer = this.hide.delay(this.options.hideDelay, this, element); + this.fireForParent(event, element); + }, + + setTitle: function(title){ + if (this._titleElement){ + this._titleElement.empty(); + this.fill(this._titleElement, title); + } + return this; + }, + + setText: function(text){ + if (this._textElement){ + this._textElement.empty(); + this.fill(this._textElement, text); + } + return this; + }, + + fireForParent: function(event, element){ + element = element.getParent(); + if (!element || element == document.body) return; + if (element.retrieve('tip:enter')) element.fireEvent('mouseenter', event); + else this.fireForParent(event, element); + }, + + elementMove: function(event, element){ + this.position(event); + }, + + position: function(event){ + if (!this.tip) document.id(this); + + var size = window.getSize(), scroll = window.getScroll(), + tip = {x: this.tip.offsetWidth, y: this.tip.offsetHeight}, + props = {x: 'left', y: 'top'}, + bounds = {y: false, x2: false, y2: false, x: false}, + obj = {}; + + for (var z in props){ + obj[props[z]] = event.page[z] + this.options.offset[z]; + if (obj[props[z]] < 0) bounds[z] = true; + if ((obj[props[z]] + tip[z] - scroll[z]) > size[z] - this.options.windowPadding[z]){ + obj[props[z]] = event.page[z] - this.options.offset[z] - tip[z]; + bounds[z+'2'] = true; + } + } + + this.fireEvent('bound', bounds); + this.tip.setStyles(obj); + }, + + fill: function(element, contents){ + if (typeof contents == 'string') element.set('html', contents); + else element.adopt(contents); + }, + + show: function(element){ + if (!this.tip) document.id(this); + if (!this.tip.getParent()) this.tip.inject(document.body); + this.fireEvent('show', [this.tip, element]); + }, + + hide: function(element){ + if (!this.tip) document.id(this); + this.fireEvent('hide', [this.tip, element]); + } + +}); + +})(); + diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index 213c0d96..d4c65e3c 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -265,16 +265,37 @@ Page.Settings = new Class({ }, createGroup: function(group){ + + if((typeOf(group.description) == 'array')){ + var hint = new Element('span.hint.more_hint', { + 'html': group.description[0], + 'title': group.description[1] + }); + var tip = new Tips(hint, { + 'fixed': true, + 'offset': {'x': 0, 'y': 0}, + 'onShow': function(tip, hint){ + tip.setStyles({ + 'margin-top': hint.getSize().y, + 'visibility': 'hidden', + 'display': 'block' + }).fade('in'); + } + }); + } + else { + var hint = new Element('span.hint', { + 'html': group.description || '' + }) + } + + return new Element('fieldset', { 'class': (group.advanced ? 'inlineLabels advanced' : 'inlineLabels') + ' group_' + (group.name || '') + ' subtab_' + (group.subtab || '') - }).adopt( + }).grab( new Element('h2', { 'text': group.label || (group.name).capitalize() - }).adopt( - new Element('span.hint', { - 'html': group.description || '' - }) - ) + }).grab(hint) ); }, @@ -343,10 +364,33 @@ var OptionBase = new Class({ createHint: function(){ var self = this; - if(self.options.description) - new Element('p.formHint', { - 'html': self.options.description - }).inject(self.el); + if(self.options.description){ + + + if((typeOf(self.options.description) == 'array')){ + var hint = new Element('p.formHint.more_hint', { + 'html': self.options.description[0], + 'title': self.options.description[1] + }).inject(self.el); + var tip = new Tips(hint, { + 'fixed': true, + 'offset': {'x': 0, 'y': 0}, + 'onShow': function(tip, hint){ + tip.setStyles({ + 'margin-left': 13, + 'margin-top': hint.getSize().y+3, + 'visibility': 'hidden', + 'display': 'block' + }).fade('in'); + } + }); + } + else { + var hint = new Element('p.formHint', { + 'html': self.options.description || '' + }).inject(self.el) + } + } }, afterInject: function(){ diff --git a/couchpotato/static/style/settings.css b/couchpotato/static/style/settings.css index 744531a9..f3df2869 100644 --- a/couchpotato/static/style/settings.css +++ b/couchpotato/static/style/settings.css @@ -545,12 +545,31 @@ .page .combined_table .head abbr:first-child { display: none; } - .page .combined_table .head abbr.host { - margin-right: 190px; + .page .combined_table .head abbr.host { margin-right: 120px; } + .page .combined_table input.host { width: 140px; } + .page .section_newznab .combined_table .head abbr.host { margin-right: 200px; } + .page .section_newznab .combined_table input.host { width: 220px; } + + .page .combined_table .head abbr.name { margin-right: 57px; } + .page .combined_table input.name { width: 120px; } + .page .combined_table .head abbr.api_key { margin-right: 75px; } + + .page .combined_table .head abbr.pass_key { margin-right: 71px; } + .page .combined_table input.pass_key { width: 113px; } + + .page .section_newznab .combined_table .head abbr.api_key { margin-right: 185px; } + .page .section_newznab .combined_table input.api_key { width: 223px; } + + .page .combined_table .seed_ratio, + .page .combined_table .seed_time { + width: 70px; + text-align: center; + margin-left: 10px; } - .page .combined_table .head abbr.api_key { - margin-right: 171px; + .page .combined_table .seed_time { + margin-right: 10px; } + .page .combined_table .head .extra_score, .page .combined_table .extra_score { width: 70px; @@ -699,4 +718,20 @@ .active .group_imdb_automation:not(.disabled) { background: url('../images/imdb_watchlist.png') no-repeat right 50px; min-height: 210px; -} \ No newline at end of file +} + +.tip-wrap { + background: #FFF; + color: #000; + padding: 10px; + width: 300px; + z-index: 200; +} + .more_hint:after { + position: relative; + font-family: 'Elusive-Icons'; + content: "\e089"; + display: inline-block; + top: 1px; + left: 6px; + }