Browse Source

Downloading of single releases and more

pull/51/merge
Ruud 14 years ago
parent
commit
cb5fc694ab
  1. 12
      couchpotato/core/downloaders/blackhole/main.py
  2. 19
      couchpotato/core/downloaders/sabnzbd/main.py
  3. 17
      couchpotato/core/plugins/base.py
  4. 81
      couchpotato/core/plugins/movie/static/movie.css
  5. 84
      couchpotato/core/plugins/movie/static/movie.js
  6. 4
      couchpotato/core/plugins/movie/static/search.js
  7. 2
      couchpotato/core/plugins/quality/main.py
  8. 55
      couchpotato/core/plugins/release/main.py
  9. 45
      couchpotato/core/plugins/renamer/main.py
  10. 16
      couchpotato/core/plugins/scanner/main.py
  11. 49
      couchpotato/core/plugins/searcher/main.py
  12. 20
      couchpotato/core/providers/base.py
  13. 10
      couchpotato/core/providers/nzb/newzbin/main.py
  14. 14
      couchpotato/core/providers/nzb/newznab/main.py
  15. 4
      couchpotato/core/providers/nzb/nzbindex/main.py
  16. 2
      couchpotato/core/providers/nzb/nzbmatrix/main.py
  17. 2
      couchpotato/core/providers/nzb/nzbs/main.py
  18. 3
      couchpotato/core/settings/model.py
  19. BIN
      couchpotato/static/images/edit.png
  20. 0
      couchpotato/static/images/icon.check.png
  21. 0
      couchpotato/static/images/icon.delete.png
  22. BIN
      couchpotato/static/images/icon.download.png
  23. BIN
      couchpotato/static/images/icon.edit.png
  24. 0
      couchpotato/static/images/icon.imdb.png
  25. 0
      couchpotato/static/images/icon.rating.png
  26. BIN
      couchpotato/static/images/icon.refresh.png
  27. BIN
      couchpotato/static/images/reload.png
  28. 4
      couchpotato/static/scripts/page/wanted.js
  29. 12
      couchpotato/static/style/main.css
  30. 2
      couchpotato/static/style/page/settings.css
  31. 88
      libs/multipartpost.py

12
couchpotato/core/downloaders/blackhole/main.py

@ -27,17 +27,15 @@ class Blackhole(Downloader):
try: try:
if not os.path.isfile(fullPath): if not os.path.isfile(fullPath):
log.info('Downloading %s to %s.' % (data.get('type'), 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:
log.debug('Failed download file: %s' % data.get('name')) file = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
return False
with open(fullPath, 'wb') as f: with open(fullPath, 'wb') as f:
f.write(file) f.write(file)
except:
log.debug('Failed download file: %s' % data.get('name'))
return False
return True return True
else: else:

19
couchpotato/core/downloaders/sabnzbd/main.py

@ -6,6 +6,7 @@ from urllib import urlencode
import base64 import base64
import os import os
import re import re
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@ -37,15 +38,11 @@ class Sabnzbd(Downloader):
params = { params = {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
'cat': self.conf('category'), 'cat': self.conf('category'),
'mode': 'addurl', 'mode': 'addfile',
'name': data.get('url'),
'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)), 'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)),
} }
# sabNzbd complains about "invalid archive file" for newzbin urls nzb_file = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
# added using addurl, works fine with addid
if data.get('addbyid'):
params['mode'] = 'addid'
if pp: if pp:
params['script'] = pp_script_fn params['script'] = pp_script_fn
@ -53,9 +50,9 @@ class Sabnzbd(Downloader):
url = cleanHost(self.conf('host')) + "api?" + urlencode(params) url = cleanHost(self.conf('host')) + "api?" + urlencode(params)
try: try:
data = self.urlopen(url) data = self.urlopen(url, params = {"nzbfile": (params['nzbname'] + ".nzb", nzb_file)}, multipart = True)
except Exception, e: except Exception:
log.error("Unable to connect to SAB: %s" % e) log.error("Unable to connect to SAB: %s" % traceback.format_exc())
return False return False
result = data.strip() result = data.strip()
@ -63,7 +60,7 @@ class Sabnzbd(Downloader):
log.error("SABnzbd didn't return anything.") log.error("SABnzbd didn't return anything.")
return False return False
log.debug("Result text from SAB: " + result) log.debug("Result text from SAB: " + result[:40])
if result == "ok": if result == "ok":
log.info("NZB sent to SAB successfully.") log.info("NZB sent to SAB successfully.")
return True return True
@ -71,7 +68,7 @@ class Sabnzbd(Downloader):
log.error("Incorrect username/password.") log.error("Incorrect username/password.")
return False return False
else: else:
log.error("Unknown error: " + result) log.error("Unknown error: " + result[:40])
return False return False
def buildPp(self, imdb_id): def buildPp(self, imdb_id):

