from couchpotato import md5, get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.encoding import ss, toUnicode from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from .index import ReleaseIndex, ReleaseStatusIndex, ReleaseIDIndex, ReleaseDownloadIndex from couchpotato.environment import Env from inspect import ismethod, isfunction from sqlalchemy.exc import InterfaceError import os import time import traceback log = CPLog(__name__) class Release(Plugin): def __init__(self): addApiView('release.manual_download', self.manualDownload, docs = { 'desc': 'Send a release manually to the downloaders', 'params': { 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} } }) addApiView('release.delete', self.deleteView, docs = { 'desc': 'Delete releases', 'params': { 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} } }) addApiView('release.ignore', self.ignore, docs = { 'desc': 'Toggle ignore, for bad or wrong releases', 'params': { 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} } }) addEvent('release.add', self.add) addEvent('release.download', self.download) addEvent('release.try_download_result', self.tryDownloadResult) addEvent('release.create_from_search', self.createFromSearch) addEvent('release.delete', self.delete) addEvent('release.clean', self.clean) addEvent('release.update_status', self.updateStatus) addEvent('database.setup', self.databaseSetup) # Clean releases that didn't have activity in the last week addEvent('app.load', self.cleanDone) fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 4) def databaseSetup(self): db = get_db() # Release media_id index try: db.add_index(ReleaseIndex(db.path, 'release')) except: log.debug('Index already exists') db.edit_index(ReleaseIndex(db.path, 'release')) # Release status index try: db.add_index(ReleaseStatusIndex(db.path, 'release_status')) except: log.debug('Index already exists') db.edit_index(ReleaseStatusIndex(db.path, 'release_status')) # Release identifier index try: db.add_index(ReleaseIDIndex(db.path, 'release_identifier')) except: log.debug('Index already exists') db.edit_index(ReleaseIDIndex(db.path, 'release_identifier')) # Release identifier index try: db.add_index(ReleaseDownloadIndex(db.path, 'release_download')) except: log.debug('Index already exists') db.edit_index(ReleaseDownloadIndex(db.path, 'release_download')) def cleanDone(self): log.debug('Removing releases from dashboard') now = time.time() week = 262080 db = get_db() # get movies last_edit more than a week ago medias = db.run('media', 'with_status', ['done']) for media in medias: if media.get('last_edit', 0) > (now - week): continue for rel in db.run('release', 'for_media', media['_id']): # Remove all available releases if rel['status'] in ['available']: self.delete(rel['_id']) # Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the move elif rel['status'] in ['snatched', 'downloaded']: self.updateStatus(rel['_id'], status = 'ignore') def add(self, group): try: db = get_db() release_identifier = '%s.%s.%s' % (group['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier']) # Add movie if it doesn't exist try: media = db.get('media', group['identifier'], with_doc = True)['doc'] except: media = fireEvent('movie.add', params = { 'identifier': group['identifier'], 'profile_id': None, }, search_after = False, status = 'done', single = True) # Add Release release = { '_t': 'release', 'media_id': media['_id'], 'identifier': release_identifier, 'quality': group['meta_data']['quality'].get('identifier'), 'last_edit': int(time.time()), 'status': 'done' } try: r = db.get('release_identifier', release_identifier, with_doc = True)['doc'] r['media_id'] = media['_id'] except: r = db.insert(release) # Update with ref and _id release.update({ '_id': r['_id'], '_rev': r['_rev'], }) # Empty out empty file groups release['files'] = dict((k, v) for k, v in group['files'].items() if v) db.update(release) fireEvent('media.restatus', media['_id']) return True except: log.error('Failed: %s', traceback.format_exc()) return False def deleteView(self, id = None, **kwargs): return { 'success': self.delete(id) } def delete(self, release_id): try: db = get_db() rel = db.get('id', release_id) db.delete(rel) return True except: log.error('Failed: %s', traceback.format_exc()) return False def clean(self, release_id): try: db = get_db() rel = db.get('id', release_id) if len(rel.get('files')) == 0: self.delete(rel['_id']) else: files = [] for release_file in rel.get('files'): if os.path.isfile(ss(release_file['path'])): files.append(release_file) rel['files'] = files db.update(rel) return True except: log.error('Failed: %s', traceback.format_exc()) return False def ignore(self, release_id = None, **kwargs): db = get_db() try: rel = db.get('id', release_id, with_doc = True) self.updateStatus(release_id, 'available' if rel['status'] in ['ignored', 'failed'] else 'ignored') return { 'success': True } except: log.error('Failed: %s', traceback.format_exc()) return { 'success': False } def manualDownload(self, id = None, **kwargs): db = get_db() try: release = db.get('id', id) item = release['info'] movie = db.get('id', release['media_id']) fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Snatching "%s"' % item['name']) # Get matching provider provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) if item.get('protocol') != 'torrent_magnet': item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download success = self.download(data = item, media = movie, manual = True) if success: fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name']) return { 'success': success == True } except: log.error('Couldn\'t find release with id: %s: %s', (id, traceback.format_exc())) return { 'success': False } def download(self, data, media, manual = False): # Test to see if any downloaders are enabled for this type downloader_enabled = fireEvent('download.enabled', manual, data, single = True) if not downloader_enabled: log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', data.get('protocol')) return False # Download NZB or torrent file filedata = None if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))): try: filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) except: log.error('Tried to download, but the "%s" provider gave an error: %s', (data.get('protocol'), traceback.format_exc())) return False if filedata == 'try_next': return filedata elif not filedata: return False # Send NZB or torrent file to downloader download_result = fireEvent('download', data = data, media = media, manual = manual, filedata = filedata, single = True) if not download_result: log.info('Tried to download, but the "%s" downloader gave an error', data.get('protocol')) return False log.debug('Downloader result: %s', download_result) try: db = get_db() try: rls = db.get('release_identifier', md5(data['url']), with_doc = True)['doc'] except: log.error('No release found to store download information in') return False renamer_enabled = Env.setting('enabled', 'renamer') # Save download-id info if returned if isinstance(download_result, dict): rls['download_info'] = download_result db.update(rls) log_movie = '%s (%s) in %s' % (getTitle(media), media['info']['year'], rls['quality']) snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) log.info(snatch_message) fireEvent('%s.snatched' % data['type'], message = snatch_message, data = rls) # Mark release as snatched if renamer_enabled: self.updateStatus(rls['_id'], status = 'snatched') # If renamer isn't used, mark media done if finished or release downloaded else: if media['status'] == 'active': profile = db.get('id', media['profile_id']) finished = False if rls['quality'] in profile['qualities']: nr = profile['qualities'].index(rls['quality']) finished = profile['finish'][nr] if finished: log.info('Renamer disabled, marking media as finished: %s', log_movie) # Mark release done self.updateStatus(rls['_id'], status = 'done') # Mark media done mdia = db.get('id', media['_id']) mdia['status'] = 'done' mdia['last_edit'] = int(time.time()) db.update(mdia) return True # Assume release downloaded self.updateStatus(rls['_id'], status = 'downloaded') except: log.error('Failed storing download status: %s', traceback.format_exc()) return False return True def tryDownloadResult(self, results, media, quality_custom, manual = False): for rel in results: if not quality_custom.get('finish', False) and quality_custom.get('wait_for', 0) > 0 and rel.get('age') <= quality_custom.get('wait_for', 0): log.info('Ignored, waiting %s days: %s', (quality_custom.get('wait_for'), rel['name'])) continue if rel['status'] in ['ignored', 'failed']: log.info('Ignored: %s', rel['name']) continue if rel['score'] <= 0: log.info('Ignored, score to low: %s', rel['name']) continue downloaded = fireEvent('release.download', data = rel, media = media, manual = manual, single = True) if downloaded is True: return True elif downloaded != 'try_next': break return False def createFromSearch(self, search_results, media, quality): try: db = get_db() found_releases = [] for rel in search_results: rel_identifier = md5(rel['url']) found_releases.append(rel_identifier) release = { '_t': 'release', 'identifier': rel_identifier, 'media_id': media.get('_id'), 'quality': quality.get('identifier'), 'status': 'available', 'last_edit': int(time.time()), 'info': {} } try: rls = db.get('release_identifier', rel_identifier, with_doc = True)['doc'] except: rls = db.insert(release) rls.update(release) # Update info, but filter out functions for info in rel: try: if not isinstance(rel[info], (str, unicode, int, long, float)): continue rls['info'][info] = toUnicode(rel[info]) except InterfaceError: log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc())) db.update(rls) # Update release in search_results rel['status'] = rls.get('status') return found_releases except: log.error('Failed: %s', traceback.format_exc()) return [] def updateStatus(self, release_id, status = None): if not status: return False try: db = get_db() rel = db.get('id', release_id) if rel and rel.get('status') != status: release_name = rel.get('name') if rel.get('files'): for file_type in rel.get('files', {}): if file_type == 'movie': for release_file in rel['files'][file_type]: release_name = os.path.basename(release_file) break #update status in Db log.debug('Marking release %s as %s', (release_name, status)) rel['status'] = status rel['last_edit'] = int(time.time()) db.update(rel) #Update all movie info as there is no release update function fireEvent('notify.frontend', type = 'release.update_status', data = rel) return True except: log.error('Failed: %s', traceback.format_exc()) return False