Compare commits

..

3 Commits

Author SHA1 Message Date
Ruud f99b40c2f3 Runner fs encoding 2014-10-06 08:53:17 +02:00
Ruud ae00e83c9d Path helpers 2014-10-06 08:52:48 +02:00
Ruud d4f2f12924 Force logging utf8 2014-10-06 08:16:40 +02:00
281 changed files with 18379 additions and 10348 deletions
-2
View File
@@ -3,5 +3,3 @@
/_source/ /_source/
.project .project
.pydevproject .pydevproject
node_modules
.tmp
+1 -1
View File
@@ -61,7 +61,7 @@ class Loader(object):
self.log = CPLog(__name__) self.log = CPLog(__name__)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%H:%M:%S') formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%H:%M:%S')
hdlr = handlers.RotatingFileHandler(os.path.join(self.log_dir, 'error.log'), 'a', 500000, 10) hdlr = handlers.RotatingFileHandler(os.path.join(self.log_dir, 'error.log'), 'a', 500000, 10, encoding = 'utf-8')
hdlr.setLevel(logging.CRITICAL) hdlr.setLevel(logging.CRITICAL)
hdlr.setFormatter(formatter) hdlr.setFormatter(formatter)
self.log.logger.addHandler(hdlr) self.log.logger.addHandler(hdlr)
-121
View File
@@ -1,121 +0,0 @@
'use strict';
module.exports = function(grunt){
require('time-grunt')(grunt);
// Configurable paths
var config = {
tmp: '.tmp',
base: 'couchpotato',
css_dest: 'couchpotato/static/style/combined.min.css'
};
grunt.initConfig({
// Project settings
config: config,
// Make sure code styles are up to par and there are no obvious mistakes
jshint: {
options: {
reporter: require('jshint-stylish'),
unused: false,
camelcase: false,
devel: true
},
all: [
'<%= config.base %>/{,**/}*.js',
'!<%= config.base %>/static/scripts/vendor/{,**/}*.js'
]
},
// Compiles Sass to CSS and generates necessary files if requested
sass: {
options: {
compass: true,
update: true
},
server: {
files: [{
expand: true,
cwd: '<%= config.base %>/',
src: ['**/*.scss'],
dest: '<%= config.tmp %>/styles/',
ext: '.css'
}]
}
},
// Add vendor prefixed styles
autoprefixer: {
options: {
browsers: ['> 1%', 'Android >= 2.1', 'Chrome >= 21', 'Explorer >= 7', 'Firefox >= 17', 'Opera >= 12.1', 'Safari >= 6.0']
},
dist: {
files: [{
expand: true,
cwd: '<%= config.tmp %>/styles/',
src: '{,**/}*.css',
dest: '<%= config.tmp %>/styles/'
}]
}
},
cssmin: {
dist: {
files: {
'<%= config.css_dest %>': ['<%= config.tmp %>/styles/**/*.css']
}
}
},
shell: {
runCouchPotato: {
command: 'python CouchPotato.py'
}
},
// COOL TASKS ==============================================================
watch: {
scss: {
files: ['<%= config.base %>/**/*.{scss,sass}'],
tasks: ['sass:server', 'autoprefixer', 'cssmin']
},
js: {
files: [
'<%= config.base %>/**/*.js'
],
tasks: ['jshint']
},
livereload: {
options: {
livereload: 35729
},
files: [
'<%= config.css_dest %>'
]
}
},
concurrent: {
options: {
logConcurrentOutput: true
},
tasks: ['shell:runCouchPotato', 'sass:server', 'autoprefixer', 'cssmin', 'watch']
}
});
grunt.loadNpmTasks('grunt-contrib-jshint');
//grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-sass');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-autoprefixer');
grunt.loadNpmTasks('grunt-concurrent');
grunt.loadNpmTasks('grunt-shell');
grunt.registerTask('default', ['concurrent']);
};
-45
View File
@@ -1,45 +0,0 @@
# First, require any additional compass plugins installed on your system.
# require 'zen-grids'
require 'susy'
# require 'breakpoint'
# Toggle this between :development and :production when deploying the CSS to the
# live server. Development mode will retain comments and spacing from the
# original Sass source and adds line numbering comments for easier debugging.
environment = :development
# environment = :development
# In development, we can turn on the FireSass-compatible debug_info.
firesass = false
# firesass = true
# Location of the your project's resources.
# Set this to the root of your project. All resource locations above are
# considered to be relative to this path.
http_path = "/"
# To use relative paths to assets in your compiled CSS files, set this to true.
# relative_assets = true
##
## You probably don't need to edit anything below this.
##
sass_dir = "./"
css_dir = "./static/style_compiled"
# You can select your preferred output style here (can be overridden via the command line):
# output_style = :expanded or :nested or :compact or :compressed
output_style = (environment == :development) ? :expanded : :compressed
# To disable debugging comments that display the original location of your selectors. Uncomment:
# line_comments = false
# Pass options to sass. For development, we turn on the FireSass-compatible
# debug_info if the firesass config variable above is true.
sass_options = (environment == :development && firesass == true) ? {:debug_info => true} : {}
-9
View File
@@ -40,8 +40,6 @@ class WebHandler(BaseHandler):
return return
try: try:
if route == 'robots.txt':
self.set_header('Content-Type', 'text/plain')
self.write(views[route]()) self.write(views[route]())
except: except:
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc())) log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
@@ -62,13 +60,6 @@ def index():
addView('', index) addView('', index)
# Web view
def robots():
return 'User-agent: * \n' \
'Disallow: /'
addView('robots.txt', robots)
# API docs # API docs
def apiDocs(): def apiDocs():
routes = list(api.keys()) routes = list(api.keys())
+22 -26
View File
@@ -7,7 +7,6 @@ import urllib
from couchpotato.core.helpers.request import getParams from couchpotato.core.helpers.request import getParams
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, asynchronous from tornado.web import RequestHandler, asynchronous
@@ -51,22 +50,24 @@ class NonBlockHandler(RequestHandler):
start, stop = api_nonblock[route] start, stop = api_nonblock[route]
self.stopper = stop self.stopper = stop
start(self.sendData, last_id = self.get_argument('last_id', None)) start(self.onNewMessage, last_id = self.get_argument('last_id', None))
def sendData(self, response): def onNewMessage(self, response):
if not self.request.connection.stream.closed(): if self.request.connection.stream.closed():
try: self.on_connection_close()
self.finish(response) return
except:
log.debug('Failed doing nonblock request, probably already closed: %s', (traceback.format_exc()))
try: self.finish({'success': False, 'error': 'Failed returning results'})
except: pass
self.removeStopper() try:
self.finish(response)
except:
log.debug('Failed doing nonblock request, probably already closed: %s', (traceback.format_exc()))
try: self.finish({'success': False, 'error': 'Failed returning results'})
except: pass
def on_connection_close(self):
def removeStopper(self):
if self.stopper: if self.stopper:
self.stopper(self.sendData) self.stopper(self.onNewMessage)
self.stopper = None self.stopper = None
@@ -82,11 +83,10 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
# Blocking API handler # Blocking API handler
class ApiHandler(RequestHandler): class ApiHandler(RequestHandler):
route = None
@asynchronous @asynchronous
def get(self, route, *args, **kwargs): def get(self, route, *args, **kwargs):
self.route = route = route.strip('/') route = route.strip('/')
if not api.get(route): if not api.get(route):
self.write('API call doesn\'t seem to exist') self.write('API call doesn\'t seem to exist')
self.finish() self.finish()
@@ -123,15 +123,11 @@ class ApiHandler(RequestHandler):
except: except:
log.error('Failed write error "%s": %s', (route, traceback.format_exc())) log.error('Failed write error "%s": %s', (route, traceback.format_exc()))
self.unlock() api_locks[route].release()
post = get post = get
def taskFinished(self, result, route): def taskFinished(self, result, route):
IOLoop.current().add_callback(self.sendData, result, route)
self.unlock()
def sendData(self, result, route):
if not self.request.connection.stream.closed(): if not self.request.connection.stream.closed():
try: try:
@@ -139,12 +135,14 @@ class ApiHandler(RequestHandler):
jsonp_callback = self.get_argument('callback_func', default = None) jsonp_callback = self.get_argument('callback_func', default = None)
if jsonp_callback: if jsonp_callback:
self.set_header('Content-Type', 'text/javascript') self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
self.finish(str(jsonp_callback) + '(' + json.dumps(result) + ')') self.set_header("Content-Type", "text/javascript")
self.finish()
elif isinstance(result, tuple) and result[0] == 'redirect': elif isinstance(result, tuple) and result[0] == 'redirect':
self.redirect(result[1]) self.redirect(result[1])
else: else:
self.finish(result) self.write(result)
self.finish()
except UnicodeDecodeError: except UnicodeDecodeError:
log.error('Failed proper encode: %s', traceback.format_exc()) log.error('Failed proper encode: %s', traceback.format_exc())
except: except:
@@ -152,9 +150,7 @@ class ApiHandler(RequestHandler):
try: self.finish({'success': False, 'error': 'Failed returning results'}) try: self.finish({'success': False, 'error': 'Failed returning results'})
except: pass except: pass
def unlock(self): api_locks[route].release()
try: api_locks[self.route].release()
except: pass
def addApiView(route, func, static = False, docs = None, **kwargs): def addApiView(route, func, static = False, docs = None, **kwargs):
+3 -3
View File
@@ -181,13 +181,13 @@ class Core(Plugin):
return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key')) return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
def version(self): def version(self):
ver = fireEvent('updater.info', single = True) or {'version': {}} ver = fireEvent('updater.info', single = True)
if os.name == 'nt': platf = 'windows' if os.name == 'nt': platf = 'windows'
elif 'Darwin' in platform.platform(): platf = 'osx' elif 'Darwin' in platform.platform(): platf = 'osx'
else: platf = 'linux' else: platf = 'linux'
return '%s - %s-%s - v2' % (platf, ver.get('version').get('type') or 'unknown', ver.get('version').get('hash') or 'unknown') return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
def versionView(self, **kwargs): def versionView(self, **kwargs):
return { return {
@@ -290,7 +290,7 @@ config = [{
}, },
{ {
'name': 'permission_file', 'name': 'permission_file',
'default': '0644', 'default': '0755',
'label': 'File CHMOD', 'label': 'File CHMOD',
'description': 'See Folder CHMOD description, but for files', 'description': 'See Folder CHMOD description, but for files',
}, },
+93 -46
View File
@@ -1,5 +1,6 @@
import os import os
import re import re
import traceback
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
@@ -7,6 +8,8 @@ from couchpotato.core.helpers.variable import tryInt
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 minify.cssmin import cssmin
from minify.jsmin import jsmin
from tornado.web import StaticFileHandler from tornado.web import StaticFileHandler
@@ -19,26 +22,30 @@ class ClientScript(Plugin):
core_static = { core_static = {
'style': [ 'style': [
'style/combined.min.css', 'style/main.css',
'style/uniform.generic.css',
'style/uniform.css',
'style/settings.css',
], ],
'script': [ 'script': [
'scripts/vendor/mootools.js', 'scripts/library/mootools.js',
'scripts/vendor/mootools_more.js', 'scripts/library/mootools_more.js',
'scripts/vendor/form_replacement/form_check.js',
'scripts/vendor/form_replacement/form_radio.js',
'scripts/vendor/form_replacement/form_dropdown.js',
'scripts/vendor/form_replacement/form_selectoption.js',
'scripts/vendor/Array.stableSort.js',
'scripts/vendor/history.js',
'scripts/library/uniform.js', 'scripts/library/uniform.js',
'scripts/library/form_replacement/form_check.js',
'scripts/library/form_replacement/form_radio.js',
'scripts/library/form_replacement/form_dropdown.js',
'scripts/library/form_replacement/form_selectoption.js',
'scripts/library/question.js', 'scripts/library/question.js',
'scripts/library/scrollspy.js', 'scripts/library/scrollspy.js',
'scripts/library/spin.js',
'scripts/library/Array.stableSort.js',
'scripts/library/async.js',
'scripts/couchpotato.js', 'scripts/couchpotato.js',
'scripts/api.js', 'scripts/api.js',
'scripts/library/history.js',
'scripts/page.js', 'scripts/page.js',
'scripts/block.js', 'scripts/block.js',
'scripts/block/navigation.js', 'scripts/block/navigation.js',
'scripts/block/header.js',
'scripts/block/footer.js', 'scripts/block/footer.js',
'scripts/block/menu.js', 'scripts/block/menu.js',
'scripts/page/home.js', 'scripts/page/home.js',
@@ -47,9 +54,8 @@ class ClientScript(Plugin):
], ],
} }
watches = {} urls = {'style': {}, 'script': {}}
minified = {'style': {}, 'script': {}}
original_paths = {'style': {}, 'script': {}}
paths = {'style': {}, 'script': {}} paths = {'style': {}, 'script': {}}
comment = { comment = {
'style': '/*** %s:%d ***/\n', 'style': '/*** %s:%d ***/\n',
@@ -68,7 +74,8 @@ class ClientScript(Plugin):
addEvent('clientscript.get_styles', self.getStyles) addEvent('clientscript.get_styles', self.getStyles)
addEvent('clientscript.get_scripts', self.getScripts) addEvent('clientscript.get_scripts', self.getScripts)
addEvent('app.load', self.compile) if not Env.get('dev'):
addEvent('app.load', self.minify)
self.addCore() self.addCore()
@@ -84,7 +91,7 @@ class ClientScript(Plugin):
else: else:
self.registerStyle(core_url, file_path, position = 'front') self.registerStyle(core_url, file_path, position = 'front')
def compile(self): def minify(self):
# Create cache dir # Create cache dir
cache = Env.get('cache_dir') cache = Env.get('cache_dir')
@@ -95,43 +102,47 @@ class ClientScript(Plugin):
for file_type in ['style', 'script']: for file_type in ['style', 'script']:
ext = 'js' if file_type is 'script' else 'css' ext = 'js' if file_type is 'script' else 'css'
positions = self.original_paths.get(file_type, {}) positions = self.paths.get(file_type, {})
for position in positions: for position in positions:
files = positions.get(position) files = positions.get(position)
self._compile(file_type, files, position, position + '.' + ext) self._minify(file_type, files, position, position + '.' + ext)
def _compile(self, file_type, paths, position, out): def _minify(self, file_type, files, position, out):
cache = Env.get('cache_dir') cache = Env.get('cache_dir')
out_name = out out_name = out
minified_dir = os.path.join(cache, 'minified') out = os.path.join(cache, 'minified', out_name)
data_combined = ''
new_paths = []
for x in paths:
file_path, url_path = x
raw = []
for file_path in files:
f = open(file_path, 'r').read() f = open(file_path, 'r').read()
if not Env.get('dev'): if file_type == 'script':
data = f data = jsmin(f)
data_combined += self.comment.get(file_type) % (ss(file_path), int(os.path.getmtime(file_path)))
data_combined += data + '\n\n'
else: else:
new_paths.append(x) data = self.prefix(f)
data = cssmin(data)
data = data.replace('../images/', '../static/images/')
data = data.replace('../fonts/', '../static/fonts/')
data = data.replace('../../static/', '../static/') # Replace inside plugins
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
# Combine all files together with some comments # Combine all files together with some comments
if not Env.get('dev'): data = ''
for r in raw:
data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
data += r.get('data') + '\n\n'
out_path = os.path.join(minified_dir, out_name) self.createFile(out, data.strip())
self.createFile(out_path, data_combined.strip())
minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out))) if not self.minified.get(file_type):
new_paths.append((out_path, {'url': minified_url})) self.minified[file_type] = {}
if not self.minified[file_type].get(position):
self.minified[file_type][position] = []
self.paths[file_type][position] = new_paths minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out)))
self.minified[file_type][position].append(minified_url)
def getStyles(self, *args, **kwargs): def getStyles(self, *args, **kwargs):
return self.get('style', *args, **kwargs) return self.get('style', *args, **kwargs)
@@ -139,12 +150,22 @@ class ClientScript(Plugin):
def getScripts(self, *args, **kwargs): def getScripts(self, *args, **kwargs):
return self.get('script', *args, **kwargs) return self.get('script', *args, **kwargs)
def get(self, type, location = 'head'): def get(self, type, as_html = False, location = 'head'):
if type in self.paths and location in self.paths[type]:
paths = self.paths[type][location]
return [x[1] for x in paths]
return [] data = '' if as_html else []
try:
try:
if not Env.get('dev'):
return self.minified[type][location]
except:
pass
return self.urls[type][location]
except:
log.error('Error getting minified %s, %s: %s', (type, location, traceback.format_exc()))
return data
def registerStyle(self, api_path, file_path, position = 'head'): def registerStyle(self, api_path, file_path, position = 'head'):
self.register(api_path, file_path, 'style', position) self.register(api_path, file_path, 'style', position)
@@ -156,10 +177,36 @@ class ClientScript(Plugin):
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path))) api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
if not self.original_paths[type].get(location): if not self.urls[type].get(location):
self.original_paths[type][location] = [] self.urls[type][location] = []
self.original_paths[type][location].append((file_path, api_path)) self.urls[type][location].append(api_path)
if not self.paths[type].get(location): if not self.paths[type].get(location):
self.paths[type][location] = [] self.paths[type][location] = []
self.paths[type][location].append((file_path, api_path)) self.paths[type][location].append(file_path)
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
prefix_tags = ['ms', 'moz', 'webkit']
def prefix(self, data):
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
new_data = ''
colon_split = trimmed_data.split(';')
for splt in colon_split:
curl_split = splt.strip().split('{')
for curly in curl_split:
curly = curly.strip()
for prop in self.prefix_properties:
if curly[:len(prop) + 1] == prop + ':':
for tag in self.prefix_tags:
new_data += ' -%s-%s; ' % (tag, curly)
new_data += curly + (' { ' if len(curl_split) > 1 else ' ')
new_data += '; '
new_data = new_data.replace('{ ;', '; ').replace('} ;', '} ')
return new_data
@@ -16,8 +16,8 @@ var DownloadersBase = new Class({
var setting_page = App.getPage('Settings'); var setting_page = App.getPage('Settings');
setting_page.addEvent('create', function(){ setting_page.addEvent('create', function(){
Object.each(setting_page.tabs.downloaders.groups, self.addTestButton.bind(self)); Object.each(setting_page.tabs.downloaders.groups, self.addTestButton.bind(self))
}); })
}, },
@@ -44,19 +44,19 @@ var DownloadersBase = new Class({
if(json.success){ if(json.success){
message = new Element('span.success', { message = new Element('span.success', {
'text': 'Connection successful' 'text': 'Connection successful'
}).inject(button, 'after'); }).inject(button, 'after')
} }
else { else {
var msg_text = 'Connection failed. Check logs for details.'; var msg_text = 'Connection failed. Check logs for details.';
if(json.hasOwnProperty('msg')) msg_text = json.msg; if(json.hasOwnProperty('msg')) msg_text = json.msg;
message = new Element('span.failed', { message = new Element('span.failed', {
'text': msg_text 'text': msg_text
}).inject(button, 'after'); }).inject(button, 'after')
} }
(function(){ (function(){
message.destroy(); message.destroy();
}).delay(3000); }).delay(3000)
} }
}); });
} }
+8 -17
View File
@@ -205,28 +205,19 @@ class GitUpdater(BaseUpdater):
def getVersion(self): def getVersion(self):
if not self.version: if not self.version:
hash = None
date = None
branch = self.branch
try: try:
output = self.repo.getHead() # Yes, please output = self.repo.getHead() # Yes, please
log.debug('Git version output: %s', output.hash) log.debug('Git version output: %s', output.hash)
self.version = {
hash = output.hash[:8] 'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.repo.getCurrentBranch().name or self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())),
date = output.getDate() 'hash': output.hash[:8],
branch = self.repo.getCurrentBranch().name 'date': output.getDate(),
'type': 'git',
'branch': self.repo.getCurrentBranch().name
}
except Exception as e: except Exception as e:
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e) log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
return 'No GIT'
self.version = {
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, branch, hash or 'unknown_hash', datetime.fromtimestamp(date) if date else 'unknown_date'),
'hash': hash,
'date': date,
'type': 'git',
'branch': branch
}
return self.version return self.version
@@ -27,7 +27,7 @@ var UpdaterBase = new Class({
App.trigger('message', ['No updates available']); App.trigger('message', ['No updates available']);
} }
} }
}); })
}, },
@@ -50,8 +50,8 @@ var UpdaterBase = new Class({
self.message.destroy(); self.message.destroy();
} }
} }
}); })
}, (timeout || 0)); }, (timeout || 0))
}, },
@@ -84,7 +84,7 @@ var UpdaterBase = new Class({
'click': self.doUpdate.bind(self) 'click': self.doUpdate.bind(self)
} }
}) })
).inject(document.body); ).inject(document.body)
}, },
doUpdate: function(){ doUpdate: function(){
@@ -96,7 +96,7 @@ var UpdaterBase = new Class({
if(json.success) if(json.success)
self.updating(); self.updating();
else else
App.unBlockPage(); App.unBlockPage()
} }
}); });
}, },
-2
View File
@@ -621,8 +621,6 @@ class Database(object):
except OperationalError: except OperationalError:
log.error('Migrating from faulty database, probably a (too) old version: %s', traceback.format_exc()) log.error('Migrating from faulty database, probably a (too) old version: %s', traceback.format_exc())
rename_old = True
except: except:
log.error('Migration failed: %s', traceback.format_exc()) log.error('Migration failed: %s', traceback.format_exc())
-36
View File
@@ -20,31 +20,14 @@ class Blackhole(DownloaderBase):
status_support = False status_support = False
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {} if not media: media = {}
if not data: data = {} if not data: data = {}
directory = self.conf('directory') directory = self.conf('directory')
# The folder needs to exist
if not directory or not os.path.isdir(directory): if not directory or not os.path.isdir(directory):
log.error('No directory set for blackhole %s download.', data.get('protocol')) log.error('No directory set for blackhole %s download.', data.get('protocol'))
else: else:
try: try:
# Filedata can be empty, which probably means it a magnet link
if not filedata or len(filedata) < 50: if not filedata or len(filedata) < 50:
try: try:
if data.get('protocol') == 'torrent_magnet': if data.get('protocol') == 'torrent_magnet':
@@ -53,16 +36,13 @@ class Blackhole(DownloaderBase):
except: except:
log.error('Failed download torrent via magnet url: %s', traceback.format_exc()) log.error('Failed download torrent via magnet url: %s', traceback.format_exc())
# If it's still empty, don't know what to do!
if not filedata or len(filedata) < 50: if not filedata or len(filedata) < 50:
log.error('No nzb/torrent available: %s', data.get('url')) log.error('No nzb/torrent available: %s', data.get('url'))
return False return False
# Create filename with imdb id and other nice stuff
file_name = self.createFileName(data, filedata, media) file_name = self.createFileName(data, filedata, media)
full_path = os.path.join(directory, file_name) full_path = os.path.join(directory, file_name)
# People want thinks nice and tidy, create a subdir
if self.conf('create_subdir'): if self.conf('create_subdir'):
try: try:
new_path = os.path.splitext(full_path)[0] new_path = os.path.splitext(full_path)[0]
@@ -73,8 +53,6 @@ class Blackhole(DownloaderBase):
log.error('Couldnt create sub dir, reverting to old one: %s', full_path) log.error('Couldnt create sub dir, reverting to old one: %s', full_path)
try: try:
# Make sure the file doesn't exist yet, no need in overwriting it
if not os.path.isfile(full_path): if not os.path.isfile(full_path):
log.info('Downloading %s to %s.', (data.get('protocol'), full_path)) log.info('Downloading %s to %s.', (data.get('protocol'), full_path))
with open(full_path, 'wb') as f: with open(full_path, 'wb') as f:
@@ -96,10 +74,6 @@ class Blackhole(DownloaderBase):
return False return False
def test(self): def test(self):
""" Test and see if the directory is writable
:return: boolean
"""
directory = self.conf('directory') directory = self.conf('directory')
if directory and os.path.isdir(directory): if directory and os.path.isdir(directory):
@@ -114,10 +88,6 @@ class Blackhole(DownloaderBase):
return False return False
def getEnabledProtocol(self): def getEnabledProtocol(self):
""" What protocols is this downloaded used for
:return: list with protocols
"""
if self.conf('use_for') == 'both': if self.conf('use_for') == 'both':
return super(Blackhole, self).getEnabledProtocol() return super(Blackhole, self).getEnabledProtocol()
elif self.conf('use_for') == 'torrent': elif self.conf('use_for') == 'torrent':
@@ -126,12 +96,6 @@ class Blackhole(DownloaderBase):
return ['nzb'] return ['nzb']
def isEnabled(self, manual = False, data = None): def isEnabled(self, manual = False, data = None):
""" Check if protocol is used (and enabled)
:param manual: The user has clicked to download a link through the webUI
:param data: dict returned from provider
Contains the release information
:return: boolean
"""
if not data: data = {} if not data: data = {}
for_protocol = ['both'] for_protocol = ['both']
if data and 'torrent' in data.get('protocol'): if data and 'torrent' in data.get('protocol'):
-34
View File
@@ -25,18 +25,8 @@ class Deluge(DownloaderBase):
drpc = None drpc = None
def connect(self, reconnect = False): def connect(self, reconnect = False):
""" Connect to the delugeRPC, re-use connection when already available
:param reconnect: force reconnect
:return: DelugeRPC instance
"""
# Load host from config and split out port. # Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).split(':') host = cleanHost(self.conf('host'), protocol = False).split(':')
# Force host assignment
if len(host) == 1:
host.append(80)
if not isInt(host[1]): if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
@@ -47,20 +37,6 @@ class Deluge(DownloaderBase):
return self.drpc return self.drpc
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {} if not media: media = {}
if not data: data = {} if not data: data = {}
@@ -115,21 +91,11 @@ class Deluge(DownloaderBase):
return self.downloadReturnId(remote_torrent) return self.downloadReturnId(remote_torrent)
def test(self): def test(self):
""" Check if connection works
:return: bool
"""
if self.connect(True) and self.drpc.test(): if self.connect(True) and self.drpc.test():
return True return True
return False return False
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking Deluge download status.') log.debug('Checking Deluge download status.')
-427
View File
@@ -1,427 +0,0 @@
from base64 import b16encode, b32decode, b64encode
from distutils.version import LooseVersion
from hashlib import sha1
import httplib
import json
import os
import re
import urllib2
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from bencode import bencode as benc, bdecode
log = CPLog(__name__)
autoload = 'Hadouken'
class Hadouken(DownloaderBase):
protocol = ['torrent', 'torrent_magnet']
hadouken_api = None
def connect(self):
# Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
if not self.conf('api_key'):
log.error('Config properties are not filled in correctly, API key is missing.')
return False
self.hadouken_api = HadoukenAPI(host[0], port = host[1], api_key = self.conf('api_key'))
return True
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {}
if not data: data = {}
log.debug("Sending '%s' (%s) to Hadouken.", (data.get('name'), data.get('protocol')))
if not self.connect():
return False
torrent_params = {}
if self.conf('label'):
torrent_params['label'] = self.conf('label')
torrent_filename = self.createFileName(data, filedata, media)
if data.get('protocol') == 'torrent_magnet':
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
torrent_params['trackers'] = self.torrent_trackers
torrent_params['name'] = torrent_filename
else:
info = bdecode(filedata)['info']
torrent_hash = sha1(benc(info)).hexdigest().upper()
# Convert base 32 to hex
if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash))
# Send request to Hadouken
if data.get('protocol') == 'torrent_magnet':
self.hadouken_api.add_magnet_link(data.get('url'), torrent_params)
else:
self.hadouken_api.add_file(filedata, torrent_params)
return self.downloadReturnId(torrent_hash)
def test(self):
""" Tests the given host:port and API key """
if not self.connect():
return False
version = self.hadouken_api.get_version()
if not version:
log.error('Could not get Hadouken version.')
return False
# The minimum required version of Hadouken is 4.5.6.
if LooseVersion(version) >= LooseVersion('4.5.6'):
return True
log.error('Hadouken v4.5.6 (or newer) required. Found v%s', version)
return False
def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking Hadouken download status.')
if not self.connect():
return []
release_downloads = ReleaseDownloadList(self)
queue = self.hadouken_api.get_by_hash_list(ids)
if not queue:
return []
for torrent in queue:
if torrent is None:
continue
torrent_filelist = self.hadouken_api.get_files_by_hash(torrent['InfoHash'])
torrent_files = []
save_path = torrent['SavePath']
# The 'Path' key for each file_item contains
# the full path to the single file relative to the
# torrents save path.
# For a single file torrent the result would be,
# - Save path: "C:\Downloads"
# - file_item['Path'] = "file1.iso"
# Resulting path: "C:\Downloads\file1.iso"
# For a multi file torrent the result would be,
# - Save path: "C:\Downloads"
# - file_item['Path'] = "dirname/file1.iso"
# Resulting path: "C:\Downloads\dirname/file1.iso"
for file_item in torrent_filelist:
torrent_files.append(sp(os.path.join(save_path, file_item['Path'])))
release_downloads.append({
'id': torrent['InfoHash'].upper(),
'name': torrent['Name'],
'status': self.get_torrent_status(torrent),
'seed_ratio': self.get_seed_ratio(torrent),
'original_status': torrent['State'],
'timeleft': -1,
'folder': sp(save_path if len(torrent_files == 1) else os.path.join(save_path, torrent['Name'])),
'files': torrent_files
})
return release_downloads
def get_seed_ratio(self, torrent):
""" Returns the seed ratio for a given torrent.
Keyword arguments:
torrent -- The torrent to calculate seed ratio for.
"""
up = torrent['TotalUploadedBytes']
down = torrent['TotalDownloadedBytes']
if up > 0 and down > 0:
return up / down
return 0
def get_torrent_status(self, torrent):
""" Returns the CouchPotato status for a given torrent.
Keyword arguments:
torrent -- The torrent to translate status for.
"""
if torrent['IsSeeding'] and torrent['IsFinished'] and torrent['Paused']:
return 'completed'
if torrent['IsSeeding']:
return 'seeding'
return 'busy'
def pause(self, release_download, pause = True):
""" Pauses or resumes the torrent specified by the ID field
in release_download.
Keyword arguments:
release_download -- The CouchPotato release_download to pause/resume.
pause -- Boolean indicating whether to pause or resume.
"""
if not self.connect():
return False
return self.hadouken_api.pause(release_download['id'], pause)
def removeFailed(self, release_download):
""" Removes a failed torrent and also remove the data associated with it.
Keyword arguments:
release_download -- The CouchPotato release_download to remove.
"""
log.info('%s failed downloading, deleting...', release_download['name'])
if not self.connect():
return False
return self.hadouken_api.remove(release_download['id'], remove_data = True)
def processComplete(self, release_download, delete_files = False):
""" Removes the completed torrent from Hadouken and optionally removes the data
associated with it.
Keyword arguments:
release_download -- The CouchPotato release_download to remove.
delete_files: Boolean indicating whether to remove the associated data.
"""
log.debug('Requesting Hadouken to remove the torrent %s%s.',
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
if not self.connect():
return False
return self.hadouken_api.remove(release_download['id'], remove_data = delete_files)
class HadoukenAPI(object):
def __init__(self, host = 'localhost', port = 7890, api_key = None):
self.url = 'http://' + str(host) + ':' + str(port)
self.api_key = api_key
self.requestId = 0;
self.opener = urllib2.build_opener()
self.opener.addheaders = [('User-agent', 'couchpotato-hadouken-client/1.0'), ('Accept', 'application/json')]
if not api_key:
log.error('API key missing.')
def add_file(self, filedata, torrent_params):
""" Add a file to Hadouken with the specified parameters.
Keyword arguments:
filedata -- The binary torrent data.
torrent_params -- Additional parameters for the file.
"""
data = {
'method': 'torrents.addFile',
'params': [b64encode(filedata), torrent_params]
}
return self._request(data)
def add_magnet_link(self, magnetLink, torrent_params):
""" Add a magnet link to Hadouken with the specified parameters.
Keyword arguments:
magnetLink -- The magnet link to send.
torrent_params -- Additional parameters for the magnet link.
"""
data = {
'method': 'torrents.addUrl',
'params': [magnetLink, torrent_params]
}
return self._request(data)
def get_by_hash_list(self, infoHashList):
""" Gets a list of torrents filtered by the given info hash list.
Keyword arguments:
infoHashList -- A list of info hashes.
"""
data = {
'method': 'torrents.getByInfoHashList',
'params': [infoHashList]
}
return self._request(data)
def get_files_by_hash(self, infoHash):
""" Gets a list of files for the torrent identified by the
given info hash.
Keyword arguments:
infoHash -- The info hash of the torrent to return files for.
"""
data = {
'method': 'torrents.getFiles',
'params': [infoHash]
}
return self._request(data)
def get_version(self):
""" Gets the version, commitish and build date of Hadouken. """
data = {
'method': 'core.getVersion',
'params': None
}
result = self._request(data)
if not result:
return False
return result['Version']
def pause(self, infoHash, pause):
""" Pauses/unpauses the torrent identified by the given info hash.
Keyword arguments:
infoHash -- The info hash of the torrent to operate on.
pause -- If true, pauses the torrent. Otherwise resumes.
"""
data = {
'method': 'torrents.pause',
'params': [infoHash]
}
if not pause:
data['method'] = 'torrents.resume'
return self._request(data)
def remove(self, infoHash, remove_data = False):
""" Removes the torrent identified by the given info hash and
optionally removes the data as well.
Keyword arguments:
infoHash -- The info hash of the torrent to remove.
remove_data -- If true, removes the data associated with the torrent.
"""
data = {
'method': 'torrents.remove',
'params': [infoHash, remove_data]
}
return self._request(data)
def _request(self, data):
self.requestId += 1
data['jsonrpc'] = '2.0'
data['id'] = self.requestId
request = urllib2.Request(self.url + '/jsonrpc', data = json.dumps(data))
request.add_header('Authorization', 'Token ' + self.api_key)
request.add_header('Content-Type', 'application/json')
try:
f = self.opener.open(request)
response = f.read()
f.close()
obj = json.loads(response)
if not 'error' in obj.keys():
return obj['result']
log.error('JSONRPC error, %s: %s', obj['error']['code'], obj['error']['message'])
except httplib.InvalidURL as err:
log.error('Invalid Hadouken host, check your config %s', err)
except urllib2.HTTPError as err:
if err.code == 401:
log.error('Invalid Hadouken API key, check your config')
else:
log.error('Hadouken HTTPError: %s', err)
except urllib2.URLError as err:
log.error('Unable to connect to Hadouken %s', err)
return False
config = [{
'name': 'hadouken',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'hadouken',
'label': 'Hadouken',
'description': 'Use <a href="http://www.hdkn.net">Hadouken</a> (>= v4.5.6) to download torrents.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent'
},
{
'name': 'host',
'default': 'localhost:7890'
},
{
'name': 'api_key',
'label': 'API key',
'type': 'password'
},
{
'name': 'label',
'description': 'Label to add torrent as.'
}
]
}
]
}]
+3 -28
View File
@@ -23,20 +23,6 @@ class NZBGet(DownloaderBase):
rpc = 'xmlrpc' rpc = 'xmlrpc'
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {} if not media: media = {}
if not data: data = {} if not data: data = {}
@@ -85,10 +71,6 @@ class NZBGet(DownloaderBase):
return False return False
def test(self): def test(self):
""" Check if connection works
:return: bool
"""
rpc = self.getRPC() rpc = self.getRPC()
try: try:
@@ -109,13 +91,6 @@ class NZBGet(DownloaderBase):
return True return True
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking NZBGet download status.') log.debug('Checking NZBGet download status.')
@@ -188,12 +163,12 @@ class NZBGet(DownloaderBase):
nzb_id = nzb['NZBID'] nzb_id = nzb['NZBID']
if nzb_id in ids: if nzb_id in ids:
log.debug('Found %s in NZBGet history. TotalStatus: %s, ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['Status'], nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log'])) log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
release_downloads.append({ release_downloads.append({
'id': nzb_id, 'id': nzb_id,
'name': nzb['NZBFilename'], 'name': nzb['NZBFilename'],
'status': 'completed' if 'SUCCESS' in nzb['Status'] else 'failed', 'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
'original_status': nzb['Status'], 'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
'timeleft': str(timedelta(seconds = 0)), 'timeleft': str(timedelta(seconds = 0)),
'folder': sp(nzb['DestDir']) 'folder': sp(nzb['DestDir'])
}) })
-25
View File
@@ -24,20 +24,6 @@ class NZBVortex(DownloaderBase):
session_id = None session_id = None
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {} if not media: media = {}
if not data: data = {} if not data: data = {}
@@ -59,10 +45,6 @@ class NZBVortex(DownloaderBase):
return False return False
def test(self): def test(self):
""" Check if connection works
:return: bool
"""
try: try:
login_result = self.login() login_result = self.login()
except: except:
@@ -71,13 +53,6 @@ class NZBVortex(DownloaderBase):
return login_result return login_result
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
raw_statuses = self.call('nzb') raw_statuses = self.call('nzb')
-18
View File
@@ -19,20 +19,6 @@ class Pneumatic(DownloaderBase):
status_support = False status_support = False
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {} if not media: media = {}
if not data: data = {} if not data: data = {}
@@ -77,10 +63,6 @@ class Pneumatic(DownloaderBase):
return False return False
def test(self): def test(self):
""" Check if connection works
:return: bool
"""
directory = self.conf('directory') directory = self.conf('directory')
if directory and os.path.isdir(directory): if directory and os.path.isdir(directory):
@@ -1,68 +0,0 @@
from .main import PutIO
def autoload():
return PutIO()
config = [{
'name': 'putio',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'putio',
'label': 'put.io',
'description': 'This will start a torrent download on <a href="http://put.io">Put.io</a>.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'oauth_token',
'label': 'oauth_token',
'description': 'This is the OAUTH_TOKEN from your putio API',
'advanced': True,
},
{
'name': 'folder',
'description': ('The folder on putio where you want the upload to go','Will find the first first folder that matches this name'),
'default': 0,
},
{
'name': 'callback_host',
'description': 'External reachable url to CP so put.io can do it\'s thing',
},
{
'name': 'download',
'description': 'Set this to have CouchPotato download the file from Put.io',
'type': 'bool',
'default': 0,
},
{
'name': 'delete_file',
'description': ('Set this to remove the file from putio after sucessful download','Does nothing if you don\'t select download'),
'type': 'bool',
'default': 0,
},
{
'name': 'download_dir',
'type': 'directory',
'label': 'Download Directory',
'description': 'The Directory to download files to, does nothing if you don\'t select download',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
}]
-181
View File
@@ -1,181 +0,0 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEventAsync
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from pio import api as pio
import datetime
log = CPLog(__name__)
autoload = 'Putiodownload'
class PutIO(DownloaderBase):
protocol = ['torrent', 'torrent_magnet']
downloading_list = []
oauth_authenticate = 'https://api.couchpota.to/authorize/putio/'
def __init__(self):
addApiView('downloader.putio.getfrom', self.getFromPutio, docs = {
'desc': 'Allows you to download file from prom Put.io',
})
addApiView('downloader.putio.auth_url', self.getAuthorizationUrl)
addApiView('downloader.putio.credentials', self.getCredentials)
addEvent('putio.download', self.putioDownloader)
return super(PutIO, self).__init__()
# This is a recusive function to check for the folders
def recursionFolder(self, client, folder = 0, tfolder = ''):
files = client.File.list(folder)
for f in files:
if f.content_type == 'application/x-directory':
if f.name == tfolder:
return f.id
else:
result = self.recursionFolder(client, f.id, tfolder)
if result != 0:
return result
return 0
# This will check the root for the folder, and kick of recusively checking sub folder
def convertFolder(self, client, folder):
if folder == 0:
return 0
else:
return self.recursionFolder(client, 0, folder)
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
log.info('Sending "%s" to put.io', data.get('name'))
url = data.get('url')
client = pio.Client(self.conf('oauth_token'))
putioFolder = self.convertFolder(client, self.conf('folder'))
log.debug('putioFolder ID is %s', putioFolder)
# It might be possible to call getFromPutio from the renamer if we can then we don't need to do this.
# Note callback_host is NOT our address, it's the internet host that putio can call too
callbackurl = None
if self.conf('download'):
callbackurl = 'http://' + self.conf('callback_host') + '%sdownloader.putio.getfrom/' %Env.get('api_base'.strip('/'))
resp = client.Transfer.add_url(url, callback_url = callbackurl, parent_id = putioFolder)
log.debug('resp is %s', resp.id);
return self.downloadReturnId(resp.id)
def test(self):
try:
client = pio.Client(self.conf('oauth_token'))
if client.File.list():
return True
except:
log.info('Failed to get file listing, check OAUTH_TOKEN')
return False
def getAuthorizationUrl(self, host = None, **kwargs):
callback_url = cleanHost(host) + '%sdownloader.putio.credentials/' % (Env.get('api_base').lstrip('/'))
log.debug('callback_url is %s', callback_url)
target_url = self.oauth_authenticate + "?target=" + callback_url
log.debug('target_url is %s', target_url)
return {
'success': True,
'url': target_url,
}
def getCredentials(self, **kwargs):
try:
oauth_token = kwargs.get('oauth')
except:
return 'redirect', Env.get('web_base') + 'settings/downloaders/'
log.debug('oauth_token is: %s', oauth_token)
self.conf('oauth_token', value = oauth_token);
return 'redirect', Env.get('web_base') + 'settings/downloaders/'
def getAllDownloadStatus(self, ids):
log.debug('Checking putio download status.')
client = pio.Client(self.conf('oauth_token'))
transfers = client.Transfer.list()
log.debug(transfers);
release_downloads = ReleaseDownloadList(self)
for t in transfers:
if t.id in ids:
log.debug('downloading list is %s', self.downloading_list)
if t.status == "COMPLETED" and self.conf('download') == False :
status = 'completed'
# So check if we are trying to download something
elif t.status == "COMPLETED" and self.conf('download') == True:
# Assume we are done
status = 'completed'
if not self.downloading_list:
now = datetime.datetime.utcnow()
date_time = datetime.datetime.strptime(t.finished_at,"%Y-%m-%dT%H:%M:%S")
# We need to make sure a race condition didn't happen
if (now - date_time) < datetime.timedelta(minutes=5):
# 5 minutes haven't passed so we wait
status = 'busy'
else:
# If we have the file_id in the downloading_list mark it as busy
if str(t.file_id) in self.downloading_list:
status = 'busy'
else:
status = 'busy'
release_downloads.append({
'id' : t.id,
'name': t.name,
'status': status,
'timeleft': t.estimated_time,
})
return release_downloads
def putioDownloader(self, fid):
log.info('Put.io Real downloader called with file_id: %s',fid)
client = pio.Client(self.conf('oauth_token'))
log.debug('About to get file List')
putioFolder = self.convertFolder(client, self.conf('folder'))
log.debug('PutioFolderID is %s', putioFolder)
files = client.File.list(parent_id=putioFolder)
downloaddir = self.conf('download_dir')
for f in files:
if str(f.id) == str(fid):
client.File.download(f, dest = downloaddir, delete_after_download = self.conf('delete_file'))
# Once the download is complete we need to remove it from the running list.
self.downloading_list.remove(fid)
return True
def getFromPutio(self, **kwargs):
try:
file_id = str(kwargs.get('file_id'))
except:
return {
'success' : False,
}
log.info('Put.io Download has been called file_id is %s', file_id)
if file_id not in self.downloading_list:
self.downloading_list.append(file_id)
fireEventAsync('putio.download',fid = file_id)
return {
'success': True,
}
return {
'success': False,
}
@@ -1,68 +0,0 @@
var PutIODownloader = new Class({
initialize: function(){
var self = this;
App.addEvent('loadSettings', self.addRegisterButton.bind(self));
},
addRegisterButton: function(){
var self = this;
var setting_page = App.getPage('Settings');
setting_page.addEvent('create', function(){
var fieldset = setting_page.tabs.downloaders.groups.putio,
l = window.location;
var putio_set = 0;
fieldset.getElements('input[type=text]').each(function(el){
putio_set += +(el.get('value') !== '');
});
new Element('.ctrlHolder').adopt(
// Unregister button
(putio_set > 0) ?
[
self.unregister = new Element('a.button.red', {
'text': 'Unregister "'+fieldset.getElement('input[name*=oauth_token]').get('value')+'"',
'events': {
'click': function(){
fieldset.getElements('input[name*=oauth_token]').set('value', '').fireEvent('change');
self.unregister.destroy();
self.unregister_or.destroy();
}
}
}),
self.unregister_or = new Element('span[text=or]')
]
: null,
// Register button
new Element('a.button', {
'text': putio_set > 0 ? 'Register a different account' : 'Register your put.io account',
'events': {
'click': function(){
Api.request('downloader.putio.auth_url', {
'data': {
'host': l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '')
},
'onComplete': function(json){
window.location = json.url;
}
});
}
}
})
).inject(fieldset.getElement('.test_button'), 'before');
});
}
});
window.addEvent('domready', function(){
new PutIODownloader();
});
@@ -41,30 +41,12 @@ class qBittorrent(DownloaderBase):
return self.qb return self.qb
def test(self): def test(self):
""" Check if connection works
:return: bool
"""
if self.connect(): if self.connect():
return True return True
return False return False
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {} if not media: media = {}
if not data: data = {} if not data: data = {}
@@ -113,14 +95,6 @@ class qBittorrent(DownloaderBase):
return 'busy' return 'busy'
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking qBittorrent download status.') log.debug('Checking qBittorrent download status.')
if not self.connect(): if not self.connect():
-26
View File
@@ -84,10 +84,6 @@ class rTorrent(DownloaderBase):
return self.rt return self.rt
def test(self): def test(self):
""" Check if connection works
:return: bool
"""
if self.connect(True): if self.connect(True):
return True return True
@@ -98,20 +94,6 @@ class rTorrent(DownloaderBase):
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {} if not media: media = {}
if not data: data = {} if not data: data = {}
@@ -179,14 +161,6 @@ class rTorrent(DownloaderBase):
return 'completed' return 'completed'
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking rTorrent download status.') log.debug('Checking rTorrent download status.')
if not self.connect(): if not self.connect():
-27
View File
@@ -21,21 +21,6 @@ class Sabnzbd(DownloaderBase):
protocol = ['nzb'] protocol = ['nzb']
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
"""
Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {} if not media: media = {}
if not data: data = {} if not data: data = {}
@@ -84,11 +69,6 @@ class Sabnzbd(DownloaderBase):
return False return False
def test(self): def test(self):
""" Check if connection works
Return message if an old version of SAB is used
:return: bool
"""
try: try:
sab_data = self.call({ sab_data = self.call({
'mode': 'version', 'mode': 'version',
@@ -109,13 +89,6 @@ class Sabnzbd(DownloaderBase):
return True return True
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking SABnzbd download status.') log.debug('Checking SABnzbd download status.')
+1 -20
View File
@@ -19,21 +19,6 @@ class Synology(DownloaderBase):
status_support = False status_support = False
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
"""
Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {} if not media: media = {}
if not data: data = {} if not data: data = {}
@@ -65,10 +50,6 @@ class Synology(DownloaderBase):
return self.downloadReturnId('') if response else False return self.downloadReturnId('') if response else False
def test(self): def test(self):
""" Check if connection works
:return: bool
"""
host = cleanHost(self.conf('host'), protocol = False).split(':') host = cleanHost(self.conf('host'), protocol = False).split(':')
try: try:
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password')) srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
@@ -137,7 +118,7 @@ class SynologyRPC(object):
def _req(self, url, args, files = None): def _req(self, url, args, files = None):
response = {'success': False} response = {'success': False}
try: try:
req = requests.post(url, data = args, files = files, verify = False) req = requests.post(url, data = args, files = files)
req.raise_for_status() req.raise_for_status()
response = json.loads(req.text) response = json.loads(req.text)
if response['success']: if response['success']:
+2 -32
View File
@@ -34,21 +34,6 @@ class Transmission(DownloaderBase):
return self.trpc return self.trpc
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
"""
Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {} if not media: media = {}
if not data: data = {} if not data: data = {}
@@ -93,32 +78,19 @@ class Transmission(DownloaderBase):
log.error('Failed sending torrent to Transmission') log.error('Failed sending torrent to Transmission')
return False return False
data = remote_torrent.get('torrent-added') or remote_torrent.get('torrent-duplicate')
# Change settings of added torrents # Change settings of added torrents
if torrent_params: if torrent_params:
self.trpc.set_torrent(data['hashString'], torrent_params) self.trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
log.info('Torrent sent to Transmission successfully.') log.info('Torrent sent to Transmission successfully.')
return self.downloadReturnId(data['hashString']) return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
def test(self): def test(self):
""" Check if connection works
:return: bool
"""
if self.connect() and self.trpc.get_session(): if self.connect() and self.trpc.get_session():
return True return True
return False return False
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking Transmission download status.') log.debug('Checking Transmission download status.')
@@ -147,8 +119,6 @@ class Transmission(DownloaderBase):
status = 'failed' status = 'failed'
elif torrent['status'] == 0 and torrent['percentDone'] == 1: elif torrent['status'] == 0 and torrent['percentDone'] == 1:
status = 'completed' status = 'completed'
elif torrent['status'] == 16 and torrent['percentDone'] == 1:
status = 'completed'
elif torrent['status'] in [5, 6]: elif torrent['status'] in [5, 6]:
status = 'seeding' status = 'seeding'
-26
View File
@@ -51,21 +51,6 @@ class uTorrent(DownloaderBase):
return self.utorrent_api return self.utorrent_api
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
"""
Send a torrent/nzb file to the downloader
:param data: dict returned from provider
Contains the release information
:param media: media dict with information
Used for creating the filename when possible
:param filedata: downloaded torrent/nzb filedata
The file gets downloaded in the searcher and send to this function
This is done to have failed checking before using the downloader, so the downloader
doesn't need to worry about that
:return: boolean
One faile returns false, but the downloaded should log his own errors
"""
if not media: media = {} if not media: media = {}
if not data: data = {} if not data: data = {}
@@ -135,10 +120,6 @@ class uTorrent(DownloaderBase):
return self.downloadReturnId(torrent_hash) return self.downloadReturnId(torrent_hash)
def test(self): def test(self):
""" Check if connection works
:return: bool
"""
if self.connect(): if self.connect():
build_version = self.utorrent_api.get_build() build_version = self.utorrent_api.get_build()
if not build_version: if not build_version:
@@ -150,13 +131,6 @@ class uTorrent(DownloaderBase):
return False return False
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self, ids):
""" Get status of all active downloads
:param ids: list of (mixed) downloader ids
Used to match the releases for this downloader as there could be
other downloaders active that it should ignore
:return: list of releases
"""
log.debug('Checking uTorrent download status.') log.debug('Checking uTorrent download status.')
+16 -8
View File
@@ -37,19 +37,27 @@ def toUnicode(original, *args):
except: except:
try: try:
detected = detect(original) detected = detect(original)
try: if detected.get('encoding') == 'utf-8':
if detected.get('confidence') > 0.8: return original.decode('utf-8')
return original.decode(detected.get('encoding'))
except:
pass
return ek(original, *args) return ek(original, *args)
except: except:
raise raise
except: except:
log.error('Unable to decode value "%s..." : %s ', (repr(original)[:20], traceback.format_exc())) log.error('Unable to decode value "%s..." : %s ', (repr(original)[:20], traceback.format_exc()))
return 'ERROR DECODING STRING' ascii_text = str(original).encode('string_escape')
return toUnicode(ascii_text)
def toUTF8(original):
try:
if isinstance(original, str) and len(original) > 0:
# Try to detect
detected = detect(original)
return original.decode(detected.get('encoding')).encode('utf-8')
else:
return original
except:
#log.error('Failed encoding to UTF8: %s', traceback.format_exc())
raise
def ss(original, *args): def ss(original, *args):
@@ -95,7 +103,7 @@ def ek(original, *args):
if isinstance(original, (str, unicode)): if isinstance(original, (str, unicode)):
try: try:
from couchpotato.environment import Env from couchpotato.environment import Env
return original.decode(Env.get('encoding'), 'ignore') return original.decode(Env.get('encoding'))
except UnicodeDecodeError: except UnicodeDecodeError:
raise raise
+51
View File
@@ -0,0 +1,51 @@
import os
from chardet import detect
from couchpotato import Env
fs_enc = Env.get('fs_encoding')
def list_dir(path, full_path = True):
"""
List directory don't error when it doesn't exist
"""
path = unicode_path(path)
if os.path.isdir(path):
for f in os.listdir(path):
if full_path:
yield join(path, f)
else:
yield f
def join(*args):
"""
Join path, encode properly before joining
"""
return os.path.join(*[safe(x) for x in args])
def unicode_path(path):
"""
Convert back to unicode
:param path: path string
"""
if isinstance(path, str):
detected = detect(path)
print detected
path = path.decode(detected.get('encoding'))
path = path.decode('unicode_escape')
return path
def safe(path):
if isinstance(path, unicode):
return path.encode('unicode_escape')
return path
+9 -8
View File
@@ -1,5 +1,6 @@
import logging import logging
import re import re
import traceback
class CPLog(object): class CPLog(object):
@@ -54,19 +55,19 @@ class CPLog(object):
def safeMessage(self, msg, replace_tuple = ()): def safeMessage(self, msg, replace_tuple = ()):
from couchpotato.core.helpers.encoding import ss, toUnicode from couchpotato.core.helpers.encoding import ss, toUTF8
msg = ss(msg) msg = toUTF8(msg)
try: try:
if isinstance(replace_tuple, tuple): if isinstance(replace_tuple, tuple):
msg = msg % tuple([ss(x) if not isinstance(x, (int, float)) else x for x in list(replace_tuple)]) msg = msg % tuple([toUTF8(x) for x in list(replace_tuple)])
elif isinstance(replace_tuple, dict): elif isinstance(replace_tuple, dict):
msg = msg % dict((k, ss(v) if not isinstance(v, (int, float)) else v) for k, v in replace_tuple.iteritems()) msg = msg % dict((k, toUTF8(v)) for k, v in replace_tuple.iteritems())
else: else:
msg = msg % ss(replace_tuple) msg = msg % toUTF8(replace_tuple)
except Exception as e: except:
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e)) self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, traceback.format_exc()))
self.setup() self.setup()
if not self.is_develop: if not self.is_develop:
@@ -83,4 +84,4 @@ class CPLog(object):
except: except:
pass pass
return toUnicode(msg) return toUTF8(msg)
+2 -11
View File
@@ -1,10 +1,9 @@
import os import os
import traceback import traceback
from couchpotato import CPLog, md5 from couchpotato import CPLog
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import six import six
@@ -93,15 +92,7 @@ class MediaBase(Plugin):
if not isinstance(image, (str, unicode)): if not isinstance(image, (str, unicode)):
continue continue
# Check if it has top image if file_type not in existing_files or len(existing_files.get(file_type, [])) == 0:
filename = '%s.%s' % (md5(image), getExt(image))
existing = existing_files.get(file_type, [])
has_latest = False
for x in existing:
if filename in x:
has_latest = True
if not has_latest or file_type not in existing_files or len(existing_files.get(file_type, [])) == 0:
file_path = fireEvent('file.download', url = image, single = True) file_path = fireEvent('file.download', url = image, single = True)
if file_path: if file_path:
existing_files[file_type] = [toUnicode(file_path)] existing_files[file_type] = [toUnicode(file_path)]
+2 -7
View File
@@ -456,11 +456,6 @@ class MediaPlugin(MediaBase):
deleted = True deleted = True
elif new_media_status: elif new_media_status:
media['status'] = new_media_status media['status'] = new_media_status
# Remove profile (no use for in manage)
if new_media_status == 'done':
media['profile_id'] = None
db.update(media) db.update(media)
fireEvent('media.untag', media['_id'], 'recent', single = True) fireEvent('media.untag', media['_id'], 'recent', single = True)
@@ -496,7 +491,7 @@ class MediaPlugin(MediaBase):
} }
}) })
def restatus(self, media_id, tag_recent = True, allowed_restatus = None): def restatus(self, media_id, tag_recent = True):
try: try:
db = get_db() db = get_db()
@@ -531,7 +526,7 @@ class MediaPlugin(MediaBase):
m['status'] = previous_status m['status'] = previous_status
# Only update when status has changed # Only update when status has changed
if previous_status != m['status'] and (not allowed_restatus or m['status'] in allowed_restatus): if previous_status != m['status']:
db.update(m) db.update(m)
# Tag media as recent # Tag media as recent
@@ -94,8 +94,6 @@ class Provider(Plugin):
try: try:
data = XMLTree.fromstring(ss(data)) data = XMLTree.fromstring(ss(data))
return self.getElements(data, item_path) return self.getElements(data, item_path)
except XMLTree.ParseError:
log.error('Invalid XML returned, check "%s" manually for issues', url)
except: except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
@@ -68,12 +68,8 @@ class Base(NZBProvider, RSS):
if not date: if not date:
date = self.getTextElement(nzb, 'pubDate') date = self.getTextElement(nzb, 'pubDate')
nzb_id = self.getTextElement(nzb, 'guid').split('/')[-1:].pop()
name = self.getTextElement(nzb, 'title') name = self.getTextElement(nzb, 'title')
detail_url = self.getTextElement(nzb, 'guid')
nzb_id = detail_url.split('/')[-1:].pop()
if '://' not in detail_url:
detail_url = (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id)
if not name: if not name:
continue continue
@@ -107,7 +103,7 @@ class Base(NZBProvider, RSS):
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024, 'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host), 'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
'detail_url': detail_url, 'detail_url': (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id),
'content': self.getTextElement(nzb, 'description'), 'content': self.getTextElement(nzb, 'description'),
'description': description, 'description': description,
'score': host['extra_score'], 'score': host['extra_score'],
@@ -187,7 +183,7 @@ class Base(NZBProvider, RSS):
return 'try_next' return 'try_next'
try: try:
data = self.urlopen(url, show_error = False, headers = {'User-Agent': Env.getIdentifier()}) data = self.urlopen(url, show_error = False)
self.limits_reached[host] = False self.limits_reached[host] = False
return data return data
except HTTPError as e: except HTTPError as e:
@@ -1,9 +1,13 @@
from urlparse import urlparse, parse_qs
import time
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.nzb.base import NZBProvider from couchpotato.core.media._base.providers.nzb.base import NZBProvider
from dateutil.parser import parse
log = CPLog(__name__) log = CPLog(__name__)
@@ -12,19 +16,27 @@ log = CPLog(__name__)
class Base(NZBProvider, RSS): class Base(NZBProvider, RSS):
urls = { urls = {
'search': 'https://api.omgwtfnzbs.org/json/?%s', 'search': 'https://rss.omgwtfnzbs.org/rss-search.php?%s',
'detail_url': 'https://omgwtfnzbs.org/details.php?id=%s',
} }
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds
cat_ids = [ cat_ids = [
([15], ['dvdrip', 'scr', 'r5', 'tc', 'ts', 'cam']), ([15], ['dvdrip']),
([15, 16], ['brrip']), ([15, 16], ['brrip']),
([16], ['720p', '1080p', 'bd50']), ([16], ['720p', '1080p', 'bd50']),
([17], ['dvdr']), ([17], ['dvdr']),
] ]
cat_backup_id = 'movie' cat_backup_id = 'movie'
def search(self, movie, quality):
if quality['identifier'] in fireEvent('quality.pre_releases', single = True):
return []
return super(Base, self).search(movie, quality)
def _searchOnTitle(self, title, movie, quality, results): def _searchOnTitle(self, title, movie, quality, results):
q = '%s %s' % (title, movie['info']['year']) q = '%s %s' % (title, movie['info']['year'])
@@ -35,20 +47,22 @@ class Base(NZBProvider, RSS):
'api': self.conf('api_key', default = ''), 'api': self.conf('api_key', default = ''),
}) })
nzbs = self.getJsonData(self.urls['search'] % params) nzbs = self.getRSSData(self.urls['search'] % params)
if isinstance(nzbs, list): for nzb in nzbs:
for nzb in nzbs:
results.append({ enclosure = self.getElement(nzb, 'enclosure').attrib
'id': nzb.get('nzbid'), nzb_id = parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0]
'name': toUnicode(nzb.get('release')),
'age': self.calculateAge(tryInt(nzb.get('usenetage'))), results.append({
'size': tryInt(nzb.get('sizebytes')) / 1024 / 1024, 'id': nzb_id,
'url': nzb.get('getnzb'), 'name': toUnicode(self.getTextElement(nzb, 'title')),
'detail_url': nzb.get('details'), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, 'pubDate')).timetuple()))),
'description': nzb.get('weblink') 'size': tryInt(enclosure['length']) / 1024 / 1024,
}) 'url': enclosure['url'],
'detail_url': self.urls['detail_url'] % nzb_id,
'description': self.getTextElement(nzb, 'description')
})
config = [{ config = [{
@@ -13,11 +13,11 @@ log = CPLog(__name__)
class Base(TorrentProvider): class Base(TorrentProvider):
urls = { urls = {
'test': 'https://www.bit-hdtv.com/', 'test': 'http://www.bit-hdtv.com/',
'login': 'https://www.bit-hdtv.com/takelogin.php', 'login': 'http://www.bit-hdtv.com/takelogin.php',
'login_check': 'https://www.bit-hdtv.com/messages.php', 'login_check': 'http://www.bit-hdtv.com/messages.php',
'detail': 'https://www.bit-hdtv.com/details.php?id=%s', 'detail': 'http://www.bit-hdtv.com/details.php?id=%s',
'search': 'https://www.bit-hdtv.com/torrents.php?', 'search': 'http://www.bit-hdtv.com/torrents.php?',
} }
# Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken # Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken
@@ -93,7 +93,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'BiT-HDTV', 'name': 'BiT-HDTV',
'description': '<a href="https://bit-hdtv.com">BiT-HDTV</a>', 'description': '<a href="http://bit-hdtv.com">BiT-HDTV</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABMklEQVR4AZ3Qu0ojcQCF8W9MJcQbJNgEEQUbQVIqWgnaWfkIvoCgggixEAmIhRtY2GV3w7KwU61B0EYIxmiw0YCik84ipaCuc0nmP5dcjIUgOjqDvxf4OAdf9mnMLcUJyPyGSCP+YRdC+Kp8iagJKhuS+InYRhTGgDbeV2uEMand4ZRxizjXHQEimxhraAnUr73BNqQxMiNeV2SwcjTLEVtb4Zl10mXutvOWm2otw5Sxz6TGTbdd6ncuYvVLXAXrvM+ruyBpy1S3JLGDfUQ1O6jn5vTsrJXvqSt4UNfj6vxTRPxBHER5QeSirhLGk/5rWN+ffB1XZuxjnDy1q87m7TS+xOGA+Iv4gfkbaw+nOMXHDHnITGEk0VfRFnn4Po4vNYm6RGukmggR0L08+l+e4HMeASo/i6AJUjLgAAAAAElFTkSuQmCC', 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABMklEQVR4AZ3Qu0ojcQCF8W9MJcQbJNgEEQUbQVIqWgnaWfkIvoCgggixEAmIhRtY2GV3w7KwU61B0EYIxmiw0YCik84ipaCuc0nmP5dcjIUgOjqDvxf4OAdf9mnMLcUJyPyGSCP+YRdC+Kp8iagJKhuS+InYRhTGgDbeV2uEMand4ZRxizjXHQEimxhraAnUr73BNqQxMiNeV2SwcjTLEVtb4Zl10mXutvOWm2otw5Sxz6TGTbdd6ncuYvVLXAXrvM+ruyBpy1S3JLGDfUQ1O6jn5vTsrJXvqSt4UNfj6vxTRPxBHER5QeSirhLGk/5rWN+ffB1XZuxjnDy1q87m7TS+xOGA+Iv4gfkbaw+nOMXHDHnITGEk0VfRFnn4Po4vNYm6RGukmggR0L08+l+e4HMeASo/i6AJUjLgAAAAAElFTkSuQmCC',
'options': [ 'options': [
@@ -1,130 +0,0 @@
import re
import traceback
from couchpotato.core.helpers.variable import tryInt, getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://hdaccess.net/',
'detail': 'https://hdaccess.net/details.php?id=%s',
'search': 'https://hdaccess.net/searchapi.php?apikey=%s&username=%s&imdbid=%s&internal=%s',
'download': 'https://hdaccess.net/grab.php?torrent=%s&apikey=%s',
}
http_time_between_calls = 1 # Seconds
def _search(self, movie, quality, results):
data = self.getJsonData(self.urls['search'] % (self.conf('apikey'), self.conf('username'), getIdentifier(movie), self.conf('internal_only')))
if data:
try:
#for result in data[]:
for key, result in data.iteritems():
if tryInt(result['total_results']) == 0:
return
torrentscore = self.conf('extra_score')
releasegroup = result['releasegroup']
resolution = result['resolution']
encoding = result['encoding']
freeleech = tryInt(result['freeleech'])
seeders = tryInt(result['seeders'])
torrent_desc = '/ %s / %s / %s / %s seeders' % (releasegroup, resolution, encoding, seeders)
if freeleech > 0 and self.conf('prefer_internal'):
torrent_desc += '/ Internal'
torrentscore += 200
if seeders == 0:
torrentscore = 0
name = result['release_name']
year = tryInt(result['year'])
results.append({
'id': tryInt(result['torrentid']),
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
'url': self.urls['download'] % (result['torrentid'], self.conf('apikey')),
'detail_url': self.urls['detail'] % result['torrentid'],
'size': tryInt(result['size']),
'seeders': tryInt(result['seeders']),
'leechers': tryInt(result['leechers']),
'age': tryInt(result['age']),
'score': torrentscore
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
config = [{
'name': 'hdaccess',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'HDAccess',
'wizard': True,
'description': '<a href="https://hdaccess.net">HDAccess</a>',
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAADuUlEQVQ4yz3T209bdQAH8O/vnNNzWno5FIpAKZdSLi23gWMDtumWuSXOyzJj9M1kyIOPS1xiYuKe9GUPezZZnGIiMTqTxS1bdIuYkG2MWKBAKYVszOgKFkrbA+259HfO+fli/PwPHzI+Pg5CCEAI2VcUlEsl1tHdU7P5bGOkWChEaaUCwvHpmkD93POn6bwgCMQGAMYYYwyCruuQnE7SPzjIstvb8l+bm5fXkokJSmlQEkUQAIpSRH5vd0tyum7I/sA1Z5VH2ctmiGWZjHw4McE1NAZtQ9fD25kXt1VN7es7dNjuGRjiJFeVpWo6slsZPhF/Ys/PPeIs2056ff7zIOS5rpU5/viJEwwEnu3Mi18dojjw0aWP6amz57h9RSE/35zinq2nuGjvIQwOj7K2SKeZWkk0auXSSZ+/ZopSy+CbW1pQKpWu6Jr2/qVPPqWRjm6HWi6Tm999g3RyGbndLCqGgVBrO3F7fHykK0YX47NNtGLYlBq/c+H2iD+3k704dHQUDcFmQVXLyP6zhfTqCl45fQYjx17FemoJunoAk1bQFGoVhkdPwNC0ix2dMT+3llodM02rKdo7gN3dHAEhuH/vNgDg3Pl3cPaNt2GZJpYX5lBbFwClBukfGobL5WrayW6NccVCISY4HIQxYts2Q3J5CXOPHuLlo6NoCoXQ2hbG0JFRpJYWcVDIQ5ZlyL5qW5b9hNlWjKsYBgzDgKppMCoGHty7A0orOHbyNNweL+obGnDm9TdhWSYS8Vn4a2shOZ0QJRGSKIHjeGGtWNhjqqpyG+k04k8eozPai9ZwByavf4kfpyZxZGwMfYOHsbwQx34hB5dL4syKweRq/xpXHwzNapqWSSYWMDszzYqFPEaOn4KiKJiZfoCZ6d8Am+GtC++iXCpjaf4P9vefT8HzfKarp3eWRKMxCILwuWXSz977YIK2RTodDoGH1+OG1+tDlbsKkuiAJEngeWBjNUUnv7rucIiOLyzTvMKJTgnVtbVXLctK3L31g+NAUajL5bEptaDpOnTdgGkzVHl9drms0ju3fnJIkphoaQtfbQiFwAcCAY5wnCE5Xff3i8XX4o9nGksH+8zl9hAGZlWMCivkc9z0L3fZ999+LTCGZKi55YJTFHfye3sc6e/vB88LpK6+iWlqSS4WcpcNXZtwOp3B6mo/REmCSSkEgd+qq3vpRkt75Fp9Y1BZWZwnhq4zEovF/u/MATAti4U7umvyu9kR27aikihC9vvTnV2xufVUMu/2uIksy/9tZvgX49fLmAMx3bsAAAAASUVORK5CYII=',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
'description': 'Enter your site username.',
},
{
'name': 'apikey',
'default': '',
'label': 'API Key',
'description': 'Enter your site api key. This can be find on <a href="https://hdaccess.net/usercp.php?action=security">Profile Security</a>',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 0,
'description': 'Will not be (re)moved until this seed ratio is met. HDAccess minimum is 1:1.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 0,
'description': 'Will not be (re)moved until this seed time (in hours) is met. HDAccess minimum is 48 hours.',
},
{
'name': 'prefer_internal',
'advanced': True,
'type': 'bool',
'default': 1,
'description': 'Favors internal releases over non-internal releases.',
},
{
'name': 'internal_only',
'advanced': True,
'label': 'Internal Only',
'type': 'bool',
'default': False,
'description': 'Only download releases marked as HDAccess internal',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]
@@ -29,9 +29,6 @@ class Base(TorrentProvider):
} }
post_data.update(params) post_data.update(params)
if self.conf('internal_only'):
post_data.update({'origin': [1]})
try: try:
result = self.getJsonData(self.urls['api'], data = json.dumps(post_data)) result = self.getJsonData(self.urls['api'], data = json.dumps(post_data))
@@ -113,14 +110,6 @@ config = [{
'default': 0, 'default': 0,
'description': 'Starting score for each release found via this provider.', 'description': 'Starting score for each release found via this provider.',
}, },
{
'name': 'internal_only',
'advanced': True,
'label': 'Internal Only',
'type': 'bool',
'default': False,
'description': 'Only download releases marked as HDBits internal'
}
], ],
}, },
], ],
@@ -14,11 +14,11 @@ log = CPLog(__name__)
class Base(TorrentProvider): class Base(TorrentProvider):
urls = { urls = {
'test': 'https://iptorrents.eu/', 'test': 'https://www.iptorrents.com/',
'base_url': 'https://iptorrents.eu', 'base_url': 'https://www.iptorrents.com',
'login': 'https://iptorrents.eu/torrents/', 'login': 'https://www.iptorrents.com/torrents/',
'login_check': 'https://iptorrents.eu/inbox.php', 'login_check': 'https://www.iptorrents.com/inbox.php',
'search': 'https://iptorrents.eu/torrents/?%s%%s&q=%s&qf=ti&p=%%d', 'search': 'https://www.iptorrents.com/torrents/?%s%%s&q=%s&qf=ti&p=%%d',
} }
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds
@@ -120,7 +120,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'IPTorrents', 'name': 'IPTorrents',
'description': '<a href="https://iptorrents.eu">IPTorrents</a>', 'description': '<a href="http://www.iptorrents.com">IPTorrents</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABRklEQVR42qWQO0vDUBiG8zeKY3EqQUtNO7g0J6ZJ1+ifKIIFQXAqDYKCyaaYxM3udrZLHdRFhXrZ6liCW6mubfk874EESgqaeOCF7/Y8hEh41aq6yZi2nyZgBGya9XKtZs4No05pAkZV2YbEmyMMsoSxLQeC46wCTdPPY4HruPQyGIhF97qLWsS78Miydn4XdK46NJ9OsQAYBzMIMf8MQ9wtCnTdWCaIDx/u7uljOIQEe0hiIWPamSTLay3+RxOCSPI9+RJAo7Er9r2bnqjBFAqyK+VyK4f5/Cr5ni8OFKVCz49PFI5GdNvvU7ttE1M1zMU+8AMqFksEhrMnQsBDzqmDAwzx2ehRLwT7yyCI+vSC99c3mozH1NxrJgWWtR1BOECfEJSVCm6WCzJGCA7+IWhBsM4zywDPwEp4vCjx2DzBH2ODAfsDb33Ps6dQwJgAAAAASUVORK5CYII=', 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABRklEQVR42qWQO0vDUBiG8zeKY3EqQUtNO7g0J6ZJ1+ifKIIFQXAqDYKCyaaYxM3udrZLHdRFhXrZ6liCW6mubfk874EESgqaeOCF7/Y8hEh41aq6yZi2nyZgBGya9XKtZs4No05pAkZV2YbEmyMMsoSxLQeC46wCTdPPY4HruPQyGIhF97qLWsS78Miydn4XdK46NJ9OsQAYBzMIMf8MQ9wtCnTdWCaIDx/u7uljOIQEe0hiIWPamSTLay3+RxOCSPI9+RJAo7Er9r2bnqjBFAqyK+VyK4f5/Cr5ni8OFKVCz49PFI5GdNvvU7ttE1M1zMU+8AMqFksEhrMnQsBDzqmDAwzx2ehRLwT7yyCI+vSC99c3mozH1NxrJgWWtR1BOECfEJSVCm6WCzJGCA7+IWhBsM4zywDPwEp4vCjx2DzBH2ODAfsDb33Ps6dQwJgAAAAASUVORK5CYII=',
'options': [ 'options': [
@@ -42,7 +42,6 @@ class Base(TorrentProvider):
link = result.find('td', attrs = {'class': 'ttr_name'}).find('a') link = result.find('td', attrs = {'class': 'ttr_name'}).find('a')
url = result.find('td', attrs = {'class': 'td_dl'}).find('a') url = result.find('td', attrs = {'class': 'td_dl'}).find('a')
seeders = result.find('td', attrs = {'class': 'ttr_seeders'}).find('a')
leechers = result.find('td', attrs = {'class': 'ttr_leechers'}).find('a') leechers = result.find('td', attrs = {'class': 'ttr_leechers'}).find('a')
torrent_id = link['href'].replace('details?id=', '') torrent_id = link['href'].replace('details?id=', '')
@@ -52,7 +51,7 @@ class Base(TorrentProvider):
'url': self.urls['download'] % url['href'], 'url': self.urls['download'] % url['href'],
'detail_url': self.urls['detail'] % torrent_id, 'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(result.find('td', attrs = {'class': 'ttr_size'}).contents[0]), 'size': self.parseSize(result.find('td', attrs = {'class': 'ttr_size'}).contents[0]),
'seeders': tryInt(seeders.string) if seeders else 0, 'seeders': tryInt(result.find('td', attrs = {'class': 'ttr_seeders'}).find('a').string),
'leechers': tryInt(leechers.string) if leechers else 0, 'leechers': tryInt(leechers.string) if leechers else 0,
'get_more_info': self.getMoreInfo, 'get_more_info': self.getMoreInfo,
}) })
@@ -1,7 +1,7 @@
import traceback import traceback
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
@@ -56,12 +56,11 @@ class Base(TorrentProvider):
full_id = link['href'].replace('details.php?id=', '') full_id = link['href'].replace('details.php?id=', '')
torrent_id = full_id[:6] torrent_id = full_id[:6]
name = toUnicode(link.get('title', link.contents[0]).encode('ISO-8859-1')).strip()
results.append({ results.append({
'id': torrent_id, 'id': torrent_id,
'name': name, 'name': link.contents[0],
'url': self.urls['download'] % (torrent_id, name), 'url': self.urls['download'] % (torrent_id, link.contents[0]),
'detail_url': self.urls['detail'] % torrent_id, 'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]), 'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]),
'seeders': tryInt(cells[8].find('span').contents[0]), 'seeders': tryInt(cells[8].find('span').contents[0]),
@@ -1,4 +1,3 @@
import re
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
@@ -9,12 +8,12 @@ log = CPLog(__name__)
class Base(TorrentProvider): class Base(TorrentProvider):
urls = { urls = {
'test': 'https://torrentday.eu/', 'test': 'http://www.td.af/',
'login': 'https://torrentday.eu/torrents/', 'login': 'http://www.td.af/torrents/',
'login_check': 'https://torrentday.eu/userdetails.php', 'login_check': 'http://www.torrentday.com/userdetails.php',
'detail': 'https://torrentday.eu/details.php?id=%s', 'detail': 'http://www.td.af/details.php?id=%s',
'search': 'https://torrentday.eu/V3/API/API.php', 'search': 'http://www.td.af/V3/API/API.php',
'download': 'https://torrentday.eu/download.php/%s/%s', 'download': 'http://www.td.af/download.php/%s/%s',
} }
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds
@@ -56,10 +55,6 @@ class Base(TorrentProvider):
} }
def loginSuccess(self, output): def loginSuccess(self, output):
often = re.search('You tried too often, please wait .*</div>', output)
if often:
raise Exception(often.group(0)[:-6].strip())
return 'Password not correct' not in output return 'Password not correct' not in output
def loginCheckSuccess(self, output): def loginCheckSuccess(self, output):
@@ -73,7 +68,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'TorrentDay', 'name': 'TorrentDay',
'description': '<a href="https://torrentday.eu/">TorrentDay</a>', 'description': '<a href="http://www.td.af/">TorrentDay</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC5ElEQVQ4y12TXUgUURTH//fO7Di7foeQJH6gEEEIZZllVohfSG/6UA+RSFAQQj74VA8+Bj30lmAlRVSEvZRfhNhaka5ZUG1paKaW39tq5O6Ou+PM3M4o6m6X+XPPzD3zm/+dcy574r515WfIW8CZBM4YAA5Gc/aQC3yd7oXYEONcsISE5dTDh91HS0t7FEWhBUAeN9ynV/d9qJAgE4AECURAcVsGlCCnly26LMA0IQwTa52dje3d3e3hcPi8qqrrMjcVYI3EHCQZlkFOHBwR2QHh2ASAAIJxWGAQEDxjePhs3527XjJwnb37OHBq0T+Tyyjh+9KnEzNJ7nouc1Q/3A3HGsOvnJy+PSUlj81w2Lny9WuJ6+3AmTjD4HOcrdR2dWXLRQePvyaSLfQOPMPC8mC9iHCsOxSyzJCelzdSXlNzD5ujpb25Wbfc/XXJemTXF4+nnCNq+AMLe50uFfEJTiw4GXSFtiHL0SnIq66+p0kSArqO+eH3RdsAv9+f5vW7L7GICq6rmM8XBCAXlBw90rOyxibn5yzfkg/L09M52/jxqdESaIrBXHYZZbB1GX8cEpySxKIB8S5XcOnvqpli1zuwmrTtoLjw5LOK/eeuWsE4JH5IRPaPZKiKigmPp+5pa+u1aEjIMhEgrRkmi9mgxGUhM7LNJSzOzsE3+cOeExovXOjdytE0LV4zqNZUtV0uZzAGoGkhDH/2YHZiErmv4uyWQnZZWc+hoqL3WzlTExN5hhA8IEwkZWZOxwB++30YG/9GkYCPvqAaHAW5uWPROW86OmqCprUR7z1yZDAGQNuCvkoB/baIKUBWMTYymv+gra3eJNvjXu+B562tFyXqTJ6YuHK8rKwvBmC3vR7cOCPQLWFz8LnfXWUrJo9U19BwMyUlJRjTSMJ2ENxUiGxq9KXQfwqYlnWstvbR5aamG9g0uzM8Q4OFt++3NNixQ2NgYmeN03FOTUv7XVpV9aKisvLl1vN/WVhNc/Fi1NEAAAAASUVORK5CYII=', 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC5ElEQVQ4y12TXUgUURTH//fO7Di7foeQJH6gEEEIZZllVohfSG/6UA+RSFAQQj74VA8+Bj30lmAlRVSEvZRfhNhaka5ZUG1paKaW39tq5O6Ou+PM3M4o6m6X+XPPzD3zm/+dcy574r515WfIW8CZBM4YAA5Gc/aQC3yd7oXYEONcsISE5dTDh91HS0t7FEWhBUAeN9ynV/d9qJAgE4AECURAcVsGlCCnly26LMA0IQwTa52dje3d3e3hcPi8qqrrMjcVYI3EHCQZlkFOHBwR2QHh2ASAAIJxWGAQEDxjePhs3527XjJwnb37OHBq0T+Tyyjh+9KnEzNJ7nouc1Q/3A3HGsOvnJy+PSUlj81w2Lny9WuJ6+3AmTjD4HOcrdR2dWXLRQePvyaSLfQOPMPC8mC9iHCsOxSyzJCelzdSXlNzD5ujpb25Wbfc/XXJemTXF4+nnCNq+AMLe50uFfEJTiw4GXSFtiHL0SnIq66+p0kSArqO+eH3RdsAv9+f5vW7L7GICq6rmM8XBCAXlBw90rOyxibn5yzfkg/L09M52/jxqdESaIrBXHYZZbB1GX8cEpySxKIB8S5XcOnvqpli1zuwmrTtoLjw5LOK/eeuWsE4JH5IRPaPZKiKigmPp+5pa+u1aEjIMhEgrRkmi9mgxGUhM7LNJSzOzsE3+cOeExovXOjdytE0LV4zqNZUtV0uZzAGoGkhDH/2YHZiErmv4uyWQnZZWc+hoqL3WzlTExN5hhA8IEwkZWZOxwB++30YG/9GkYCPvqAaHAW5uWPROW86OmqCprUR7z1yZDAGQNuCvkoB/baIKUBWMTYymv+gra3eJNvjXu+B562tFyXqTJ6YuHK8rKwvBmC3vR7cOCPQLWFz8LnfXWUrJo9U19BwMyUlJRjTSMJ2ENxUiGxq9KXQfwqYlnWstvbR5aamG9g0uzM8Q4OFt++3NNixQ2NgYmeN03FOTUv7XVpV9aKisvLl1vN/WVhNc/Fi1NEAAAAASUVORK5CYII=',
'options': [ 'options': [
@@ -17,7 +17,7 @@ class Base(TorrentProvider):
'login': 'https://www.torrentleech.org/user/account/login/', 'login': 'https://www.torrentleech.org/user/account/login/',
'login_check': 'https://torrentleech.org/user/messages', 'login_check': 'https://torrentleech.org/user/messages',
'detail': 'https://www.torrentleech.org/torrent/%s', 'detail': 'https://www.torrentleech.org/torrent/%s',
'search': 'https://www.torrentleech.org/torrents/browse/index/query/%s/categories/%s', 'search': 'https://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
'download': 'https://www.torrentleech.org%s', 'download': 'https://www.torrentleech.org%s',
} }
@@ -13,12 +13,12 @@ log = CPLog(__name__)
class Base(TorrentProvider): class Base(TorrentProvider):
urls = { urls = {
'test': 'https://theshack.us.to/', 'test': 'http://torrentshack.eu/',
'login': 'https://theshack.us.to/login.php', 'login': 'http://torrentshack.eu/login.php',
'login_check': 'https://theshack.us.to/inbox.php', 'login_check': 'http://torrentshack.eu/inbox.php',
'detail': 'https://theshack.us.to/torrent/%s', 'detail': 'http://torrentshack.eu/torrent/%s',
'search': 'https://theshack.us.to/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1', 'search': 'http://torrentshack.eu/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1',
'download': 'https://theshack.us.to/%s', 'download': 'http://torrentshack.eu/%s',
} }
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds
@@ -42,7 +42,6 @@ class Base(TorrentProvider):
link = result.find('span', attrs = {'class': 'torrent_name_link'}).parent link = result.find('span', attrs = {'class': 'torrent_name_link'}).parent
url = result.find('td', attrs = {'class': 'torrent_td'}).find('a') url = result.find('td', attrs = {'class': 'torrent_td'}).find('a')
size = result.find('td', attrs = {'class': 'size'}).contents[0].strip('\n ')
tds = result.find_all('td') tds = result.find_all('td')
results.append({ results.append({
@@ -50,7 +49,7 @@ class Base(TorrentProvider):
'name': six.text_type(link.span.string).translate({ord(six.u('\xad')): None}), 'name': six.text_type(link.span.string).translate({ord(six.u('\xad')): None}),
'url': self.urls['download'] % url['href'], 'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % link['href'], 'detail_url': self.urls['download'] % link['href'],
'size': self.parseSize(size), 'size': self.parseSize(result.find_all('td')[5].string),
'seeders': tryInt(tds[len(tds)-2].string), 'seeders': tryInt(tds[len(tds)-2].string),
'leechers': tryInt(tds[len(tds)-1].string), 'leechers': tryInt(tds[len(tds)-1].string),
}) })
@@ -22,12 +22,12 @@ class Base(TorrentMagnetProvider, RSS):
http_time_between_calls = 0 http_time_between_calls = 0
def _searchOnTitle(self, title, media, quality, results): def _search(self, media, quality, results):
search_url = self.urls['verified_search'] if self.conf('verified_only') else self.urls['search'] search_url = self.urls['verified_search'] if self.conf('verified_only') else self.urls['search']
# Create search parameters # Create search parameters
search_params = self.buildUrl(title, media, quality) search_params = self.buildUrl(media)
smin = quality.get('size_min') smin = quality.get('size_min')
smax = quality.get('size_max') smax = quality.get('size_max')
@@ -2,25 +2,28 @@ import traceback
from couchpotato.core.helpers.variable import tryInt, getIdentifier from couchpotato.core.helpers.variable import tryInt, getIdentifier
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
log = CPLog(__name__) log = CPLog(__name__)
class Base(TorrentProvider): class Base(TorrentMagnetProvider):
urls = { urls = {
'test': '%s/api/v2', 'test': '%s/api',
'search': '%s/api/v2/list_movies.json?limit=50&query_term=%s' 'search': '%s/api/list.json?keywords=%s&quality=%s',
'detail': '%s/api/movie.json?id=%s'
} }
http_time_between_calls = 1 # seconds http_time_between_calls = 1 # seconds
proxy_list = [ proxy_list = [
'https://yts.re', 'http://yify.unlocktorrent.com',
'https://yts.wf', 'http://yify-torrents.com.come.in',
'https://yts.im', 'http://yts.re',
'http://yts.im'
'http://yify-torrents.im',
] ]
def search(self, movie, quality): def search(self, movie, quality):
@@ -36,31 +39,28 @@ class Base(TorrentProvider):
if not domain: if not domain:
return return
search_url = self.urls['search'] % (domain, getIdentifier(movie)) search_url = self.urls['search'] % (domain, getIdentifier(movie), quality['identifier'])
data = self.getJsonData(search_url) data = self.getJsonData(search_url)
data = data.get('data')
if isinstance(data, dict) and data.get('movies'): if data and data.get('MovieList'):
try: try:
for result in data.get('movies'): for result in data.get('MovieList'):
for release in result.get('torrents', []): if result['Quality'] and result['Quality'] not in result['MovieTitle']:
title = result['MovieTitle'] + ' BrRip ' + result['Quality']
else:
title = result['MovieTitle'] + ' BrRip'
if release['quality'] and release['quality'] not in result['title_long']: results.append({
title = result['title_long'] + ' BRRip ' + release['quality'] 'id': result['MovieID'],
else: 'name': title,
title = result['title_long'] + ' BRRip' 'url': result['TorrentMagnetUrl'],
'detail_url': self.urls['detail'] % (domain, result['MovieID']),
results.append({ 'size': self.parseSize(result['Size']),
'id': release['hash'], 'seeders': tryInt(result['TorrentSeeds']),
'name': title, 'leechers': tryInt(result['TorrentPeers']),
'url': release['url'], })
'detail_url': result['url'],
'size': self.parseSize(release['size']),
'seeders': tryInt(release['seeds']),
'leechers': tryInt(release['peers']),
})
except: except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
@@ -0,0 +1,277 @@
.search_form {
display: inline-block;
vertical-align: middle;
position: absolute;
right: 105px;
top: 0;
text-align: right;
height: 100%;
transition: all .4s cubic-bezier(0.9,0,0.1,1);
z-index: 20;
border: 0 solid transparent;
border-bottom-width: 4px;
}
.search_form:hover {
border-color: #047792;
}
@media all and (max-width: 480px) {
.search_form {
right: 44px;
}
}
.search_form.focused,
.search_form.shown {
border-color: #04bce6;
}
.search_form .input {
height: 100%;
overflow: hidden;
width: 45px;
transition: all .4s cubic-bezier(0.9,0,0.1,1);
}
.search_form.focused .input,
.search_form.shown .input {
width: 380px;
background: #4e5969;
}
.search_form .input input {
border-radius: 0;
display: block;
border: 0;
background: none;
color: #FFF;
font-size: 25px;
height: 100%;
width: 100%;
opacity: 0;
padding: 0 40px 0 10px;
transition: all .4s ease-in-out .2s;
}
.search_form.focused .input input,
.search_form.shown .input input {
opacity: 1;
}
.search_form input::-ms-clear {
width : 0;
height: 0;
}
@media all and (max-width: 480px) {
.search_form .input input {
font-size: 15px;
}
.search_form.focused .input,
.search_form.shown .input {
width: 277px;
}
}
.search_form .input a {
position: absolute;
top: 0;
right: 0;
width: 44px;
height: 100%;
cursor: pointer;
vertical-align: middle;
text-align: center;
line-height: 66px;
font-size: 15px;
color: #FFF;
}
.search_form .input a:after {
content: "\e03e";
}
.search_form.shown.filled .input a:after {
content: "\e04e";
}
@media all and (max-width: 480px) {
.search_form .input a {
line-height: 44px;
}
}
.search_form .results_container {
text-align: left;
position: absolute;
background: #5c697b;
margin: 4px 0 0;
width: 470px;
min-height: 50px;
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
display: none;
}
@media all and (max-width: 480px) {
.search_form .results_container {
width: 320px;
}
}
.search_form.focused.filled .results_container,
.search_form.shown.filled .results_container {
display: block;
}
.search_form .results {
max-height: 570px;
overflow-x: hidden;
}
.media_result {
overflow: hidden;
height: 50px;
position: relative;
}
.media_result .options {
position: absolute;
height: 100%;
top: 0;
left: 30px;
right: 0;
padding: 13px;
border: 1px solid transparent;
border-width: 1px 0;
border-radius: 0;
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
}
.media_result .options > .in_library_wanted {
margin-top: -7px;
}
.media_result .options > div {
border: 0;
}
.media_result .options .thumbnail {
vertical-align: middle;
}
.media_result .options select {
vertical-align: middle;
display: inline-block;
margin-right: 10px;
}
.media_result .options select[name=title] { width: 170px; }
.media_result .options select[name=profile] { width: 90px; }
.media_result .options select[name=category] { width: 80px; }
@media all and (max-width: 480px) {
.media_result .options select[name=title] { width: 90px; }
.media_result .options select[name=profile] { width: 50px; }
.media_result .options select[name=category] { width: 50px; }
}
.media_result .options .button {
vertical-align: middle;
display: inline-block;
}
.media_result .options .message {
height: 100%;
font-size: 20px;
color: #fff;
line-height: 20px;
}
.media_result .data {
position: absolute;
height: 100%;
top: 0;
left: 30px;
right: 0;
background: #5c697b;
cursor: pointer;
border-top: 1px solid rgba(255,255,255, 0.08);
transition: all .4s cubic-bezier(0.9,0,0.1,1);
}
.media_result .data.open {
left: 100% !important;
}
.media_result:last-child .data { border-bottom: 0; }
.media_result .in_wanted, .media_result .in_library {
position: absolute;
bottom: 2px;
left: 14px;
font-size: 11px;
}
.media_result .thumbnail {
width: 34px;
min-height: 100%;
display: block;
margin: 0;
vertical-align: top;
}
.media_result .info {
position: absolute;
top: 20%;
left: 15px;
right: 7px;
vertical-align: middle;
}
.media_result .info h2 {
margin: 0;
font-weight: normal;
font-size: 20px;
padding: 0;
}
.search_form .info h2 {
position: absolute;
width: 100%;
}
.media_result .info h2 .title {
display: block;
margin: 0;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.search_form .info h2 .title {
position: absolute;
width: 88%;
}
.media_result .info h2 .year {
padding: 0 5px;
text-align: center;
position: absolute;
width: 12%;
right: 0;
}
@media all and (max-width: 480px) {
.search_form .info h2 .year {
font-size: 12px;
margin-top: 7px;
}
}
.search_form .mask,
.media_result .mask {
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
}
@@ -1,4 +1,4 @@
var BlockSearch = new Class({ Block.Search = new Class({
Extends: BlockBase, Extends: BlockBase,
@@ -9,46 +9,45 @@ var BlockSearch = new Class({
var focus_timer = 0; var focus_timer = 0;
self.el = new Element('div.search_form').adopt( self.el = new Element('div.search_form').adopt(
new Element('a.icon-search', { new Element('div.input').adopt(
'events': { self.input = new Element('input', {
'click': self.clear.bind(self), 'placeholder': 'Search & add a new media',
'touchend': self.clear.bind(self)
}
}),
new Element('div.wrapper').adopt(
self.result_container = new Element('div.results_container', {
'tween': {
'duration': 200
},
'events': { 'events': {
'mousewheel': function(e){ 'input': self.keyup.bind(self),
(e).stopPropagation(); 'paste': self.keyup.bind(self),
'change': self.keyup.bind(self),
'keyup': self.keyup.bind(self),
'focus': function(){
if(focus_timer) clearTimeout(focus_timer);
self.el.addClass('focused');
if(this.get('value'))
self.hideResults(false)
},
'blur': function(){
focus_timer = (function(){
self.el.removeClass('focused')
}).delay(100);
} }
} }
}).grab( }),
self.results = new Element('div.results') new Element('a.icon2', {
), 'events': {
new Element('div.input').grab( 'click': self.clear.bind(self),
self.input = new Element('input', { 'touchend': self.clear.bind(self)
'placeholder': 'Search & add a new media', }
'events': { })
'input': self.keyup.bind(self), ),
'paste': self.keyup.bind(self), self.result_container = new Element('div.results_container', {
'change': self.keyup.bind(self), 'tween': {
'keyup': self.keyup.bind(self), 'duration': 200
'focus': function(){ },
if(focus_timer) clearTimeout(focus_timer); 'events': {
if(this.get('value')) 'mousewheel': function(e){
self.hideResults(false); (e).stopPropagation();
}, }
'blur': function(){ }
focus_timer = (function(){ }).adopt(
self.el.removeClass('focused'); self.results = new Element('div.results')
}).delay(100);
}
}
})
)
) )
); );
@@ -68,12 +67,11 @@ var BlockSearch = new Class({
self.last_q = ''; self.last_q = '';
self.input.set('value', ''); self.input.set('value', '');
self.el.addClass('focused');
self.input.focus(); self.input.focus();
self.media = {}; self.media = {};
self.results.empty(); self.results.empty();
self.el.removeClass('filled'); self.el.removeClass('filled')
} }
}, },
@@ -107,7 +105,7 @@ var BlockSearch = new Class({
self.api_request.cancel(); self.api_request.cancel();
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer); if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer);
self.autocomplete_timer = self.autocomplete.delay(300, self); self.autocomplete_timer = self.autocomplete.delay(300, self)
} }
}, },
@@ -117,10 +115,10 @@ var BlockSearch = new Class({
if(!self.q()){ if(!self.q()){
self.hideResults(true); self.hideResults(true);
return; return
} }
self.list(); self.list()
}, },
list: function(){ list: function(){
@@ -141,7 +139,7 @@ var BlockSearch = new Class({
'q': q 'q': q
}, },
'onComplete': self.fill.bind(self, q) 'onComplete': self.fill.bind(self, q)
}); })
} }
else else
self.fill(q, cache); self.fill(q, cache);
@@ -160,25 +158,30 @@ var BlockSearch = new Class({
Object.each(json, function(media){ Object.each(json, function(media){
if(typeOf(media) == 'array'){ if(typeOf(media) == 'array'){
Object.each(media, function(me){ Object.each(media, function(m){
var m = new window['BlockSearch' + me.type.capitalize() + 'Item'](me); var m = new Block.Search[m.type.capitalize() + 'Item'](m);
$(m).inject(self.results); $(m).inject(self.results);
self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m; self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m;
if(q == m.imdb) if(q == m.imdb)
m.showOptions(); m.showOptions()
}); });
} }
}); });
self.mask.fade('out'); // Calculate result heights
var w = window.getSize(),
rc = self.result_container.getCoordinates();
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px');
self.mask.fade('out')
}, },
loading: function(bool){ loading: function(bool){
this.el[bool ? 'addClass' : 'removeClass']('loading'); this.el[bool ? 'addClass' : 'removeClass']('loading')
}, },
q: function(){ q: function(){
@@ -1,242 +0,0 @@
@import "couchpotato/static/style/mixins";
.search_form {
display: inline-block;
z-index: 200;
width: 44px;
position: relative;
.icon-search {
position: absolute;
z-index: 2;
top: 50%;
left: 0;
height: 100%;
cursor: pointer;
text-align: center;
color: #FFF;
font-size: 20px;
@include translateY(-50%);
}
.wrapper {
position: absolute;
left: 44px;
bottom: 0;
background: $primary_color;
border-radius: $border_radius 0 0 $border_radius;
display: none;
box-shadow: 0 0 15px 2px rgba(0,0,0,.15);
&:before {
@include transform(rotate(45deg));
content: '';
display: block;
position: absolute;
height: 10px;
width: 10px;
background: $primary_color;
left: -6px;
bottom: 16px;
z-index: 1;
}
}
.input {
background: $background_color;
border-radius: $border_radius 0 0 $border_radius;
position: relative;
left: 4px;
height: 44px;
overflow: hidden;
width: 100%;
input {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 1;
&::-ms-clear {
width : 0;
height: 0;
}
}
}
&.focused,
&.shown {
border-color: #04bce6;
.wrapper {
display: block;
width: 380px;
}
.input {
input {
opacity: 1;
}
}
}
.results_container {
min-height: 50px;
text-align: left;
position: relative;
left: 4px;
display: none;
background: $background_color;
border-radius: $border_radius 0 0 0;
overflow: hidden;
.results {
max-height: 280px;
overflow-x: hidden;
.media_result {
overflow: hidden;
height: 50px;
position: relative;
.options {
position: absolute;
height: 100%;
top: 0;
left: 30px;
right: 0;
padding: 10px;
background: rgba(0,0,0,.3);
> .in_library_wanted {
margin-top: -7px;
}
> div {
border: 0;
@include flexbox();
}
.thumbnail {
vertical-align: middle;
}
select {
vertical-align: middle;
display: inline-block;
margin-right: 10px;
min-width: 70px;
@include flex(1 auto);
}
.button {
@include flex(1 auto);
vertical-align: middle;
display: inline-block;
}
.message {
height: 100%;
font-size: 20px;
color: #fff;
line-height: 20px;
}
}
.thumbnail {
width: 30px;
min-height: 100%;
display: block;
margin: 0;
vertical-align: top;
}
.data {
position: absolute;
height: 100%;
top: 0;
left: 30px;
right: 0;
cursor: pointer;
border-top: 1px solid rgba(255,255,255, 0.08);
transition: all .4s cubic-bezier(0.9,0,0.1,1);
@include translateX(0%);
background: $background_color;
&.open {
@include translateX(100%);
}
.in_wanted,
.in_library {
position: absolute;
bottom: 2px;
left: 14px;
font-size: 11px;
}
.info {
position: absolute;
top: 20%;
left: 15px;
right: 7px;
vertical-align: middle;
h2 {
margin: 0;
font-weight: 300;
font-size: 1.25em;
padding: 0;
position: absolute;
width: 100%;
@include flexbox();
.title {
display: inline-block;
margin: 0;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@include flex(1 auto);
}
.year {
opacity: .4;
padding: 0 5px;
width: auto;
display: none;
}
}
}
}
&:hover .info h2 .year {
display: inline-block;
}
&:last-child .data {
border-bottom: 0;
}
}
}
}
&.focused.filled,
&.shown.filled {
.results_container {
display: block;
}
.input {
border-radius: 0 0 0 $border_radius;
}
}
}
+1 -1
View File
@@ -65,7 +65,7 @@ class MovieBase(MovieTypeBase):
return False return False
elif not params.get('info'): elif not params.get('info'):
try: try:
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), adding = True, single = True) is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True)
if not is_movie: if not is_movie:
msg = 'Can\'t add movie, seems to be a TV show.' msg = 'Can\'t add movie, seems to be a TV show.'
log.error(msg) log.error(msg)
@@ -1,52 +0,0 @@
var MovieDetails = new Class({
Extends: BlockBase,
sections: null,
initialize: function(parent, options){
var self = this;
self.sections = {};
self.el = new Element('div',{
'class': 'page active movie_details level_' + (options.level || 0)
}).adopt(
self.overlay = new Element('div.overlay', {
'events': {
'click': self.close.bind(self)
}
}).grab(
new Element('a.close.icon-left-arrow')
),
self.content = new Element('div.content').grab(
new Element('h1', {
'text': parent.getTitle() + (parent.get('year') ? ' (' + parent.get('year') + ')' : '')
})
)
);
self.addSection('description', new Element('div', {
'text': parent.get('plot')
}));
},
addSection: function(name, section_el){
var self = this;
name = name.toLowerCase();
self.content.grab(
self.sections[name] = new Element('div', {
'class': 'section section_' + name
}).grab(section_el)
);
},
close: function(){
var self = this;
self.el.dispose();
}
});
@@ -45,16 +45,15 @@ var MovieList = new Class({
}) : null }) : null
); );
self.changeView(self.getSavedView() || self.options.view || 'thumb'); if($(window).getSize().x <= 480 && !self.options.force_view)
self.changeView('list');
// Create the alphabet nav else
if(self.options.navigation) self.changeView(self.getSavedView() || self.options.view || 'details');
self.createNavigation();
self.getMovies(); self.getMovies();
App.on('movie.added', self.movieAdded.bind(self)); App.on('movie.added', self.movieAdded.bind(self));
App.on('movie.deleted', self.movieDeleted.bind(self)); App.on('movie.deleted', self.movieDeleted.bind(self))
}, },
movieDeleted: function(notification){ movieDeleted: function(notification){
@@ -68,7 +67,7 @@ var MovieList = new Class({
self.setCounter(self.counter_count-1); self.setCounter(self.counter_count-1);
self.total_movies--; self.total_movies--;
} }
}); })
} }
self.checkIfEmpty(); self.checkIfEmpty();
@@ -90,11 +89,15 @@ var MovieList = new Class({
create: function(){ create: function(){
var self = this; var self = this;
// Create the alphabet nav
if(self.options.navigation)
self.createNavigation();
if(self.options.load_more) if(self.options.load_more)
self.scrollspy = new ScrollSpy({ self.scrollspy = new ScrollSpy({
min: function(){ min: function(){
var c = self.load_more.getCoordinates(); var c = self.load_more.getCoordinates();
return c.top - window.document.getSize().y - 300; return c.top - window.document.getSize().y - 300
}, },
onEnter: self.loadMore.bind(self) onEnter: self.loadMore.bind(self)
}); });
@@ -135,7 +138,7 @@ var MovieList = new Class({
self.empty_message = null; self.empty_message = null;
} }
if(self.total_movies && count === 0 && !self.empty_message){ if(self.total_movies && count == 0 && !self.empty_message){
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') + var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : ''); (self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
@@ -227,33 +230,30 @@ var MovieList = new Class({
), ),
new Element('div.menus').adopt( new Element('div.menus').adopt(
self.navigation_counter = new Element('span.counter[title=Total]'), self.navigation_counter = new Element('span.counter[title=Total]'),
self.filter_menu = new BlockMenu(self, { self.filter_menu = new Block.Menu(self, {
'class': 'filter', 'class': 'filter'
'button_class': 'icon-filter'
}), }),
self.navigation_actions = new Element('div.actions', { self.navigation_actions = new Element('ul.actions', {
'events': { 'events': {
'click': function(e, el){ 'click:relay(li)': function(e, el){
(e).stop();
var new_view = self.current_view == 'list' ? 'thumb' : 'list';
var a = 'active'; var a = 'active';
self.navigation_actions.getElements('.'+a).removeClass(a); self.navigation_actions.getElements('.'+a).removeClass(a);
self.changeView(new_view); self.changeView(el.get('data-view'));
this.addClass(a);
self.navigation_actions.getElement('[data-view='+new_view+']')
.addClass(a);
el.inject(el.getParent(), 'top');
el.getSiblings().hide();
setTimeout(function(){
el.getSiblings().setStyle('display', null);
}, 100)
} }
} }
}), }),
self.navigation_menu = new BlockMenu(self, { self.navigation_menu = new Block.Menu(self, {
'class': 'extra', 'class': 'extra'
'button_class': 'icon-dots'
}) })
) )
); ).inject(self.el, 'top');
// Mass edit // Mass edit
self.mass_edit_select_class = new Form.Check(self.mass_edit_select); self.mass_edit_select_class = new Form.Check(self.mass_edit_select);
@@ -261,7 +261,7 @@ var MovieList = new Class({
new Element('option', { new Element('option', {
'value': profile.get('_id'), 'value': profile.get('_id'),
'text': profile.get('label') 'text': profile.get('label')
}).inject(self.mass_edit_quality); }).inject(self.mass_edit_quality)
}); });
self.filter_menu.addLink( self.filter_menu.addLink(
@@ -273,7 +273,7 @@ var MovieList = new Class({
'change': self.search.bind(self) 'change': self.search.bind(self)
} }
}) })
).addClass('search icon-search'); ).addClass('search');
var available_chars; var available_chars;
self.filter_menu.addEvent('open', function(){ self.filter_menu.addEvent('open', function(){
@@ -289,8 +289,8 @@ var MovieList = new Class({
available_chars = json.chars; available_chars = json.chars;
available_chars.each(function(c){ available_chars.each(function(c){
self.letters[c.capitalize()].addClass('available'); self.letters[c.capitalize()].addClass('available')
}); })
} }
}); });
@@ -301,23 +301,23 @@ var MovieList = new Class({
'events': { 'events': {
'click:relay(li.available)': function(e, el){ 'click:relay(li.available)': function(e, el){
self.activateLetter(el.get('data-letter')); self.activateLetter(el.get('data-letter'));
self.getMovies(true); self.getMovies(true)
} }
} }
}) })
); );
// Actions // Actions
['thumb', 'list'].each(function(view){ ['mass_edit', 'details', 'list'].each(function(view){
var current = self.current_view == view; var current = self.current_view == view;
new Element('a', { new Element('li', {
'class': 'icon-' + view + (current ? ' active ' : ''), 'class': 'icon2 ' + view + (current ? ' active ' : ''),
'data-view': view 'data-view': view
}).inject(self.navigation_actions, current ? 'top' : 'bottom'); }).inject(self.navigation_actions, current ? 'top' : 'bottom');
}); });
// All // All
self.letters.all = new Element('li.letter_all.available.active', { self.letters['all'] = new Element('li.letter_all.available.active', {
'text': 'ALL' 'text': 'ALL'
}).inject(self.navigation_alpha); }).inject(self.navigation_alpha);
@@ -346,7 +346,7 @@ var MovieList = new Class({
var selected = 0, var selected = 0,
movies = self.movies.length; movies = self.movies.length;
self.movies.each(function(movie){ self.movies.each(function(movie){
selected += movie.isSelected() ? 1 : 0; selected += movie.isSelected() ? 1 : 0
}); });
var indeterminate = selected > 0 && selected < movies, var indeterminate = selected > 0 && selected < movies,
@@ -441,10 +441,10 @@ var MovieList = new Class({
var ids = []; var ids = [];
self.movies.each(function(movie){ self.movies.each(function(movie){
if (movie.isSelected()) if (movie.isSelected())
ids.include(movie.get('_id')); ids.include(movie.get('_id'))
}); });
return ids; return ids
}, },
massEditToggleAll: function(){ massEditToggleAll: function(){
@@ -453,10 +453,10 @@ var MovieList = new Class({
var select = self.mass_edit_select.get('checked'); var select = self.mass_edit_select.get('checked');
self.movies.each(function(movie){ self.movies.each(function(movie){
movie.select(select); movie.select(select)
}); });
self.calculateSelected(); self.calculateSelected()
}, },
reset: function(){ reset: function(){
@@ -493,12 +493,12 @@ var MovieList = new Class({
.addClass(new_view+'_list'); .addClass(new_view+'_list');
self.current_view = new_view; self.current_view = new_view;
Cookie.write(self.options.identifier+'_view3', new_view, {duration: 1000}); Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000});
}, },
getSavedView: function(){ getSavedView: function(){
var self = this; var self = this;
return Cookie.read(self.options.identifier+'_view3'); return Cookie.read(self.options.identifier+'_view2');
}, },
search: function(){ search: function(){
@@ -537,7 +537,7 @@ var MovieList = new Class({
self.load_more.set('text', 'loading...'); self.load_more.set('text', 'loading...');
} }
if(self.movies.length === 0 && self.options.loader){ if(self.movies.length == 0 && self.options.loader){
self.loader_first = new Element('div.loading').adopt( self.loader_first = new Element('div.loading').adopt(
new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'}) new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
@@ -590,7 +590,7 @@ var MovieList = new Class({
loadMore: function(){ loadMore: function(){
var self = this; var self = this;
if(self.offset >= self.options.limit) if(self.offset >= self.options.limit)
self.getMovies(); self.getMovies()
}, },
store: function(movies){ store: function(movies){
@@ -603,7 +603,7 @@ var MovieList = new Class({
checkIfEmpty: function(){ checkIfEmpty: function(){
var self = this; var self = this;
var is_empty = self.movies.length === 0 && (self.total_movies === 0 || self.total_movies === undefined); var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
if(self.title) if(self.title)
self.title[is_empty ? 'hide' : 'show'](); self.title[is_empty ? 'hide' : 'show']();
@@ -1,4 +1,4 @@
var MoviesManage = new Class({ Page.Manage = new Class({
Extends: PageBase, Extends: PageBase,
@@ -126,12 +126,12 @@ var MoviesManage = new Class({
(folder_progress.eta > 0 ? ', ' + new Date ().increment('second', folder_progress.eta).timeDiffInWords().replace('from now', 'to go') : '') (folder_progress.eta > 0 ? ', ' + new Date ().increment('second', folder_progress.eta).timeDiffInWords().replace('from now', 'to go') : '')
}), }),
new Element('span.percentage', {'text': folder_progress.total ? Math.round(((folder_progress.total-folder_progress.to_go)/folder_progress.total)*100) + '%' : '0%'}) new Element('span.percentage', {'text': folder_progress.total ? Math.round(((folder_progress.total-folder_progress.to_go)/folder_progress.total)*100) + '%' : '0%'})
).inject(self.progress_container); ).inject(self.progress_container)
}); });
} }
} }
}); })
}, 1000); }, 1000);
}, },
@@ -141,10 +141,10 @@ var MoviesManage = new Class({
for (folder in progress_object) { for (folder in progress_object) {
if (progress_object.hasOwnProperty(folder)) { if (progress_object.hasOwnProperty(folder)) {
temp_array.push(folder); temp_array.push(folder)
} }
} }
return temp_array.stableSort(); return temp_array.stableSort()
} }
}); });
@@ -2,10 +2,7 @@ var MovieAction = new Class({
Implements: [Options], Implements: [Options],
class_name: 'action', class_name: 'action icon2',
label: 'UNKNOWN',
button: null,
details: null,
initialize: function(movie, options){ initialize: function(movie, options){
var self = this; var self = this;
@@ -14,33 +11,20 @@ var MovieAction = new Class({
self.movie = movie; self.movie = movie;
self.create(); self.create();
if(self.el)
if(self.button) self.el.addClass(self.class_name)
self.button.addClass(self.class_name);
}, },
create: function(){}, create: function(){},
getButton: function(){
return this.button || null;
},
getDetails: function(){
return this.details || null;
},
getLabel: function(){
return this.label;
},
disable: function(){ disable: function(){
if(this.el) if(this.el)
this.el.addClass('disable'); this.el.addClass('disable')
}, },
enable: function(){ enable: function(){
if(this.el) if(this.el)
this.el.removeClass('disable'); this.el.removeClass('disable')
}, },
getTitle: function(){ getTitle: function(){
@@ -53,7 +37,7 @@ var MovieAction = new Class({
try { try {
return self.movie.original_title ? self.movie.original_title : self.movie.titles[0]; return self.movie.original_title ? self.movie.original_title : self.movie.titles[0];
} }
catch(e2){ catch(e){
return 'Unknown'; return 'Unknown';
} }
} }
@@ -62,10 +46,10 @@ var MovieAction = new Class({
get: function(key){ get: function(key){
var self = this; var self = this;
try { try {
return self.movie.get(key); return self.movie.get(key)
} }
catch(e){ catch(e){
return self.movie[key]; return self.movie[key]
} }
}, },
@@ -79,7 +63,7 @@ var MovieAction = new Class({
}, },
toElement: function(){ toElement: function(){
return this.el || null; return this.el || null
} }
}); });
@@ -96,8 +80,7 @@ MA.IMDB = new Class({
self.id = self.movie.getIdentifier ? self.movie.getIdentifier() : self.get('imdb'); self.id = self.movie.getIdentifier ? self.movie.getIdentifier() : self.get('imdb');
self.button = new Element('a.imdb', { self.el = new Element('a.imdb', {
'text': 'IMDB',
'title': 'Go to the IMDB page of ' + self.getTitle(), 'title': 'Go to the IMDB page of ' + self.getTitle(),
'href': 'http://www.imdb.com/title/'+self.id+'/', 'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank' 'target': '_blank'
@@ -111,11 +94,22 @@ MA.IMDB = new Class({
MA.Release = new Class({ MA.Release = new Class({
Extends: MovieAction, Extends: MovieAction,
label: 'Releases',
create: function(){ create: function(){
var self = this; var self = this;
self.el = new Element('a.releases.download', {
'title': 'Show the releases that are available for ' + self.getTitle(),
'events': {
'click': self.show.bind(self)
}
});
if(!self.movie.data.releases || self.movie.data.releases.length == 0)
self.el.hide();
else
self.showHelper();
App.on('movie.searcher.ended', function(notification){ App.on('movie.searcher.ended', function(notification){
if(self.movie.data._id != notification.data._id) return; if(self.movie.data._id != notification.data._id) return;
@@ -124,7 +118,7 @@ MA.Release = new Class({
// Releases are currently displayed // Releases are currently displayed
if(self.options_container.isDisplayed()){ if(self.options_container.isDisplayed()){
self.options_container.destroy(); self.options_container.destroy();
self.getDetails(); self.createReleases();
} }
else { else {
self.options_container.destroy(); self.options_container.destroy();
@@ -135,7 +129,16 @@ MA.Release = new Class({
}, },
getDetails: function(refresh){ show: function(e){
var self = this;
if(e)
(e).preventDefault();
self.createReleases();
},
createReleases: function(refresh){
var self = this; var self = this;
if(!self.options_container || refresh){ if(!self.options_container || refresh){
@@ -159,14 +162,14 @@ MA.Release = new Class({
var quality = Quality.getQuality(release.quality) || {}, var quality = Quality.getQuality(release.quality) || {},
info = release.info || {}, info = release.info || {},
provider = self.get(release, 'provider') + (info.provider_extra ? self.get(release, 'provider_extra') : ''); provider = self.get(release, 'provider') + (info['provider_extra'] ? self.get(release, 'provider_extra') : '');
var release_name = self.get(release, 'name'); var release_name = self.get(release, 'name');
if(release.files && release.files.length > 0){ if(release.files && release.files.length > 0){
try { try {
var movie_file = release.files.filter(function(file){ var movie_file = release.files.filter(function(file){
var type = File.Type.get(file.type_id); var type = File.Type.get(file.type_id);
return type && type.identifier == 'movie'; return type && type.identifier == 'movie'
}).pick(); }).pick();
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast(); release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
} }
@@ -174,19 +177,19 @@ MA.Release = new Class({
} }
// Create release // Create release
release.el = new Element('div', { release['el'] = new Element('div', {
'class': 'item '+release.status, 'class': 'item '+release.status,
'id': 'release_'+release._id 'id': 'release_'+release._id
}).adopt( }).adopt(
new Element('span.name', {'text': release_name, 'title': release_name}), new Element('span.name', {'text': release_name, 'title': release_name}),
new Element('span.status', {'text': release.status, 'class': 'status '+release.status}), new Element('span.status', {'text': release.status, 'class': 'release_status '+release.status}),
new Element('span.quality', {'text': quality.label + (release.is_3d ? ' 3D' : '') || 'n/a'}), new Element('span.quality', {'text': quality.label + (release.is_3d ? ' 3D' : '') || 'n/a'}),
new Element('span.size', {'text': info.size ? Math.floor(self.get(release, 'size')) : 'n/a'}), new Element('span.size', {'text': info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
new Element('span.age', {'text': self.get(release, 'age')}), new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}), new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', { 'text': provider, 'title': provider }), new Element('span.provider', { 'text': provider, 'title': provider }),
info.detail_url ? new Element('a.info.icon2', { info['detail_url'] ? new Element('a.info.icon2', {
'href': info.detail_url, 'href': info['detail_url'],
'target': '_blank' 'target': '_blank'
}) : new Element('a'), }) : new Element('a'),
new Element('a.download.icon2', { new Element('a.download.icon2', {
@@ -280,7 +283,7 @@ MA.Release = new Class({
new Element('span.or', { new Element('span.or', {
'text': 'or pick one below' 'text': 'or pick one below'
})] : null })] : null
); )
} }
self.last_release = null; self.last_release = null;
@@ -288,7 +291,9 @@ MA.Release = new Class({
} }
return self.options_container; // Show it
self.options_container.inject(self.movie, 'top');
self.movie.slide('in', self.options_container);
}, },
@@ -337,13 +342,13 @@ MA.Release = new Class({
'click': self.markMovieDone.bind(self) 'click': self.markMovieDone.bind(self)
} }
}) })
); )
} }
}, },
get: function(release, type){ get: function(release, type){
return (release.info && release.info[type] !== undefined) ? release.info[type] : 'n/a'; return (release.info && release.info[type] !== undefined) ? release.info[type] : 'n/a'
}, },
download: function(release){ download: function(release){
@@ -381,7 +386,7 @@ MA.Release = new Class({
'data': { 'data': {
'id': release._id 'id': release._id
} }
}); })
}, },
@@ -398,7 +403,7 @@ MA.Release = new Class({
movie.set('tween', { movie.set('tween', {
'duration': 300, 'duration': 300,
'onComplete': function(){ 'onComplete': function(){
self.movie.destroy(); self.movie.destroy()
} }
}); });
movie.tween('height', 0); movie.tween('height', 0);
@@ -424,35 +429,49 @@ MA.Trailer = new Class({
Extends: MovieAction, Extends: MovieAction,
id: null, id: null,
label: 'Trailer',
getDetails: function(){ create: function(){
var self = this; var self = this;
if(!self.player_container){ self.el = new Element('a.trailer', {
var id = 'trailer-'+randomString(); 'title': 'Watch the trailer of ' + self.getTitle(),
self.player_container = new Element('div.icon-play[id='+id+']', { 'events': {
'events': { 'click': self.watch.bind(self)
'click': function(e){ }
self.watch(id); });
}
}
});
self.container = new Element('div.trailer_container')
.grab(self.player_container);
}
return self.player_container;
}, },
watch: function(){ watch: function(offset){
var self = this; var self = this;
var data_url = 'https://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18', var data_url = 'https://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18';
url = data_url.substitute({ var url = data_url.substitute({
'title': encodeURI(self.getTitle()), 'title': encodeURI(self.getTitle()),
'year': self.get('year') 'year': self.get('year'),
}); 'offset': offset || 1
}),
size = $(self.movie).getSize(),
height = self.options.height || (size.x/16)*9,
id = 'trailer-'+randomString();
self.player_container = new Element('div[id='+id+']');
self.container = new Element('div.hide.trailer_container')
.adopt(self.player_container)
.inject($(self.movie), 'top');
self.container.setStyle('height', 0);
self.container.removeClass('hide');
self.close_button = new Element('a.hide.hide_trailer', {
'text': 'Hide trailer',
'events': {
'click': self.stop.bind(self)
}
}).inject(self.movie);
self.container.setStyle('height', height);
$(self.movie).setStyle('height', height);
new Request.JSONP({ new Request.JSONP({
'url': url, 'url': url,
@@ -472,6 +491,8 @@ MA.Trailer = new Class({
} }
}); });
self.close_button.removeClass('hide');
var quality_set = false; var quality_set = false;
var change_quality = function(state){ var change_quality = function(state){
if(!quality_set && (state.data == 1 || state.data || 2)){ if(!quality_set && (state.data == 1 || state.data || 2)){
@@ -487,9 +508,7 @@ MA.Trailer = new Class({
self.player.addEventListener('onStateChange', change_quality); self.player.addEventListener('onStateChange', change_quality);
} }
}).send(); }).send()
return self.container;
}, },
@@ -504,7 +523,7 @@ MA.Trailer = new Class({
setTimeout(function(){ setTimeout(function(){
self.container.destroy(); self.container.destroy();
self.close_button.destroy(); self.close_button.destroy();
}, 1800); }, 1800)
} }
@@ -517,8 +536,7 @@ MA.Edit = new Class({
create: function(){ create: function(){
var self = this; var self = this;
self.button = new Element('a.edit', { self.el = new Element('a.edit', {
'text': 'Edit',
'title': 'Change movie information, like title and quality.', 'title': 'Change movie information, like title and quality.',
'events': { 'events': {
'click': self.editMovie.bind(self) 'click': self.editMovie.bind(self)
@@ -567,7 +585,7 @@ MA.Edit = new Class({
// Fill categories // Fill categories
var categories = CategoryList.getAll(); var categories = CategoryList.getAll();
if(categories.length === 0) if(categories.length == 0)
self.category_select.hide(); self.category_select.hide();
else { else {
self.category_select.show(); self.category_select.show();
@@ -641,8 +659,7 @@ MA.Refresh = new Class({
create: function(){ create: function(){
var self = this; var self = this;
self.button = new Element('a.refresh', { self.el = new Element('a.refresh', {
'text': 'Refresh',
'title': 'Refresh the movie info and do a forced search', 'title': 'Refresh the movie info and do a forced search',
'events': { 'events': {
'click': self.doRefresh.bind(self) 'click': self.doRefresh.bind(self)
@@ -653,7 +670,7 @@ MA.Refresh = new Class({
doRefresh: function(e){ doRefresh: function(e){
var self = this; var self = this;
(e).stop(); (e).preventDefault();
Api.request('media.refresh', { Api.request('media.refresh', {
'data': { 'data': {
@@ -669,18 +686,17 @@ MA.Readd = new Class({
Extends: MovieAction, Extends: MovieAction,
create: function(){ create: function(){
var self = this, var self = this;
movie_done = self.movie.data.status == 'done',
snatched;
var movie_done = self.movie.data.status == 'done';
if(self.movie.data.releases && !movie_done) if(self.movie.data.releases && !movie_done)
snatched = self.movie.data.releases.filter(function(release){ var snatched = self.movie.data.releases.filter(function(release){
return release.status && (release.status == 'snatched' || release.status == 'seeding' || release.status == 'downloaded' || release.status == 'done'); return release.status && (release.status == 'snatched' || release.status == 'seeding' || release.status == 'downloaded' || release.status == 'done');
}).length; }).length;
if(movie_done || snatched && snatched > 0) if(movie_done || snatched && snatched > 0)
self.el = new Element('a.readd', { self.el = new Element('a.readd', {
'title': 'Re-add the movie and mark all previous snatched/downloaded as ignored', 'title': 'Readd the movie and mark all previous snatched/downloaded as ignored',
'events': { 'events': {
'click': self.doReadd.bind(self) 'click': self.doReadd.bind(self)
} }
@@ -776,7 +792,7 @@ MA.Delete = new Class({
movie.set('tween', { movie.set('tween', {
'duration': 300, 'duration': 300,
'onComplete': function(){ 'onComplete': function(){
self.movie.destroy(); self.movie.destroy()
} }
}); });
movie.tween('height', 0); movie.tween('height', 0);
@@ -831,7 +847,7 @@ MA.Files = new Class({
new Element('div.file.item').adopt( new Element('div.file.item').adopt(
new Element('span.name', {'text': file}), new Element('span.name', {'text': file}),
new Element('span.type', {'text': type}) new Element('span.type', {'text': type})
).inject(rel); ).inject(rel)
}); });
}); });
}); });
File diff suppressed because it is too large Load Diff
@@ -2,51 +2,22 @@ var Movie = new Class({
Extends: BlockBase, Extends: BlockBase,
actions: [], action: {},
details: null,
initialize: function(list, options, data){ initialize: function(list, options, data){
var self = this; var self = this;
self.data = data; self.data = data;
self.view = options.view || 'details';
self.list = list; self.list = list;
self.el = new Element('a.movie', { self.el = new Element('div.movie');
'events': {
'click': function(e){
(e).stop();
self.openDetails();
}
}
});
self.profile = Quality.getProfile(data.profile_id) || {}; self.profile = Quality.getProfile(data.profile_id) || {};
self.category = CategoryList.getCategory(data.category_id) || {}; self.category = CategoryList.getCategory(data.category_id) || {};
self.parent(self, options); self.parent(self, options);
self.addEvents(); self.addEvents();
if(data.identifiers.imdb == 'tt1228705')
self.openDetails();
},
openDetails: function(){
var self = this;
if(!self.details){
self.details = new MovieDetails(self, {
'level': 3
});
// Add action items
self.actions.each(function(action, nr){
var details = action.getDetails();
if(details)
self.details.addSection(action.getLabel(), details);
});
}
App.getPageContainer().grab(self.details);
}, },
addEvents: function(){ addEvents: function(){
@@ -59,6 +30,7 @@ var Movie = new Class({
if(self.data._id != notification.data._id) return; if(self.data._id != notification.data._id) return;
self.busy(false); self.busy(false);
self.removeView();
self.update.delay(2000, self, notification); self.update.delay(2000, self, notification);
}; };
App.on('movie.update', self.global_events['movie.update']); App.on('movie.update', self.global_events['movie.update']);
@@ -75,7 +47,7 @@ var Movie = new Class({
// Remove spinner // Remove spinner
self.global_events['movie.searcher.ended'] = function(notification){ self.global_events['movie.searcher.ended'] = function(notification){
if(notification.data && self.data._id == notification.data._id) if(notification.data && self.data._id == notification.data._id)
self.busy(false); self.busy(false)
}; };
App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']); App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
@@ -90,7 +62,7 @@ var Movie = new Class({
var updated = false; var updated = false;
self.data.releases.each(function(release){ self.data.releases.each(function(release){
if(release._id == data._id){ if(release._id == data._id){
release.status = data.status; release['status'] = data.status;
updated = true; updated = true;
} }
}); });
@@ -130,12 +102,12 @@ var Movie = new Class({
if(self.mask) if(self.mask)
self.mask.destroy(); self.mask.destroy();
if(self.spinner) if(self.spinner)
self.spinner.destroy(); self.spinner.el.destroy();
self.spinner = null; self.spinner = null;
self.mask = null; self.mask = null;
}, timeout || 400); }, timeout || 400);
} }
}, timeout || 1000); }, timeout || 1000)
} }
else if(!self.spinner) { else if(!self.spinner) {
self.createMask(); self.createMask();
@@ -158,6 +130,7 @@ var Movie = new Class({
self.data = notification.data; self.data = notification.data;
self.el.empty(); self.el.empty();
self.removeView();
self.profile = Quality.getProfile(self.data.profile_id) || {}; self.profile = Quality.getProfile(self.data.profile_id) || {};
self.category = CategoryList.getCategory(self.data.category_id) || {}; self.category = CategoryList.getCategory(self.data.category_id) || {};
@@ -177,7 +150,7 @@ var Movie = new Class({
if(self.data.info.release_date) if(self.data.info.release_date)
[self.data.info.release_date.dvd, self.data.info.release_date.theater].each(function(timestamp){ [self.data.info.release_date.dvd, self.data.info.release_date.theater].each(function(timestamp){
if (timestamp > 0 && (eta === null || Math.abs(timestamp - now) < Math.abs(eta - now))) if (timestamp > 0 && (eta == null || Math.abs(timestamp - now) < Math.abs(eta - now)))
eta = timestamp; eta = timestamp;
}); });
@@ -190,7 +163,7 @@ var Movie = new Class({
self.select_checkbox = new Element('input[type=checkbox].inlay', { self.select_checkbox = new Element('input[type=checkbox].inlay', {
'events': { 'events': {
'change': function(){ 'change': function(){
self.fireEvent('select'); self.fireEvent('select')
} }
} }
}), }),
@@ -208,6 +181,9 @@ var Movie = new Class({
'text': self.data.info.year || 'n/a' 'text': self.data.info.year || 'n/a'
}) })
), ),
self.description = new Element('div.description.tiny_scroll', {
'text': self.data.info.plot
}),
self.eta = eta_date && (now+8035200 > eta) ? new Element('div.eta', { self.eta = eta_date && (now+8035200 > eta) ? new Element('div.eta', {
'text': eta_date, 'text': eta_date,
'title': 'ETA' 'title': 'ETA'
@@ -217,24 +193,19 @@ var Movie = new Class({
'click': function(e){ 'click': function(e){
var releases = self.el.getElement('.actions .releases'); var releases = self.el.getElement('.actions .releases');
if(releases.isVisible()) if(releases.isVisible())
releases.fireEvent('click', [e]); releases.fireEvent('click', [e])
} }
} }
}) })
), ),
self.actions_el = new Element('div.actions', { self.actions = new Element('div.actions')
'events': {
'click': function(e){
(e).stopPropagation();
}
}
})
) )
); );
if(!self.thumbnail) if(!self.thumbnail)
self.el.addClass('no_thumbnail'); self.el.addClass('no_thumbnail');
//self.changeView(self.view);
self.select_checkbox_class = new Form.Check(self.select_checkbox); self.select_checkbox_class = new Form.Check(self.select_checkbox);
// Add profile // Add profile
@@ -242,9 +213,9 @@ var Movie = new Class({
self.profile.getTypes().each(function(type){ self.profile.getTypes().each(function(type){
var q = self.addQuality(type.get('quality'), type.get('3d')); var q = self.addQuality(type.get('quality'), type.get('3d'));
if((type.finish === true || type.get('finish')) && !q.hasClass('finish')){ if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
q.addClass('finish'); q.addClass('finish');
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.'); q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
} }
}); });
@@ -252,20 +223,17 @@ var Movie = new Class({
// Add releases // Add releases
self.updateReleases(); self.updateReleases();
self.options.actions.each(function(action){ Object.each(self.options.actions, function(action, key){
var action = new action(self), self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
button = action.getButton(); if(action.el)
if(button) self.actions.adopt(action)
self.actions_el.grab(button);
self.actions.push(action);
}); });
}, },
updateReleases: function(){ updateReleases: function(){
var self = this; var self = this;
if(!self.data.releases || self.data.releases.length === 0) return; if(!self.data.releases || self.data.releases.length == 0) return;
self.data.releases.each(function(release){ self.data.releases.each(function(release){
@@ -277,7 +245,7 @@ var Movie = new Class({
if (q && !q.hasClass(status)){ if (q && !q.hasClass(status)){
q.addClass(status); q.addClass(status);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status); q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
} }
}); });
@@ -303,7 +271,7 @@ var Movie = new Class({
else if(self.data.info.titles.length > 0) else if(self.data.info.titles.length > 0)
return self.getUnprefixedTitle(self.data.info.titles[0]); return self.getUnprefixedTitle(self.data.info.titles[0]);
return 'Unknown movie'; return 'Unknown movie'
}, },
getUnprefixedTitle: function(t){ getUnprefixedTitle: function(t){
@@ -316,6 +284,49 @@ var Movie = new Class({
return t; return t;
}, },
slide: function(direction, el){
var self = this;
if(direction == 'in'){
self.temp_view = self.view;
self.changeView('details');
self.el.addEvent('outerClick', function(){
self.removeView();
self.slide('out')
});
el.show();
self.data_container.addClass('hide_right');
}
else {
self.el.removeEvents('outerClick');
setTimeout(function(){
if(self.el)
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
}, 600);
self.data_container.removeClass('hide_right');
}
},
changeView: function(new_view){
var self = this;
if(self.el)
self.el
.removeClass(self.view+'_view')
.addClass(new_view+'_view');
self.view = new_view;
},
removeView: function(){
var self = this;
self.el.removeClass(self.view+'_view')
},
getIdentifier: function(){ getIdentifier: function(){
var self = this; var self = this;
@@ -328,12 +339,12 @@ var Movie = new Class({
}, },
get: function(attr){ get: function(attr){
return this.data[attr] || this.data.info[attr]; return this.data[attr] || this.data.info[attr]
}, },
select: function(bool){ select: function(bool){
var self = this; var self = this;
self.select_checkbox_class[bool ? 'check' : 'uncheck'](); self.select_checkbox_class[bool ? 'check' : 'uncheck']()
}, },
isSelected: function(){ isSelected: function(){
@@ -1,367 +0,0 @@
@import "couchpotato/static/style/mixins";
.page.movies {
z-index: 21; // Sets navigation above
bottom: auto;
}
.page.movies_wanted, .page.movies_manage {
top: $header_height;
padding: 0;
}
.list_list {
font-weight: 300;
.poster {
display: none;
}
.movie {
display: block;
border-top: 1px solid $theme_off;
position: relative;
cursor: pointer;
&:last-child {
border-bottom: none;
}
&:hover {
background: rgba(0,0,0,.1);
}
.data {
padding: $padding/2 $padding;
.info {
@include flexbox();
flex-flow: row nowrap;
.title {
@include flex(1 auto);
.year {
display: inline-block;
margin-left: 10px;
opacity: .5;
}
}
.quality span {
float: left;
color: #FFF;
font-size: .7em;
padding: 2px 4px;
background: rgba(0,0,0,.2);
border-radius: 1px;
margin: 2px 0 0 2px;
}
}
}
}
}
.thumb_list {
font-size: 12px;
padding: 0 $padding;
.movie {
@include span(6);
float: left;
margin-bottom: $padding;
position: relative;
&:nth-child(4n+4){
@include span(last);
}
&:nth-child(4n+5){
clear: both;
}
.poster {
border-radius: $border_radius;
overflow: hidden;
width: 100%;
float: left;
}
.data {
clear: both;
.info {
height: 44px;
.title {
@include flexbox();
padding: 3px 0;
span {
@include flex(1 auto);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.year {
display: inline-block;
margin-left: 5px;
opacity: .5;
}
}
.quality {
white-space: nowrap;
overflow: hidden;
span {
color: #FFF;
font-size: .8em;
padding: 2px 4px;
background: rgba(0,0,0,.2);
border-radius: 1px;
margin-right: 2px;
}
}
}
}
.actions {
position: absolute;
top: $padding / 2;
right: $padding / 2;
display: none;
a {
display: block;
background: $background_color;
padding: $padding / 3;
width: auto;
margin-bottom: 1px;
clear: both;
float: right;
}
}
&:hover .actions {
display: block;
}
.mask {
bottom: 44px;
border-radius: $border_radius;
}
}
}
.check {
position: absolute;
top: 0;
left: $padding;
display: none;
}
.eta {
display: none;
}
.page.movie_details {
$gab-width: $header_width/3;
.overlay {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: $header_width;
background: rgba(0,0,0,.6);
border-radius: 3px 0 0 3px;
.close {
display: inline-block;
text-align: center;
font-size: 60px;
line-height: $header_height;
color: #FFF;
width: $gab-width;
cursor: pointer;
height: 100%;
}
}
.content {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: $header_width + $gab-width;
background: $background_color;
z-index: 200;
border-radius: 3px 0 0 3px;
h1 {
margin: 0;
padding: 0 $padding;
font-size: 24px;
line-height: $header_height;
color: rgba(0,0,0,.5);
font-weight: 300;
}
.section {
padding: $padding $padding;
border-top: 1px solid rgba(0,0,0,.1);
}
}
.releases {
.buttons {
margin-bottom: $padding/2;
}
.item span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
}
.item .name {
@include flex(1 auto);
text-align: left;
}
.status { min-width: 70px; max-width: 70px; }
.quality { min-width: 60px; max-width: 60px; }
.size { min-width: 40px; max-width: 40px; }
.age { min-width: 40px; max-width: 40px; }
.score { min-width: 45px; max-width: 45px; }
.provider { min-width: 110px; max-width: 110px; }
}
}
.alph_nav {
.mass_edit_form {
display: none;
}
.menus {
margin-right: $padding;
.button {
padding: 0 $padding/2;
line-height: $header_height;
color: rgba(0, 0, 0, 0.5);
}
.counter, .more_menu, .actions {
float: left;
}
.counter {
line-height: $header_height;
}
.actions {
a {
display: none;
}
.active {
display: inline-block;
}
}
.filter {
.wrapper {
width: 320px;
}
.button {
margin-top: -2px;
}
.search {
position: relative;
&:before {
position: absolute;
height: 100%;
line-height: 38px;
padding-left: $padding/2;
font-size: 16px;
opacity: .5;
}
input {
width: 100%;
padding: $padding/2 $padding/2 $padding/2 $padding*1.5;
background: $background_color;
border: none;
border-bottom: 1px solid $theme_off;
}
}
.numbers {
padding: $padding/2;
li {
float: left;
width: 10%;
height: 30px;
line-height: 30px;
text-align: center;
color: rgba(0,0,0,.2);
cursor: default;
&.active {
background: $theme_off;
}
&.available {
color: rgba(0,0,0,1);
cursor: pointer;
&:hover {
background: $theme_off;
}
}
}
}
}
.more_menu {
&.show .button {
color: rgba(0, 0, 0, 1);
}
.wrapper {
top: $header_height - 10px;
padding-top: 4px;
border-radius: $border_radius $border_radius 0 0;
&:before {
top: 0;
left: auto;
right: 22px;
}
ul {
border-radius: $border_radius $border_radius 0 0;
}
}
}
}
}
@@ -1,49 +0,0 @@
Page.Movies = new Class({
Extends: PageBase,
name: 'movies',
sub_pages: ['Wanted', 'Manage'],
default_page: 'Wanted',
current_page: null,
initialize: function(parent, options){
var self = this;
self.parent(parent, options);
self.navigation = new BlockNavigation();
$(self.navigation).inject(self.el, 'top');
},
defaultAction: function(action, params){
var self = this;
if(self.current_page){
self.current_page.hide();
if(self.current_page.list && self.current_page.list.navigation)
self.current_page.list.navigation.dispose();
}
var route = new Route();
route.parse(action);
var page_name = route.getPage() != 'index' ? route.getPage().capitalize() : self.default_page;
var page = self.sub_pages.filter(function(page){
return page.name == page_name;
}).pick()['class'];
page.open(route.getAction() || 'index', params);
page.show();
if(page.list && page.list.navigation)
page.list.navigation.inject(self.navigation);
self.current_page = page;
self.navigation.activate(page_name.toLowerCase());
}
});
@@ -1,4 +1,4 @@
var BlockSearchMovieItem = new Class({ Block.Search.MovieItem = new Class({
Implements: [Options, Events], Implements: [Options, Events],
@@ -31,11 +31,9 @@ var BlockSearchMovieItem = new Class({
} }
}).adopt( }).adopt(
self.info_container = new Element('div.info').adopt( self.info_container = new Element('div.info').adopt(
new Element('h2', { new Element('h2').adopt(
'title': self.getTitle()
}).adopt(
self.title = new Element('span.title', { self.title = new Element('span.title', {
'text': self.getTitle() 'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
}), }),
self.year = info.year ? new Element('span.year', { self.year = info.year ? new Element('span.year', {
'text': info.year 'text': info.year
@@ -50,7 +48,7 @@ var BlockSearchMovieItem = new Class({
self.alternativeTitle({ self.alternativeTitle({
'title': title 'title': title
}); });
}); })
}, },
alternativeTitle: function(alternative){ alternativeTitle: function(alternative){
@@ -70,7 +68,7 @@ var BlockSearchMovieItem = new Class({
}, },
get: function(key){ get: function(key){
return this.info[key]; return this.info[key]
}, },
showOptions: function(){ showOptions: function(){
@@ -79,7 +77,7 @@ var BlockSearchMovieItem = new Class({
self.createOptions(); self.createOptions();
self.data_container.addClass('open'); self.data_container.addClass('open');
self.el.addEvent('outerClick', self.closeOptions.bind(self)); self.el.addEvent('outerClick', self.closeOptions.bind(self))
}, },
@@ -87,7 +85,7 @@ var BlockSearchMovieItem = new Class({
var self = this; var self = this;
self.data_container.removeClass('open'); self.data_container.removeClass('open');
self.el.removeEvents('outerClick'); self.el.removeEvents('outerClick')
}, },
add: function(e){ add: function(e){
@@ -134,11 +132,10 @@ var BlockSearchMovieItem = new Class({
if(!self.options_el.hasClass('set')){ if(!self.options_el.hasClass('set')){
var in_library;
if(info.in_library){ if(info.in_library){
in_library = []; var in_library = [];
(info.in_library.releases || []).each(function(release){ (info.in_library.releases || []).each(function(release){
in_library.include(release.quality); in_library.include(release.quality)
}); });
} }
@@ -174,14 +171,14 @@ var BlockSearchMovieItem = new Class({
Array.each(self.alternative_titles, function(alt){ Array.each(self.alternative_titles, function(alt){
new Element('option', { new Element('option', {
'text': alt.title 'text': alt.title
}).inject(self.title_select); }).inject(self.title_select)
}); });
// Fill categories // Fill categories
var categories = CategoryList.getAll(); var categories = CategoryList.getAll();
if(categories.length === 0) if(categories.length == 0)
self.category_select.hide(); self.category_select.hide();
else { else {
self.category_select.show(); self.category_select.show();
@@ -202,12 +199,12 @@ var BlockSearchMovieItem = new Class({
new Element('option', { new Element('option', {
'value': profile.get('_id'), 'value': profile.get('_id'),
'text': profile.get('label') 'text': profile.get('label')
}).inject(self.profile_select); }).inject(self.profile_select)
}); });
self.options_el.addClass('set'); self.options_el.addClass('set');
if(categories.length === 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 && if(categories.length == 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 &&
!(self.info.in_wanted && self.info.in_wanted.profile_id || in_library)) !(self.info.in_wanted && self.info.in_wanted.profile_id || in_library))
self.add(); self.add();
@@ -221,12 +218,12 @@ var BlockSearchMovieItem = new Class({
self.mask = new Element('div.mask').inject(self.el).fade('hide'); self.mask = new Element('div.mask').inject(self.el).fade('hide');
createSpinner(self.mask); createSpinner(self.mask);
self.mask.fade('in'); self.mask.fade('in')
}, },
toElement: function(){ toElement: function(){
return this.el; return this.el
} }
}); });
@@ -1,4 +1,4 @@
var MoviesWanted = new Class({ Page.Wanted = new Class({
Extends: PageBase, Extends: PageBase,
@@ -10,7 +10,7 @@ var MoviesWanted = new Class({
indexAction: function(){ indexAction: function(){
var self = this; var self = this;
if(!self.list){ if(!self.wanted){
self.manual_search = new Element('a', { self.manual_search = new Element('a', {
'title': 'Force a search for the full wanted list', 'title': 'Force a search for the full wanted list',
@@ -20,6 +20,7 @@ var MoviesWanted = new Class({
} }
}); });
self.scan_folder = new Element('a', { self.scan_folder = new Element('a', {
'title': 'Scan a folder and rename all movies in it', 'title': 'Scan a folder and rename all movies in it',
'text': 'Manual folder scan', 'text': 'Manual folder scan',
@@ -29,7 +30,7 @@ var MoviesWanted = new Class({
}); });
// Wanted movies // Wanted movies
self.list = new MovieList({ self.wanted = new MovieList({
'identifier': 'wanted', 'identifier': 'wanted',
'status': 'active', 'status': 'active',
'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Edit, MA.Refresh, MA.Readd, MA.Delete], 'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Edit, MA.Refresh, MA.Readd, MA.Delete],
@@ -37,7 +38,7 @@ var MoviesWanted = new Class({
'menu': [self.manual_search, self.scan_folder], 'menu': [self.manual_search, self.scan_folder],
'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted') 'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted')
}); });
$(self.list).inject(self.el); $(self.wanted).inject(self.el);
// Check if search is in progress // Check if search is in progress
self.startProgressInterval.delay(4000, self); self.startProgressInterval.delay(4000, self);
@@ -90,7 +91,7 @@ var MoviesWanted = new Class({
}; };
if(!self.folder_browser){ if(!self.folder_browser){
self.folder_browser = new Option.Directory("Scan", "folder", "", options); self.folder_browser = new Option['Directory']("Scan", "folder", "", options);
self.folder_browser.save = function() { self.folder_browser.save = function() {
var folder = self.folder_browser.getValue(); var folder = self.folder_browser.getValue();
@@ -264,11 +264,3 @@
height: 40px; height: 40px;
} }
@media all and (max-width: 480px) {
.toggle_menu h2 {
font-size: 16px;
text-align: center;
height: 30px;
}
}
@@ -44,12 +44,11 @@ var Charts = new Class({
if( Cookie.read('suggestions_charts_menu_selected') === 'charts'){ if( Cookie.read('suggestions_charts_menu_selected') === 'charts'){
self.show(); self.show();
self.fireEvent.delay(0, self, 'created');
} }
else else
self.el.hide(); self.el.hide();
self.fireEvent.delay(0, self, 'created');
}, },
fill: function(json){ fill: function(json){
@@ -59,7 +58,7 @@ var Charts = new Class({
self.el_refreshing_text.hide(); self.el_refreshing_text.hide();
self.el_refresh_link.show(); self.el_refresh_link.show();
if(!json || json.count === 0){ if(!json || json.count == 0){
self.el_no_charts_enabled.show(); self.el_no_charts_enabled.show();
self.el_refresh_link.show(); self.el_refresh_link.show();
self.el_refreshing_text.hide(); self.el_refreshing_text.hide();
@@ -84,16 +83,17 @@ var Charts = new Class({
Object.each(chart.list, function(movie){ Object.each(chart.list, function(movie){
var m = new BlockSearchMovieItem(movie, { var m = new Block.Search.MovieItem(movie, {
'onAdded': function(){ 'onAdded': function(){
self.afterAdded(m, movie); self.afterAdded(m, movie)
} }
}); });
var in_database_class = (chart.hide_wanted && movie.in_wanted) ? 'hidden' : (movie.in_wanted ? 'chart_in_wanted' : ((chart.hide_library && movie.in_library) ? 'hidden': (movie.in_library ? 'chart_in_library' : ''))), var in_database_class = (chart.hide_wanted && movie.in_wanted) ? 'hidden' : (movie.in_wanted ? 'chart_in_wanted' : ((chart.hide_library && movie.in_library) ? 'hidden': (movie.in_library ? 'chart_in_library' : ''))),
in_database_title = movie.in_wanted ? 'Movie in wanted list' : (movie.in_library ? 'Movie in library' : ''); in_database_title = movie.in_wanted ? 'Movie in wanted list' : (movie.in_library ? 'Movie in library' : '');
m.el.addClass(in_database_class) m.el
.addClass(in_database_class)
.grab( .grab(
new Element('div.chart_number', { new Element('div.chart_number', {
'text': it++, 'text': it++,
@@ -135,7 +135,7 @@ var Charts = new Class({
'text': plot, 'text': plot,
'events': { 'events': {
'click': function(){ 'click': function(){
this.toggleClass('full'); this.toggleClass('full')
} }
} }
}) : null }) : null
@@ -1,89 +0,0 @@
import re
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.automation.base import Automation
log = CPLog(__name__)
autoload = 'CrowdAI'
class CrowdAI(Automation, RSS):
interval = 1800
def getIMDBids(self):
movies = []
urls = dict(zip(splitString(self.conf('automation_urls')), [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]))
for url in urls:
if not urls[url]:
continue
rss_movies = self.getRSSData(url)
for movie in rss_movies:
description = self.getTextElement(movie, 'description')
grabs = 0
for item in movie:
if item.attrib.get('name') == 'grabs':
grabs = item.attrib.get('value')
break
if int(grabs) > tryInt(self.conf('number_grabs')):
title = re.match(r'.*Title: .a href.*/">(.*) \(\d{4}\).*', description).group(1)
log.info2('%s grabs for movie: %s, enqueue...', (grabs, title))
year = re.match(r'.*Year: (\d{4}).*', description).group(1)
imdb = self.search(title, year)
if imdb and self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
return movies
config = [{
'name': 'crowdai',
'groups': [
{
'tab': 'automation',
'list': 'automation_providers',
'name': 'crowdai_automation',
'label': 'CrowdAI',
'description': 'Imports from any newznab powered NZB providers RSS feed depending on the number of grabs per movie. Go to your newznab site and find the RSS section. Then copy the copy paste the link under "Movies > x264 feed" here.',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_urls_use',
'label': 'Use',
'default': '1',
},
{
'name': 'automation_urls',
'label': 'url',
'type': 'combined',
'combine': ['automation_urls_use', 'automation_urls'],
'default': 'http://YOUR_PROVIDER/rss?t=THE_MOVIE_CATEGORY&i=YOUR_USER_ID&r=YOUR_API_KEY&res=2&rls=2&num=100',
},
{
'name': 'number_grabs',
'default': '500',
'label': 'Grab threshold',
'description': 'Number of grabs required',
},
],
},
],
}]
@@ -48,12 +48,11 @@ class Letterboxd(Automation):
soup = BeautifulSoup(self.getHTMLData(self.url % username)) soup = BeautifulSoup(self.getHTMLData(self.url % username))
for movie in soup.find_all('li', attrs = {'class': 'poster-container'}): for movie in soup.find_all('a', attrs = {'class': 'frame'}):
img = movie.find('img', movie) match = removeEmpty(self.pattern.split(movie['title']))
title = img.get('alt')
movies.append({ movies.append({
'title': title 'title': match[0],
'year': match[1]
}) })
return movies return movies
@@ -39,14 +39,15 @@ class Rottentomatoes(Automation, RSS):
if result: if result:
log.info2('Something smells...')
rating = tryInt(self.getTextElement(movie, rating_tag)) rating = tryInt(self.getTextElement(movie, rating_tag))
name = result.group(0) name = result.group(0)
print rating, tryInt(self.conf('tomatometer_percent'))
if rating < tryInt(self.conf('tomatometer_percent')): if rating < tryInt(self.conf('tomatometer_percent')):
log.info2('%s seems to be rotten...', name) log.info2('%s seems to be rotten...', name)
else: else:
log.info2('Found %s with fresh rating %s', (name, rating))
log.info2('Found %s fresh enough movies, enqueuing: %s', (rating, name))
year = datetime.datetime.now().strftime("%Y") year = datetime.datetime.now().strftime("%Y")
imdb = self.search(name, year) imdb = self.search(name, year)
@@ -69,15 +69,12 @@ class CouchPotatoApi(MovieProvider):
name_enc = base64.b64encode(ss(name)) name_enc = base64.b64encode(ss(name))
return self.getJsonData(self.urls['validate'] % name_enc, headers = self.getRequestHeaders()) return self.getJsonData(self.urls['validate'] % name_enc, headers = self.getRequestHeaders())
def isMovie(self, identifier = None, adding = False): def isMovie(self, identifier = None):
if not identifier: if not identifier:
return return
url = self.urls['is_movie'] % identifier data = self.getJsonData(self.urls['is_movie'] % identifier, headers = self.getRequestHeaders())
url += '?adding=1' if adding else ''
data = self.getJsonData(url, headers = self.getRequestHeaders())
if data: if data:
return data.get('is_movie', True) return data.get('is_movie', True)
@@ -4,7 +4,6 @@ from couchpotato import tryInt
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.base import MovieProvider from couchpotato.core.media.movie.providers.base import MovieProvider
from requests import HTTPError
log = CPLog(__name__) log = CPLog(__name__)
@@ -33,14 +32,12 @@ class FanartTV(MovieProvider):
try: try:
url = self.urls['api'] % identifier url = self.urls['api'] % identifier
fanart_data = self.getJsonData(url, show_error = False) fanart_data = self.getJsonData(url)
if fanart_data: if fanart_data:
log.debug('Found images for %s', fanart_data.get('name')) log.debug('Found images for %s', fanart_data.get('name'))
images = self._parseMovie(fanart_data) images = self._parseMovie(fanart_data)
except HTTPError as e:
log.debug('Failed getting extra art for %s: %s',
(identifier, e))
except: except:
log.error('Failed getting extra art for %s: %s', log.error('Failed getting extra art for %s: %s',
(identifier, traceback.format_exc())) (identifier, traceback.format_exc()))
@@ -2,7 +2,6 @@ import json
import re import re
import traceback import traceback
from couchpotato import Env
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString
@@ -18,8 +17,8 @@ autoload = 'OMDBAPI'
class OMDBAPI(MovieProvider): class OMDBAPI(MovieProvider):
urls = { urls = {
'search': 'http://www.omdbapi.com/?type=movie&%s', 'search': 'http://www.omdbapi.com/?%s',
'info': 'http://www.omdbapi.com/?type=movie&i=%s', 'info': 'http://www.omdbapi.com/?i=%s',
} }
http_time_between_calls = 0 http_time_between_calls = 0
@@ -39,8 +38,7 @@ class OMDBAPI(MovieProvider):
} }
cache_key = 'omdbapi.cache.%s' % q cache_key = 'omdbapi.cache.%s' % q
url = self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')}) cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')}), timeout = 3)
cached = self.getCache(cache_key, url, timeout = 3, headers = {'User-Agent': Env.getIdentifier()})
if cached: if cached:
result = self.parseMovie(cached) result = self.parseMovie(cached)
@@ -58,7 +56,7 @@ class OMDBAPI(MovieProvider):
return {} return {}
cache_key = 'omdbapi.cache.%s' % identifier cache_key = 'omdbapi.cache.%s' % identifier
cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3, headers = {'User-Agent': Env.getIdentifier()}) cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3)
if cached: if cached:
result = self.parseMovie(cached) result = self.parseMovie(cached)
@@ -1,10 +1,11 @@
import traceback import traceback
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode, ss, tryUrlencode from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.base import MovieProvider from couchpotato.core.media.movie.providers.base import MovieProvider
import tmdb3
log = CPLog(__name__) log = CPLog(__name__)
@@ -12,66 +13,54 @@ autoload = 'TheMovieDb'
class TheMovieDb(MovieProvider): class TheMovieDb(MovieProvider):
MAX_EXTRATHUMBS = 4
http_time_between_calls = .35
configuration = {
'images': {
'secure_base_url': 'https://image.tmdb.org/t/p/',
},
}
def __init__(self): def __init__(self):
addEvent('info.search', self.search, priority = 3)
addEvent('movie.search', self.search, priority = 3)
addEvent('movie.info', self.getInfo, priority = 3) addEvent('movie.info', self.getInfo, priority = 3)
addEvent('movie.info_by_tmdb', self.getInfo) addEvent('movie.info_by_tmdb', self.getInfo)
addEvent('app.load', self.config)
def config(self): # Configure TMDB settings
configuration = self.request('configuration') tmdb3.set_key(self.conf('api_key'))
if configuration: tmdb3.set_cache('null')
self.configuration = configuration
def search(self, q, limit = 3): def search(self, q, limit = 12):
""" Find movie by name """ """ Find movie by name """
if self.isDisabled(): if self.isDisabled():
return False return False
log.debug('Searching for movie: %s', q) search_string = simplifyString(q)
cache_key = 'tmdb.cache.%s.%s' % (search_string, limit)
results = self.getCache(cache_key)
raw = None if not results:
try: log.debug('Searching for movie: %s', q)
name_year = fireEvent('scanner.name_year', q, single = True)
raw = self.request('search/movie', {
'query': name_year.get('name', q),
'year': name_year.get('year'),
'search_type': 'ngram' if limit > 1 else 'phrase'
}, return_key = 'results')
except:
log.error('Failed searching TMDB for "%s": %s', (q, traceback.format_exc()))
results = [] raw = None
if raw:
try: try:
nr = 0 raw = tmdb3.searchMovie(search_string)
except:
log.error('Failed searching TMDB for "%s": %s', (search_string, traceback.format_exc()))
for movie in raw: results = []
parsed_movie = self.parseMovie(movie, extended = False) if raw:
if parsed_movie: try:
results.append(parsed_movie) nr = 0
nr += 1 for movie in raw:
if nr == limit: results.append(self.parseMovie(movie, extended = False))
break
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results]) nr += 1
if nr == limit:
break
return results log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
except SyntaxError as e:
log.error('Failed to parse XML response: %s', e) self.setCache(cache_key, results)
return False return results
except SyntaxError as e:
log.error('Failed to parse XML response: %s', e)
return False
return results return results
@@ -80,91 +69,101 @@ class TheMovieDb(MovieProvider):
if not identifier: if not identifier:
return {} return {}
result = self.parseMovie({ cache_key = 'tmdb.cache.%s%s' % (identifier, '.ex' if extended else '')
'id': identifier result = self.getCache(cache_key)
}, extended = extended)
return result or {} if not result:
try:
log.debug('Getting info: %s', cache_key)
# noinspection PyArgumentList
movie = tmdb3.Movie(identifier)
try: exists = movie.title is not None
except: exists = False
if exists:
result = self.parseMovie(movie, extended = extended)
self.setCache(cache_key, result)
else:
result = {}
except:
log.error('Failed getting info for %s: %s', (identifier, traceback.format_exc()))
return result
def parseMovie(self, movie, extended = True): def parseMovie(self, movie, extended = True):
# Do request, append other items cache_key = 'tmdb.cache.%s%s' % (movie.id, '.ex' if extended else '')
movie = self.request('movie/%s' % movie.get('id'), { movie_data = self.getCache(cache_key)
'append_to_response': 'alternative_titles' + (',images,casts' if extended else '')
})
if not movie:
return
# Images if not movie_data:
poster = self.getImage(movie, type = 'poster', size = 'w154')
poster_original = self.getImage(movie, type = 'poster', size = 'original')
backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
extra_thumbs = self.getMultImages(movie, type = 'backdrops', size = 'original') if extended else []
images = { # Images
'poster': [poster] if poster else [], poster = self.getImage(movie, type = 'poster', size = 'w154')
#'backdrop': [backdrop] if backdrop else [], poster_original = self.getImage(movie, type = 'poster', size = 'original')
'poster_original': [poster_original] if poster_original else [], backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
'backdrop_original': [backdrop_original] if backdrop_original else [], extra_thumbs = self.getMultImages(movie, type = 'backdrops', size = 'original', n = self.MAX_EXTRATHUMBS, skipfirst = True)
'actors': {},
'extra_thumbs': extra_thumbs
}
# Genres images = {
try: 'poster': [poster] if poster else [],
genres = [genre.get('name') for genre in movie.get('genres', [])] #'backdrop': [backdrop] if backdrop else [],
except: 'poster_original': [poster_original] if poster_original else [],
genres = [] 'backdrop_original': [backdrop_original] if backdrop_original else [],
'actors': {},
'extra_thumbs': extra_thumbs
}
# 1900 is the same as None # Genres
year = str(movie.get('release_date') or '')[:4] try:
if not movie.get('release_date') or year == '1900' or year.lower() == 'none': genres = [genre.name for genre in movie.genres]
year = None except:
genres = []
# Gather actors data # 1900 is the same as None
actors = {} year = str(movie.releasedate or '')[:4]
if extended: if not movie.releasedate or year == '1900' or year.lower() == 'none':
year = None
# Full data # Gather actors data
cast = movie.get('casts', {}).get('cast', []) actors = {}
if extended:
for cast_item in movie.cast:
try:
actors[toUnicode(cast_item.name)] = toUnicode(cast_item.character)
images['actors'][toUnicode(cast_item.name)] = self.getImage(cast_item, type = 'profile', size = 'original')
except:
log.debug('Error getting cast info for %s: %s', (cast_item, traceback.format_exc()))
for cast_item in cast: movie_data = {
try: 'type': 'movie',
actors[toUnicode(cast_item.get('name'))] = toUnicode(cast_item.get('character')) 'via_tmdb': True,
images['actors'][toUnicode(cast_item.get('name'))] = self.getImage(cast_item, type = 'profile', size = 'original') 'tmdb_id': movie.id,
except: 'titles': [toUnicode(movie.title)],
log.debug('Error getting cast info for %s: %s', (cast_item, traceback.format_exc())) 'original_title': movie.originaltitle,
'images': images,
'imdb': movie.imdb,
'runtime': movie.runtime,
'released': str(movie.releasedate),
'year': tryInt(year, None),
'plot': movie.overview,
'genres': genres,
'collection': getattr(movie.collection, 'name', None),
'actor_roles': actors
}
movie_data = { movie_data = dict((k, v) for k, v in movie_data.items() if v)
'type': 'movie',
'via_tmdb': True,
'tmdb_id': movie.get('id'),
'titles': [toUnicode(movie.get('title'))],
'original_title': movie.get('original_title'),
'images': images,
'imdb': movie.get('imdb_id'),
'runtime': movie.get('runtime'),
'released': str(movie.get('release_date')),
'year': tryInt(year, None),
'plot': movie.get('overview'),
'genres': genres,
'collection': getattr(movie.get('belongs_to_collection'), 'name', None),
'actor_roles': actors
}
movie_data = dict((k, v) for k, v in movie_data.items() if v) # Add alternative names
if movie_data['original_title'] and movie_data['original_title'] not in movie_data['titles']:
movie_data['titles'].append(movie_data['original_title'])
# Add alternative names if extended:
if movie_data['original_title'] and movie_data['original_title'] not in movie_data['titles']: for alt in movie.alternate_titles:
movie_data['titles'].append(movie_data['original_title']) alt_name = alt.title
if alt_name and alt_name not in movie_data['titles'] and alt_name.lower() != 'none' and alt_name is not None:
movie_data['titles'].append(alt_name)
# Add alternative titles # Cache movie parsed
alternate_titles = movie.get('alternative_titles', {}).get('titles', []) self.setCache(cache_key, movie_data)
for alt in alternate_titles:
alt_name = alt.get('title')
if alt_name and alt_name not in movie_data['titles'] and alt_name.lower() != 'none' and alt_name is not None:
movie_data['titles'].append(alt_name)
return movie_data return movie_data
@@ -172,41 +171,36 @@ class TheMovieDb(MovieProvider):
image_url = '' image_url = ''
try: try:
path = movie.get('%s_path' % type) image_url = getattr(movie, type).geturl(size = size)
image_url = '%s%s%s' % (self.configuration['images']['secure_base_url'], size, path)
except: except:
log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie)))) log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie))))
return image_url return image_url
def getMultImages(self, movie, type = 'backdrops', size = 'original'): def getMultImages(self, movie, type = 'backdrops', size = 'original', n = -1, skipfirst = False):
"""
If n < 0, return all images. Otherwise return n images.
If n > len(getattr(movie, type)), then return all images.
If skipfirst is True, then it will skip getattr(movie, type)[0]. This
is because backdrops[0] is typically backdrop.
"""
image_urls = [] image_urls = []
try: try:
for image in movie.get('images', {}).get(type, [])[1:5]: images = getattr(movie, type)
image_urls.append(self.getImage(image, 'file', size)) if n < 0 or n > len(images):
num_images = len(images)
else:
num_images = n
for i in range(int(skipfirst), num_images + int(skipfirst)):
image_urls.append(images[i].geturl(size = size))
except: except:
log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie)))) log.debug('Failed getting %i %s.%s for "%s"', (n, type, size, ss(str(movie))))
return image_urls return image_urls
def request(self, call = '', params = {}, return_key = None):
params = dict((k, v) for k, v in params.items() if v)
params = tryUrlencode(params)
try:
url = 'http://api.themoviedb.org/3/%s?api_key=%s%s' % (call, self.conf('api_key'), '&%s' % params if params else '')
data = self.getJsonData(url, show_error = False)
except:
log.debug('Movie not found: %s, %s', (call, params))
data = None
if data and return_key and return_key in data:
data = data.get(return_key)
return data
def isDisabled(self): def isDisabled(self):
if self.conf('api_key') == '': if self.conf('api_key') == '':
log.error('No API key provided.') log.error('No API key provided.')
@@ -11,7 +11,7 @@ autoload = 'Bitsoup'
class Bitsoup(MovieProvider, Base): class Bitsoup(MovieProvider, Base):
cat_ids = [ cat_ids = [
([17], ['3d']), ([17], ['3d']),
([80], ['720p', '1080p']), ([41], ['720p', '1080p']),
([20], ['dvdr']), ([20], ['dvdr']),
([19], ['brrip', 'dvdrip']), ([19], ['brrip', 'dvdrip']),
] ]
@@ -1,11 +0,0 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.hdaccess import Base
from couchpotato.core.media.movie.providers.base import MovieProvider
log = CPLog(__name__)
autoload = 'HDAccess'
class HDAccess(MovieProvider, Base):
pass
@@ -13,7 +13,7 @@ class IPTorrents(MovieProvider, Base):
([87], ['3d']), ([87], ['3d']),
([48], ['720p', '1080p', 'bd50']), ([48], ['720p', '1080p', 'bd50']),
([72], ['cam', 'ts', 'tc', 'r5', 'scr']), ([72], ['cam', 'ts', 'tc', 'r5', 'scr']),
([7, 48, 20], ['dvdrip', 'brrip']), ([7,48], ['dvdrip', 'brrip']),
([6], ['dvdr']), ([6], ['dvdr']),
] ]
@@ -16,12 +16,12 @@ class TorrentLeech(MovieProvider, Base):
([9], ['ts', 'tc']), ([9], ['ts', 'tc']),
([10], ['r5', 'scr']), ([10], ['r5', 'scr']),
([11], ['dvdrip']), ([11], ['dvdrip']),
([13, 14], ['brrip']), ([14], ['brrip']),
([12], ['dvdr']), ([12], ['dvdr']),
] ]
def buildUrl(self, title, media, quality): def buildUrl(self, title, media, quality):
return ( return (
tryUrlencode(title.replace(':', '')), tryUrlencode(title.replace(':', '')),
','.join([str(x) for x in self.getCatId(quality)]) self.getCatId(quality)[0]
) )
@@ -22,8 +22,8 @@ class TorrentShack(MovieProvider, Base):
# Movies-SD Pack - 983 (not included) # Movies-SD Pack - 983 (not included)
cat_ids = [ cat_ids = [
([970, 320], ['bd50']), ([970], ['bd50']),
([300, 320], ['720p', '1080p']), ([300], ['720p', '1080p']),
([350], ['dvdr']), ([350], ['dvdr']),
([400], ['brrip', 'dvdrip']), ([400], ['brrip', 'dvdrip']),
] ]
@@ -1,5 +1,6 @@
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.torrent.torrentz import Base from couchpotato.core.media._base.providers.torrent.torrentz import Base
from couchpotato.core.media.movie.providers.base import MovieProvider from couchpotato.core.media.movie.providers.base import MovieProvider
@@ -10,5 +11,5 @@ autoload = 'Torrentz'
class Torrentz(MovieProvider, Base): class Torrentz(MovieProvider, Base):
def buildUrl(self, title, media, quality): def buildUrl(self, media):
return tryUrlencode('"%s %s"' % (title, media['info']['year'])) return tryUrlencode('"%s"' % fireEvent('library.query', media, single = True))
@@ -12,7 +12,7 @@ autoload = 'RottenTomatoes'
class RottenTomatoes(UserscriptBase): class RottenTomatoes(UserscriptBase):
includes = ['*://www.rottentomatoes.com/m/*'] includes = ['*://www.rottentomatoes.com/m/*/']
excludes = ['*://www.rottentomatoes.com/m/*/*/'] excludes = ['*://www.rottentomatoes.com/m/*/*/']
version = 2 version = 2
+13 -14
View File
@@ -166,8 +166,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
'quality': q_identifier, 'quality': q_identifier,
'finish': profile['finish'][index], 'finish': profile['finish'][index],
'wait_for': tryInt(profile['wait_for'][index]), 'wait_for': tryInt(profile['wait_for'][index]),
'3d': profile['3d'][index] if profile.get('3d') else False, '3d': profile['3d'][index] if profile.get('3d') else False
'minimum_score': profile.get('minimum_score', 1),
} }
could_not_be_released = not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year']) could_not_be_released = not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year'])
@@ -203,14 +202,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
quality['custom'] = quality_custom quality['custom'] = quality_custom
results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or [] results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or []
results_count = len(results)
# Check if movie isn't deleted while searching
if not fireEvent('media.get', movie.get('_id'), single = True):
break
# Add them to this movie releases list
found_releases += fireEvent('release.create_from_search', results, movie, quality, single = True)
results_count = len(found_releases)
total_result_count += results_count total_result_count += results_count
if results_count == 0: if results_count == 0:
log.debug('Nothing found for %s in %s', (default_title, quality['label'])) log.debug('Nothing found for %s in %s', (default_title, quality['label']))
@@ -218,9 +210,17 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
# Keep track of releases found outside ETA window # Keep track of releases found outside ETA window
outside_eta_results += results_count if could_not_be_released else 0 outside_eta_results += results_count if could_not_be_released else 0
# Check if movie isn't deleted while searching
if not fireEvent('media.get', movie.get('_id'), single = True):
break
# Add them to this movie releases list
found_releases += fireEvent('release.create_from_search', results, movie, quality, single = True)
# Don't trigger download, but notify user of available releases # Don't trigger download, but notify user of available releases
if could_not_be_released and results_count > 0: if could_not_be_released:
log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title)) if results_count > 0:
log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title))
# Try find a valid result and download it # Try find a valid result and download it
if (force_download or not could_not_be_released or always_search) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True): if (force_download or not could_not_be_released or always_search) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True):
@@ -394,9 +394,8 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
log.info('Trying next release for: %s', getTitle(media)) log.info('Trying next release for: %s', getTitle(media))
self.single(media, manual = manual, force_download = force_download) self.single(media, manual = manual, force_download = force_download)
return True return True
return False
except: except:
log.error('Failed searching for next release: %s', traceback.format_exc()) log.error('Failed searching for next release: %s', traceback.format_exc())
return False return False
@@ -52,7 +52,7 @@ var SuggestList = new Class({
else else
self.hide(); self.hide();
self.fireEvent.delay(0, self, 'created'); self.fireEvent('created');
}, },
@@ -60,16 +60,16 @@ var SuggestList = new Class({
var self = this; var self = this;
if(!json || json.count === 0){ if(!json || json.count == 0){
self.el.hide(); self.el.hide();
} }
else { else {
Object.each(json.suggestions, function(movie){ Object.each(json.suggestions, function(movie){
var m = new BlockSearchMovieItem(movie, { var m = new Block.Search.MovieItem(movie, {
'onAdded': function(){ 'onAdded': function(){
self.afterAdded(m, movie); self.afterAdded(m, movie)
} }
}); });
m.data_container.grab( m.data_container.grab(
@@ -114,7 +114,7 @@ var SuggestList = new Class({
'text': plot, 'text': plot,
'events': { 'events': {
'click': function(){ 'click': function(){
this.toggleClass('full'); this.toggleClass('full')
} }
} }
}) : null }) : null
+13 -13
View File
@@ -14,7 +14,6 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from .index import NotificationIndex, NotificationUnreadIndex from .index import NotificationIndex, NotificationUnreadIndex
from couchpotato.environment import Env from couchpotato.environment import Env
from tornado.ioloop import IOLoop
log = CPLog(__name__) log = CPLog(__name__)
@@ -111,11 +110,11 @@ class CoreNotifier(Notification):
if limit_offset: if limit_offset:
splt = splitString(limit_offset) splt = splitString(limit_offset)
limit = tryInt(splt[0]) limit = splt[0]
offset = tryInt(0 if len(splt) is 1 else splt[1]) offset = 0 if len(splt) is 1 else splt[1]
results = db.all('notification', limit = limit, offset = offset, with_doc = True) results = db.get_many('notification', limit = limit, offset = offset, with_doc = True)
else: else:
results = db.all('notification', limit = 200, with_doc = True) results = db.get_many('notification', limit = 200, with_doc = True)
notifications = [] notifications = []
for n in results: for n in results:
@@ -149,15 +148,16 @@ class CoreNotifier(Notification):
def notify(self, message = '', data = None, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {} if not data: data = {}
n = {
'_t': 'notification',
'time': int(time.time()),
}
try: try:
db = get_db() db = get_db()
n['message'] = toUnicode(message) data['notification_type'] = listener if listener else 'unknown'
n = {
'_t': 'notification',
'time': int(time.time()),
'message': toUnicode(message)
}
if data.get('sticky'): if data.get('sticky'):
n['sticky'] = True n['sticky'] = True
@@ -170,7 +170,7 @@ class CoreNotifier(Notification):
return True return True
except: except:
log.error('Failed notify "%s": %s', (n, traceback.format_exc())) log.error('Failed notify: %s', traceback.format_exc())
def frontend(self, type = 'notification', data = None, message = None): def frontend(self, type = 'notification', data = None, message = None):
if not data: data = {} if not data: data = {}
@@ -190,7 +190,7 @@ class CoreNotifier(Notification):
while len(self.listeners) > 0 and not self.shuttingDown(): while len(self.listeners) > 0 and not self.shuttingDown():
try: try:
listener, last_id = self.listeners.pop() listener, last_id = self.listeners.pop()
IOLoop.current().add_callback(listener, { listener({
'success': True, 'success': True,
'result': [notification], 'result': [notification],
}) })
@@ -20,8 +20,8 @@ var NotificationBase = new Class({
self.notifications = []; self.notifications = [];
App.addEvent('load', function(){ App.addEvent('load', function(){
App.block.notification = new BlockMenu(self, { App.block.notification = new Block.Menu(self, {
'button_class': 'icon-notifications', 'button_class': 'icon2.eye-open',
'class': 'notification_menu', 'class': 'notification_menu',
'onOpen': self.markAsRead.bind(self) 'onOpen': self.markAsRead.bind(self)
}); });
@@ -32,7 +32,7 @@ var NotificationBase = new Class({
window.addEvent('load', function(){ window.addEvent('load', function(){
self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 100, self); self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 100, self);
}); })
}, },
@@ -46,15 +46,16 @@ var NotificationBase = new Class({
new Element('span.'+(result.read ? 'read' : '' )).adopt( new Element('span.'+(result.read ? 'read' : '' )).adopt(
new Element('span.message', {'html': result.message}), new Element('span.message', {'html': result.message}),
new Element('span.added', {'text': added.timeDiffInWords(), 'title': added}) new Element('span.added', {'text': added.timeDiffInWords(), 'title': added})
), 'top'); )
, 'top');
self.notifications.include(result); self.notifications.include(result);
if((result.important !== undefined || result.sticky !== undefined) && !result.read){ if((result.important !== undefined || result.sticky !== undefined) && !result.read){
var sticky = true; var sticky = true;
App.trigger('message', [result.message, sticky, result]); App.trigger('message', [result.message, sticky, result])
} }
else if(!result.read){ else if(!result.read){
self.setBadge(self.notifications.filter(function(n){ return !n.read; }).length); self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
} }
}, },
@@ -62,7 +63,7 @@ var NotificationBase = new Class({
setBadge: function(value){ setBadge: function(value){
var self = this; var self = this;
self.badge.set('text', value); self.badge.set('text', value);
self.badge[value ? 'show' : 'hide'](); self.badge[value ? 'show' : 'hide']()
}, },
markAsRead: function(force_ids){ markAsRead: function(force_ids){
@@ -71,13 +72,13 @@ var NotificationBase = new Class({
if(!force_ids) { if(!force_ids) {
var rn = self.notifications.filter(function(n){ var rn = self.notifications.filter(function(n){
return !n.read && n.important === undefined; return !n.read && n.important === undefined
}); });
ids = []; var ids = [];
rn.each(function(n){ rn.each(function(n){
ids.include(n._id); ids.include(n._id)
}); })
} }
if(ids.length > 0) if(ids.length > 0)
@@ -86,9 +87,9 @@ var NotificationBase = new Class({
'ids': ids.join(',') 'ids': ids.join(',')
}, },
'onSuccess': function(){ 'onSuccess': function(){
self.setBadge(''); self.setBadge('')
} }
}); })
}, },
@@ -103,7 +104,7 @@ var NotificationBase = new Class({
self.request = Api.request('notification.listener', { self.request = Api.request('notification.listener', {
'data': {'init':true}, 'data': {'init':true},
'onSuccess': function(json){ 'onSuccess': function(json){
self.processData(json, true); self.processData(json, true)
} }
}).send(); }).send();
@@ -111,7 +112,7 @@ var NotificationBase = new Class({
if(self.request && self.request.isRunning()){ if(self.request && self.request.isRunning()){
self.request.cancel(); self.request.cancel();
self.startPoll(); self.startPoll()
} }
}, 120000); }, 120000);
@@ -129,15 +130,15 @@ var NotificationBase = new Class({
self.request = Api.request('nonblock/notification.listener', { self.request = Api.request('nonblock/notification.listener', {
'onSuccess': function(json){ 'onSuccess': function(json){
self.processData(json, false); self.processData(json, false)
}, },
'data': { 'data': {
'last_id': self.last_id 'last_id': self.last_id
}, },
'onFailure': function(){ 'onFailure': function(){
self.startPoll.delay(2000, self); self.startPoll.delay(2000, self)
} }
}).send(); }).send()
}, },
@@ -159,7 +160,7 @@ var NotificationBase = new Class({
}); });
if(json.result.length > 0) if(json.result.length > 0)
self.last_id = json.result.getLast().message_id; self.last_id = json.result.getLast().message_id
} }
// Restart poll // Restart poll
@@ -174,11 +175,11 @@ var NotificationBase = new Class({
var new_message = new Element('div', { var new_message = new Element('div', {
'class': 'message' + (sticky ? ' sticky' : ''), 'class': 'message' + (sticky ? ' sticky' : ''),
'html': '<div class="inner">' + message + '</div>' 'html': message
}).inject(self.message_container, 'top'); }).inject(self.message_container, 'top');
setTimeout(function(){ setTimeout(function(){
new_message.addClass('show'); new_message.addClass('show')
}, 10); }, 10);
var hide_message = function(){ var hide_message = function(){
@@ -210,8 +211,8 @@ var NotificationBase = new Class({
var setting_page = App.getPage('Settings'); var setting_page = App.getPage('Settings');
setting_page.addEvent('create', function(){ setting_page.addEvent('create', function(){
Object.each(setting_page.tabs.notifications.groups, self.addTestButton.bind(self)); Object.each(setting_page.tabs.notifications.groups, self.addTestButton.bind(self))
}); })
}, },
@@ -234,21 +235,20 @@ var NotificationBase = new Class({
button.set('text', button_name); button.set('text', button_name);
var message;
if(json.success){ if(json.success){
message = new Element('span.success', { var message = new Element('span.success', {
'text': 'Notification successful' 'text': 'Notification successful'
}).inject(button, 'after'); }).inject(button, 'after')
} }
else { else {
message = new Element('span.failed', { var message = new Element('span.failed', {
'text': 'Notification failed. Check logs for details.' 'text': 'Notification failed. Check logs for details.'
}).inject(button, 'after'); }).inject(button, 'after')
} }
(function(){ (function(){
message.destroy(); message.destroy();
}).delay(3000); }).delay(3000)
} }
}); });
} }
@@ -0,0 +1,68 @@
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from pynmwp import PyNMWP
import six
log = CPLog(__name__)
autoload = 'NotifyMyWP'
class NotifyMyWP(Notification):
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
keys = splitString(self.conf('api_key'))
p = PyNMWP(keys, self.conf('dev_key'))
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
for key in keys:
if not response[key]['Code'] == six.u('200'):
log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message']))
return False
return response
config = [{
'name': 'notifymywp',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'notifymywp',
'label': 'Windows Phone',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'api_key',
'description': 'Multiple keys seperated by a comma. Maximum of 5.'
},
{
'name': 'dev_key',
'advanced': True,
},
{
'name': 'priority',
'default': 0,
'type': 'dropdown',
'values': [('Very Low', -2), ('Moderate', -1), ('Normal', 0), ('High', 1), ('Emergency', 2)],
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]
@@ -23,26 +23,6 @@ config = [{
'default': 'localhost', 'default': 'localhost',
'description': 'Hostname/IP, default localhost' 'description': 'Hostname/IP, default localhost'
}, },
{
'name': 'username',
'label': 'Username',
'default': '',
'description': 'Required for myPlex'
},
{
'name': 'password',
'label': 'Password',
'default': '',
'type': 'password',
'description': 'Required for myPlex'
},
{
'name': 'auth_token',
'label': 'Auth Token',
'default': '',
'advanced': True,
'description': 'Required for myPlex'
},
{ {
'name': 'clients', 'name': 'clients',
'default': '', 'default': '',
+4 -39
View File
@@ -35,45 +35,10 @@ class PlexServer(object):
if path.startswith('/'): if path.startswith('/'):
path = path[1:] path = path[1:]
#Maintain support for older Plex installations without myPlex data = self.plex.urlopen('%s/%s' % (
if not self.plex.conf('auth_token') and not self.plex.conf('username') and not self.plex.conf('password'): self.createHost(self.plex.conf('media_server'), port = 32400),
data = self.plex.urlopen('%s/%s' % ( path
self.createHost(self.plex.conf('media_server'), port = 32400), ))
path
))
else:
#Fetch X-Plex-Token if it doesn't exist but a username/password do
if not self.plex.conf('auth_token') and (self.plex.conf('username') and self.plex.conf('password')):
import urllib2, base64
log.info("Fetching a new X-Plex-Token from plex.tv")
username = self.plex.conf('username')
password = self.plex.conf('password')
req = urllib2.Request("https://plex.tv/users/sign_in.xml", data="")
authheader = "Basic %s" % base64.encodestring('%s:%s' % (username, password))[:-1]
req.add_header("Authorization", authheader)
req.add_header("X-Plex-Product", "Couchpotato Notifier")
req.add_header("X-Plex-Client-Identifier", "b3a6b24dcab2224bdb101fc6aa08ea5e2f3147d6")
req.add_header("X-Plex-Version", "1.0")
try:
response = urllib2.urlopen(req)
except urllib2.URLError, e:
log.info("Error fetching token from plex.tv")
try:
auth_tree = etree.parse(response)
token = auth_tree.findall(".//authentication-token")[0].text
self.plex.conf('auth_token', token)
except (ValueError, IndexError) as e:
log.info("Error parsing plex.tv response: " + ex(e))
#Add X-Plex-Token header for myPlex support workaround
data = self.plex.urlopen('%s/%s?X-Plex-Token=%s' % (
self.createHost(self.plex.conf('media_server'), port = 32400),
path,
self.plex.conf('auth_token')
))
if data_type == 'xml': if data_type == 'xml':
return etree.fromstring(data) return etree.fromstring(data)
@@ -16,7 +16,7 @@ var TwitterNotification = new Class({
var twitter_set = 0; var twitter_set = 0;
fieldset.getElements('input[type=text]').each(function(el){ fieldset.getElements('input[type=text]').each(function(el){
twitter_set += +(el.get('value') !== ''); twitter_set += +(el.get('value') != '');
}); });
@@ -57,7 +57,7 @@ var TwitterNotification = new Class({
} }
}) })
).inject(fieldset.getElement('.test_button'), 'before'); ).inject(fieldset.getElement('.test_button'), 'before');
}); })
} }
-68
View File
@@ -1,68 +0,0 @@
import traceback
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
autoload = 'Webhook'
class Webhook(Notification):
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
post_data = {
'message': toUnicode(message)
}
if getIdentifier(data):
post_data.update({
'imdb_id': getIdentifier(data)
})
headers = {
'Content-type': 'application/x-www-form-urlencoded'
}
try:
self.urlopen(self.conf('url'), headers = headers, data = post_data, show_error = False)
return True
except:
log.error('Webhook notification failed: %s', traceback.format_exc())
return False
config = [{
'name': 'webhook',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'webhook',
'label': 'Webhook',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'url',
'description': 'The URL to send notification data to when '
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
}
]
}
]
}]
+3 -3
View File
@@ -39,7 +39,7 @@ class Plugin(object):
_locks = {} _locks = {}
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:34.0) Gecko/20100101 Firefox/34.0' user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20130519 Firefox/24.0'
http_last_use = {} http_last_use = {}
http_time_between_calls = 0 http_time_between_calls = 0
http_failed_request = {} http_failed_request = {}
@@ -206,7 +206,7 @@ class Plugin(object):
if self.http_failed_disabled[host] > (time.time() - 900): if self.http_failed_disabled[host] > (time.time() - 900):
log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host) log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host)
if not show_error: if not show_error:
raise Exception('Disabled calls to %s for 15 minutes because so many failed requests' % host) raise Exception('Disabled calls to %s for 15 minutes because so many failed requests')
else: else:
return '' return ''
else: else:
@@ -279,7 +279,7 @@ class Plugin(object):
wait = (last_use - now) + self.http_time_between_calls wait = (last_use - now) + self.http_time_between_calls
if wait > 0: if wait > 0:
log.debug('Waiting for %s, %d seconds', (self.getName(), max(1, wait))) log.debug('Waiting for %s, %d seconds', (self.getName(), wait))
time.sleep(min(wait, 30)) time.sleep(min(wait, 30))
def beforeCall(self, handler): def beforeCall(self, handler):
-1
View File
@@ -87,7 +87,6 @@ class FileBrowser(Plugin):
try: try:
dirs = self.getDirectories(path = path, show_hidden = show_hidden) dirs = self.getDirectories(path = path, show_hidden = show_hidden)
except: except:
log.error('Failed getting directory "%s" : %s', (path, traceback.format_exc()))
dirs = [] dirs = []
parent = os.path.dirname(path.rstrip(os.path.sep)) parent = os.path.dirname(path.rstrip(os.path.sep))
@@ -52,7 +52,7 @@ var CategoryListBase = new Class({
}); });
}); })
}, },
@@ -71,7 +71,7 @@ var CategoryListBase = new Class({
'events': { 'events': {
'click': function(){ 'click': function(){
var category = self.createCategory(); var category = self.createCategory();
$(category).inject(self.category_container); $(category).inject(self.category_container)
} }
} }
}) })
@@ -79,15 +79,15 @@ var CategoryListBase = new Class({
// Add categories, that aren't part of the core (for editing) // Add categories, that aren't part of the core (for editing)
Array.each(self.categories, function(category){ Array.each(self.categories, function(category){
$(category).inject(self.category_container); $(category).inject(self.category_container)
}); });
}, },
getCategory: function(id){ getCategory: function(id){
return this.categories.filter(function(category){ return this.categories.filter(function(category){
return category.data._id == id; return category.data._id == id
}).pick(); }).pick()
}, },
getAll: function(){ getAll: function(){
@@ -97,7 +97,7 @@ var CategoryListBase = new Class({
createCategory: function(data){ createCategory: function(data){
var self = this; var self = this;
data = data || {'id': randomString()}; var data = data || {'id': randomString()};
var category = new Category(data); var category = new Category(data);
self.categories.include(category); self.categories.include(category);
@@ -225,7 +225,7 @@ var Category = new Class({
) )
); );
self.makeSortable(); self.makeSortable()
}, },
@@ -248,7 +248,7 @@ var Category = new Class({
} }
}); });
}).delay(delay || 0, self); }).delay(delay || 0, self)
}, },
@@ -262,13 +262,13 @@ var Category = new Class({
'preferred' : self.el.getElement('.category_preferred input').get('value'), 'preferred' : self.el.getElement('.category_preferred input').get('value'),
'ignored' : self.el.getElement('.category_ignored input').get('value'), 'ignored' : self.el.getElement('.category_ignored input').get('value'),
'destination': self.data.destination 'destination': self.data.destination
}; }
}, },
del: function(){ del: function(){
var self = this; var self = this;
if(self.data.label === undefined){ if(self.data.label == undefined){
self.el.destroy(); self.el.destroy();
return; return;
} }
@@ -318,11 +318,11 @@ var Category = new Class({
}, },
get: function(attr){ get: function(attr){
return this.data[attr]; return this.data[attr]
}, },
toElement: function(){ toElement: function(){
return this.el; return this.el
} }
}); });
+4 -5
View File
@@ -1,9 +1,9 @@
import codecs
import os import os
import re import re
import traceback import traceback
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.helpers.variable import tryInt, splitString
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
@@ -103,9 +103,8 @@ class Logging(Plugin):
if not os.path.isfile(path): if not os.path.isfile(path):
break break
f = open(path, 'r') f = codecs.open(path, 'r', 'utf-8')
log_content = toUnicode(f.read()) raw_lines = self.toList(f.read())
raw_lines = self.toList(log_content)
raw_lines.reverse() raw_lines.reverse()
brk = False brk = False
@@ -131,7 +130,7 @@ class Logging(Plugin):
def toList(self, log_content = ''): def toList(self, log_content = ''):
logs_raw = toUnicode(log_content).split('[0m\n') logs_raw = log_content.split('[0m\n')
logs = [] logs = []
for log_line in logs_raw: for log_line in logs_raw:
+15 -14
View File
@@ -8,19 +8,20 @@ Page.Log = new Class({
has_tab: false, has_tab: false,
log_items: [], log_items: [],
report_text: '### Steps to reproduce:\n'+ report_text: '\
'1. ..\n'+ ### Steps to reproduce:\n\
'2. ..\n'+ 1. ..\n\
'\n'+ 2. ..\n\
'### Information:\n'+ \n\
'Movie(s) I have this with: ...\n'+ ### Information:\n\
'Quality of the movie being searched: ...\n'+ Movie(s) I have this with: ...\n\
'Providers I use: ...\n'+ Quality of the movie being searched: ...\n\
'Version of CouchPotato: {version}\n'+ Providers I use: ...\n\
'Running on: ...\n'+ Version of CouchPotato: {version}\n\
'\n'+ Running on: ...\n\
'### Logs:\n'+ \n\
'```\n{issue}```', ### Logs:\n\
```\n{issue}```',
indexAction: function () { indexAction: function () {
var self = this; var self = this;
@@ -132,7 +133,7 @@ Page.Log = new Class({
new Element('span.message', { new Element('span.message', {
'text': log.message 'text': log.message
}) })
)); ))
}); });
return elements; return elements;
+1 -1
View File
@@ -123,7 +123,7 @@ class Manage(Plugin):
fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder) fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder)
onFound = self.createAddToLibrary(folder, added_identifiers) onFound = self.createAddToLibrary(folder, added_identifiers)
fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, check_file_date = False, on_found = onFound, single = True) fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, on_found = onFound, single = True)
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
-2
View File
@@ -86,7 +86,6 @@ class ProfilePlugin(Plugin):
'label': toUnicode(kwargs.get('label')), 'label': toUnicode(kwargs.get('label')),
'order': tryInt(kwargs.get('order', 999)), 'order': tryInt(kwargs.get('order', 999)),
'core': kwargs.get('core', False), 'core': kwargs.get('core', False),
'minimum_score': tryInt(kwargs.get('minimum_score', 1)),
'qualities': [], 'qualities': [],
'wait_for': [], 'wait_for': [],
'stop_after': [], 'stop_after': [],
@@ -218,7 +217,6 @@ class ProfilePlugin(Plugin):
'label': toUnicode(profile.get('label')), 'label': toUnicode(profile.get('label')),
'order': order, 'order': order,
'qualities': profile.get('qualities'), 'qualities': profile.get('qualities'),
'minimum_score': 1,
'finish': [], 'finish': [],
'wait_for': [], 'wait_for': [],
'stop_after': [], 'stop_after': [],
@@ -51,11 +51,6 @@
margin: 0 5px !important; margin: 0 5px !important;
} }
.profile .wait_for .minimum_score_input {
width: 40px !important;
text-align: left;
}
.profile .types { .profile .types {
padding: 0; padding: 0;
margin: 0 20px 0 -4px; margin: 0 20px 0 -4px;
@@ -53,21 +53,12 @@ var Profile = new Class({
}), }),
new Element('span', {'text':'day(s) for a better quality '}), new Element('span', {'text':'day(s) for a better quality '}),
new Element('span.advanced', {'text':'and keep searching'}), new Element('span.advanced', {'text':'and keep searching'}),
// "After a checked quality is found and downloaded, continue searching for even better quality releases for the entered number of days." // "After a checked quality is found and downloaded, continue searching for even better quality releases for the entered number of days."
new Element('input.inlay.xsmall.stop_after_input.advanced', { new Element('input.inlay.xsmall.stop_after_input.advanced', {
'type':'text', 'type':'text',
'value': data.stop_after && data.stop_after.length > 0 ? data.stop_after[0] : 0 'value': data.stop_after && data.stop_after.length > 0 ? data.stop_after[0] : 0
}), }),
new Element('span.advanced', {'text':'day(s) for a better (checked) quality.'}), new Element('span.advanced', {'text':'day(s) for a better (checked) quality.'})
// Minimum score of
new Element('span.advanced', {'html':'<br/>Releases need a minimum score of'}),
new Element('input.advanced.inlay.xsmall.minimum_score_input', {
'size': 4,
'type':'text',
'value': data.minimum_score || 1
})
) )
); );
@@ -81,7 +72,7 @@ var Profile = new Class({
'quality': quality, 'quality': quality,
'finish': data.finish[nr] || false, 'finish': data.finish[nr] || false,
'3d': data['3d'] ? data['3d'][nr] || false : false '3d': data['3d'] ? data['3d'][nr] || false : false
}); })
}); });
} }
@@ -123,7 +114,7 @@ var Profile = new Class({
} }
}); });
}).delay(delay, self); }).delay(delay, self)
}, },
@@ -135,7 +126,6 @@ var Profile = new Class({
'label' : self.el.getElement('.quality_label input').get('value'), 'label' : self.el.getElement('.quality_label input').get('value'),
'wait_for' : self.el.getElement('.wait_for_input').get('value'), 'wait_for' : self.el.getElement('.wait_for_input').get('value'),
'stop_after' : self.el.getElement('.stop_after_input').get('value'), 'stop_after' : self.el.getElement('.stop_after_input').get('value'),
'minimum_score' : self.el.getElement('.minimum_score_input').get('value'),
'types': [] 'types': []
}; };
@@ -148,7 +138,7 @@ var Profile = new Class({
}); });
}); });
return data; return data
}, },
addType: function(data){ addType: function(data){
@@ -177,7 +167,7 @@ var Profile = new Class({
var self = this; var self = this;
return self.types.filter(function(type){ return self.types.filter(function(type){
return type.get('quality'); return type.get('quality')
}); });
}, },
@@ -231,15 +221,15 @@ var Profile = new Class({
}, },
get: function(attr){ get: function(attr){
return this.data[attr]; return this.data[attr]
}, },
isCore: function(){ isCore: function(){
return this.data.core; return this.data.core
}, },
toElement: function(){ toElement: function(){
return this.el; return this.el
} }
}); });
@@ -342,7 +332,7 @@ Profile.Type = new Class({
'text': q.label, 'text': q.label,
'value': q.identifier, 'value': q.identifier,
'data-allow_3d': q.allow_3d 'data-allow_3d': q.allow_3d
}).inject(self.qualities); }).inject(self.qualities)
}); });
self.qualities.set('value', self.data.quality); self.qualities.set('value', self.data.quality);
@@ -358,7 +348,7 @@ Profile.Type = new Class({
'quality': self.qualities.get('value'), 'quality': self.qualities.get('value'),
'finish': +self.finish.checked, 'finish': +self.finish.checked,
'3d': +self['3d'].checked '3d': +self['3d'].checked
}; }
}, },
get: function(key){ get: function(key){
+6 -13
View File
@@ -30,10 +30,10 @@ class QualityPlugin(Plugin):
{'identifier': 'dvdr', 'size': (3000, 10000), 'median_size': 4500, 'label': 'DVD-R', 'alternative': ['br2dvd', ('dvd', 'r')], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'median_size': 4500, 'label': 'DVD-R', 'alternative': ['br2dvd', ('dvd', 'r')], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'median_size': 1500, 'label': 'DVD-Rip', 'width': 720, 'alternative': [('dvd', 'rip')], 'allow': [], 'ext':['avi'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, {'identifier': 'dvdrip', 'size': (600, 2400), 'median_size': 1500, 'label': 'DVD-Rip', 'width': 720, 'alternative': [('dvd', 'rip')], 'allow': [], 'ext':['avi'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
{'identifier': 'scr', 'size': (600, 1600), 'median_size': 700, 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr', 'webrip', ('web', 'rip')], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': []}, {'identifier': 'scr', 'size': (600, 1600), 'median_size': 700, 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr', 'webrip', ('web', 'rip')], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': []},
{'identifier': 'r5', 'size': (600, 1000), 'median_size': 700, 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr', '720p', '1080p'], 'ext':[]}, {'identifier': 'r5', 'size': (600, 1000), 'median_size': 700, 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr', '720p'], 'ext':[]},
{'identifier': 'tc', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': ['720p', '1080p'], 'ext':[]}, {'identifier': 'tc', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': ['720p'], 'ext':[]},
{'identifier': 'ts', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': ['720p', '1080p'], 'ext':[]}, {'identifier': 'ts', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': ['720p'], 'ext':[]},
{'identifier': 'cam', 'size': (600, 1000), 'median_size': 700, 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': ['720p', '1080p'], 'ext':[]} {'identifier': 'cam', 'size': (600, 1000), 'median_size': 700, 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': ['720p'], 'ext':[]}
] ]
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr'] pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
threed_tags = { threed_tags = {
@@ -240,7 +240,7 @@ class QualityPlugin(Plugin):
# Add additional size score if only 1 size validated # Add additional size score if only 1 size validated
if len(size_scores) == 1: if len(size_scores) == 1:
self.calcScore(score, size_scores[0], 7) self.calcScore(score, size_scores[0], 8)
del size_scores del size_scores
# Return nothing if all scores are <= 0 # Return nothing if all scores are <= 0
@@ -278,8 +278,6 @@ class QualityPlugin(Plugin):
'ext': 5, 'ext': 5,
} }
scored_on = []
# Check alt and tags # Check alt and tags
for tag_type in ['identifier', 'alternative', 'tags', 'label']: for tag_type in ['identifier', 'alternative', 'tags', 'label']:
qualities = quality.get(tag_type, []) qualities = quality.get(tag_type, [])
@@ -291,13 +289,10 @@ class QualityPlugin(Plugin):
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
score += points.get(tag_type) score += points.get(tag_type)
if isinstance(alt, (str, unicode)) and ss(alt.lower()) in words and ss(alt.lower()) not in scored_on: if isinstance(alt, (str, unicode)) and ss(alt.lower()) in words:
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
score += points.get(tag_type) score += points.get(tag_type)
# Don't score twice on same tag
scored_on.append(ss(alt).lower())
# Check extention # Check extention
for ext in quality.get('ext', []): for ext in quality.get('ext', []):
if ext == extension: if ext == extension:
@@ -490,8 +485,6 @@ class QualityPlugin(Plugin):
'Movie Name (2015).mp4': {'size': 6500, 'quality': 'brrip'}, 'Movie Name (2015).mp4': {'size': 6500, 'quality': 'brrip'},
'Movie Name.2014.720p Web-Dl Aac2.0 h264-ReleaseGroup': {'size': 3800, 'quality': 'brrip'}, 'Movie Name.2014.720p Web-Dl Aac2.0 h264-ReleaseGroup': {'size': 3800, 'quality': 'brrip'},
'Movie Name.2014.720p.WEBRip.x264.AC3-ReleaseGroup': {'size': 3000, 'quality': 'scr'}, 'Movie Name.2014.720p.WEBRip.x264.AC3-ReleaseGroup': {'size': 3000, 'quality': 'scr'},
'Movie.Name.2014.1080p.HDCAM.-.ReleaseGroup': {'size': 5300, 'quality': 'cam'},
'Movie.Name.2014.720p.HDSCR.4PARTS.MP4.AAC.ReleaseGroup': {'size': 2401, 'quality': 'scr'},
} }
correct = 0 correct = 0
@@ -12,20 +12,20 @@ var QualityBase = new Class({
self.profiles = []; self.profiles = [];
Array.each(data.profiles, self.createProfilesClass.bind(self)); Array.each(data.profiles, self.createProfilesClass.bind(self));
App.addEvent('loadSettings', self.addSettings.bind(self)); App.addEvent('loadSettings', self.addSettings.bind(self))
}, },
getProfile: function(id){ getProfile: function(id){
return this.profiles.filter(function(profile){ return this.profiles.filter(function(profile){
return profile.data._id == id; return profile.data._id == id
}).pick(); }).pick()
}, },
// Hide items when getting profiles // Hide items when getting profiles
getActiveProfiles: function(){ getActiveProfiles: function(){
return Array.filter(this.profiles, function(profile){ return Array.filter(this.profiles, function(profile){
return !profile.data.hide; return !profile.data.hide
}); });
}, },
@@ -37,7 +37,7 @@ var QualityBase = new Class({
} }
catch(e){} catch(e){}
return {}; return {}
}, },
addSettings: function(){ addSettings: function(){
@@ -58,7 +58,7 @@ var QualityBase = new Class({
self.createProfileOrdering(); self.createProfileOrdering();
self.createSizes(); self.createSizes();
}); })
}, },
@@ -68,7 +68,7 @@ var QualityBase = new Class({
createProfiles: function(){ createProfiles: function(){
var self = this; var self = this;
var non_core_profiles = Array.filter(self.profiles, function(profile){ return !profile.isCore(); }); var non_core_profiles = Array.filter(self.profiles, function(profile){ return !profile.isCore() });
var count = non_core_profiles.length; var count = non_core_profiles.length;
self.settings.createGroup({ self.settings.createGroup({
@@ -81,7 +81,7 @@ var QualityBase = new Class({
'events': { 'events': {
'click': function(){ 'click': function(){
var profile = self.createProfilesClass(); var profile = self.createProfilesClass();
$(profile).inject(self.profile_container); $(profile).inject(self.profile_container)
} }
} }
}) })
@@ -89,7 +89,7 @@ var QualityBase = new Class({
// Add profiles, that aren't part of the core (for editing) // Add profiles, that aren't part of the core (for editing)
Array.each(non_core_profiles, function(profile){ Array.each(non_core_profiles, function(profile){
$(profile).inject(self.profile_container); $(profile).inject(self.profile_container)
}); });
}, },
@@ -97,7 +97,7 @@ var QualityBase = new Class({
createProfilesClass: function(data){ createProfilesClass: function(data){
var self = this; var self = this;
data = data || {'id': randomString()}; var data = data || {'id': randomString()};
var profile = new Profile(data); var profile = new Profile(data);
self.profiles.include(profile); self.profiles.include(profile);
@@ -190,6 +190,7 @@ var QualityBase = new Class({
'name': 'sizes' 'name': 'sizes'
}).inject(self.content); }).inject(self.content);
new Element('div.item.head.ctrlHolder').adopt( new Element('div.item.head.ctrlHolder').adopt(
new Element('span.label', {'text': 'Quality'}), new Element('span.label', {'text': 'Quality'}),
new Element('span.min', {'text': 'Min'}), new Element('span.min', {'text': 'Min'}),
@@ -203,7 +204,7 @@ var QualityBase = new Class({
'value': quality.size_min, 'value': quality.size_min,
'events': { 'events': {
'keyup': function(e){ 'keyup': function(e){
self.changeSize(quality.identifier, 'size_min', e.target.get('value')); self.changeSize(quality.identifier, 'size_min', e.target.get('value'))
} }
} }
}), }),
@@ -211,11 +212,11 @@ var QualityBase = new Class({
'value': quality.size_max, 'value': quality.size_max,
'events': { 'events': {
'keyup': function(e){ 'keyup': function(e){
self.changeSize(quality.identifier, 'size_max', e.target.get('value')); self.changeSize(quality.identifier, 'size_max', e.target.get('value'))
} }
} }
}) })
).inject(group); ).inject(group)
}); });
}, },
@@ -234,7 +235,7 @@ var QualityBase = new Class({
'value': value 'value': value
} }
}); });
}).delay(300); }).delay(300)
} }

Some files were not shown because too many files have changed in this diff Show More