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