17
couchpotato/core/plugins/base.py

@ -4,7 +4,9 @@ from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.helpers import send_from_directory from flask.helpers import send_from_directory
from libs.multipartpost import MultipartPostHandler
from urlparse import urlparse from urlparse import urlparse
import cookielib
import glob import glob
import math import math
import os.path import os.path
@ -80,7 +82,7 @@ class Plugin(object):
return False return False
# http request # http request
def urlopen(self, url, timeout = 10, params = {}, headers = {}): def urlopen(self, url, timeout = 10, params = {}, headers = {}, multipart = False):
socket.setdefaulttimeout(timeout) socket.setdefaulttimeout(timeout)
@ -88,15 +90,24 @@ class Plugin(object):
self.wait(host) self.wait(host)
try: try:
log.info('Opening url: %s, params: %s' % (url, params))
if multipart:
log.info('Opening multipart url: %s, params: %s' % (url, params.iterkeys()))
request = urllib2.Request(url, params, headers)
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 data = urllib.urlencode(params) if len(params) > 0 else None
request = urllib2.Request(url, data, headers) request = urllib2.Request(url, data, headers)
data = urllib2.urlopen(request).read() data = urllib2.urlopen(request).read()
except IOError, e: except IOError, e:
log.error('Failed opening url, %s: %s' % (url, e)) log.error('Failed opening url, %s: %s' % (url, e))
data = None raise
self.http_last_use[host] = time.time() self.http_last_use[host] = time.time()

