diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py index a0d142b..f59a70b 100644 --- a/couchpotato/core/downloaders/blackhole/main.py +++ b/couchpotato/core/downloaders/blackhole/main.py @@ -27,18 +27,16 @@ class Blackhole(Downloader): try: if not os.path.isfile(fullPath): log.info('Downloading %s to %s.' % (data.get('type'), fullPath)) - if isfunction(data.get('download')): - file = data.get('download')() - else: - file = self.urlopen(data.get('url')) - if not file or file == '': + try: + file = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) + + with open(fullPath, 'wb') as f: + f.write(file) + except: log.debug('Failed download file: %s' % data.get('name')) return False - with open(fullPath, 'wb') as f: - f.write(file) - return True else: log.info('File %s already exists.' % fullPath) diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index e49522f..8d56a8f 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -6,6 +6,7 @@ from urllib import urlencode import base64 import os import re +import traceback log = CPLog(__name__) @@ -37,15 +38,11 @@ class Sabnzbd(Downloader): params = { 'apikey': self.conf('api_key'), 'cat': self.conf('category'), - 'mode': 'addurl', - 'name': data.get('url'), + 'mode': 'addfile', 'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)), } - # sabNzbd complains about "invalid archive file" for newzbin urls - # added using addurl, works fine with addid - if data.get('addbyid'): - params['mode'] = 'addid' + nzb_file = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) if pp: params['script'] = pp_script_fn @@ -53,9 +50,9 @@ class Sabnzbd(Downloader): url = cleanHost(self.conf('host')) + "api?" + urlencode(params) try: - data = self.urlopen(url) - except Exception, e: - log.error("Unable to connect to SAB: %s" % e) + data = self.urlopen(url, params = {"nzbfile": (params['nzbname'] + ".nzb", nzb_file)}, multipart = True) + except Exception: + log.error("Unable to connect to SAB: %s" % traceback.format_exc()) return False result = data.strip() @@ -63,7 +60,7 @@ class Sabnzbd(Downloader): log.error("SABnzbd didn't return anything.") return False - log.debug("Result text from SAB: " + result) + log.debug("Result text from SAB: " + result[:40]) if result == "ok": log.info("NZB sent to SAB successfully.") return True @@ -71,7 +68,7 @@ class Sabnzbd(Downloader): log.error("Incorrect username/password.") return False else: - log.error("Unknown error: " + result) + log.error("Unknown error: " + result[:40]) return False def buildPp(self, imdb_id): diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index e844a53..6e34215 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -4,7 +4,9 @@ from couchpotato.core.helpers.variable import getExt from couchpotato.core.logger import CPLog from couchpotato.environment import Env from flask.helpers import send_from_directory +from libs.multipartpost import MultipartPostHandler from urlparse import urlparse +import cookielib import glob import math import os.path @@ -80,7 +82,7 @@ class Plugin(object): return False # http request - def urlopen(self, url, timeout = 10, params = {}, headers = {}): + def urlopen(self, url, timeout = 10, params = {}, headers = {}, multipart = False): socket.setdefaulttimeout(timeout) @@ -88,15 +90,24 @@ class Plugin(object): self.wait(host) try: - log.info('Opening url: %s, params: %s' % (url, params)) - data = urllib.urlencode(params) if len(params) > 0 else None - request = urllib2.Request(url, data, headers) + if multipart: + log.info('Opening multipart url: %s, params: %s' % (url, params.iterkeys())) + request = urllib2.Request(url, params, headers) - data = urllib2.urlopen(request).read() + cookies = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler) + + data = opener.open(request).read() + else: + log.info('Opening url: %s, params: %s' % (url, params)) + data = urllib.urlencode(params) if len(params) > 0 else None + request = urllib2.Request(url, data, headers) + + data = urllib2.urlopen(request).read() except IOError, e: log.error('Failed opening url, %s: %s' % (url, e)) - data = None + raise self.http_last_use[host] = time.time() diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index d708869..ff5fd6a 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -1,4 +1,7 @@ -/* @override http://localhost:5000/static/movie_plugin/movie.css */ +/* @override + http://localhost:5000/static/movie_plugin/movie.css + http://192.168.1.20:5000/static/movie_plugin/movie.css +*/ .movies { padding: 20px 0; @@ -79,13 +82,21 @@ float: left; width: 5%; padding: 0 0 0 3%; - background: url('../images/rating.png') no-repeat left center; } .movies .info .description { clear: both; width: 95%; } + + .movies .data .quality span { + padding: 5px; + font-weight: bold; + } + + .movies .data .quality .available { color: orange; } + .movies .data .quality .snatched { color: lightgreen; } + .movies .data .actions { position: absolute; right: 15px; @@ -96,17 +107,14 @@ .movies .data:hover .action:hover { opacity: 1; } .movies .data .action { - background: no-repeat center; + background-repeat: no-repeat; + background-position: center; display: inline-block; width: 20px; height: 20px; padding: 3px; opacity: 0; } - .movies .data .action.refresh { background-image: url('../images/reload.png'); } - .movies .data .action.delete { background-image: url('../images/delete.png'); } - .movies .data .action.edit { background-image: url('../images/edit.png'); } - .movies .data .action.imdb { background-image: url('../images/imdb.png'); } .movies .delete_container { clear: both; @@ -142,6 +150,65 @@ padding: 2%; } + .movies .options .releases { + height: 157px; + overflow: auto; + margin: -20px -20px -20px 110px; + padding: 15px 0 5px; + } + .movies .options .releases .item { + border-bottom: 1px solid rgba(255,255,255,0.1); + } + .movies .options .releases .item:last-child { border: 0; } + .movies .options .releases .item:nth-child(even) { + background: rgba(255,255,255,0.05); + } + .movies .options .releases .item:not(.head):hover { + background: rgba(255,255,255,0.03); + } + + .movies .options .releases .item > * { + display: inline-block; + padding: 0 5px; + width: 50px; + min-height: 24px; + white-space: nowrap; + text-overflow: ellipsis; + -moz-text-overflow: ellipsis; + text-align: center; + vertical-align: top; + border-left: 1px solid rgba(255, 255, 255, 0.1); + } + .movies .options .releases .item > *:first-child { + border: 0; + } + .movies .options .releases .provider { + width: 120px; + } + .movies .options .releases .name { + width: 420px; + overflow: hidden; + text-align: left; + padding: 0 10px; + } + + .movies .options .releases a { + width: 16px !important; + height: 16px; + opacity: 0.8; + } + .movies .options .releases a:hover { + opacity: 1; + } + + .movies .options .releases .head > * { + font-weight: bold; + font-size: 14px; + padding-top: 4px; + padding-bottom: 4px; + height: auto; + } + .movies .alph_nav ul { list-style: none; padding: 0; diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index 2d24416..5b6bf2d 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -32,7 +32,7 @@ var Movie = new Class({ self.year = new Element('div.year', { 'text': self.data.library.year || 'Unknown' }), - self.rating = new Element('div.rating', { + self.rating = new Element('div.rating.icon', { 'text': self.data.library.rating }), self.description = new Element('div.description', { @@ -47,11 +47,18 @@ var Movie = new Class({ ); self.profile.get('types').each(function(type){ + + // Check if quality is snatched + var is_snatched = self.data.releases.filter(function(release){ + return release.quality_id == type.quality_id && release.status.identifier == 'snatched' + }).pick(); + var q = Quality.getQuality(type.quality_id); new Element('span', { - 'text': ' '+q.label + 'text': q.label, + 'class': is_snatched ? 'snatched' : '' }).inject(self.quality); - }) + }); Object.each(self.options.actions, function(action, key){ self.actions.adopt( @@ -127,7 +134,7 @@ var Movie = new Class({ var MovieAction = new Class({ - class_name: 'action', + class_name: 'action icon', initialize: function(movie){ var self = this; @@ -193,7 +200,7 @@ var ReleaseAction = new Class({ self.id = self.movie.get('identifier'); - self.el = new Element('a.releases', { + self.el = new Element('a.releases.icon.download', { 'title': 'Show the releases that are available for ' + self.movie.getTitle(), 'events': { 'click': self.show.bind(self) @@ -211,16 +218,77 @@ var ReleaseAction = new Class({ $(self.movie.thumbnail).clone(), self.release_container = new Element('div.releases') ).inject(self.movie, 'top'); + + // Header + new Element('div.item.head').adopt( + new Element('span.name', {'text': 'Release name'}), + new Element('span.quality', {'text': 'Quality'}), + new Element('span.age', {'text': 'Age'}), + new Element('span.score', {'text': 'Score'}), + new Element('span.provider', {'text': 'Provider'}) + ).inject(self.release_container) Array.each(self.movie.data.releases, function(release){ - p(release); new Element('div', { - 'text': release.title - }).inject(self.release_container) + 'class': 'item ' + release.status.identifier + }).adopt( + new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}), + new Element('span.quality', {'text': release.quality.label}), + 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')}), + new Element('a.download.icon', { + 'events': { + 'click': function(e){ + (e).stop(); + self.download(release); + } + } + }), + new Element('a.delete.icon', { + 'events': { + 'click': function(e){ + (e).stop(); + self.delete(release); + } + } + }) + ).inject(self.release_container) }); } self.movie.slide('in'); }, + + download: function(release){ + var self = this; + + p(release) + + Api.request('release.download', { + 'data': { + 'id': release.id + } + }); + }, + + delete: function(release){ + var self = this; + + Api.request('release.delete', { + 'data': { + 'id': release.id + } + }) + + }, + + get: function(release, type){ + var self = this; + + return (release.info.filter(function(info){ + return type == info.identifier + }).pick() || {}).value + } }); \ No newline at end of file diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js index 5e66d30..6748311 100644 --- a/couchpotato/core/plugins/movie/static/search.js +++ b/couchpotato/core/plugins/movie/static/search.js @@ -29,7 +29,7 @@ Block.Search = new Class({ }).adopt( new Element('div.pointer'), self.results = new Element('div.results') - ).fade('hide') + ).hide() ); self.spinner = new Spinner(self.result_container); @@ -52,7 +52,7 @@ Block.Search = new Class({ if(self.hidden == bool) return; - self.result_container.fade(bool ? 0 : 1) + self.result_container[bool ? 'hide' : 'show'](); if(bool){ History.removeEvent('change', self.hideResults.bind(self, !bool)); diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 6676d9f..d01dc36 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -13,7 +13,7 @@ log = CPLog(__name__) class QualityPlugin(Plugin): qualities = [ - {'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': []}, + {'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate']}, {'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']}, {'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']}, {'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']}, diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 96f7a3d..0749364 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -1,8 +1,10 @@ from couchpotato import get_session +from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent +from couchpotato.core.helpers.request import getParam, jsonified from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import File, Release, Movie +from couchpotato.core.settings.model import File, Release as Relea, Movie from sqlalchemy.sql.expression import and_, or_ log = CPLog(__name__) @@ -13,6 +15,8 @@ class Release(Plugin): def __init__(self): addEvent('release.add', self.add) + addApiView('release.download', self.download) + def add(self, group): db = get_session() @@ -30,22 +34,22 @@ class Release(Plugin): db.add(movie) db.commit() - # Add release + # Add Release snatched_status = fireEvent('status.get', 'snatched', single = True) - release = db.query(Release).filter( + rel = db.query(Relea).filter( or_( - Release.identifier == identifier, - and_(Release.identifier.startswith(group['library']['identifier'], Release.status_id == snatched_status.get('id'))) + Relea.identifier == identifier, + and_(Relea.identifier.startswith(group['library']['identifier'], Relea.status_id == snatched_status.get('id'))) ) ).first() - if not release: - release = Release( + if not rel: + rel = Relea( identifier = identifier, movie = movie, quality_id = group['meta_data']['quality'].get('id'), status_id = done_status.get('id') ) - db.add(release) + db.add(rel) db.commit() # Add each file type @@ -54,10 +58,10 @@ class Release(Plugin): added_file = self.saveFile(file, type = type, include_media_info = type is 'movie') try: added_file = db.query(File).filter_by(id = added_file.get('id')).one() - release.files.append(added_file) + Relea.files.append(added_file) db.commit() except Exception, e: - log.debug('Failed to attach "%s" to release: %s' % (file, e)) + log.debug('Failed to attach "%s" to Relea: %s' % (file, e)) db.remove() @@ -73,3 +77,34 @@ class Release(Plugin): # Check database and update/insert if necessary return fireEvent('file.add', path = file, part = self.getPartNumber(file), type = self.file_types[type], properties = properties, single = True) + def download(self): + + db = get_session() + id = getParam('id') + + rel = db.query(Relea).filter_by(id = id).first() + if rel: + item = {} + for info in rel.info: + item[info.identifier] = info.value + + # Get matching provider + provider = fireEvent('provider.belongs_to', item['url'], single = True) + item['download'] = provider.download + + fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({ + 'profile': {'types': {'quality': {}}}, + 'releases': {'status': {}, 'quality': {}}, + 'library': {'titles': {}, 'files':{}}, + 'files': {} + })) + + return jsonified({ + 'success': True + }) + else: + log.error('Couldn\'t find release with id: %s' % id) + + return jsonified({ + 'success': False + }) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index a3c1859..2715a85 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -4,7 +4,7 @@ from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import getExt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library +from couchpotato.core.settings.model import Library, Movie import os.path import re import shutil @@ -134,8 +134,18 @@ class Renamer(Plugin): # Move DVD files (no structure renaming) if group['is_dvd'] and file_type is 'movie': - structure_dir = file.split(group['dirname'])[-1].lstrip(os.path.sep) - rename_files[file] = os.path.join(destination, final_folder_name, structure_dir) + found = False + for top_dir in ['video_ts', 'audio_ts', 'bdmv', 'certificate']: + has_string = file.lower().find(os.path.sep + top_dir + os.path.sep) + if has_string >= 0: + structure_dir = file[has_string:].lstrip(os.path.sep) + rename_files[file] = os.path.join(destination, final_folder_name, structure_dir) + found = True + break + + if not found: + log.error('Could not determin dvd structure for: %s' % file) + # Do rename others else: rename_files[file] = os.path.join(destination, final_folder_name, final_file_name) @@ -162,15 +172,41 @@ class Renamer(Plugin): # Before renaming, remove the lower quality files db = get_session() + library = db.query(Library).filter_by(identifier = group['library']['identifier']).first() done_status = fireEvent('status.get', 'done', single = True) + active_status = fireEvent('status.get', 'active', single = True) + for movie in library.movies: + + # Mark movie "done" onces it found the quality with the finish check + try: + if movie.status_id == active_status.get('id'): + for type in movie.profile.types: + if type.quality_id == group['meta_data']['quality']['id'] and type.finish: + movie.status_id = done_status.get('id') + db.commit() + except Exception, e: + log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc())) + + # Go over current movie releases for release in movie.releases: + + # This is where CP removes older, lesser quality releases if release.quality.order > group['meta_data']['quality']['order']: log.info('Removing older release for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label)) + + for file in release.files: + log.info('Removing (not really) "%s"' % file.path) + + # When a release already exists elif release.status_id is done_status.get('id'): + + # Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc if release.quality.order is group['meta_data']['quality']['order']: log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label)) + + # Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan else: log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label)) @@ -190,10 +226,7 @@ class Renamer(Plugin): break - for file in release.files: - log.info('Removing (not really) "%s"' % file.path) - - # Rename + # Rename all files marked for src in rename_files: if rename_files[src]: diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 537f7b9..d4b8245 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -23,11 +23,11 @@ class Scanner(Plugin): 'trailer': 1048576, # 1MB } ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files - ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts'] + ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate'] extensions = { 'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'], 'dvd': ['vts_*', 'vob'], - 'nfo': ['nfo', 'txt', 'tag'], + 'nfo': ['nfo', 'nfo-orig', 'txt', 'tag'], 'subtitle': ['sub', 'srt', 'ssa', 'ass'], 'subtitle_extra': ['idx'], 'trailer': ['mov', 'mp4', 'flv'] @@ -210,13 +210,13 @@ class Scanner(Plugin): group['parentdir'] = os.path.dirname(movie_file) group['dirname'] = None - folders = group['parentdir'].replace(folder, '').split(os.path.sep) - folders.reverse() + folder_names = group['parentdir'].replace(folder, '').split(os.path.sep) + folder_names.reverse() # Try and get a proper dirname, so no "A", "Movie", "Download" etc - for folder in folders: - if folder.lower() not in self.ignore_names and len(folder) > 2: - group['dirname'] = folder + for folder_name in folder_names: + if folder_name.lower() not in self.ignore_names and len(folder_name) > 2: + group['dirname'] = folder_name break break @@ -426,7 +426,7 @@ class Scanner(Plugin): if list(set(file.lower().split(os.path.sep)) & set(['video_ts', 'audio_ts'])): return True - for needle in ['vts_', 'video_ts', 'audio_ts']: + for needle in ['vts_', 'video_ts', 'audio_ts', 'bdmv', 'certificate']: if needle in file.lower(): return True diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 2f1e993..877e56e 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -7,6 +7,7 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Movie, Release, ReleaseInfo from couchpotato.environment import Env import re +import traceback log = CPLog(__name__) @@ -17,6 +18,7 @@ class Searcher(Plugin): addEvent('searcher.all', self.all) addEvent('searcher.single', self.single) addEvent('searcher.correct_movie', self.correctMovie) + addEvent('searcher.download', self.download) # Schedule cronjob fireEvent('schedule.cron', 'searcher.all', self.all, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) @@ -34,7 +36,7 @@ class Searcher(Plugin): for movie in movies: - self.single(movie.to_dict(deep = { + self.single(movie.to_dict({ 'profile': {'types': {'quality': {}}}, 'releases': {'status': {}, 'quality': {}}, 'library': {'titles': {}, 'files':{}}, @@ -47,11 +49,8 @@ class Searcher(Plugin): def single(self, movie): - downloaded_status = fireEvent('status.get', 'downloaded', single = True) available_status = fireEvent('status.get', 'available', single = True) - snatched_status = fireEvent('status.get', 'snatched', single = True) - successful = False for type in movie['profile']['types']: has_better_quality = 0 @@ -85,37 +84,19 @@ class Searcher(Plugin): db.commit() for info in nzb: - rls_info = ReleaseInfo( - identifier = info, - value = nzb[info] - ) - rls.info.append(rls_info) + try: + rls_info = ReleaseInfo( + identifier = info, + value = nzb[info] + ) + rls.info.append(rls_info) + except Exception: + log.debug('Couldn\'t add %s to ReleaseInfo: %s' % (info, traceback.format_exc())) db.commit() for nzb in sorted_results: - successful = fireEvent('download', data = nzb, movie = movie, single = True) - - if successful: - - # Mark release as snatched - db = get_session() - rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first() - rls.status_id = snatched_status.get('id') - db.commit() - - # Mark movie snatched if quality is finish-checked - if type['finish']: - mvie = db.query(Movie).filter_by(id = movie['id']).first() - mvie.status_id = snatched_status.get('id') - db.commit() - - log.info('Downloading of %s successful.' % nzb.get('name')) - fireEvent('movie.snatched', message = 'Downloading of %s successful.' % nzb.get('name'), data = rls.to_dict()) - - return True - - return False + return self.download(data = nzb, movie = movie) else: log.info('Better quality (%s) already available or snatched for %s' % (type['quality']['label'], default_title)) break @@ -126,6 +107,26 @@ class Searcher(Plugin): return False + def download(self, data, movie): + + snatched_status = fireEvent('status.get', 'snatched', single = True) + + successful = fireEvent('download', data = data, movie = movie, single = True) + + if successful: + + # Mark release as snatched + db = get_session() + rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() + rls.status_id = snatched_status.get('id') + db.commit() + + log.info('Downloading of %s successful.' % data.get('name')) + fireEvent('movie.snatched', message = 'Downloading of %s successful.' % data.get('name'), data = rls.to_dict()) + + return True + + return False def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs): diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index df27865..7e08440 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -2,7 +2,6 @@ from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env -from urllib2 import URLError from urlparse import urlparse import re import time @@ -60,6 +59,20 @@ class YarrProvider(Provider): sizeMb = ['mb', 'mib'] sizeKb = ['kb', 'kib'] + def __init__(self): + addEvent('provider.belongs_to', self.belongsTo) + + def belongsTo(self, url, host = None): + try: + hostname = urlparse(url).hostname + download_url = host if host else self.urls['download'] + if hostname in download_url: + return self + except: + log.debug('Url % s doesn\'t belong to %s' % (url, self.getName())) + + return + def parseSize(self, size): sizeRaw = size.lower() @@ -96,11 +109,16 @@ class NZBProvider(YarrProvider): type = 'nzb' def __init__(self): + super(NZBProvider, self).__init__() + addEvent('provider.nzb.search', self.search) addEvent('provider.yarr.search', self.search) addEvent('provider.nzb.feed', self.feed) + def download(self, url = '', nzb_id = ''): + return self.urlopen(url) + def feed(self): return [] diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py index 8933be9..e7e157a 100644 --- a/couchpotato/core/providers/nzb/newzbin/main.py +++ b/couchpotato/core/providers/nzb/newzbin/main.py @@ -13,10 +13,9 @@ log = CPLog(__name__) class Newzbin(NZBProvider, RSS): urls = { - 'search': 'https://www.newzbin.com/search/', 'download': 'http://www.newzbin.com/api/dnzb/', + 'search': 'https://www.newzbin.com/search/', } - searchUrl = 'https://www.newzbin.com/search/' format_ids = { 2: ['scr'], @@ -36,7 +35,7 @@ class Newzbin(NZBProvider, RSS): def search(self, movie, quality): results = [] - if self.isDisabled() or not self.isAvailable(self.searchUrl): + if self.isDisabled() or not self.isAvailable(self.urls['search']): return results format_id = self.getFormatId(type) @@ -97,11 +96,12 @@ class Newzbin(NZBProvider, RSS): new = { 'id': id, 'type': 'nzb', + 'provider': self.getName(), 'name': title, 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'size': self.parseSize(size), 'url': str(self.getTextElement(nzb, '{%s}nzb' % REPORT_NS)), - 'download': lambda: self.download(id), + 'download': self.download, 'detail_url': str(self.getTextElement(nzb, 'link')), 'description': self.getTextElement(nzb, "description"), 'check_nzb': False, @@ -121,7 +121,7 @@ class Newzbin(NZBProvider, RSS): return results - def download(self, nzb_id): + def download(self, url = '', nzb_id = ''): try: log.info('Download nzb from newzbin, report id: %s ' % nzb_id) diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index 17ac383..028162e 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -5,6 +5,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import NZBProvider from dateutil.parser import parse from urllib import urlencode +from urlparse import urlparse import time import xml.etree.ElementTree as XMLTree @@ -130,11 +131,13 @@ class Newznab(NZBProvider, RSS): id = self.getTextElement(nzb, "guid").split('/')[-1:].pop() new = { 'id': id, + 'provider': self.getName(), 'type': 'nzb', 'name': self.getTextElement(nzb, "title"), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'size': int(size) / 1024 / 1024, 'url': (self.getUrl(host['host'], self.urls['download']) % id) + self.getApiExt(host), + 'download': self.download, 'detail_url': (self.getUrl(host['host'], self.urls['detail']) % id) + self.getApiExt(host), 'content': self.getTextElement(nzb, "description"), } @@ -173,6 +176,17 @@ class Newznab(NZBProvider, RSS): return list + def belongsTo(self, url): + + hosts = self.getHosts() + + for host in hosts: + result = super(Newznab, self).belongsTo(url, host = host['host']) + if result: + return result + + return + def getUrl(self, host, type): return cleanHost(host) + 'api?t=' + type diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py index a1b377c..f44e586 100644 --- a/couchpotato/core/providers/nzb/nzbindex/main.py +++ b/couchpotato/core/providers/nzb/nzbindex/main.py @@ -14,7 +14,7 @@ log = CPLog(__name__) class NzbIndex(NZBProvider, RSS): urls = { - 'download': 'http://www.nzbindex.nl/download/%s/%s', + 'download': 'http://www.nzbindex.nl/download/', 'api': 'http://www.nzbindex.nl/rss/', #http://www.nzbindex.nl/rss/?q=due+date+720p&age=1000&sort=agedesc&minsize=3500&maxsize=10000 } @@ -63,10 +63,12 @@ class NzbIndex(NZBProvider, RSS): new = { 'id': id, 'type': 'nzb', + 'provider': self.getName(), 'name': self.getTextElement(nzb, "title"), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'size': enclosure['length'], 'url': enclosure['url'], + 'download': self.download, 'detail_url': enclosure['url'].replace('/download/', '/release/'), 'description': self.getTextElement(nzb, "description"), 'check_nzb': True, diff --git a/couchpotato/core/providers/nzb/nzbmatrix/main.py b/couchpotato/core/providers/nzb/nzbmatrix/main.py index 1104915..da602ad 100644 --- a/couchpotato/core/providers/nzb/nzbmatrix/main.py +++ b/couchpotato/core/providers/nzb/nzbmatrix/main.py @@ -81,10 +81,12 @@ class NZBMatrix(NZBProvider, RSS): new = { 'id': id, 'type': 'nzb', + 'provider': self.getName(), 'name': title, 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'size': self.parseSize(size), 'url': self.urls['download'] % id + self.getApiExt(), + 'download': self.download, 'detail_url': self.urls['detail'] % id, 'description': self.getTextElement(nzb, "description"), 'check_nzb': True, diff --git a/couchpotato/core/providers/nzb/nzbs/main.py b/couchpotato/core/providers/nzb/nzbs/main.py index 8648932..a86f9d7 100644 --- a/couchpotato/core/providers/nzb/nzbs/main.py +++ b/couchpotato/core/providers/nzb/nzbs/main.py @@ -71,10 +71,12 @@ class Nzbs(NZBProvider, RSS): new = { 'id': id, 'type': 'nzb', + 'provider': self.getName(), 'name': self.getTextElement(nzb, "title"), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'size': self.parseSize(self.getTextElement(nzb, "description").split('
')[1].split('">')[1]), 'url': self.urls['download'] % (id, self.getApiExt()), + 'download': self.download, 'detail_url': self.urls['detail'] % id, 'description': self.getTextElement(nzb, "description"), 'check_nzb': True, diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 4b96bfe..b2efd06 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -47,6 +47,9 @@ class Library(Entity): files = ManyToMany('File') info = OneToMany('LibraryInfo') + def title(self): + return self.titles[0]['title'] + class LibraryInfo(Entity): """""" diff --git a/couchpotato/static/images/edit.png b/couchpotato/static/images/edit.png deleted file mode 100644 index 5319252..0000000 Binary files a/couchpotato/static/images/edit.png and /dev/null differ diff --git a/couchpotato/static/images/check.png b/couchpotato/static/images/icon.check.png similarity index 100% rename from couchpotato/static/images/check.png rename to couchpotato/static/images/icon.check.png diff --git a/couchpotato/static/images/delete.png b/couchpotato/static/images/icon.delete.png similarity index 100% rename from couchpotato/static/images/delete.png rename to couchpotato/static/images/icon.delete.png diff --git a/couchpotato/static/images/icon.download.png b/couchpotato/static/images/icon.download.png new file mode 100644 index 0000000..ca3d043 Binary files /dev/null and b/couchpotato/static/images/icon.download.png differ diff --git a/couchpotato/static/images/icon.edit.png b/couchpotato/static/images/icon.edit.png new file mode 100644 index 0000000..19ff8bd Binary files /dev/null and b/couchpotato/static/images/icon.edit.png differ diff --git a/couchpotato/static/images/imdb.png b/couchpotato/static/images/icon.imdb.png similarity index 100% rename from couchpotato/static/images/imdb.png rename to couchpotato/static/images/icon.imdb.png diff --git a/couchpotato/static/images/rating.png b/couchpotato/static/images/icon.rating.png similarity index 100% rename from couchpotato/static/images/rating.png rename to couchpotato/static/images/icon.rating.png diff --git a/couchpotato/static/images/icon.refresh.png b/couchpotato/static/images/icon.refresh.png new file mode 100644 index 0000000..257cfee Binary files /dev/null and b/couchpotato/static/images/icon.refresh.png differ diff --git a/couchpotato/static/images/reload.png b/couchpotato/static/images/reload.png deleted file mode 100644 index 031f2fd..0000000 Binary files a/couchpotato/static/images/reload.png and /dev/null differ diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index da98232..b1a60c6 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -27,7 +27,7 @@ var MovieActions = {}; MovieActions.Wanted = { 'IMBD': IMDBAction - //,'releases': ReleaseAction + ,'releases': ReleaseAction ,'Edit': new Class({ @@ -207,7 +207,6 @@ MovieActions.Wanted = { self.chain( function(){ - $(movie).mask().addClass('loading'); self.callChain(); }, function(){ @@ -236,6 +235,5 @@ MovieActions.Wanted = { MovieActions.Snatched = { 'IMBD': IMDBAction - ,'Releases': ReleaseAction ,'Delete': MovieActions.Wanted.Delete }; \ No newline at end of file diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index e0d9c38..293c568 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -137,10 +137,18 @@ form { } /*** Icons ***/ -.icon.delete { - background: url('../images/delete.png') no-repeat; +.icon { display: inline-block; + background: center no-repeat; } +.icon.delete { background-image: url('../images/icon.delete.png'); } +.icon.download { background-image: url('../images/icon.download.png'); } +.icon.edit { background-image: url('../images/icon.edit.png'); } +.icon.check { background-image: url('../images/icon.check.png'); } +.icon.folder { background-image: url('../images/icon.folder.png'); } +.icon.imdb { background-image: url('../images/icon.imdb.png'); } +.icon.refresh { background-image: url('../images/icon.refresh.png'); } +.icon.rating { background-image: url('../images/icon.rating.png'); } /*** Navigation ***/ .header { diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index 2ce8954..ae93a2f 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -103,7 +103,7 @@ border: 0; } .page.settings .ctrlHolder.save_success:not(:first-child) { - background: url('../../images/check.png') no-repeat 7px center; + background: url('../../images/icon.check.png') no-repeat 7px center; } .page.settings .ctrlHolder:last-child { border: none; } .page.settings .ctrlHolder:hover { background-color: rgba(255,255,255,0.05); } diff --git a/libs/multipartpost.py b/libs/multipartpost.py new file mode 100644 index 0000000..38dfbd1 --- /dev/null +++ b/libs/multipartpost.py @@ -0,0 +1,88 @@ +#!/usr/bin/python + +#### +# 06/2010 Nic Wolfe +# 02/2006 Will Holcomb +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# + +import urllib +import urllib2 +import mimetools, mimetypes +import os, sys + +# Controls how sequences are uncoded. If true, elements may be given multiple values by +# assigning a sequence. +doseq = 1 + +class MultipartPostHandler(urllib2.BaseHandler): + handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first + + def http_request(self, request): + data = request.get_data() + if data is not None and type(data) != str: + v_files = [] + v_vars = [] + try: + for(key, value) in data.items(): + if type(value) in (file, list, tuple): + v_files.append((key, value)) + else: + v_vars.append((key, value)) + except TypeError: + systype, value, traceback = sys.exc_info() + raise TypeError, "not a valid non-string sequence or mapping object", traceback + + if len(v_files) == 0: + data = urllib.urlencode(v_vars, doseq) + else: + boundary, data = MultipartPostHandler.multipart_encode(v_vars, v_files) + contenttype = 'multipart/form-data; boundary=%s' % boundary + if(request.has_header('Content-Type') + and request.get_header('Content-Type').find('multipart/form-data') != 0): + print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data') + request.add_unredirected_header('Content-Type', contenttype) + + request.add_data(data) + return request + + @staticmethod + def multipart_encode(vars, files, boundary = None, buffer = None): + if boundary is None: + boundary = mimetools.choose_boundary() + if buffer is None: + buffer = '' + for(key, value) in vars: + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"' % key + buffer += '\r\n\r\n' + value + '\r\n' + for(key, fd) in files: + + # allow them to pass in a file or a tuple with name & data + if type(fd) == file: + name_in = fd.name + fd.seek(0) + data_in = fd.read() + elif type(fd) in (tuple, list): + name_in, data_in = fd + + filename = os.path.basename(name_in) + contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename) + buffer += 'Content-Type: %s\r\n' % contenttype + # buffer += 'Content-Length: %s\r\n' % file_size + buffer += '\r\n' + data_in + '\r\n' + buffer += '--%s--\r\n\r\n' % boundary + return boundary, buffer + + https_request = http_request