diff --git a/CouchPotato.py b/CouchPotato.py index 21b208a2..b1620c0c 100755 --- a/CouchPotato.py +++ b/CouchPotato.py @@ -9,7 +9,7 @@ import socket import subprocess import sys import traceback - +import time # Root path base_path = dirname(os.path.abspath(__file__)) @@ -96,6 +96,10 @@ class Loader(object): except: self.log.critical(traceback.format_exc()) + # Release log files and shutdown logger + logging.shutdown() + time.sleep(3) + args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:] subprocess.Popen(args) except: diff --git a/README.md b/README.md index 2eb0f2f3..b0e4222d 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ Once a movie is found, it will send it to SABnzbd or download the torrent to a s ## Running from Source -CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed also. +CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed also. -Windows: +Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for more details: -* Install [PyWin32 2.7](http://sourceforge.net/projects/pywin32/files/pywin32/Build%20217/) and [GIT](http://git-scm.com/) +* Install [Python 2.7](http://www.python.org/download/releases/2.7.3/) +* Then install [PyWin32 2.7](http://sourceforge.net/projects/pywin32/files/pywin32/Build%20217/) and [GIT](http://git-scm.com/) * If you come and ask on the forums 'why directory selection no work?', I will kill a kitten, also this is because you need PyWin32 * Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files. * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`. diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 74e04de6..c910edd2 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -4,22 +4,28 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env import os +import re +import traceback log = CPLog(__name__) - class Downloader(Plugin): type = [] def __init__(self): addEvent('download', self.download) + addEvent('download.status', self.getDownloadStatus) - def download(self, data = {}): + def download(self, data = {}, movie = {}, manual = False, filedata = None): + pass + + def getDownloadStatus(self, data = {}, movie = {}): pass def createNzbName(self, data, movie): - return '%s%s' % (toSafeString(data.get('name')), self.cpTag(movie)) + tag = self.cpTag(movie) + return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag) def createFileName(self, data, filedata, movie): name = os.path.join(self.createNzbName(data, movie)) @@ -41,6 +47,18 @@ class Downloader(Plugin): return is_correct + def magnetToTorrent(self, magnet_link): + torrent_hash = re.findall('urn:btih:([\w]{40})', magnet_link)[0] + url = 'http://torrage.com/torrent/%s.torrent' % torrent_hash + + try: + filedata = self.urlopen(url) + return filedata + except: + log.error('Failed converting magnet url to torrent: %s, %s', (url, traceback.format_exc())) + + return False + def isDisabled(self, manual): return not self.isEnabled(manual) diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py index a3f03617..4713adf7 100644 --- a/couchpotato/core/downloaders/blackhole/main.py +++ b/couchpotato/core/downloaders/blackhole/main.py @@ -8,7 +8,7 @@ log = CPLog(__name__) class Blackhole(Downloader): - type = ['nzb', 'torrent'] + type = ['nzb', 'torrent', 'torrent_magnet'] def download(self, data = {}, movie = {}, manual = False, filedata = None): if self.isDisabled(manual) or (not self.isCorrectType(data.get('type')) or (not self.conf('use_for') in ['both', data.get('type')])): @@ -20,8 +20,16 @@ class Blackhole(Downloader): else: try: if not filedata or len(filedata) < 50: - log.error('No nzb/torrent available!') - return False + try: + if data.get('type') == 'torrent_magnet': + filedata = self.magnetToTorrent(data.get('url')) + data['type'] = 'torrent' + except: + log.error('Failed download torrent via magnet url: %s', traceback.format_exc()) + + if not filedata or len(filedata) < 50: + log.error('No nzb/torrent available: %s', data.get('url')) + return False fullPath = os.path.join(directory, self.createFileName(data, filedata, movie)) diff --git a/couchpotato/core/downloaders/sabnzbd/__init__.py b/couchpotato/core/downloaders/sabnzbd/__init__.py index ac0ce05c..927a9ff2 100644 --- a/couchpotato/core/downloaders/sabnzbd/__init__.py +++ b/couchpotato/core/downloaders/sabnzbd/__init__.py @@ -35,11 +35,17 @@ config = [{ }, { 'name': 'manual', - 'default': 0, + 'default': False, 'type': 'bool', 'advanced': True, 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', }, + { + 'name': 'delete_failed', + 'default': True, + 'type': 'bool', + 'description': 'Delete a release after the download has failed.', + }, ], } ], diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index 64b24509..86d5459d 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.logger import CPLog import traceback +import json log = CPLog(__name__) @@ -39,14 +40,14 @@ class Sabnzbd(Downloader): try: if params.get('mode') is 'addfile': - data = self.urlopen(url, timeout = 60, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False) + sab = self.urlopen(url, timeout = 60, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False) else: - data = self.urlopen(url, timeout = 60, show_error = False) + sab = self.urlopen(url, timeout = 60, show_error = False) except: - log.error(traceback.format_exc()) + log.error('Failed sending release: %s', traceback.format_exc()) return False - result = data.strip() + result = sab.strip() if not result: log.error("SABnzbd didn't return anything.") return False @@ -61,3 +62,101 @@ class Sabnzbd(Downloader): else: log.error("Unknown error: " + result[:40]) return False + + def getDownloadStatus(self, data = {}, movie = {}): + if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')): + return + + nzbname = self.createNzbName(data, movie) + log.info('Checking download status of "%s" at SABnzbd.', nzbname) + + # Go through Queue + params = { + 'apikey': self.conf('api_key'), + 'mode': 'queue', + 'output': 'json' + } + url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params) + + try: + sab = self.urlopen(url, timeout = 60, show_error = False) + except: + log.error('Failed checking status: %s', traceback.format_exc()) + return False + + try: + history = json.loads(sab) + except: + log.debug("Result text from SAB: " + sab[:40]) + log.error('Failed parsing json status: %s', traceback.format_exc()) + return False + + for slot in history['queue']['slots']: + if slot['cat'] == self.conf('category'): + log.debug('Found %s in SabNZBd queue, which is %s, with %s left', (slot['filename'], slot['status'], slot['timeleft'])) + if slot['filename'] == nzbname: + return slot['status'].lower() + + # Go through history items + params = { + 'apikey': self.conf('api_key'), + 'mode': 'history', + 'output': 'json' + } + url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params) + + try: + sab = self.urlopen(url, timeout = 60, show_error = False) + except: + log.error('Failed getting history: %s', traceback.format_exc()) + return + + try: + history = json.loads(sab) + except: + log.debug("Result text from SAB: " + sab[:40]) + log.error('Failed parsing history json: %s', traceback.format_exc()) + return + + for slot in history['history']['slots']: + if slot['category'] == self.conf('category'): + log.debug('Found %s in SabNZBd history, which has %s', (slot['name'], slot['status'])) + if slot['name'] == nzbname: + if slot['status'] == 'Failed' or slot['fail_message'].strip(): + + # Delete failed download + if self.conf('delete_failed', default = True): + + log.info('%s failed downloading, deleting...', slot['name']) + params = { + 'apikey': self.conf('api_key'), + 'mode': 'history', + 'name': 'delete', + 'del_files': '1', + 'value': slot['nzo_id'] + } + url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params) + + try: + sab = self.urlopen(url, timeout = 60, show_error = False) + except: + log.error('Failed deleting: %s', traceback.format_exc()) + return False + + result = sab.strip() + if not result: + log.error("SABnzbd didn't return anything.") + + log.debug("Result text from SAB: " + result[:40]) + if result == "ok": + log.info('SabNZBd deleted failed release %s successfully.', slot['name']) + elif result == "Missing authentication": + log.error("Incorrect username/password or API?.") + else: + log.error("Unknown error: " + result[:40]) + + return 'failed' + else: + return slot['status'].lower() + + return 'not_found' diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index 595a39ed..db9db843 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -216,7 +216,7 @@ var NotificationBase = new Class({ }, testButtonName: function(fieldset){ - var name = fieldset.getElement('h2').get('text'); + var name = String(fieldset.getElement('h2').innerHTML).substring(0,String(fieldset.getElement('h2').innerHTML).indexOf(" 1 + ) - response = nma.push(self.default_title, self.event , message, self.conf('priority'), batch_mode = len(keys) > 1) - + successful = 0 for key in keys: if not response[str(key)]['code'] == u'200': log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message'])) + else: + successful += 1 - return response + return successful == len(keys) diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index 4f6e62a7..1c83bdd5 100644 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -13,11 +13,15 @@ class XBMC(Notification): def notify(self, message = '', data = {}, listener = None): if self.isDisabled(): return - for host in [x.strip() for x in self.conf('host').split(",")]: - self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host) - self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host) + hosts = [x.strip() for x in self.conf('host').split(",")] + successful = 0 + for host in hosts: + if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host): + successful += 1 + if self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host): + successful += 1 - return True + return successful == len(hosts)*2 def send(self, command, host): diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index c30ee99a..f809c579 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -340,6 +340,32 @@ .movies .movie .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 .load_more { display: block; diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index 92a66efd..17234b92 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -140,28 +140,33 @@ var Movie = new Class({ self.profile.getTypes().each(function(type){ var q = self.addQuality(type.quality_id || type.get('quality_id')); - if(type.finish == true || type.get('finish')) + 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 done releases - Array.each(self.data.releases, function(release){ + 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) + + 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.actions.adopt( - self.action[key.toLowerCase()] = new self.options.actions[key](self) - ) + self.action[key.toLowerCase()] = action = new self.options.actions[key](self) + if(action.el) + self.actions.adopt(action) }); if(!self.data.library.rating) @@ -175,7 +180,8 @@ var Movie = new Class({ var q = Quality.getQuality(quality_id); return new Element('span', { 'text': q.label, - 'class': 'q_'+q.identifier + ' q_id' + q.id + 'class': 'q_'+q.identifier + ' q_id' + q.id, + 'title': '' }).inject(self.quality); }, @@ -280,6 +286,31 @@ var MovieAction = new Class({ this.el.removeClass('disable') }, + createMask: function(){ + var self = this; + self.mask = new Element('div.mask', { + 'styles': { + 'z-index': '1' + } + }).inject(self.movie, 'top').fade('hide'); + self.positionMask(); + }, + + positionMask: function(){ + var self = this, + movie = $(self.movie), + s = movie.getSize() + + return; + + return self.mask.setStyles({ + 'width': s.x, + 'height': s.y + }).position({ + 'relativeTo': movie + }) + }, + toElement: function(){ return this.el || null } @@ -298,19 +329,10 @@ var IMDBAction = new Class({ self.el = new Element('a.imdb', { 'title': 'Go to the IMDB page of ' + self.movie.getTitle(), - 'events': { - 'click': self.gotoIMDB.bind(self) - } + 'href': 'http://www.imdb.com/title/'+self.id+'/' }); if(!self.id) self.disable(); - }, - - gotoIMDB: function(e){ - var self = this; - (e).preventDefault(); - - window.open('http://www.imdb.com/title/'+self.id+'/'); } }); @@ -318,13 +340,10 @@ var IMDBAction = new Class({ var ReleaseAction = new Class({ Extends: MovieAction, - id: null, create: function(){ var self = this; - self.id = self.movie.get('identifier'); - self.el = new Element('a.releases.icon.download', { 'title': 'Show the releases that are available for ' + self.movie.getTitle(), 'events': { @@ -332,15 +351,33 @@ var ReleaseAction = new Class({ } }); + var buttons_done = false; + + self.movie.data.releases.sortBy('-info.score').each(function(release){ + if(buttons_done) return; + + var status = Status.get(release.status_id); + + if((status.identifier == 'ignored' || status.identifier == 'failed') || (!self.next_release && status.identifier == 'available')){ + self.hide_on_click = false; + self.show(); + buttons_done = true; + } + + }); + }, show: function(e){ var self = this; - (e).preventDefault(); + if(e) + (e).preventDefault(); if(!self.options_container){ self.options_container = new Element('div.options').adopt( - self.release_container = new Element('div.releases.table') + self.release_container = new Element('div.releases.table').adopt( + self.trynext_container = new Element('div.buttons.try_container') + ) ).inject(self.movie, 'top'); // Header @@ -354,29 +391,35 @@ var ReleaseAction = new Class({ new Element('span.provider', {'text': 'Provider'}) ).inject(self.release_container) - Array.each(self.movie.data.releases, function(release){ + 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; - try { - var details_url = info.filter(function(item){ return item.identifier == 'detail_url' }).pick().value; - } catch(e){} + if( status.identifier == 'ignored' || status.identifier == 'failed'){ + self.last_release = release; + } + else if(!self.next_release && status.identifier == 'available'){ + self.next_release = release; + } + // Create release new Element('div', { - 'class': 'item '+status.identifier, + 'class': 'item '+status.identifier + + (self.next_release && self.next_release.id == release.id ? ' next_release' : '') + + (self.last_release && self.last_release.id == release.id ? ' last_release' : ''), 'id': 'release_'+release.id }).adopt( new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}), new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}), - new Element('span.quality', {'text': quality.label || 'n/a'}), - new Element('span.size', {'text': (self.get(release, 'size'))}), + 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': self.get(release, 'provider')}), - details_url ? new Element('a.info.icon', { - 'href': details_url, + release.info['detail_url'] ? new Element('a.info.icon', { + 'href': release.info['detail_url'], 'target': '_blank' }) : null, new Element('a.download.icon', { @@ -400,17 +443,37 @@ var ReleaseAction = new Class({ ).inject(self.release_container) }); + self.trynext_container.adopt( + new Element('span.or', { + 'text': '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': 'or' + }) : null, + self.next_release ? [new Element('a.button.green', { + 'text': self.last_release ? 'another release' : 'the best release', + 'events': { + 'click': self.tryNextRelease.bind(self) + } + }), + new Element('span.or', { + 'text': 'or pick one below' + })] : null + ) + } self.movie.slide('in', self.options_container); }, get: function(release, type){ - var self = this; - - return (release.info.filter(function(info){ - return type == info.identifier - }).pick() || {}).value || 'n/a' + return release.info[type] || 'n/a' }, download: function(release){ @@ -444,6 +507,25 @@ var ReleaseAction = new Class({ } }) + }, + + tryNextRelease: function(movie_id){ + var self = this; + + if(self.last_release) + self.ignore(self.last_release); + + if(self.next_release) + self.download(self.next_release); + + }, + + trySameRelease: function(movie_id){ + var self = this; + + if(self.last_release) + self.download(self.last_release); + } }); diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css index 4f43ffe1..76130cb0 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -186,7 +186,6 @@ .movie_result .info h2 span { padding: 0 5px; - content: ")"; } .movie_result .info h2 span:before { content: "("; } diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 0abc065e..9046034c 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -170,7 +170,9 @@ class Release(Plugin): # Get matching provider provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) - item['download'] = provider.download + + if item['type'] != 'torrent_magnet': + item['download'] = provider.download success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({ 'profile': {'types': {'quality': {}}}, diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index 59439544..21076d68 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -86,15 +86,6 @@ config = [{ 'label': 'Separator', 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', }, - { - 'advanced': True, - 'name': 'run_every', - 'label': 'Run every', - 'default': 1, - 'type': 'int', - 'unit': 'min(s)', - 'description': 'Search for new movies inside the folder every X minutes.', - }, ], }, { 'tab': 'renamer', diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 2cd09e48..9a443ed2 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -30,7 +30,7 @@ class Renamer(Plugin): addEvent('renamer.scan', self.scan) addEvent('app.load', self.scan) - fireEvent('schedule.interval', 'renamer.scan', self.scan, minutes = self.conf('run_every')) + #fireEvent('schedule.interval', 'renamer.scan', self.scan, minutes = self.conf('run_every')) def scanView(self): @@ -327,6 +327,11 @@ class Renamer(Plugin): try: if os.path.isfile(src): os.remove(src) + + parent_dir = os.path.normpath(os.path.dirname(src)) + if os.path.isdir(parent_dir) and destination != parent_dir: + self.deleteEmptyFolder(parent_dir, show_error = False) + except: log.error('Failed removing %s: %s', (src, traceback.format_exc())) self.tagDir(group, 'failed_remove') @@ -464,8 +469,9 @@ class Renamer(Plugin): def replaceDoubles(self, string): return string.replace(' ', ' ').replace(' .', '.') - def deleteEmptyFolder(self, folder): + def deleteEmptyFolder(self, folder, show_error = True): + loge = log.error if show_error else log.debug for root, dirs, files in os.walk(folder): for dir_name in dirs: @@ -474,9 +480,9 @@ class Renamer(Plugin): try: os.rmdir(full_path) except: - log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc())) + loge('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc())) try: os.rmdir(folder) except: - log.error('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc())) + loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc())) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 7e47ed96..0bc47681 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -303,7 +303,15 @@ class Scanner(Plugin): break if file_too_new: - log.info('Files seem to be still unpacking or just unpacked (created on %s), ignoring for now: %s', (time.ctime(file_time[0]), identifier)) + try: + time_string = time.ctime(file_time[0]) + except: + try: + time_string = time.ctime(file_time[1]) + except: + time_string = 'unknown' + + log.info('Files seem to be still unpacking or just unpacked (created on %s), ignoring for now: %s', (time_string, identifier)) # Delete the unsorted list del group['unsorted_files'] diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py index f499e2bd..e2c2bb83 100644 --- a/couchpotato/core/plugins/searcher/__init__.py +++ b/couchpotato/core/plugins/searcher/__init__.py @@ -39,6 +39,21 @@ config = [{ 'type': 'dropdown', 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')], }, + { + 'advanced': True, + 'name': 'run_every', + 'label': 'Run every', + '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', + }, + { + 'name': 'next_on_failed', + 'default': True, + 'type': 'bool', + 'description': 'Try the next best release for a movie after a download failed.', + }, ], }, { 'tab': 'searcher', diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index dfefdab2..afabeae8 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -1,6 +1,8 @@ from couchpotato import get_session +from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import simplifyString, toUnicode +from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.variable import md5, getImdb, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin @@ -25,9 +27,19 @@ class Searcher(Plugin): addEvent('searcher.single', self.single) addEvent('searcher.correct_movie', self.correctMovie) addEvent('searcher.download', self.download) + addEvent('searcher.check_snatched', self.checkSnatched) + + 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'}, + }, + }) # Schedule cronjob fireEvent('schedule.cron', 'searcher.all', self.all_movies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) + fireEvent('schedule.interval', 'searcher.check_snatched', self.checkSnatched, minutes = self.conf('run_every')) + def all_movies(self): @@ -138,7 +150,7 @@ class Searcher(Plugin): for info in nzb: try: - if not isinstance(nzb[info], (str, unicode, int, long)): + if not isinstance(nzb[info], (str, unicode, int, long, float)): continue rls_info = ReleaseInfo( @@ -239,7 +251,6 @@ class Searcher(Plugin): def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs): imdb_results = kwargs.get('imdb_results', False) - single_category = kwargs.get('single_category', False) retention = Env.setting('retention', section = 'nzb') if nzb.get('seeds') is None and retention < nzb.get('age', 0): @@ -272,7 +283,7 @@ class Searcher(Plugin): 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, single_category = single_category): + if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality): log.info('Wrong: %s, looking for %s', (nzb['name'], quality['label'])) return False @@ -324,7 +335,7 @@ class Searcher(Plugin): 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 = {}, single_category = False): + def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}): name = nzb['name'] size = nzb.get('size', 0) @@ -355,9 +366,6 @@ class Searcher(Plugin): if found.get(allowed): del found[allowed] - if (len(found) == 0 and single_category): - return False - return not (found.get(preferred_quality['identifier']) and len(found) == 1) def checkIMDB(self, haystack, imdbId): @@ -439,3 +447,96 @@ class Searcher(Plugin): return False + + def checkSnatched(self): + snatched_status = fireEvent('status.get', 'snatched', single = True) + ignored_status = fireEvent('status.get', 'ignored', single = True) + failed_status = fireEvent('status.get', 'failed', single = True) + + db = get_session() + rels = db.query(Release).filter_by(status_id = snatched_status.get('id')) + + if rels: + log.debug('Checking status snatched releases...') + + scanrequired = False + + for rel in rels: + + # Get current selected title + default_title = '' + for title in rel.movie.library.titles: + if title.default: default_title = title.title + + log.debug('Checking snatched movie: %s' , default_title) + + item = {} + for info in rel.info: + item[info.identifier] = info.value + + movie_dict = fireEvent('movie.get', rel.movie_id, single = True) + + # check status + downloadstatus = fireEvent('download.status', data = item, movie = movie_dict, single = True) + if not downloadstatus: + log.debug('Download status functionality is not implemented for active downloaders.') + scanrequired = True + else: + log.debug('Download status: %s' , downloadstatus) + + if downloadstatus == 'failed': + if self.conf('next_on_failed'): + self.tryNextRelease(rel.movie_id) + else: + rel.status_id = failed_status.get('id') + db.commit() + + log.info('Download of %s failed.', item['name']) + + elif downloadstatus == 'completed': + log.info('Download of %s completed!', item['name']) + scanrequired = True + + elif downloadstatus == 'not_found': + log.info('%s not found in downloaders', item['name']) + rel.status_id = ignored_status.get('id') + db.commit() + + # Note that Queued, Downloading, Paused, Repair and Unpackimg are also available as status for SabNZBd + if scanrequired: + fireEvent('renamer.scan') + + def tryNextReleaseView(self): + + trynext = self.tryNextRelease(getParam('id')) + + return jsonified({ + 'success': trynext + }) + + def tryNextRelease(self, movie_id, manual = False): + + snatched_status = fireEvent('status.get', 'snatched', single = True) + ignored_status = fireEvent('status.get', 'ignored', single = True) + + try: + movie_dict = fireEvent('movie.get', movie_id, single = True) + + 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() + + 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 diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py index af2e8792..91c2858f 100644 --- a/couchpotato/core/plugins/status/main.py +++ b/couchpotato/core/plugins/status/main.py @@ -19,6 +19,7 @@ class StatusPlugin(Plugin): 'downloaded': 'Downloaded', 'wanted': 'Wanted', 'snatched': 'Snatched', + 'failed': 'Failed', 'deleted': 'Deleted', 'ignored': 'Ignored', } diff --git a/couchpotato/core/providers/nzb/mysterbin/__init__.py b/couchpotato/core/providers/nzb/mysterbin/__init__.py index 0c759555..3f0d1d33 100644 --- a/couchpotato/core/providers/nzb/mysterbin/__init__.py +++ b/couchpotato/core/providers/nzb/mysterbin/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'providers', 'name': 'Mysterbin', - 'description': '', + 'description': 'Free provider, less accurate. See Mysterbin', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/providers/nzb/mysterbin/main.py b/couchpotato/core/providers/nzb/mysterbin/main.py index 008f24f8..edf2c70e 100644 --- a/couchpotato/core/providers/nzb/mysterbin/main.py +++ b/couchpotato/core/providers/nzb/mysterbin/main.py @@ -88,7 +88,7 @@ class Mysterbin(NZBProvider): new['score'] = fireEvent('score.calculate', new, movie, single = True) is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, - imdb_results = False, single_category = False, single = True) + imdb_results = False, single = True) if is_correct_movie: results.append(new) self.found(new) diff --git a/couchpotato/core/providers/nzb/newzbin/__init__.py b/couchpotato/core/providers/nzb/newzbin/__init__.py index 4ebd849d..06b25486 100644 --- a/couchpotato/core/providers/nzb/newzbin/__init__.py +++ b/couchpotato/core/providers/nzb/newzbin/__init__.py @@ -10,6 +10,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'providers', 'name': 'newzbin', + 'description': 'See Newzbin', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index ff2aba86..5485c63b 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -96,13 +96,12 @@ class Newznab(NZBProvider, RSS): url = "%s&%s" % (self.getUrl(host['host'], self.urls['search']), arguments) cache_key = 'newznab.%s.%s.%s' % (host['host'], movie['library']['identifier'], cat_id[0]) - single_cat = (len(cat_id) == 1 and cat_id[0] != self.cat_backup_id) - results = self.createItems(url, cache_key, host, single_cat = single_cat, movie = movie, quality = quality) + results = self.createItems(url, cache_key, host, movie = movie, quality = quality) return results - def createItems(self, url, cache_key, host, single_cat = False, movie = None, quality = None, for_feed = False): + def createItems(self, url, cache_key, host, movie = None, quality = None, for_feed = False): results = [] data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) @@ -146,7 +145,7 @@ class Newznab(NZBProvider, RSS): if not for_feed: is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, - imdb_results = True, single_category = single_cat, single = True) + imdb_results = True, single = True) if is_correct_movie: new['score'] = fireEvent('score.calculate', new, movie, single = True) diff --git a/couchpotato/core/providers/nzb/nzbclub/__init__.py b/couchpotato/core/providers/nzb/nzbclub/__init__.py index 9c14e10f..e3387a3e 100644 --- a/couchpotato/core/providers/nzb/nzbclub/__init__.py +++ b/couchpotato/core/providers/nzb/nzbclub/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'providers', 'name': 'NZBClub', - 'description': '', + 'description': 'Free provider, less accurate. See NZBClub', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py index dedddb85..d047a8a8 100644 --- a/couchpotato/core/providers/nzb/nzbclub/main.py +++ b/couchpotato/core/providers/nzb/nzbclub/main.py @@ -86,7 +86,7 @@ class NZBClub(NZBProvider, RSS): is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, - imdb_results = False, single_category = False, single = True) + imdb_results = False, single = True) if is_correct_movie: new['score'] = fireEvent('score.calculate', new, movie, single = True) diff --git a/couchpotato/core/providers/nzb/nzbindex/__init__.py b/couchpotato/core/providers/nzb/nzbindex/__init__.py index 8a3261bf..59b2730d 100644 --- a/couchpotato/core/providers/nzb/nzbindex/__init__.py +++ b/couchpotato/core/providers/nzb/nzbindex/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'providers', 'name': 'nzbindex', - 'description': 'Free provider, but less accurate.', + 'description': 'Free provider, less accurate. See NZBIndex', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py index 3d6384c0..ee2092c2 100644 --- a/couchpotato/core/providers/nzb/nzbindex/main.py +++ b/couchpotato/core/providers/nzb/nzbindex/main.py @@ -93,7 +93,7 @@ class NzbIndex(NZBProvider, RSS): is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, - imdb_results = False, single_category = False, single = True) + imdb_results = False, single = True) if is_correct_movie: new['score'] = fireEvent('score.calculate', new, movie, single = True) diff --git a/couchpotato/core/providers/nzb/nzbmatrix/__init__.py b/couchpotato/core/providers/nzb/nzbmatrix/__init__.py index 82b6ef6e..8fc6b408 100644 --- a/couchpotato/core/providers/nzb/nzbmatrix/__init__.py +++ b/couchpotato/core/providers/nzb/nzbmatrix/__init__.py @@ -11,6 +11,7 @@ config = [{ 'subtab': 'providers', 'name': 'nzbmatrix', 'label': 'NZBMatrix', + 'description': 'See NZBMatrix', 'wizard': True, 'options': [ { diff --git a/couchpotato/core/providers/nzb/nzbmatrix/main.py b/couchpotato/core/providers/nzb/nzbmatrix/main.py index c6c5be91..9539eac1 100644 --- a/couchpotato/core/providers/nzb/nzbmatrix/main.py +++ b/couchpotato/core/providers/nzb/nzbmatrix/main.py @@ -22,7 +22,7 @@ class NZBMatrix(NZBProvider, RSS): cat_ids = [ ([50], ['bd50']), ([42, 53], ['720p', '1080p']), - ([2], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), + ([2, 9], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), ([54], ['brrip']), ([1], ['dvdr']), ] @@ -49,7 +49,6 @@ class NZBMatrix(NZBProvider, RSS): url = "%s?%s" % (self.urls['search'], arguments) cache_key = 'nzbmatrix.%s.%s' % (movie['library'].get('identifier'), cat_ids) - single_cat = True data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) if data: @@ -86,7 +85,7 @@ class NZBMatrix(NZBProvider, RSS): is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, - imdb_results = True, single_category = single_cat, single = True) + imdb_results = True, single = True) if is_correct_movie: new['score'] = fireEvent('score.calculate', new, movie, single = True) diff --git a/couchpotato/core/providers/nzb/nzbsrus/__init__.py b/couchpotato/core/providers/nzb/nzbsrus/__init__.py new file mode 100644 index 00000000..70f3e3f0 --- /dev/null +++ b/couchpotato/core/providers/nzb/nzbsrus/__init__.py @@ -0,0 +1,40 @@ +from .main import Nzbsrus + +def start(): + return Nzbsrus() + +config = [{ + 'name': 'nzbsrus', + 'groups': [ + { + 'tab': 'searcher', + 'subtab': 'providers', + 'name': 'nzbsrus', + 'label': 'Nzbsrus', + 'description': 'See NZBsRus', + 'wizard': True, + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + }, + { + 'name': 'userid', + 'label': 'User ID', + }, + { + 'name': 'api_key', + 'default': '', + 'label': 'Api Key', + }, + { + 'name': 'english_only', + 'default': 1, + 'type': 'bool', + 'label': 'English only', + 'description': 'Only search for English spoken movies on Nzbsrus', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/providers/nzb/nzbsrus/main.py b/couchpotato/core/providers/nzb/nzbsrus/main.py new file mode 100644 index 00000000..20a51d56 --- /dev/null +++ b/couchpotato/core/providers/nzb/nzbsrus/main.py @@ -0,0 +1,104 @@ +from couchpotato.core.event import fireEvent +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.helpers.rss import RSS +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.nzb.base import NZBProvider +from couchpotato.environment import Env +import time +import xml.etree.ElementTree as XMLTree + +log = CPLog(__name__) + +class Nzbsrus(NZBProvider, RSS): + + urls = { + 'download': 'https://www.nzbsrus.com/nzbdownload_rss.php/%s', + 'detail': 'https://www.nzbsrus.com/nzbdetails.php?id=%s', + 'search': 'https://www.nzbsrus.com/api.php?extended=1&xml=1&listname={date,grabs}', + } + + cat_ids = [ + ([90, 45, 51], ['720p', '1080p', 'brrip', 'bd50', 'dvdr']), + ([48, 51], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), + ] + cat_backup_id = 240 + + def search(self, movie, quality): + + results = [] + + if self.isDisabled(): + return results + + cat_id_string = '&'.join(['c%s=1' % x for x in self.getCatId(quality.get('identifier'))]) + + arguments = tryUrlencode({ + 'searchtext': 'imdb:' + movie['library']['identifier'][2:], + 'uid': self.conf('userid'), + 'key': self.conf('api_key'), + 'age': Env.setting('retention', section = 'nzb'), + + }) + + # check for english_only + if self.conf('english_only'): + arguments += "&lang0=1&lang3=1&lang1=1" + + url = "%s&%s&%s" % (self.urls['search'], arguments , cat_id_string) + + cache_key = 'nzbsrus_1.%s.%s' % (movie['library'].get('identifier'), cat_id_string) + single_cat = True + + data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) + if data: + try: + try: + data = XMLTree.fromstring(data) + nzbs = self.getElements(data, 'results/result') + except Exception, e: + log.debug('%s, %s', (self.getName(), e)) + return results + + for nzb in nzbs: + + title = self.getTextElement(nzb, "name") + if 'error' in title.lower(): continue + + id = self.getTextElement(nzb, "id") + size = int(round(int(self.getTextElement(nzb, "size")) / 1048576)) + age = int(round((time.time() - int(self.getTextElement(nzb, "postdate"))) / 86400)) + + new = { + 'id': id, + 'type': 'nzb', + 'provider': self.getName(), + 'name': title, + 'age': age, + 'size': size, + 'url': self.urls['download'] % id + self.getApiExt() + self.getTextElement(nzb, "key"), + 'download': self.download, + 'detail_url': self.urls['detail'] % id, + 'description': self.getTextElement(nzb, "addtext"), + 'check_nzb': True, + } + + is_correct_movie = fireEvent('searcher.correct_movie', + nzb = new, movie = movie, quality = quality, + imdb_results = True, single = True) + + if is_correct_movie: + new['score'] = fireEvent('score.calculate', new, movie, single = True) + results.append(new) + self.found(new) + + return results + except SyntaxError: + log.error('Failed to parse XML response from Nzbsrus.com') + + return results + + def download(self, url = '', nzb_id = ''): + return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()}) + + def getApiExt(self): + return '/%s/' % (self.conf('userid')) diff --git a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py index 6514643e..2401f95a 100644 --- a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py +++ b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py @@ -10,6 +10,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'providers', 'name': 'KickAssTorrents', + 'description': 'See KickAssTorrents', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/providers/torrent/kickasstorrents/main.py b/couchpotato/core/providers/torrent/kickasstorrents/main.py index c57ae8a6..6c157a6b 100644 --- a/couchpotato/core/providers/torrent/kickasstorrents/main.py +++ b/couchpotato/core/providers/torrent/kickasstorrents/main.py @@ -3,8 +3,6 @@ from couchpotato.core.event import fireEvent from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.providers.torrent.base import TorrentProvider -import StringIO -import gzip import re import traceback @@ -14,10 +12,9 @@ log = CPLog(__name__) class KickAssTorrents(TorrentProvider): urls = { - 'test': 'http://www.kat.ph/', - 'detail': 'http://www.kat.ph/%s-t%s.html', - 'search': 'http://www.kat.ph/i%s/', - 'download': 'http://torcache.net/', + 'test': 'http://kat.ph/', + 'detail': 'http://kat.ph/%s', + 'search': 'http://kat.ph/i%s/', } cat_ids = [ @@ -60,11 +57,10 @@ class KickAssTorrents(TorrentProvider): continue new = { - 'type': 'torrent', + 'type': 'torrent_magnet', 'check_nzb': False, 'description': '', 'provider': self.getName(), - 'download': self.download, 'score': 0, } @@ -77,9 +73,8 @@ class KickAssTorrents(TorrentProvider): link = td.find('div', {'class': 'torrentname'}).find_all('a')[1] new['id'] = temp.get('id')[-8:] new['name'] = link.text - new['url'] = td.find_all('a', 'idownload')[1]['href'] - if new['url'][:2] == '//': - new['url'] = 'http:%s' % new['url'] + new['url'] = td.find('a', 'imagnet')['href'] + new['detail_url'] = self.urls['detail'] % link['href'][1:] new['score'] = 20 if td.find('a', 'iverif') else 0 elif column_name is 'size': new['size'] = self.parseSize(td.text) @@ -95,7 +90,7 @@ class KickAssTorrents(TorrentProvider): new['score'] += fireEvent('score.calculate', new, movie, single = True) is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, - imdb_results = True, single_category = False, single = True) + imdb_results = True, single = True) if is_correct_movie: results.append(new) self.found(new) @@ -129,13 +124,3 @@ class KickAssTorrents(TorrentProvider): age += tryInt(nr) * mult return tryInt(age) - - def download(self, url = '', nzb_id = ''): - compressed_data = self.urlopen(url = url, headers = {'Referer': 'http://kat.ph/'}) - - compressedstream = StringIO.StringIO(compressed_data) - gzipper = gzip.GzipFile(fileobj = compressedstream) - data = gzipper.read() - - return data - diff --git a/couchpotato/core/providers/torrent/publichd/__init__.py b/couchpotato/core/providers/torrent/publichd/__init__.py index c28781e3..94d0825e 100644 --- a/couchpotato/core/providers/torrent/publichd/__init__.py +++ b/couchpotato/core/providers/torrent/publichd/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'providers', 'name': 'PublicHD', - 'description': 'Public Torrent site with only HD content.', + 'description': 'Public Torrent site with only HD content. See PublicHD', 'options': [ { 'name': 'enabled', @@ -20,4 +20,4 @@ config = [{ ], }, ], -}] \ No newline at end of file +}] diff --git a/couchpotato/core/providers/torrent/publichd/main.py b/couchpotato/core/providers/torrent/publichd/main.py index c962f935..cf0e6878 100644 --- a/couchpotato/core/providers/torrent/publichd/main.py +++ b/couchpotato/core/providers/torrent/publichd/main.py @@ -15,19 +15,9 @@ class PublicHD(TorrentProvider): urls = { 'test': 'http://publichd.eu', - 'download': 'http://publichd.eu/%s', 'detail': 'http://publichd.eu/index.php?page=torrent-details&id=%s', 'search': 'http://publichd.eu/index.php', } - - cat_ids = [ - ([9], ['bd50']), - ([5], ['1080p']), - ([2], ['720p']), - ([15, 16], ['brrip']), - ] - - cat_backup_id = 0 http_time_between_calls = 0 def search(self, movie, quality): @@ -39,9 +29,8 @@ class PublicHD(TorrentProvider): params = tryUrlencode({ 'page':'torrents', - 'search': getTitle(movie['library']) + ' ' + quality['identifier'], + 'search': '%s %s' % (getTitle(movie['library']), movie['library']['year']), 'active': 1, - 'category': self.getCatId(quality['identifier'])[0] }) url = '%s?%s' % (self.urls['search'], params) @@ -58,7 +47,7 @@ class PublicHD(TorrentProvider): for result in entries[2:len(entries) - 1]: info_url = result.find(href = re.compile('torrent-details')) - download = result.find(href = re.compile('\.torrent')) + download = result.find(href = re.compile('magnet:')) if info_url and download: @@ -67,12 +56,11 @@ class PublicHD(TorrentProvider): new = { 'id': url['id'][0], 'name': info_url.string, - 'type': 'torrent', + 'type': 'torrent_magnet', 'check_nzb': False, 'description': '', 'provider': self.getName(), - 'download': self.download, - 'url': self.urls['download'] % download['href'], + 'url': download['href'], 'detail_url': self.urls['detail'] % url['id'][0], 'size': self.parseSize(result.find_all('td')[7].string), 'seeders': tryInt(result.find_all('td')[4].string), @@ -82,7 +70,7 @@ class PublicHD(TorrentProvider): new['score'] = fireEvent('score.calculate', new, movie, single = True) is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, - imdb_results = False, single_category = False, single = True) + imdb_results = False, single = True) if is_correct_movie: results.append(new) diff --git a/couchpotato/core/providers/torrent/sceneaccess/__init__.py b/couchpotato/core/providers/torrent/sceneaccess/__init__.py index 3a128638..28b7ca33 100644 --- a/couchpotato/core/providers/torrent/sceneaccess/__init__.py +++ b/couchpotato/core/providers/torrent/sceneaccess/__init__.py @@ -10,6 +10,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'providers', 'name': 'SceneAccess', + 'description': 'See SceneAccess', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/providers/torrent/sceneaccess/main.py b/couchpotato/core/providers/torrent/sceneaccess/main.py index 62cd7dcd..39598d61 100644 --- a/couchpotato/core/providers/torrent/sceneaccess/main.py +++ b/couchpotato/core/providers/torrent/sceneaccess/main.py @@ -86,7 +86,7 @@ class SceneAccess(TorrentProvider): new['score'] = fireEvent('score.calculate', new, movie, single = True) is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, - imdb_results = False, single_category = False, single = True) + imdb_results = False, single = True) if is_correct_movie: results.append(new) diff --git a/couchpotato/core/providers/torrent/scenehd/__init__.py b/couchpotato/core/providers/torrent/scenehd/__init__.py index d4b7a0b8..c9f18be8 100644 --- a/couchpotato/core/providers/torrent/scenehd/__init__.py +++ b/couchpotato/core/providers/torrent/scenehd/__init__.py @@ -10,6 +10,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'providers', 'name': 'SceneHD', + 'description': 'See SceneHD', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/providers/torrent/scenehd/main.py b/couchpotato/core/providers/torrent/scenehd/main.py index 89d847d2..596cb5bb 100644 --- a/couchpotato/core/providers/torrent/scenehd/main.py +++ b/couchpotato/core/providers/torrent/scenehd/main.py @@ -79,7 +79,7 @@ class SceneHD(TorrentProvider): new['score'] = fireEvent('score.calculate', new, movie, single = True) is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, - imdb_results = imdb_results, single_category = False, single = True) + imdb_results = imdb_results, single = True) if is_correct_movie: results.append(new) diff --git a/couchpotato/core/providers/torrent/thepiratebay/__init__.py b/couchpotato/core/providers/torrent/thepiratebay/__init__.py index 8aa49111..2f8872e4 100644 --- a/couchpotato/core/providers/torrent/thepiratebay/__init__.py +++ b/couchpotato/core/providers/torrent/thepiratebay/__init__.py @@ -9,7 +9,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'providers', 'name': 'ThePirateBay', - 'description': 'The world\'s largest bittorrent tracker.', + 'description': 'The world\'s largest bittorrent tracker. See ThePirateBay', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/providers/torrent/thepiratebay/main.py b/couchpotato/core/providers/torrent/thepiratebay/main.py index cdb86a75..7b102958 100644 --- a/couchpotato/core/providers/torrent/thepiratebay/main.py +++ b/couchpotato/core/providers/torrent/thepiratebay/main.py @@ -120,7 +120,7 @@ class ThePirateBay(TorrentProvider): new['score'] = fireEvent('score.calculate', new, movie, single = True) is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, - imdb_results = False, single_category = False, single = True) + imdb_results = False, single = True) if is_correct_movie: results.append(new) diff --git a/couchpotato/core/providers/torrent/torrentleech/__init__.py b/couchpotato/core/providers/torrent/torrentleech/__init__.py index 19627e11..482dfda7 100644 --- a/couchpotato/core/providers/torrent/torrentleech/__init__.py +++ b/couchpotato/core/providers/torrent/torrentleech/__init__.py @@ -10,6 +10,7 @@ config = [{ 'tab': 'searcher', 'subtab': 'providers', 'name': 'TorrentLeech', + 'description': 'See TorrentLeech', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/providers/torrent/torrentleech/main.py b/couchpotato/core/providers/torrent/torrentleech/main.py index c174a3d4..0cfc7ce3 100644 --- a/couchpotato/core/providers/torrent/torrentleech/main.py +++ b/couchpotato/core/providers/torrent/torrentleech/main.py @@ -80,7 +80,7 @@ class TorrentLeech(TorrentProvider): new['score'] = fireEvent('score.calculate', new, movie, single = True) is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, - imdb_results = imdb_results, single_category = False, single = True) + imdb_results = imdb_results, single = True) if is_correct_movie: results.append(new) diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 553c0357..2ecb48ae 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -103,6 +103,22 @@ class Release(Entity): files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True) info = OneToMany('ReleaseInfo', cascade = 'all, delete-orphan') + def to_dict(self, deep = {}, exclude = []): + orig_dict = super(Release, self).to_dict(deep = deep, exclude = exclude) + + new_info = {} + for info in orig_dict.get('info', []): + + value = info['value'] + try: value = int(info['value']) + except: pass + + new_info[info['identifier']] = value + + orig_dict['info'] = new_info + + return orig_dict + class ReleaseInfo(Entity): """Properties that can be bound to a file for off-line usage""" diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index c0860a99..61ee45bd 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -29,6 +29,7 @@ var CouchPotato = new Class({ History.addEvent('change', self.openPage.bind(self)); self.c.addEvent('click:relay(a[href^=/]:not([target]))', self.pushState.bind(self)); + self.c.addEvent('click:relay(a[href^=http])', self.openDerefered.bind(self)); }, getOption: function(name){ @@ -187,7 +188,7 @@ var CouchPotato = new Class({ restart: function(message, title){ var self = this; - self.blockPage(message || 'Restarting... please wait. If this takes to long, something must have gone wrong.', title); + self.blockPage(message || 'Restarting... please wait. If this takes too long, something must have gone wrong.', title); Api.request('app.restart'); self.checkAvailable(1000); }, @@ -216,7 +217,7 @@ var CouchPotato = new Class({ Updater.check(onComplete) - self.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates'); + self.blockPage('Please wait. If this takes too long, something must have gone wrong.', 'Checking for updates'); self.checkAvailable(3000); }, @@ -269,6 +270,17 @@ var CouchPotato = new Class({ createUrl: function(action, params){ return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '') + }, + + openDerefered: function(e, el){ + (e).stop(); + + var url = 'http://www.dereferer.org/?' + el.get('href'); + + if(el.get('target') == '_blank' || (e.meta && Browser.Platform.mac) || (e.control && !Browser.Platform.mac)) + window.open(url); + else + window.location = url; } }); @@ -419,15 +431,18 @@ function randomString(length, extra) { return 0; }; - Array.implement('sortBy', function(){ - keyPaths.empty(); - Array.each(arguments, function(argument) { - switch (typeOf(argument)) { - case "array": saveKeyPath(argument); break; - case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break; - } - }); - return this.sort(comparer); + Array.implement({ + sortBy: function(){ + keyPaths.empty(); + + Array.each(arguments, function(argument) { + switch (typeOf(argument)) { + case "array": saveKeyPath(argument); break; + case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break; + } + }); + return this.sort(comparer); + } }); })(); diff --git a/couchpotato/static/scripts/page/about.js b/couchpotato/static/scripts/page/about.js index 93687b49..8ac38ba6 100644 --- a/couchpotato/static/scripts/page/about.js +++ b/couchpotato/static/scripts/page/about.js @@ -58,6 +58,8 @@ var AboutSettingTab = new Class({ } } }), + new Element('dt[text=Updater]'), + self.updater_type = new Element('dd.updater'), new Element('dt[text=ID]'), new Element('dd', {'text': App.getOption('pid')}), new Element('dt[text=Directories]'), @@ -103,12 +105,8 @@ var AboutSettingTab = new Class({ ), new Element('div.donate', { 'html': - 'Or, buy me a (24 pack) Pepsi, for while I\'m coding ;)' + - '
' + - '' + - '' + - '' + - '
' + 'Or support me via:' + + '' }) ); @@ -119,6 +117,7 @@ var AboutSettingTab = new Class({ var self = this; var date = new Date(json.version.date * 1000); self.version_text.set('text', json.version.hash + ' ('+date.toUTCString()+')'); + self.updater_type.set('text', json.version.type); } }); diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index 5f2dcf7e..3ef10d7a 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -31,7 +31,6 @@ window.addEvent('domready', function(){ 'IMDB': IMDBAction ,'Trailer': TrailerAction ,'Releases': ReleaseAction - ,'Edit': new Class({ Extends: MovieAction, @@ -74,20 +73,23 @@ window.addEvent('domready', function(){ new Element('option', { 'text': alt.title }).inject(self.title_select); - + if(alt['default']) self.title_select.set('value', alt.title); }); Quality.getActiveProfiles().each(function(profile){ + + var profile_id = profile.id ? profile.id : profile.data.id; + new Element('option', { - 'value': profile.id ? profile.id : profile.data.id, + 'value': profile_id, 'text': profile.label ? profile.label : profile.data.label }).inject(self.profile_select); - if(self.movie.profile) - self.profile_select.set('value', profile.id ? profile.id : profile.data.id); + if(self.movie.profile && self.movie.profile.data.id == profile_id) + self.profile_select.set('value', profile_id); }); } @@ -170,7 +172,7 @@ window.addEvent('domready', function(){ (e).preventDefault(); if(!self.delete_container){ - self.delete_container = new Element('div.delete_container').adopt( + self.delete_container = new Element('div.buttons.delete_container').adopt( new Element('a.cancel', { 'text': 'Cancel', 'events': {