81
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 { .movies {
padding: 20px 0; padding: 20px 0;
@ -79,13 +82,21 @@
float: left; float: left;
width: 5%; width: 5%;
padding: 0 0 0 3%; padding: 0 0 0 3%;
background: url('../images/rating.png') no-repeat left center;
} }
.movies .info .description { .movies .info .description {
clear: both; clear: both;
width: 95%; 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 { .movies .data .actions {
position: absolute; position: absolute;
right: 15px; right: 15px;
@ -96,17 +107,14 @@
.movies .data:hover .action:hover { opacity: 1; } .movies .data:hover .action:hover { opacity: 1; }
.movies .data .action { .movies .data .action {
background: no-repeat center; background-repeat: no-repeat;
background-position: center;
display: inline-block; display: inline-block;
width: 20px; width: 20px;
height: 20px; height: 20px;
padding: 3px; padding: 3px;
opacity: 0; 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 { .movies .delete_container {
clear: both; clear: both;
@ -142,6 +150,65 @@
padding: 2%; 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 { .movies .alph_nav ul {
list-style: none; list-style: none;
padding: 0; padding: 0;

84
couchpotato/core/plugins/movie/static/movie.js

@ -32,7 +32,7 @@ var Movie = new Class({
self.year = new Element('div.year', { self.year = new Element('div.year', {
'text': self.data.library.year || 'Unknown' 'text': self.data.library.year || 'Unknown'
}), }),
self.rating = new Element('div.rating', { self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating 'text': self.data.library.rating
}), }),
self.description = new Element('div.description', { self.description = new Element('div.description', {
@ -47,11 +47,18 @@ var Movie = new Class({
); );
self.profile.get('types').each(function(type){ 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); var q = Quality.getQuality(type.quality_id);
new Element('span', { new Element('span', {
'text': ' '+q.label 'text': q.label,
'class': is_snatched ? 'snatched' : ''
}).inject(self.quality); }).inject(self.quality);
}) });
Object.each(self.options.actions, function(action, key){ Object.each(self.options.actions, function(action, key){
self.actions.adopt( self.actions.adopt(
@ -127,7 +134,7 @@ var Movie = new Class({
var MovieAction = new Class({ var MovieAction = new Class({
class_name: 'action', class_name: 'action icon',
initialize: function(movie){ initialize: function(movie){
var self = this; var self = this;
@ -193,7 +200,7 @@ var ReleaseAction = new Class({
self.id = self.movie.get('identifier'); 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(), 'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': { 'events': {
'click': self.show.bind(self) 'click': self.show.bind(self)
@ -212,15 +219,76 @@ var ReleaseAction = new Class({
self.release_container = new Element('div.releases') self.release_container = new Element('div.releases')
).inject(self.movie, 'top'); ).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){ Array.each(self.movie.data.releases, function(release){
p(release);
new Element('div', { new Element('div', {
'text': release.title 'class': 'item ' + release.status.identifier
}).inject(self.release_container) }).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'); 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
}
}); });

4
couchpotato/core/plugins/movie/static/search.js

@ -29,7 +29,7 @@ Block.Search = new Class({
}).adopt( }).adopt(
new Element('div.pointer'), new Element('div.pointer'),
self.results = new Element('div.results') self.results = new Element('div.results')
).fade('hide') ).hide()
); );
self.spinner = new Spinner(self.result_container); self.spinner = new Spinner(self.result_container);
@ -52,7 +52,7 @@ Block.Search = new Class({
if(self.hidden == bool) return; if(self.hidden == bool) return;
self.result_container.fade(bool ? 0 : 1) self.result_container[bool ? 'hide' : 'show']();
if(bool){ if(bool){
History.removeEvent('change', self.hideResults.bind(self, !bool)); History.removeEvent('change', self.hideResults.bind(self, !bool));

2
couchpotato/core/plugins/quality/main.py

@ -13,7 +13,7 @@ log = CPLog(__name__)
class QualityPlugin(Plugin): class QualityPlugin(Plugin):
qualities = [ 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': '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': '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']}, {'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},

55
couchpotato/core/plugins/release/main.py

@ -1,8 +1,10 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin 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_ from sqlalchemy.sql.expression import and_, or_
log = CPLog(__name__) log = CPLog(__name__)
@ -13,6 +15,8 @@ class Release(Plugin):
def __init__(self): def __init__(self):
addEvent('release.add', self.add) addEvent('release.add', self.add)
addApiView('release.download', self.download)
def add(self, group): def add(self, group):
db = get_session() db = get_session()
@ -30,22 +34,22 @@ class Release(Plugin):
db.add(movie) db.add(movie)
db.commit() db.commit()
# Add release # Add Release
snatched_status = fireEvent('status.get', 'snatched', single = True) snatched_status = fireEvent('status.get', 'snatched', single = True)
release = db.query(Release).filter( rel = db.query(Relea).filter(
or_( or_(
Release.identifier == identifier, Relea.identifier == identifier,
and_(Release.identifier.startswith(group['library']['identifier'], Release.status_id == snatched_status.get('id'))) and_(Relea.identifier.startswith(group['library']['identifier'], Relea.status_id == snatched_status.get('id')))
) )
).first() ).first()
if not release: if not rel:
release = Release( rel = Relea(
identifier = identifier, identifier = identifier,
movie = movie, movie = movie,
quality_id = group['meta_data']['quality'].get('id'), quality_id = group['meta_data']['quality'].get('id'),
status_id = done_status.get('id') status_id = done_status.get('id')
) )
db.add(release) db.add(rel)
db.commit() db.commit()
# Add each file type # Add each file type
@ -54,10 +58,10 @@ class Release(Plugin):
added_file = self.saveFile(file, type = type, include_media_info = type is 'movie') added_file = self.saveFile(file, type = type, include_media_info = type is 'movie')
try: try:
added_file = db.query(File).filter_by(id = added_file.get('id')).one() 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() db.commit()
except Exception, e: 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() db.remove()
@ -73,3 +77,34 @@ class Release(Plugin):
# Check database and update/insert if necessary # 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) 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
})

45
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.helpers.variable import getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin 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 os.path
import re import re
import shutil import shutil
@ -134,8 +134,18 @@ class Renamer(Plugin):
# Move DVD files (no structure renaming) # Move DVD files (no structure renaming)
if group['is_dvd'] and file_type is 'movie': if group['is_dvd'] and file_type is 'movie':
structure_dir = file.split(group['dirname'])[-1].lstrip(os.path.sep) 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) 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 # Do rename others
else: else:
rename_files[file] = os.path.join(destination, final_folder_name, final_file_name) 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 # Before renaming, remove the lower quality files
db = get_session() db = get_session()
library = db.query(Library).filter_by(identifier = group['library']['identifier']).first() library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
active_status = fireEvent('status.get', 'active', single = True)
for movie in library.movies: 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: for release in movie.releases:
# This is where CP removes older, lesser quality releases
if release.quality.order > group['meta_data']['quality']['order']: 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)) 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'): 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']: 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)) 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: else:
log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label)) 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 break
for file in release.files: # Rename all files marked
log.info('Removing (not really) "%s"' % file.path)
# Rename
for src in rename_files: for src in rename_files:
if rename_files[src]: if rename_files[src]:

16
couchpotato/core/plugins/scanner/main.py

@ -23,11 +23,11 @@ class Scanner(Plugin):
'trailer': 1048576, # 1MB '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 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 = { extensions = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'], 'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'],
'dvd': ['vts_*', 'vob'], 'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'], 'nfo': ['nfo', 'nfo-orig', 'txt', 'tag'],
'subtitle': ['sub', 'srt', 'ssa', 'ass'], 'subtitle': ['sub', 'srt', 'ssa', 'ass'],
'subtitle_extra': ['idx'], 'subtitle_extra': ['idx'],
'trailer': ['mov', 'mp4', 'flv'] 'trailer': ['mov', 'mp4', 'flv']
@ -210,13 +210,13 @@ class Scanner(Plugin):
group['parentdir'] = os.path.dirname(movie_file) group['parentdir'] = os.path.dirname(movie_file)
group['dirname'] = None group['dirname'] = None
folders = group['parentdir'].replace(folder, '').split(os.path.sep) folder_names = group['parentdir'].replace(folder, '').split(os.path.sep)
folders.reverse() folder_names.reverse()
# Try and get a proper dirname, so no "A", "Movie", "Download" etc # Try and get a proper dirname, so no "A", "Movie", "Download" etc
for folder in folders: for folder_name in folder_names:
if folder.lower() not in self.ignore_names and len(folder) > 2: if folder_name.lower() not in self.ignore_names and len(folder_name) > 2:
group['dirname'] = folder group['dirname'] = folder_name
break break
break break
@ -426,7 +426,7 @@ class Scanner(Plugin):
if list(set(file.lower().split(os.path.sep)) & set(['video_ts', 'audio_ts'])): if list(set(file.lower().split(os.path.sep)) & set(['video_ts', 'audio_ts'])):
return True 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(): if needle in file.lower():
return True return True

49
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.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env from couchpotato.environment import Env
import re import re
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@ -17,6 +18,7 @@ class Searcher(Plugin):
addEvent('searcher.all', self.all) addEvent('searcher.all', self.all)
addEvent('searcher.single', self.single) addEvent('searcher.single', self.single)
addEvent('searcher.correct_movie', self.correctMovie) addEvent('searcher.correct_movie', self.correctMovie)
addEvent('searcher.download', self.download)
# Schedule cronjob # Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.all, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) 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: for movie in movies:
self.single(movie.to_dict(deep = { self.single(movie.to_dict({
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}}, 'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
@ -47,11 +49,8 @@ class Searcher(Plugin):
def single(self, movie): def single(self, movie):
downloaded_status = fireEvent('status.get', 'downloaded', single = True)
available_status = fireEvent('status.get', 'available', 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']: for type in movie['profile']['types']:
has_better_quality = 0 has_better_quality = 0
@ -85,47 +84,49 @@ class Searcher(Plugin):
db.commit() db.commit()
for info in nzb: for info in nzb:
try:
rls_info = ReleaseInfo( rls_info = ReleaseInfo(
identifier = info, identifier = info,
value = nzb[info] value = nzb[info]
) )
rls.info.append(rls_info) rls.info.append(rls_info)
except Exception:
log.debug('Couldn\'t add %s to ReleaseInfo: %s' % (info, traceback.format_exc()))
db.commit() db.commit()
for nzb in sorted_results: for nzb in sorted_results:
successful = fireEvent('download', data = nzb, movie = movie, single = True) 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
# Break if CP wants to shut down
if self.shuttingDown():
break
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: if successful:
# Mark release as snatched # Mark release as snatched
db = get_session() db = get_session()
rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first() rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
rls.status_id = snatched_status.get('id') rls.status_id = snatched_status.get('id')
db.commit() db.commit()
# Mark movie snatched if quality is finish-checked log.info('Downloading of %s successful.' % data.get('name'))
if type['finish']: fireEvent('movie.snatched', message = 'Downloading of %s successful.' % data.get('name'), data = rls.to_dict())
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 True
return False return False
else:
log.info('Better quality (%s) already available or snatched for %s' % (type['quality']['label'], default_title))
break
# Break if CP wants to shut down
if self.shuttingDown():
break
return False
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs): def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):

20
couchpotato/core/providers/base.py

@ -2,7 +2,6 @@ from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from urllib2 import URLError
from urlparse import urlparse from urlparse import urlparse
import re import re
import time import time
@ -60,6 +59,20 @@ class YarrProvider(Provider):
sizeMb = ['mb', 'mib'] sizeMb = ['mb', 'mib']
sizeKb = ['kb', 'kib'] 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): def parseSize(self, size):
sizeRaw = size.lower() sizeRaw = size.lower()
@ -96,11 +109,16 @@ class NZBProvider(YarrProvider):
type = 'nzb' type = 'nzb'
def __init__(self): def __init__(self):
super(NZBProvider, self).__init__()
addEvent('provider.nzb.search', self.search) addEvent('provider.nzb.search', self.search)
addEvent('provider.yarr.search', self.search) addEvent('provider.yarr.search', self.search)
addEvent('provider.nzb.feed', self.feed) addEvent('provider.nzb.feed', self.feed)
def download(self, url = '', nzb_id = ''):
return self.urlopen(url)
def feed(self): def feed(self):
return [] return []

10
couchpotato/core/providers/nzb/newzbin/main.py

@ -13,10 +13,9 @@ log = CPLog(__name__)
class Newzbin(NZBProvider, RSS): class Newzbin(NZBProvider, RSS):
urls = { urls = {
'search': 'https://www.newzbin.com/search/',
'download': 'http://www.newzbin.com/api/dnzb/', 'download': 'http://www.newzbin.com/api/dnzb/',
'search': 'https://www.newzbin.com/search/',
} }
searchUrl = 'https://www.newzbin.com/search/'
format_ids = { format_ids = {
2: ['scr'], 2: ['scr'],
@ -36,7 +35,7 @@ class Newzbin(NZBProvider, RSS):
def search(self, movie, quality): def search(self, movie, quality):
results = [] results = []
if self.isDisabled() or not self.isAvailable(self.searchUrl): if self.isDisabled() or not self.isAvailable(self.urls['search']):
return results return results
format_id = self.getFormatId(type) format_id = self.getFormatId(type)
@ -97,11 +96,12 @@ class Newzbin(NZBProvider, RSS):
new = { new = {
'id': id, 'id': id,
'type': 'nzb', 'type': 'nzb',
'provider': self.getName(),
'name': title, 'name': title,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': self.parseSize(size), 'size': self.parseSize(size),
'url': str(self.getTextElement(nzb, '{%s}nzb' % REPORT_NS)), 'url': str(self.getTextElement(nzb, '{%s}nzb' % REPORT_NS)),
'download': lambda: self.download(id), 'download': self.download,
'detail_url': str(self.getTextElement(nzb, 'link')), 'detail_url': str(self.getTextElement(nzb, 'link')),
'description': self.getTextElement(nzb, "description"), 'description': self.getTextElement(nzb, "description"),
'check_nzb': False, 'check_nzb': False,
@ -121,7 +121,7 @@ class Newzbin(NZBProvider, RSS):
return results return results
def download(self, nzb_id): def download(self, url = '', nzb_id = ''):
try: try:
log.info('Download nzb from newzbin, report id: %s ' % nzb_id) log.info('Download nzb from newzbin, report id: %s ' % nzb_id)

14
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 couchpotato.core.providers.base import NZBProvider
from dateutil.parser import parse from dateutil.parser import parse
from urllib import urlencode from urllib import urlencode
from urlparse import urlparse
import time import time
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
@ -130,11 +131,13 @@ class Newznab(NZBProvider, RSS):
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop() id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
new = { new = {
'id': id, 'id': id,
'provider': self.getName(),
'type': 'nzb', 'type': 'nzb',
'name': self.getTextElement(nzb, "title"), 'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(size) / 1024 / 1024, 'size': int(size) / 1024 / 1024,
'url': (self.getUrl(host['host'], self.urls['download']) % id) + self.getApiExt(host), '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), 'detail_url': (self.getUrl(host['host'], self.urls['detail']) % id) + self.getApiExt(host),
'content': self.getTextElement(nzb, "description"), 'content': self.getTextElement(nzb, "description"),
} }
@ -173,6 +176,17 @@ class Newznab(NZBProvider, RSS):
return list 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): def getUrl(self, host, type):
return cleanHost(host) + 'api?t=' + type return cleanHost(host) + 'api?t=' + type

4
couchpotato/core/providers/nzb/nzbindex/main.py

@ -14,7 +14,7 @@ log = CPLog(__name__)
class NzbIndex(NZBProvider, RSS): class NzbIndex(NZBProvider, RSS):
urls = { 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 '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 = { new = {
'id': id, 'id': id,
'type': 'nzb', 'type': 'nzb',
'provider': self.getName(),
'name': self.getTextElement(nzb, "title"), 'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': enclosure['length'], 'size': enclosure['length'],
'url': enclosure['url'], 'url': enclosure['url'],
'download': self.download,
'detail_url': enclosure['url'].replace('/download/', '/release/'), 'detail_url': enclosure['url'].replace('/download/', '/release/'),
'description': self.getTextElement(nzb, "description"), 'description': self.getTextElement(nzb, "description"),
'check_nzb': True, 'check_nzb': True,

2
couchpotato/core/providers/nzb/nzbmatrix/main.py

@ -81,10 +81,12 @@ class NZBMatrix(NZBProvider, RSS):
new = { new = {
'id': id, 'id': id,
'type': 'nzb', 'type': 'nzb',
'provider': self.getName(),
'name': title, 'name': title,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': self.parseSize(size), 'size': self.parseSize(size),
'url': self.urls['download'] % id + self.getApiExt(), 'url': self.urls['download'] % id + self.getApiExt(),
'download': self.download,
'detail_url': self.urls['detail'] % id, 'detail_url': self.urls['detail'] % id,
'description': self.getTextElement(nzb, "description"), 'description': self.getTextElement(nzb, "description"),
'check_nzb': True, 'check_nzb': True,

2
couchpotato/core/providers/nzb/nzbs/main.py

@ -71,10 +71,12 @@ class Nzbs(NZBProvider, RSS):
new = { new = {
'id': id, 'id': id,
'type': 'nzb', 'type': 'nzb',
'provider': self.getName(),
'name': self.getTextElement(nzb, "title"), 'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': self.parseSize(self.getTextElement(nzb, "description").split('</a><br />')[1].split('">')[1]), 'size': self.parseSize(self.getTextElement(nzb, "description").split('</a><br />')[1].split('">')[1]),
'url': self.urls['download'] % (id, self.getApiExt()), 'url': self.urls['download'] % (id, self.getApiExt()),
'download': self.download,
'detail_url': self.urls['detail'] % id, 'detail_url': self.urls['detail'] % id,
'description': self.getTextElement(nzb, "description"), 'description': self.getTextElement(nzb, "description"),
'check_nzb': True, 'check_nzb': True,

3
couchpotato/core/settings/model.py

@ -47,6 +47,9 @@ class Library(Entity):
files = ManyToMany('File') files = ManyToMany('File')
info = OneToMany('LibraryInfo') info = OneToMany('LibraryInfo')
def title(self):
return self.titles[0]['title']
class LibraryInfo(Entity): class LibraryInfo(Entity):
"""""" """"""

BIN
couchpotato/static/images/edit.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1015 B

0
couchpotato/static/images/check.png → couchpotato/static/images/icon.check.png

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 451 B

0
couchpotato/static/images/delete.png → couchpotato/static/images/icon.delete.png

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 228 B

BIN
couchpotato/static/images/icon.download.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

BIN
couchpotato/static/images/icon.edit.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

0
couchpotato/static/images/imdb.png → couchpotato/static/images/icon.imdb.png

Before

Width:  |  Height:  |  Size: 159 B

After

Width:  |  Height:  |  Size: 159 B

0
couchpotato/static/images/rating.png → couchpotato/static/images/icon.rating.png

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 283 B

BIN
couchpotato/static/images/icon.refresh.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

BIN
couchpotato/static/images/reload.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 B

4
couchpotato/static/scripts/page/wanted.js

@ -27,7 +27,7 @@ var MovieActions = {};
MovieActions.Wanted = { MovieActions.Wanted = {
'IMBD': IMDBAction 'IMBD': IMDBAction
//,'releases': ReleaseAction ,'releases': ReleaseAction
,'Edit': new Class({ ,'Edit': new Class({
@ -207,7 +207,6 @@ MovieActions.Wanted = {
self.chain( self.chain(
function(){ function(){
$(movie).mask().addClass('loading');
self.callChain(); self.callChain();
}, },
function(){ function(){
@ -236,6 +235,5 @@ MovieActions.Wanted = {
MovieActions.Snatched = { MovieActions.Snatched = {
'IMBD': IMDBAction 'IMBD': IMDBAction
,'Releases': ReleaseAction
,'Delete': MovieActions.Wanted.Delete ,'Delete': MovieActions.Wanted.Delete
}; };

12
couchpotato/static/style/main.css

@ -137,10 +137,18 @@ form {
} }
/*** Icons ***/ /*** Icons ***/
.icon.delete { .icon {
background: url('../images/delete.png') no-repeat;
display: inline-block; 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 ***/ /*** Navigation ***/
.header { .header {

2
couchpotato/static/style/page/settings.css

@ -103,7 +103,7 @@
border: 0; border: 0;
} }
.page.settings .ctrlHolder.save_success:not(:first-child) { .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:last-child { border: none; }
.page.settings .ctrlHolder:hover { background-color: rgba(255,255,255,0.05); } .page.settings .ctrlHolder:hover { background-color: rgba(255,255,255,0.05); }

88
libs/multipartpost.py

@ -0,0 +1,88 @@
#!/usr/bin/python
####
# 06/2010 Nic Wolfe <nic@wolfeden.ca>
# 02/2006 Will Holcomb <wholcomb@gmail.com>
#
# 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
Loading…
Cancel
Save