diff --git a/.gitignore b/.gitignore index e134ddb..e156f87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.pyc /data/ -/_source/ \ No newline at end of file +/_source/ +.project +.pydevproject diff --git a/CouchPotato.py b/CouchPotato.py index 21b208a..b1620c0 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 2eb0f2f..b0e4222 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/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 980e247..3ddec1f 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -305,7 +305,7 @@ class SourceUpdater(BaseUpdater): if not os.path.isdir(dirname): self.makeDir(dirname) - os.rename(fromfile, tofile) + shutil.move(fromfile, tofile) try: existing_files.remove(tofile) except ValueError: diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 74e04de..3b8b333 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -1,9 +1,12 @@ +from base64 import b32decode, b16encode from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import toSafeString from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env import os +import random +import re log = CPLog(__name__) @@ -12,14 +15,25 @@ class Downloader(Plugin): type = [] + torrent_sources = [ + 'http://torrage.com/torrent/%s.torrent', + 'http://torrage.ws/torrent/%s.torrent', + 'http://torcache.net/torrent/%s.torrent', + ] + def __init__(self): addEvent('download', self.download) + addEvent('download.status', self.getDownloadStatus) + + def download(self, data = {}, movie = {}, manual = False, filedata = None): + pass - def download(self, data = {}): + 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 +55,26 @@ class Downloader(Plugin): return is_correct + def magnetToTorrent(self, magnet_link): + torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0] + + # Convert base 32 to hex + if len(torrent_hash) == 32: + torrent_hash = b16encode(b32decode(torrent_hash)) + + sources = self.torrent_sources + random.shuffle(sources) + + for source in sources: + try: + filedata = self.urlopen(source % torrent_hash, show_error = False) + return filedata + except: + log.debug('Torrent hash "%s" wasn\'t found on: %s', (torrent_hash, source)) + + log.error('Failed converting magnet url to torrent: %s', (torrent_hash)) + 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 a3f0361..ca38fc4 100644 --- a/couchpotato/core/downloaders/blackhole/main.py +++ b/couchpotato/core/downloaders/blackhole/main.py @@ -8,10 +8,12 @@ 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')])): + if self.isDisabled(manual) or \ + (not self.isCorrectType(data.get('type')) or \ + (not self.conf('use_for') in ['both', 'torrent' if 'torrent' in data.get('type') else data.get('type')])): return directory = self.conf('directory') @@ -20,8 +22,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 ac0ce05..927a9ff 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 64b2450..abff97d 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,100 @@ 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']: + 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']: + log.debug('Found %s in SabNZBd history, which has %s', (slot['name'], slot['status'])) + if slot['name'] == nzbname: + # Note: if post process even if failed is on in SabNZBd, it will complete with a fail message + if slot['status'] == 'Failed' or (slot['status'] == 'Completed' and 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/helpers/encoding.py b/couchpotato/core/helpers/encoding.py index 9b1f575..b4f4b70 100644 --- a/couchpotato/core/helpers/encoding.py +++ b/couchpotato/core/helpers/encoding.py @@ -2,6 +2,7 @@ from couchpotato.core.logger import CPLog from string import ascii_letters, digits from urllib import quote_plus import re +import traceback import unicodedata log = CPLog(__name__) @@ -30,8 +31,8 @@ def toUnicode(original, *args): return ek(original, *args) except: raise - except UnicodeDecodeError: - log.error('Unable to decode value: %s... ', repr(original)[:20]) + except: + log.error('Unable to decode value "%s..." : %s ', (repr(original)[:20], traceback.format_exc())) ascii_text = str(original).encode('string_escape') return toUnicode(ascii_text) diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index 595a39e..db9db84 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( + application = self.default_title, + event = self.event, + description = message, + priority = 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 4f6e62a..1c83bdd 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) - - return True + 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 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 c30ee99..f809c57 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 92a66ef..abaea65 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,11 @@ 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+'/', + 'target': '_blank' }); if(!self.id) self.disable(); - }, - - gotoIMDB: function(e){ - var self = this; - (e).preventDefault(); - - window.open('http://www.imdb.com/title/'+self.id+'/'); } }); @@ -318,13 +341,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 +352,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 +392,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 +444,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 +508,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 4f43ffe..76130cb 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 0abc065..9046034 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 5943954..c3afbc8 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -74,6 +74,22 @@ config = [{ 'default': False, }, { + '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', + }, + { + 'advanced': True, + 'name': 'next_on_failed', + 'default': True, + 'type': 'bool', + 'description': 'Try the next best release for a movie after a download failed.', + }, + { 'name': 'move_leftover', 'type': 'bool', 'description': 'Move all leftover file after renaming, to the movie folder.', @@ -86,15 +102,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 2cd09e4..a696265 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -6,13 +6,13 @@ from couchpotato.core.helpers.request import jsonified from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library, File, Profile +from couchpotato.core.settings.model import Library, File, Profile, Release from couchpotato.environment import Env +import errno import os import re import shutil import traceback -import errno log = CPLog(__name__) @@ -28,9 +28,11 @@ class Renamer(Plugin): }) addEvent('renamer.scan', self.scan) + addEvent('renamer.check_snatched', self.checkSnatched) + addEvent('app.load', self.scan) - fireEvent('schedule.interval', 'renamer.scan', self.scan, minutes = self.conf('run_every')) + fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every')) def scanView(self): @@ -314,6 +316,7 @@ class Renamer(Plugin): break # Remove files + delete_folders = [] for src in remove_files: if isinstance(src, File): @@ -327,10 +330,19 @@ class Renamer(Plugin): try: if os.path.isfile(src): os.remove(src) + + parent_dir = os.path.normpath(os.path.dirname(src)) + if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and destination != parent_dir: + delete_folders.append(parent_dir) + except: log.error('Failed removing %s: %s', (src, traceback.format_exc())) self.tagDir(group, 'failed_remove') + # Delete leftover folder from older releases + for delete_folder in delete_folders: + self.deleteEmptyFolder(delete_folder, show_error = False) + # Rename all files marked group['renamed_files'] = [] for src in rename_files: @@ -464,8 +476,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 +487,74 @@ 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())) + + 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) + + done_status = fireEvent('status.get', 'done', single = True) + + db = get_session() + rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all() + + if rels: + log.debug('Checking status snatched releases...') + + scan_required = 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 + + # Check if movie has already completed and is manage tab (legacy db correction) + if rel.movie.status_id == done_status.get('id'): + log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title) + rel.status_id = ignored_status.get('id') + db.commit() + continue + + 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.') + scan_required = True + else: + log.debug('Download status: %s' , downloadstatus) + + if downloadstatus == 'failed': + if self.conf('next_on_failed'): + fireEvent('searcher.try_next_release', movie_id = 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']) + scan_required = 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 scan_required: + fireEvent('renamer.scan') diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 7e47ed9..0bc4768 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/main.py b/couchpotato/core/plugins/searcher/main.py index dfefdab..d088c66 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -1,7 +1,9 @@ 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.variable import md5, getImdb, getTitle +from couchpotato.core.helpers.request import jsonified, getParam +from couchpotato.core.helpers.variable import md5, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Movie, Release, ReleaseInfo @@ -21,15 +23,24 @@ class Searcher(Plugin): in_progress = False def __init__(self): - addEvent('searcher.all', self.all_movies) + addEvent('searcher.all', self.allMovies) addEvent('searcher.single', self.single) addEvent('searcher.correct_movie', self.correctMovie) addEvent('searcher.download', self.download) + addEvent('searcher.try_next_release', self.tryNextRelease) + + 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.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) + - def all_movies(self): + def allMovies(self): if self.in_progress: log.info('Search already in progress') @@ -138,7 +149,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 +250,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 +282,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 @@ -317,14 +327,10 @@ class Searcher(Plugin): if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0): return True - # Get the nfo and see if it contains the proper imdb url - if self.checkNFO(nzb['name'], movie['library']['identifier']): - return True - log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year'])) return False - def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}, single_category = False): + def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}): name = nzb['name'] size = nzb.get('size', 0) @@ -355,9 +361,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): @@ -398,19 +401,6 @@ class Searcher(Plugin): return False - def checkNFO(self, check_name, imdb_id): - cache_key = 'srrdb.com %s' % simplifyString(check_name) - - nfo = self.getCache(cache_key) - if not nfo: - try: - nfo = self.urlopen('http://www.srrdb.com/showfile.php?release=%s' % check_name, show_error = False) - self.setCache(cache_key, nfo) - except: - pass - - return nfo and getImdb(nfo) == imdb_id - def couldBeReleased(self, wanted_quality, dates, pre_releases): now = int(time.time()) @@ -439,3 +429,37 @@ class Searcher(Plugin): return False + + 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: + db = get_session() + rels = db.query(Release).filter_by( + status_id = snatched_status.get('id'), + movie_id = movie_id + ).all() + + for rel in rels: + rel.status_id = ignored_status.get('id') + db.commit() + + movie_dict = fireEvent('movie.get', movie_id, single = True) + log.info('Trying next release for: %s', getTitle(movie_dict['library'])) + fireEvent('searcher.single', movie_dict) + + return True + + except: + log.error('Failed searching for next release: %s', traceback.format_exc()) + return False diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py index af2e879..91c2858 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/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py index 9c5f05f..e5999ab 100644 --- a/couchpotato/core/plugins/userscript/main.py +++ b/couchpotato/core/plugins/userscript/main.py @@ -15,7 +15,7 @@ log = CPLog(__name__) class Userscript(Plugin): - version = 2 + version = 3 def __init__(self): addApiView('userscript.get//', self.getUserScript, static = True) diff --git a/couchpotato/core/plugins/userscript/template.js b/couchpotato/core/plugins/userscript/template.js index 8b07272..c30fad5 100644 --- a/couchpotato/core/plugins/userscript/template.js +++ b/couchpotato/core/plugins/userscript/template.js @@ -1,6 +1,7 @@ // ==UserScript== // @name CouchPotato UserScript // @description Add movies like a real CouchPotato +// @grant none // @version {{version}} // @match {{host}}* @@ -44,21 +45,19 @@ function create() { return A; } -if (typeof GM_addStyle == 'undefined'){ - GM_addStyle = function(css) { - var head = document.getElementsByTagName('head')[0], - style = document.createElement('style'); - if (!head) - return; +var addStyle = function(css) { + var head = document.getElementsByTagName('head')[0], + style = document.createElement('style'); + if (!head) + return; - style.type = 'text/css'; - style.textContent = css; - head.appendChild(style); - } + style.type = 'text/css'; + style.textContent = css; + head.appendChild(style); } // Styles -GM_addStyle('\ +addStyle('\ #cp_popup { font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; -moz-border-radius: 6px 0px 0px 6px; -webkit-border-radius: 6px 0px 0px 6px; border-radius: 6px 0px 0px 6px; -moz-box-shadow: 0 0 20px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.5); box-shadow: 0 0 20px rgba(0,0,0,0.5); position:fixed; z-index:9999; bottom:0; right:0; font-size:15px; margin: 20px 0; display: block; background:#4E5969; } \ #cp_popup.opened { width: 492px; } \ #cp_popup a#add_to { cursor:pointer; text-align:center; text-decoration:none; color: #000; display:block; padding:5px 0 5px 5px; } \ diff --git a/couchpotato/core/providers/movie/_modifier/main.py b/couchpotato/core/providers/movie/_modifier/main.py index d088670..5af1659 100644 --- a/couchpotato/core/providers/movie/_modifier/main.py +++ b/couchpotato/core/providers/movie/_modifier/main.py @@ -31,7 +31,7 @@ class MovieResultModifier(Plugin): order.append(imdb) if item.get('via_imdb'): - if order.index(imdb): + if order.count(imdb): order.remove(imdb) order.insert(0, imdb) diff --git a/couchpotato/core/providers/movie/imdbapi/main.py b/couchpotato/core/providers/movie/imdbapi/main.py index c81a9ab..9238f45 100644 --- a/couchpotato/core/providers/movie/imdbapi/main.py +++ b/couchpotato/core/providers/movie/imdbapi/main.py @@ -27,7 +27,7 @@ class IMDBAPI(MovieProvider): name_year = fireEvent('scanner.name_year', q, single = True) - if not q or not name_year.get('name'): + if not q or not name_year or (name_year and not name_year.get('name')): return [] cache_key = 'imdbapi.cache.%s' % q diff --git a/couchpotato/core/providers/nzb/mysterbin/__init__.py b/couchpotato/core/providers/nzb/mysterbin/__init__.py index 0c75955..3f0d1d3 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 008f24f..edf2c70 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 4ebd849..06b2548 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 ff2aba8..5485c63 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 9c14e10..e3387a3 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 dedddb8..d047a8a 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 8a3261b..59b2730 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 3d6384c..ee2092c 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 82b6ef6..8fc6b40 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 c6c5be9..9539eac 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 0000000..70f3e3f --- /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 0000000..20a51d5 --- /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 6514643..2401f95 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 c57ae8a..6c157a6 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/passthepopcorn/__init__.py b/couchpotato/core/providers/torrent/passthepopcorn/__init__.py new file mode 100644 index 0000000..67bf666 --- /dev/null +++ b/couchpotato/core/providers/torrent/passthepopcorn/__init__.py @@ -0,0 +1,36 @@ +from main import PassThePopcorn + +def start(): + return PassThePopcorn() + +config = [{ + 'name': 'passthepopcorn', + 'groups': [{ + 'tab': 'searcher', + 'subtab': 'providers', + 'name': 'PassThePopcorn', + 'description': 'See PassThePopcorn.me', + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False + }, + { + 'name': 'domain', + 'advanced': True, + 'label': 'Proxy server', + 'description': 'Domain for requests (HTTPS only!), keep empty to use default (tls.passthepopcorn.me).', + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + } + ], + }] +}] diff --git a/couchpotato/core/providers/torrent/passthepopcorn/main.py b/couchpotato/core/providers/torrent/passthepopcorn/main.py new file mode 100644 index 0000000..f9416f7 --- /dev/null +++ b/couchpotato/core/providers/torrent/passthepopcorn/main.py @@ -0,0 +1,254 @@ +from couchpotato.core.event import fireEvent +from couchpotato.core.helpers.encoding import tryUrlencode +from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.torrent.base import TorrentProvider +from dateutil.parser import parse +import cookielib +import htmlentitydefs +import json +import re +import time +import traceback +import urllib2 + +log = CPLog(__name__) + + +class PassThePopcorn(TorrentProvider): + + urls = { + 'domain': 'https://tls.passthepopcorn.me', + 'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s', + 'torrent': 'https://tls.passthepopcorn.me/torrents.php', + 'login': 'https://tls.passthepopcorn.me/login.php', + 'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d' + } + + quality_search_params = { + 'bd50': {'media': 'Blu-ray', 'format': 'BD50'}, + '1080p': {'resolution': '1080p'}, + '720p': {'resolution': '720p'}, + 'brrip': {'media': 'Blu-ray'}, + 'dvdr': {'resolution': 'anysd'}, + 'dvdrip': {'media': 'DVD'}, + 'scr': {'media': 'DVD-Screener'}, + 'r5': {'media': 'R5'}, + 'tc': {'media': 'TC'}, + 'ts': {'media': 'TS'}, + 'cam': {'media': 'CAM'} + } + + post_search_filters = { + 'bd50': {'Codec': ['BD50']}, + '1080p': {'Resolution': ['1080p']}, + '720p': {'Resolution': ['720p']}, + 'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']}, + 'dvdr': {'Codec': ['DVD5', 'DVD9']}, + 'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']}, + 'scr': {'Source': ['DVD-Screener']}, + 'r5': {'Source': ['R5']}, + 'tc': {'Source': ['TC']}, + 'ts': {'Source': ['TS']}, + 'cam': {'Source': ['CAM']} + } + + class NotLoggedInHTTPError(urllib2.HTTPError): + def __init__(self, url, code, msg, headers, fp): + urllib2.HTTPError.__init__(self, url, code, msg, headers, fp) + + class PTPHTTPRedirectHandler(urllib2.HTTPRedirectHandler): + def http_error_302(self, req, fp, code, msg, headers): + log.debug("302 detected; redirected to %s" % headers['Location']) + if (headers['Location'] != 'login.php'): + return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers) + else: + raise PassThePopcorn.NotLoggedInHTTPError(req.get_full_url(), code, msg, headers, fp) + + def search(self, movie, quality): + + results = [] + + if self.isDisabled(): + return results + + movie_title = getTitle(movie['library']) + quality_id = quality['identifier'] + + log.info('Searching for %s at quality %s' % (movie_title, quality_id)) + + params = mergeDicts(self.quality_search_params[quality_id].copy(), { + 'order_by': 'relevance', + 'order_way': 'descending', + 'searchstr': movie['library']['identifier'] + }) + + # Do login for the cookies + if not self.login_opener and not self.login(): + return results + + try: + url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params)) + txt = self.urlopen(url, opener = self.login_opener) + res = json.loads(txt) + except: + log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)' % params) + return [] + + try: + if not 'Movies' in res: + log.info("PTP search returned nothing for '%s' at quality '%s' with search parameters %s" % (movie_title, quality_id, params)) + return [] + + authkey = res['AuthKey'] + passkey = res['PassKey'] + + for ptpmovie in res['Movies']: + if not 'Torrents' in ptpmovie: + log.debug('Movie %s (%s) has NO torrents' % (ptpmovie['Title'], ptpmovie['Year'])) + continue + + log.debug('Movie %s (%s) has %d torrents' % (ptpmovie['Title'], ptpmovie['Year'], len(ptpmovie['Torrents']))) + for torrent in ptpmovie['Torrents']: + torrent_id = tryInt(torrent['Id']) + torrentdesc = '%s %s %s' % (torrent['Resolution'], torrent['Source'], torrent['Codec']) + + if 'GoldenPopcorn' in torrent and torrent['GoldenPopcorn']: + torrentdesc += ' HQ' + if 'Scene' in torrent and torrent['Scene']: + torrentdesc += ' Scene' + if 'RemasterTitle' in torrent and torrent['RemasterTitle']: + # eliminate odd characters... + torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle']) + + torrentdesc += ' (%s)' % quality_id + torrent_name = re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) - %s' % (movie_title, ptpmovie['Year'], torrentdesc)) + + def extra_check(item): + return self.torrentMeetsQualitySpec(item, type) + + def extra_score(item): + return 50 if torrent['GoldenPopcorn'] else 0 + + new = { + 'id': torrent_id, + 'type': 'torrent', + 'provider': self.getName(), + 'name': torrent_name, + 'description': '', + 'url': '%s?action=download&id=%d&authkey=%s&torrent_pass=%s' % (self.urls['torrent'], torrent_id, authkey, passkey), + 'detail_url': self.urls['detail'] % torrent_id, + 'date': tryInt(time.mktime(parse(torrent['UploadTime']).timetuple())), + 'size': tryInt(torrent['Size']) / (1024 * 1024), + 'provider': self.getName(), + 'seeders': tryInt(torrent['Seeders']), + 'leechers': tryInt(torrent['Leechers']), + 'extra_score': extra_score, + 'extra_check': extra_check, + 'download': self.loginDownload, + } + + new['score'] = fireEvent('score.calculate', new, movie, single = True) + + if fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality): + results.append(new) + self.found(new) + + return results + except: + log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) + + return [] + + def login(self): + + cookieprocessor = urllib2.HTTPCookieProcessor(cookielib.CookieJar()) + opener = urllib2.build_opener(cookieprocessor, PassThePopcorn.PTPHTTPRedirectHandler()) + opener.addheaders = [ + ('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.75 Safari/537.1'), + ('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'), + ('Accept-Language', 'en-gb,en;q=0.5'), + ('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'), + ('Keep-Alive', '115'), + ('Connection', 'keep-alive'), + ('Cache-Control', 'max-age=0'), + ] + + try: + response = opener.open(self.urls['login'], self.getLoginParams()) + except urllib2.URLError as e: + log.error('Login to PassThePopcorn failed: %s' % e) + return False + + if response.getcode() == 200: + log.debug('Login HTTP status 200; seems successful') + self.login_opener = opener + return True + else: + log.error('Login to PassThePopcorn failed: returned code %d' % response.getcode()) + return False + + def torrentMeetsQualitySpec(self, torrent, quality): + + if not quality in self.post_search_filters: + return True + + for field, specs in self.post_search_filters[quality].items(): + matches_one = False + seen_one = False + + if not field in torrent: + log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"' % (torrent['Id'], field, quality)) + continue + + for spec in specs: + if len(spec) > 0 and spec[0] == '!': + # a negative rule; if the field matches, return False + if torrent[field] == spec[1:]: + return False + else: + # a positive rule; if any of the possible positive values match the field, return True + seen_one = True + if torrent[field] == spec: + matches_one = True + + if seen_one and not matches_one: + return False + + return True + + def htmlToUnicode(self, text): + def fixup(m): + text = m.group(0) + if text[:2] == "&#": + # character reference + try: + if text[:3] == "&#x": + return unichr(int(text[3:-1], 16)) + else: + return unichr(int(text[2:-1])) + except ValueError: + pass + else: + # named entity + try: + text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) + except KeyError: + pass + return text # leave as is + return re.sub("&#?\w+;", fixup, u'%s' % text) + + def unicodeToASCII(self, text): + import unicodedata + return ''.join(c for c in unicodedata.normalize('NFKD', text) if unicodedata.category(c) != 'Mn') + + def htmlToASCII(self, text): + return self.unicodeToASCII(self.htmlToUnicode(text)) + + def getLoginParams(self): + return tryUrlencode({ + 'username': self.conf('username'), + 'password': self.conf('password'), + 'keeplogged': '1', + 'login': 'Login' + }) diff --git a/couchpotato/core/providers/torrent/publichd/__init__.py b/couchpotato/core/providers/torrent/publichd/__init__.py index c28781e..94d0825 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 c962f93..cf0e687 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 3a12863..28b7ca3 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 62cd7dc..39598d6 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 d4b7a0b..c9f18be 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 89d847d..596cb5b 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 8aa4911..2f8872e 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 cdb86a7..a4ce363 100644 --- a/couchpotato/core/providers/torrent/thepiratebay/main.py +++ b/couchpotato/core/providers/torrent/thepiratebay/main.py @@ -37,6 +37,8 @@ class ThePirateBay(TorrentProvider): 'https://piratereverse.info', 'https://tpb.pirateparty.org.uk', 'https://argumentomteemigreren.nl', + 'https://livepirate.com/', + 'https://www.getpirate.com/', ] def __init__(self): @@ -120,7 +122,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 19627e1..482dfda 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 c174a3d..0cfc7ce 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/providers/trailer/hdtrailers/main.py b/couchpotato/core/providers/trailer/hdtrailers/main.py index d11f923..09e2403 100644 --- a/couchpotato/core/providers/trailer/hdtrailers/main.py +++ b/couchpotato/core/providers/trailer/hdtrailers/main.py @@ -3,7 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import mergeDicts, getTitle from couchpotato.core.logger import CPLog from couchpotato.core.providers.trailer.base import TrailerProvider -from string import letters, digits +from string import digits, ascii_letters import re log = CPLog(__name__) @@ -46,7 +46,7 @@ class HDTrailers(TrailerProvider): movie_name = getTitle(group['library']) - url = "%s?%s" % (self.url['backup'], tryUrlencode({'s':movie_name})) + url = "%s?%s" % (self.urls['backup'], tryUrlencode({'s':movie_name})) data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url) try: @@ -100,7 +100,7 @@ class HDTrailers(TrailerProvider): return results def movieUrlName(self, string): - safe_chars = letters + digits + ' ' + safe_chars = ascii_letters + digits + ' ' r = ''.join([char if char in safe_chars else ' ' for char in string]) name = re.sub('\s+' , '-', r).lower() diff --git a/couchpotato/core/providers/userscript/allocine/main.py b/couchpotato/core/providers/userscript/allocine/main.py index 890ae22..8cc889e 100644 --- a/couchpotato/core/providers/userscript/allocine/main.py +++ b/couchpotato/core/providers/userscript/allocine/main.py @@ -1,5 +1,9 @@ -from bs4 import BeautifulSoup +from couchpotato.core.logger import CPLog from couchpotato.core.providers.userscript.base import UserscriptBase +import traceback + +log = CPLog(__name__) + class AlloCine(UserscriptBase): @@ -15,11 +19,17 @@ class AlloCine(UserscriptBase): except: return - html = BeautifulSoup(data) - title = html.find('title').contents[0].strip() - split = title.split(') - ') + name = None + year = None - name = split[0][:-5].strip() - year = split[0][-4:] + try: + start = data.find('') + end = data.find('', start) + page_title = data[start + len(''):end].strip().split('-') + + name = page_title[0].strip() + year = page_title[1].strip()[-4:] + return self.search(name, year) + except: + log.error('Failed parsing page for title and year: %s', traceback.format_exc()) - return self.search(name, year) diff --git a/couchpotato/core/providers/userscript/rottentomatoes/main.py b/couchpotato/core/providers/userscript/rottentomatoes/main.py index c611779..0b16a44 100644 --- a/couchpotato/core/providers/userscript/rottentomatoes/main.py +++ b/couchpotato/core/providers/userscript/rottentomatoes/main.py @@ -1,6 +1,10 @@ -from bs4 import BeautifulSoup -from couchpotato.core.event import fireEvent +from couchpotato.core.logger import CPLog from couchpotato.core.providers.userscript.base import UserscriptBase +import re +import traceback + +log = CPLog(__name__) + class RottenTomatoes(UserscriptBase): @@ -16,7 +20,20 @@ class RottenTomatoes(UserscriptBase): except: return - html = BeautifulSoup(data) - title = html.find('span', {'itemprop':'name'}).text - info = fireEvent('scanner.name_year', title, single = True) - return self.search(info['name'], info['year']) + try: + name = None + year = None + metas = re.findall("property=\"(video:release_date|og:title)\" content=\"([^\"]*)\"", data) + + for meta in metas: + mname, mvalue = meta + if mname == 'og:title': + name = mvalue.decode('unicode_escape') + elif mname == 'video:release_date': + year = mvalue[:4] + + if name and year: + return self.search(name, year) + + except: + log.error('Failed parsing page for title and year: %s', traceback.format_exc()) diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 553c035..2ecb48a 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 c0860a9..61ee45b 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 93687b4..8ac38ba 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 ;)' + - '<form action="https://www.paypal.com/cgi-bin/webscr" method="post">' + - '<input type="hidden" name="cmd" value="_s-xclick">' + - '<input type="hidden" name="encrypted" value="-----BEGIN PKCS7-----MIIHPwYJKoZIhvcNAQcEoIIHMDCCBywCAQExggEwMIIBLAIBADCBlDCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20CAQAwDQYJKoZIhvcNAQEBBQAEgYBUq4nmDbyDV07WGd0wijGKDf/OWNA7hd2NRaxTaCVyAoaZQEGE0DQuDUHBBk7/oqWTo5Rcp1XN0A0nbYkrajWgY21lzSivGrDlWys1UjZaq0JOI89WWcy4YJMWX8chjECxicmVvk2OWgI/SOe7fhHdK4BNhQZO9ccLpfxTi2XnEDELMAkGBSsOAwIaBQAwgbwGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQI0YRtA8KWmG6AgZjKL/bDyL4JG3JN/GlKsb6863opfWLUjwJf7P7DeR10j0YZQds516TcRrSLqCSoII9KpivUUBCMknWmch8xUy4i0tyb26aNh3un7HQ6lVBQLGfnqVvKFC0iUNa6i0gTLufDKuVjzl+WkqqiOvgsg8rAE3IG2oYBCAAgzJbvyZkD4SoMr74pWAvQS19gwGG56JWNIdCy5eTXu6CCA4cwggODMIIC7KADAgECAgEAMA0GCSqGSIb3DQEBBQUAMIGOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC1BheVBhbCBJbmMuMRMwEQYDVQQLFApsaXZlX2NlcnRzMREwDwYDVQQDFAhsaXZlX2FwaTEcMBoGCSqGSIb3DQEJARYNcmVAcGF5cGFsLmNvbTAeFw0wNDAyMTMxMDEzMTVaFw0zNTAyMTMxMDEzMTVaMIGOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC1BheVBhbCBJbmMuMRMwEQYDVQQLFApsaXZlX2NlcnRzMREwDwYDVQQDFAhsaXZlX2FwaTEcMBoGCSqGSIb3DQEJARYNcmVAcGF5cGFsLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwUdO3fxEzEtcnI7ZKZL412XvZPugoni7i7D7prCe0AtaHTc97CYgm7NsAtJyxNLixmhLV8pyIEaiHXWAh8fPKW+R017+EmXrr9EaquPmsVvTywAAE1PMNOKqo2kl4Gxiz9zZqIajOm1fZGWcGS0f5JQ2kBqNbvbg2/Za+GJ/qwUCAwEAAaOB7jCB6zAdBgNVHQ4EFgQUlp98u8ZvF71ZP1LXChvsENZklGswgbsGA1UdIwSBszCBsIAUlp98u8ZvF71ZP1LXChvsENZklGuhgZSkgZEwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLUGF5UGFsIEluYy4xEzARBgNVBAsUCmxpdmVfY2VydHMxETAPBgNVBAMUCGxpdmVfYXBpMRwwGgYJKoZIhvcNAQkBFg1yZUBwYXlwYWwuY29tggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAgV86VpqAWuXvX6Oro4qJ1tYVIT5DgWpE692Ag422H7yRIr/9j/iKG4Thia/Oflx4TdL+IFJBAyPK9v6zZNZtBgPBynXb048hsP16l2vi0k5Q2JKiPDsEfBhGI+HnxLXEaUWAcVfCsQFvd2A1sxRr67ip5y2wwBelUecP3AjJ+YcxggGaMIIBlgIBATCBlDCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20CAQAwCQYFKw4DAhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTEwMDcyNjA4NDA0NlowIwYJKoZIhvcNAQkEMRYEFICseROR67FmINx7sa6IYP7eCVoaMA0GCSqGSIb3DQEBAQUABIGAfDx2KDyUHT6ISrTSnqtVWUHJWGjtM2T41m464maJ6nH7pEu6JZUHf53vD7Ey7d0MLFmF3IfGyIw2zAGfyEJHldeluPccFLhDmrDbRdxM0D/zwtWrYUwVXKQ4v3rskdp0avadX9ZRWrQplJkVsJDcLvRY4P/EhScBiA5ughJS7xc=-----END PKCS7-----">' + - '<input type="image" src="https://www.paypal.com/en_US/i/btn/btn_donate_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">' + - '</form>' + 'Or support me via:' + + '<iframe src="http://couchpota.to/donate.html" style="border:none; height: 200px;" scrolling="no"></iframe>' }) ); @@ -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 5f2dcf7..3ef10d7 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': { diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index b7fbaab..686c3b2 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -24,39 +24,36 @@ } .page.settings .tabs a { display: block; - padding: 11px 15px; + padding: 7px 15px; font-weight: normal; - transition: all 0.1s ease-in-out; + transition: all 0.3s ease-in-out; color: rgba(255, 255, 255, 0.8); + text-shadow: none; } - .page.settings .tabs a:hover, .page.settings .tabs .active a { + .page.settings .tabs a:hover, + .page.settings .tabs .active a { background: rgb(78, 89, 105); - font-weight: bold; - font-size: 25px; color: #fff; } + .page.settings .tabs > li { + border-bottom: 1px solid rgb(78, 89, 105); + } .page.settings .tabs .subtabs { list-style: none; padding: 0; - overflow: hidden; - transition: all 1s ease-in-out; - max-height: 0; + margin: -5px 0 10px; } - .page.settings .tabs > .active .subtabs { - max-height: 300px; - } .page.settings .tabs .subtabs a { - font-size: 15px; - padding: 1px 15px; + font-size: 13px; + padding: 0 15px; font-weight: normal; - color: rgba(255, 255, 255, 0.8); - background: rgba(78, 89, 105, 0.4); + transition: all .3s ease-in-out; + color: rgba(255, 255, 255, 0.7); } .page.settings .tabs .subtabs .active a { - font-weight: bold; color: #fff; background: rgb(78, 89, 105); } diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html index 0c228dc..80a095e 100644 --- a/couchpotato/templates/_desktop.html +++ b/couchpotato/templates/_desktop.html @@ -43,7 +43,7 @@ <link href="{{ url_for('web.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" /> <link rel="apple-touch-icon" href="{{ url_for('web.static', filename='images/homescreen.png') }}" /> - + <script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script> <script type="text/javascript"> @@ -51,7 +51,6 @@ new Uniform(); Api.setup({ - 'host': {{ fireEvent('app.api_url', single = True)|tojson|safe }}, 'url': {{ url_for('api.index')|tojson|safe }}, 'path_sep': {{ sep|tojson|safe }}, 'is_remote': false