Downloading of single releases and more
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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));
|
||||
|
||||
@@ -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']},
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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]:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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 []
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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('</a><br />')[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,
|
||||
|
||||
@@ -47,6 +47,9 @@ class Library(Entity):
|
||||
files = ManyToMany('File')
|
||||
info = OneToMany('LibraryInfo')
|
||||
|
||||
def title(self):
|
||||
return self.titles[0]['title']
|
||||
|
||||
|
||||
class LibraryInfo(Entity):
|
||||
""""""
|
||||
|
||||
|
Before Width: | Height: | Size: 1015 B |
|
Before Width: | Height: | Size: 451 B After Width: | Height: | Size: 451 B |
|
Before Width: | Height: | Size: 228 B After Width: | Height: | Size: 228 B |
|
After Width: | Height: | Size: 502 B |
|
After Width: | Height: | Size: 624 B |
|
Before Width: | Height: | Size: 159 B After Width: | Height: | Size: 159 B |
|
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 726 B |
@@ -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
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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
|
||||