Compare commits

..
21 Commits
Author SHA1 Message Date
Ruud ac8dbe03b2 Py3 fixes 2014-10-06 23:02:30 +02:00
Ruud 6c2aef7a6d Six import 2014-10-06 23:01:47 +02:00
Ruud e1eb68e226 CodernityDB updates 2014-10-06 23:00:50 +02:00
Ruud 4ae02a8764 Don't write binary to configparser 2014-10-06 17:32:36 +02:00
Ruud cc59342e80 NotSupported Exception 2014-10-06 17:31:12 +02:00
Ruud 97099f4d69 Use six module 2014-10-06 17:30:58 +02:00
Ruud c50c290c3e Database helper 2014-10-06 17:28:10 +02:00
Ruud 4775e4a36a CodernityDB 2to3 2014-10-06 17:26:14 +02:00
Ruud 08cb834b4d Don't load unsupported modules 2014-10-06 16:47:38 +02:00
Ruud 46cff26d92 Don't load Twitter notifier on Python 3 2014-10-06 16:47:04 +02:00
Ruud b668e39296 Don't load xmpp on Python 3 2014-10-06 16:46:22 +02:00
Ruud 97ee16eb4e Use six on axel 2014-10-06 16:45:40 +02:00
Ruud 41fd190d38 Update cache lib 2014-10-06 16:45:26 +02:00
Ruud 2b0facb24c Update pytwitter 2014-10-06 16:44:38 +02:00
Ruud 94a29efea5 Remove tmdb3 lib 2014-10-06 16:44:22 +02:00
Ruud 827156485c Remove tmdb3 dependency 2014-10-06 16:43:46 +02:00
Ruud 6b4e6857de BeautifulSoup4 python 3 2014-10-06 11:12:35 +02:00
Ruud 5b7e814166 CodernityDB python 3 2014-10-06 10:59:22 +02:00
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
326 changed files with 27902 additions and 14817 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())
+24 -28
View File
@@ -3,11 +3,10 @@ from threading import Thread
import json import json
import threading import threading
import traceback import traceback
import urllib from six.moves 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()
@@ -102,7 +102,7 @@ class ApiHandler(RequestHandler):
kwargs = {} kwargs = {}
for x in self.request.arguments: for x in self.request.arguments:
kwargs[x] = urllib.unquote(self.get_argument(x)) kwargs[x] = urllib.parse.unquote(self.get_argument(x))
# Split array arguments # Split array arguments
kwargs = getParams(kwargs) kwargs = getParams(kwargs)
@@ -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 -4
View File
@@ -3,12 +3,12 @@ import os
import time import time
import traceback import traceback
from sqlite3 import OperationalError from sqlite3 import OperationalError
from CodernityDB3.index import Index
from CodernityDB.database import RecordNotFound
from CodernityDB.index import IndexException, IndexNotFoundException, IndexConflict
from couchpotato import CPLog from couchpotato import CPLog
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.database import IndexException, IndexNotFoundException, IndexConflict, RecordNotFound
from couchpotato.core.helpers.encoding import toUnicode, sp from couchpotato.core.helpers.encoding import toUnicode, sp
from couchpotato.core.helpers.variable import getImdb, tryInt, randomString from couchpotato.core.helpers.variable import getImdb, tryInt, randomString
@@ -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():
+2 -28
View File
@@ -1,7 +1,7 @@
from base64 import b16encode, b32decode from base64 import b16encode, b32decode
from datetime import timedelta from datetime import timedelta
from hashlib import sha1 from hashlib import sha1
from urlparse import urlparse from six.moves import urllib
import os import os
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
@@ -62,7 +62,7 @@ class rTorrent(DownloaderBase):
if self.conf('ssl') and url.startswith('httprpc://'): if self.conf('ssl') and url.startswith('httprpc://'):
url = url.replace('httprpc://', 'httprpc+https://') url = url.replace('httprpc://', 'httprpc+https://')
parsed = urlparse(url) parsed = urllib.urlparse(url)
# rpc_url is only used on http/https scgi pass-through # rpc_url is only used on http/https scgi pass-through
if parsed.scheme in ['http', 'https']: if parsed.scheme in ['http', 'https']:
@@ -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.')
+1 -1
View File
@@ -102,7 +102,7 @@ def fireEvent(name, *args, **kwargs):
# Fire # Fire
result = e(*args, **kwargs) result = e(*args, **kwargs)
result_keys = result.keys() result_keys = list(result.keys())
result_keys.sort(key = natsortKey) result_keys.sort(key = natsortKey)
if options['single'] and not options['merge']: if options['single'] and not options['merge']:
+26
View File
@@ -0,0 +1,26 @@
from six import PY2
if PY2:
from CodernityDB.database_super_thread_safe import SuperThreadSafeDatabase
from CodernityDB.index import IndexException, IndexConflict, IndexNotFoundException
from CodernityDB.database import RecordNotFound, RecordDeleted
from CodernityDB.hash_index import HashIndex
from CodernityDB.tree_index import MultiTreeBasedIndex, TreeBasedIndex
else:
from CodernityDB3.database_super_thread_safe import SuperThreadSafeDatabase
from CodernityDB3.index import IndexException, IndexConflict, IndexNotFoundException
from CodernityDB3.database import RecordNotFound, RecordDeleted
from CodernityDB3.hash_index import HashIndex
from CodernityDB3.tree_index import MultiTreeBasedIndex, TreeBasedIndex
SuperThreadSafeDatabase = SuperThreadSafeDatabase
IndexException = IndexException
IndexNotFoundException = IndexNotFoundException
IndexConflict = IndexConflict
RecordNotFound = RecordNotFound
HashIndex = HashIndex
MultiTreeBasedIndex = MultiTreeBasedIndex
TreeBasedIndex = TreeBasedIndex
RecordDeleted = RecordDeleted
+30 -16
View File
@@ -1,11 +1,11 @@
from string import ascii_letters, digits from string import ascii_letters, digits
from urllib import quote_plus
import os import os
import re import re
import traceback import traceback
import unicodedata import unicodedata
from chardet import detect from chardet import detect
from six.moves import urllib
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import six import six
@@ -16,7 +16,7 @@ log = CPLog(__name__)
def toSafeString(original): def toSafeString(original):
valid_chars = "-_.() %s%s" % (ascii_letters, digits) valid_chars = "-_.() %s%s" % (ascii_letters, digits)
cleaned_filename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore') cleaned_filename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
valid_string = ''.join(c for c in cleaned_filename if c in valid_chars) valid_string = ''.join(list(six.unichr(c) for c in cleaned_filename if six.unichr(c) in valid_chars))
return ' '.join(valid_string.split()) return ' '.join(valid_string.split())
@@ -29,7 +29,7 @@ def simplifyString(original):
def toUnicode(original, *args): def toUnicode(original, *args):
try: try:
if isinstance(original, unicode): if isinstance(original, six.text_type):
return original return original
else: else:
try: try:
@@ -37,29 +37,42 @@ 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, six.binary_type) 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):
u_original = toUnicode(original, *args) u_original = toUnicode(original, *args)
try: try:
from couchpotato.environment import Env if isinstance(u_original, six.text_type):
return u_original.encode(Env.get('encoding')) u_original = u_original.encode('unicode_escape')
else:
u_original = u_original
return six.u(u_original)
except Exception as e: except Exception as e:
log.debug('Failed ss encoding char, force UTF8: %s', e) log.debug('Failed ss encoding char, force UTF8: %s', e)
try: try:
from couchpotato.environment import Env
return u_original.encode(Env.get('encoding'), 'replace') return u_original.encode(Env.get('encoding'), 'replace')
except: except:
return u_original.encode('utf-8', 'replace') return u_original.encode('utf-8', 'replace')
@@ -75,7 +88,7 @@ def sp(path, *args):
if os.path.sep == '/' and '\\' in path: if os.path.sep == '/' and '\\' in path:
path = '/' + path.replace(':', '').replace('\\', '/') path = '/' + path.replace(':', '').replace('\\', '/')
path = os.path.normpath(ss(path, *args)) path = os.path.normpath(path)
# Remove any trailing path separators # Remove any trailing path separators
if path != os.path.sep: if path != os.path.sep:
@@ -95,7 +108,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
@@ -117,14 +130,15 @@ def stripAccents(s):
def tryUrlencode(s): def tryUrlencode(s):
new = six.u('') new = six.u('')
if isinstance(s, dict): if isinstance(s, dict):
for key, value in s.items(): for key, value in list(s.items()):
new += six.u('&%s=%s') % (key, tryUrlencode(value)) new += six.u('&%s=%s') % (key, tryUrlencode(value))
return new[1:] return new[1:]
else: else:
for letter in ss(s): for letter in ss(s):
letter = six.unichr(letter)
try: try:
new += quote_plus(letter) new += urllib.parse.quote_plus(letter)
except: except:
new += letter new += letter
+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
+2
View File
@@ -0,0 +1,2 @@
class NotSupported(Exception):
pass
+4 -4
View File
@@ -1,7 +1,7 @@
from urllib import unquote
import re import re
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from six.moves import urllib
from couchpotato.core.helpers.variable import natsortKey from couchpotato.core.helpers.variable import natsortKey
@@ -10,7 +10,7 @@ def getParams(params):
reg = re.compile('^[a-z0-9_\.]+$') reg = re.compile('^[a-z0-9_\.]+$')
# Sort keys # Sort keys
param_keys = params.keys() param_keys = list(params.keys())
param_keys.sort(key = natsortKey) param_keys.sort(key = natsortKey)
temp = {} temp = {}
@@ -28,7 +28,7 @@ def getParams(params):
for item in nested: for item in nested:
if item is nested[-1]: if item is nested[-1]:
current[item] = toUnicode(unquote(value)) current[item] = toUnicode(urllib.parse.unquote(value))
else: else:
try: try:
current[item] current[item]
@@ -37,7 +37,7 @@ def getParams(params):
current = current[item] current = current[item]
else: else:
temp[param] = toUnicode(unquote(value)) temp[param] = toUnicode(urllib.parse.unquote(value))
if temp[param].lower() in ['true', 'false']: if temp[param].lower() in ['true', 'false']:
temp[param] = temp[param].lower() != 'false' temp[param] = temp[param].lower() != 'false'
+4 -1
View File
@@ -3,6 +3,7 @@ import sys
import traceback import traceback
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.py3 import NotSupported
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from importhelper import import_module from importhelper import import_module
import six import six
@@ -131,7 +132,7 @@ class Loader(object):
return False return False
try: try:
# Load single file plugin # Load single file plugin
if isinstance(module.autoload, (str, unicode)): if isinstance(module.autoload, (six.string_types, six.text_type)):
getattr(module, module.autoload)() getattr(module, module.autoload)()
# Load folder plugin # Load folder plugin
else: else:
@@ -162,6 +163,8 @@ class Loader(object):
def loadModule(self, name): def loadModule(self, name):
try: try:
return import_module(name) return import_module(name)
except NotSupported:
log.error('Module "%s" is not supported in Python 3', name)
except ImportError: except ImportError:
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc())) log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
return None return None
+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)
+3 -12
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
@@ -90,18 +89,10 @@ class MediaBase(Plugin):
# Loop over type # Loop over type
for image in image_urls.get(image_type, []): for image in image_urls.get(image_type, []):
if not isinstance(image, (str, unicode)): if not isinstance(image, six.string_types):
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)]
+9 -8
View File
@@ -1,14 +1,14 @@
from string import ascii_letters from string import ascii_letters
from hashlib import md5 from hashlib import md5
from couchpotato.core.helpers.database import MultiTreeBasedIndex, TreeBasedIndex
from CodernityDB.tree_index import MultiTreeBasedIndex, TreeBasedIndex
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString
class MediaIndex(MultiTreeBasedIndex): class MediaIndex(MultiTreeBasedIndex):
_version = 3 _version = 3
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex""" custom_header = """from couchpotato.core.helpers.database import MultiTreeBasedIndex"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['key_format'] = '32s' kwargs['key_format'] = '32s'
@@ -62,10 +62,11 @@ class MediaTypeIndex(TreeBasedIndex):
class TitleSearchIndex(MultiTreeBasedIndex): class TitleSearchIndex(MultiTreeBasedIndex):
_version = 1 _version = 2
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex custom_header = """from couchpotato.core.helpers.database import MultiTreeBasedIndex
from itertools import izip try: from itertools import izip
except: izip = zip
from couchpotato.core.helpers.encoding import simplifyString""" from couchpotato.core.helpers.encoding import simplifyString"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -101,7 +102,7 @@ from couchpotato.core.helpers.encoding import simplifyString"""
class TitleIndex(TreeBasedIndex): class TitleIndex(TreeBasedIndex):
_version = 4 _version = 4
custom_header = """from CodernityDB.tree_index import TreeBasedIndex custom_header = """from couchpotato.core.helpers.database import TreeBasedIndex
from string import ascii_letters from string import ascii_letters
from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
@@ -134,7 +135,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
class StartsWithIndex(TreeBasedIndex): class StartsWithIndex(TreeBasedIndex):
_version = 3 _version = 3
custom_header = """from CodernityDB.tree_index import TreeBasedIndex custom_header = """from couchpotato.core.helpers.database import TreeBasedIndex
from string import ascii_letters from string import ascii_letters
from couchpotato.core.helpers.encoding import toUnicode, simplifyString""" from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
@@ -180,7 +181,7 @@ class MediaChildrenIndex(TreeBasedIndex):
class MediaTagIndex(MultiTreeBasedIndex): class MediaTagIndex(MultiTreeBasedIndex):
_version = 2 _version = 2
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex""" custom_header = """from couchpotato.core.helpers.database import MultiTreeBasedIndex"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['key_format'] = '32s' kwargs['key_format'] = '32s'
+5 -9
View File
@@ -3,15 +3,16 @@ import time
import traceback import traceback
from string import ascii_lowercase from string import ascii_lowercase
from CodernityDB.database import RecordNotFound, RecordDeleted
from couchpotato import tryInt, get_db from couchpotato import tryInt, get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.database import RecordNotFound, RecordDeleted
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString, getImdb, getTitle from couchpotato.core.helpers.variable import splitString, getImdb, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase from couchpotato.core.media import MediaBase
from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex, MediaTagIndex from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex, MediaTagIndex
import six
log = CPLog(__name__) log = CPLog(__name__)
@@ -280,7 +281,7 @@ class MediaPlugin(MediaBase):
offset = 0 offset = 0
limit = -1 limit = -1
if limit_offset: if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset splt = splitString(limit_offset) if isinstance(limit_offset, six.string_types) else limit_offset
limit = tryInt(splt[0]) limit = tryInt(splt[0])
offset = tryInt(0 if len(splt) is 1 else splt[1]) offset = tryInt(0 if len(splt) is 1 else splt[1])
@@ -456,11 +457,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 +492,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 +527,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
@@ -1,4 +1,4 @@
from urlparse import urlparse from six.moves import urllib
import json import json
import re import re
import time import time
@@ -50,7 +50,7 @@ class Provider(Plugin):
if Env.get('dev'): return True if Env.get('dev'): return True
now = time.time() now = time.time()
host = urlparse(test_url).hostname host = urllib.urlparse(test_url).hostname
if self.last_available_check.get(host) < now - 900: if self.last_available_check.get(host) < now - 900:
self.last_available_check[host] = now self.last_available_check[host] = now
@@ -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()))
@@ -221,7 +219,7 @@ class YarrProvider(Provider):
if provider and provider == self.getName(): if provider and provider == self.getName():
return self return self
hostname = urlparse(url).hostname hostname = urllib.urlparse(url).hostname
if host and hostname in host: if host and hostname in host:
return self return self
else: else:
@@ -1,4 +1,4 @@
from urlparse import urlparse from six.moves import urllib
import time import time
import traceback import traceback
import re import re
@@ -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
@@ -101,13 +97,13 @@ class Base(NZBProvider, RSS):
results.append({ results.append({
'id': nzb_id, 'id': nzb_id,
'provider_extra': urlparse(host['host']).hostname or host['host'], 'provider_extra': urllib.urlparse(host['host']).hostname or host['host'],
'name': toUnicode(name), 'name': toUnicode(name),
'name_extra': name_extra, 'name_extra': name_extra,
'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'],
@@ -179,7 +175,7 @@ class Base(NZBProvider, RSS):
return '&apikey=%s' % host['api_key'] return '&apikey=%s' % host['api_key']
def download(self, url = '', nzb_id = ''): def download(self, url = '', nzb_id = ''):
host = urlparse(url).hostname host = urllib.urlparse(url).hostname
if self.limits_reached.get(host): if self.limits_reached.get(host):
# Try again in 3 hours # Try again in 3 hours
@@ -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 six.moves import urllib
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 = urllib.parse_qs(urllib.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': [
@@ -1,4 +1,4 @@
import htmlentitydefs from six.moves import html_entities
import json import json
import re import re
import time import time
@@ -145,15 +145,15 @@ class Base(TorrentProvider):
# character reference # character reference
try: try:
if txt[:3] == "&#x": if txt[:3] == "&#x":
return unichr(int(txt[3:-1], 16)) return six.unichr(int(txt[3:-1], 16))
else: else:
return unichr(int(txt[2:-1])) return six.unichr(int(txt[2:-1]))
except ValueError: except ValueError:
pass pass
else: else:
# named entity # named entity
try: try:
txt = unichr(htmlentitydefs.name2codepoint[txt[1:-1]]) txt = six.unichr(html_entities.name2codepoint[txt[1:-1]])
except KeyError: except KeyError:
pass pass
return txt # leave as is return txt # leave as is
@@ -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',
} }
@@ -1,4 +1,4 @@
from urlparse import urlparse from six.moves import urllib
import re import re
import traceback import traceback
@@ -45,7 +45,7 @@ class Base(TorrentProvider):
results.append({ results.append({
'id': torrent.get('torrent_id'), 'id': torrent.get('torrent_id'),
'protocol': 'torrent' if re.match('^(http|https|ftp)://.*$', torrent.get('download_url')) else 'torrent_magnet', 'protocol': 'torrent' if re.match('^(http|https|ftp)://.*$', torrent.get('download_url')) else 'torrent_magnet',
'provider_extra': urlparse(host['host']).hostname or host['host'], 'provider_extra': urllib.urlparse(host['host']).hostname or host['host'],
'name': toUnicode(torrent.get('release_name')), 'name': toUnicode(torrent.get('release_name')),
'url': torrent.get('download_url'), 'url': torrent.get('download_url'),
'detail_url': torrent.get('details_url'), 'detail_url': torrent.get('details_url'),
@@ -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()))
@@ -1,4 +1,4 @@
from urlparse import urlparse from six.moves import urllib
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.encoding import simplifyString
@@ -34,7 +34,7 @@ class UserscriptBase(Plugin):
def belongsTo(self, url): def belongsTo(self, url):
host = urlparse(url).hostname host = urllib.urlparse(url).hostname
host_split = host.split('.') host_split = host.split('.')
if len(host_split) > 2: if len(host_split) > 2:
host = host[len(host_split[0]):] host = host[len(host_split[0]):]
+2 -1
View File
@@ -3,6 +3,7 @@ from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.variable import mergeDicts, getImdb from couchpotato.core.helpers.variable import mergeDicts, getImdb
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
import six
log = CPLog(__name__) log = CPLog(__name__)
@@ -30,7 +31,7 @@ class Search(Plugin):
def search(self, q = '', types = None, **kwargs): def search(self, q = '', types = None, **kwargs):
# Make sure types is the correct instance # Make sure types is the correct instance
if isinstance(types, (str, unicode)): if isinstance(types, six.string_types):
types = [types] types = [types]
elif isinstance(types, (list, tuple, set)): elif isinstance(types, (list, tuple, set)):
types = list(types) types = list(types)
@@ -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;
}
}
}
+2 -2
View File
@@ -1,10 +1,10 @@
import traceback import traceback
import time import time
from CodernityDB.database import RecordNotFound
from couchpotato import get_db from couchpotato import get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.database import RecordNotFound
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString, getTitle, getImdb, getIdentifier from couchpotato.core.helpers.variable import splitString, getTitle, getImdb, getIdentifier
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -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)
@@ -1,9 +1,9 @@
import copy import copy
import traceback import traceback
from CodernityDB.database import RecordNotFound
from couchpotato import get_db from couchpotato import get_db
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.database import RecordNotFound
from couchpotato.core.helpers.variable import mergeDicts, randomString from couchpotato.core.helpers.variable import mergeDicts, randomString
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
@@ -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,12 +2,12 @@ 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
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 six
log = CPLog(__name__) log = CPLog(__name__)
@@ -18,8 +18,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 +39,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 +57,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)
@@ -74,7 +73,7 @@ class OMDBAPI(MovieProvider):
try: try:
try: try:
if isinstance(movie, (str, unicode)): if isinstance(movie, six.string_types):
movie = json.loads(movie) movie = json.loads(movie)
except ValueError: except ValueError:
log.info('No proper json to decode') log.info('No proper json to decode')
@@ -1,7 +1,8 @@
import traceback import traceback
import time
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, ss, tryUrlencode from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss, 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.movie.providers.base import MovieProvider from couchpotato.core.media.movie.providers.base import MovieProvider
@@ -13,7 +14,7 @@ autoload = 'TheMovieDb'
class TheMovieDb(MovieProvider): class TheMovieDb(MovieProvider):
http_time_between_calls = .35 http_time_between_calls = .3
configuration = { configuration = {
'images': { 'images': {
@@ -22,8 +23,6 @@ class TheMovieDb(MovieProvider):
} }
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) addEvent('app.load', self.config)
@@ -33,45 +32,49 @@ class TheMovieDb(MovieProvider):
if configuration: if configuration:
self.configuration = configuration 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 = None #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
for movie in raw: #name_year = fireEvent('scanner.name_year', q, single = True)
parsed_movie = self.parseMovie(movie, extended = False)
if parsed_movie:
results.append(parsed_movie)
nr += 1 raw = self.request('search/movie', {
if nr == limit: 'query': q
break }, return_key = 'results')
except:
log.error('Failed searching TMDB for "%s": %s', (search_string, traceback.format_exc()))
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results]) results = []
if raw:
try:
nr = 0
return results for movie in raw:
except SyntaxError as e: results.append(self.parseMovie(movie, extended = False))
log.error('Failed to parse XML response: %s', e)
return False nr += 1
if nr == limit:
break
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
self.setCache(cache_key, results)
return results
except SyntaxError as e:
log.error('Failed to parse XML response: %s', e)
return False
return results return results
@@ -84,87 +87,93 @@ class TheMovieDb(MovieProvider):
'id': identifier 'id': identifier
}, extended = extended) }, extended = extended)
return result or {} 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.get('id'), '.ex' if extended else '')
movie = self.request('movie/%s' % movie.get('id'), { movie_data = None #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 = {
'poster': [poster] if poster else [],
#'backdrop': [backdrop] if backdrop else [],
'poster_original': [poster_original] if poster_original else [],
'backdrop_original': [backdrop_original] if backdrop_original else [],
'actors': {},
'extra_thumbs': extra_thumbs
}
# Genres
try:
genres = [genre.get('name') for genre in movie.get('genres', [])]
except:
genres = []
# 1900 is the same as None
year = str(movie.get('release_date') or '')[:4]
if not movie.get('release_date') or year == '1900' or year.lower() == 'none':
year = None
# Gather actors data
actors = {}
if extended:
# Full data # Full data
cast = movie.get('casts', {}).get('cast', []) movie = self.request('movie/%s' % movie.get('id'))
for cast_item in cast: # Images
try: poster = self.getImage(movie, type = 'poster', size = 'w154')
actors[toUnicode(cast_item.get('name'))] = toUnicode(cast_item.get('character')) poster_original = self.getImage(movie, type = 'poster', size = 'original')
images['actors'][toUnicode(cast_item.get('name'))] = self.getImage(cast_item, type = 'profile', size = 'original') backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
except: extra_thumbs = self.getMultImages(movie, type = 'backdrops', size = 'original')
log.debug('Error getting cast info for %s: %s', (cast_item, traceback.format_exc()))
movie_data = { images = {
'type': 'movie', 'poster': [poster] if poster else [],
'via_tmdb': True, #'backdrop': [backdrop] if backdrop else [],
'tmdb_id': movie.get('id'), 'poster_original': [poster_original] if poster_original else [],
'titles': [toUnicode(movie.get('title'))], 'backdrop_original': [backdrop_original] if backdrop_original else [],
'original_title': movie.get('original_title'), 'actors': {},
'images': images, 'extra_thumbs': extra_thumbs
'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) # Genres
try:
genres = [genre.get('name') for genre in movie.get('genres', [])]
except:
genres = []
# Add alternative names # 1900 is the same as None
if movie_data['original_title'] and movie_data['original_title'] not in movie_data['titles']: year = str(movie.get('release_date') or '')[:4]
movie_data['titles'].append(movie_data['original_title']) if not movie.get('release_date') or year == '1900' or year.lower() == 'none':
year = None
# Add alternative titles # Gather actors data
alternate_titles = movie.get('alternative_titles', {}).get('titles', []) actors = {}
if extended:
for alt in alternate_titles: # Full data
alt_name = alt.get('title') cast = self.request('movie/%s/casts' % movie.get('id'), return_key = 'cast')
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) for cast_item in cast:
try:
actors[toUnicode(cast_item.get('name'))] = toUnicode(cast_item.get('character'))
images['actors'][toUnicode(cast_item.get('name'))] = self.getImage(cast_item, type = 'profile', size = 'original')
except:
log.debug('Error getting cast info for %s: %s', (cast_item, traceback.format_exc()))
movie_data = {
'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'])
if extended:
# Full data
alternate_titles = self.request('movie/%s/alternative_titles' % movie.get('id'), return_key = 'titles')
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)
# Cache movie parsed
self.setCache(cache_key, movie_data)
return movie_data return movie_data
@@ -183,26 +192,23 @@ class TheMovieDb(MovieProvider):
image_urls = [] image_urls = []
try: try:
for image in movie.get('images', {}).get(type, [])[1:5]:
# Full data
images = self.request('movie/%s/images' % movie.get('id'), return_key = type)
for image in images[1:5]:
image_urls.append(self.getImage(image, 'file', size)) image_urls.append(self.getImage(image, 'file', size))
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_urls return image_urls
def request(self, call = '', params = {}, return_key = None): def request(self, call = '', params = {}, return_key = None):
params = dict((k, v) for k, v in params.items() if v)
params = tryUrlencode(params) params = tryUrlencode(params)
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, cache_timeout = 0)
try: if data and return_key and data.get(return_key):
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) data = data.get(return_key)
return data return data
@@ -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
+14 -15
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
@@ -51,8 +51,8 @@ var SuggestList = new Class({
self.show(); self.show();
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
+3 -3
View File
@@ -1,10 +1,10 @@
from CodernityDB.tree_index import TreeBasedIndex from couchpotato.core.helpers.database import TreeBasedIndex
class NotificationIndex(TreeBasedIndex): class NotificationIndex(TreeBasedIndex):
_version = 1 _version = 1
custom_header = """from CodernityDB.tree_index import TreeBasedIndex custom_header = """from couchpotato.core.helpers.database import TreeBasedIndex
import time""" import time"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -22,7 +22,7 @@ import time"""
class NotificationUnreadIndex(TreeBasedIndex): class NotificationUnreadIndex(TreeBasedIndex):
_version = 1 _version = 1
custom_header = """from CodernityDB.tree_index import TreeBasedIndex custom_header = """from couchpotato.core.helpers.database import TreeBasedIndex
import time""" import time"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
+14 -14
View File
@@ -3,18 +3,17 @@ import threading
import time import time
import traceback import traceback
import uuid import uuid
from CodernityDB.database import RecordDeleted
from couchpotato import get_db from couchpotato import get_db
from couchpotato.api import addApiView, addNonBlockApiView from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.database import RecordDeleted
from couchpotato.core.helpers.encoding import toUnicode 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.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': '',
+6 -41
View File
@@ -1,5 +1,5 @@
from datetime import timedelta, datetime from datetime import timedelta, datetime
from urlparse import urlparse from six.moves import urllib
import traceback import traceback
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
@@ -35,46 +35,11 @@ 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)
else: else:
@@ -141,7 +106,7 @@ class PlexServer(object):
def createHost(self, host, port = None): def createHost(self, host, port = None):
h = cleanHost(host) h = cleanHost(host)
p = urlparse(h) p = urllib.urlparse(h)
h = h.rstrip('/') h = h.rstrip('/')
if port and not p.port: if port and not p.port:
@@ -1,8 +1,16 @@
from .main import Twitter from six import PY3
try:
from .main import Twitter
def autoload(): def autoload():
return Twitter() return Twitter()
except:
if PY3:
from couchpotato.core.helpers.py3 import NotSupported
raise NotSupported
else:
raise
config = [{ config = [{
'name': 'twitter', 'name': 'twitter',
@@ -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.',
}
]
}
]
}]
+10 -2
View File
@@ -3,12 +3,20 @@ import traceback
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import xmpp from six import PY3
log = CPLog(__name__) log = CPLog(__name__)
autoload = 'Xmpp' try:
import xmpp
autoload = 'Xmpp'
except:
if PY3:
from couchpotato.core.helpers.py3 import NotSupported
raise NotSupported
else:
raise
class Xmpp(Notification): class Xmpp(Notification):

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