Compare commits
212 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ed4791bf | ||
|
|
adb744a526 | ||
|
|
0f82cda811 | ||
|
|
0d6c3c8ecb | ||
|
|
6598f53fd4 | ||
|
|
6b8458d87f | ||
|
|
99a0621238 | ||
|
|
c52666309a | ||
|
|
84a458d40b | ||
|
|
f8631c6d53 | ||
|
|
b19b0775c7 | ||
|
|
2dc1c1dd38 | ||
|
|
7db8b233c8 | ||
|
|
427c77a9ef | ||
|
|
94c3969f10 | ||
|
|
debd1855dd | ||
|
|
9f77597c11 | ||
|
|
afc9039625 | ||
|
|
920d3cb44e | ||
|
|
b1fc8ad862 | ||
|
|
11b9bc39ab | ||
|
|
6dcb3f3bf2 | ||
|
|
ce768f45c5 | ||
|
|
9b91d1d6c0 | ||
|
|
d9c7a97604 | ||
|
|
0fd01aa697 | ||
|
|
58615e6f9b | ||
|
|
2277322e57 | ||
|
|
18020e609e | ||
|
|
6a31b920ac | ||
|
|
c1266a36e4 | ||
|
|
578effc538 | ||
|
|
d881120013 | ||
|
|
da5318033a | ||
|
|
31df5bce01 | ||
|
|
d5622b7cba | ||
|
|
26ad1b354f | ||
|
|
7a616a81f7 | ||
|
|
275aefc3cc | ||
|
|
2b32490f72 | ||
|
|
7b9043c16b | ||
|
|
cf83f99be0 | ||
|
|
fb8a66d207 | ||
|
|
e8a3645bc6 | ||
|
|
592e40993c | ||
|
|
b00e69e222 | ||
|
|
c9b4c8167f | ||
|
|
cdb9cfe756 | ||
|
|
e52f50b204 | ||
|
|
770c2be14c | ||
|
|
ab61961a64 | ||
|
|
6aca799bbb | ||
|
|
89836be1d1 | ||
|
|
20e1283627 | ||
|
|
ee8406e026 | ||
|
|
514941b785 | ||
|
|
1510e37652 | ||
|
|
e1e39cd3f4 | ||
|
|
e1bb8c5419 | ||
|
|
17fa33a496 | ||
|
|
601f0b54cf | ||
|
|
51d44bfc3e | ||
|
|
12148217a2 | ||
|
|
132fa12ef4 | ||
|
|
1827c2e4cd | ||
|
|
f423bca06b | ||
|
|
e7b089edf5 | ||
|
|
b8b7d94a6a | ||
|
|
2c080fec3d | ||
|
|
4c68566c77 | ||
|
|
a3af784c18 | ||
|
|
ac6f295c93 | ||
|
|
2c72cd7d9f | ||
|
|
d012dc5c85 | ||
|
|
038b4c63ee | ||
|
|
17e37996c4 | ||
|
|
9318e19347 | ||
|
|
045c8f4dc8 | ||
|
|
02e25a9e25 | ||
|
|
819f619297 | ||
|
|
c303789817 | ||
|
|
8f4e03d04b | ||
|
|
229d67c086 | ||
|
|
d84897ff33 | ||
|
|
387a711538 | ||
|
|
7a1b914824 | ||
|
|
5e62801666 | ||
|
|
00d887153f | ||
|
|
1a2d79f719 | ||
|
|
6d5882001a | ||
|
|
4a6b45c65c | ||
|
|
b0d1fe5c33 | ||
|
|
a6e49098c8 | ||
|
|
ffcd36cbf4 | ||
|
|
3bf2d844a0 | ||
|
|
dd24eb8893 | ||
|
|
ac382d5131 | ||
|
|
abc9e78027 | ||
|
|
538f51dd5b | ||
|
|
c94d79cc6c | ||
|
|
9883a7a85a | ||
|
|
eea9f40501 | ||
|
|
576bcb9f4b | ||
|
|
f4a486c47b | ||
|
|
80cf144e8b | ||
|
|
cf5a774313 | ||
|
|
b9b77042dc | ||
|
|
9e96aa14b7 | ||
|
|
6a0220b496 | ||
|
|
02ff0acc64 | ||
|
|
ae6affdb52 | ||
|
|
a08df704be | ||
|
|
af9a47d528 | ||
|
|
62c5365329 | ||
|
|
ddf575a86e | ||
|
|
0155c8de2d | ||
|
|
6b9383ce92 | ||
|
|
cb8d24ef1f | ||
|
|
5bfdb121df | ||
|
|
814ddfb79f | ||
|
|
766f819c0b | ||
|
|
b8b6024592 | ||
|
|
d77cfb3e69 | ||
|
|
858d8b4291 | ||
|
|
3852fc720d | ||
|
|
5145618c39 | ||
|
|
d6cfcae45b | ||
|
|
5609536f46 | ||
|
|
f992c00eb7 | ||
|
|
87086a0336 | ||
|
|
62cb57f217 | ||
|
|
2a0e46fe00 | ||
|
|
1f7555e8fd | ||
|
|
ff43df9ef1 | ||
|
|
2e907e93e7 | ||
|
|
4d329d6a36 | ||
|
|
752191bc23 | ||
|
|
1d73fd9d7e | ||
|
|
79688c412a | ||
|
|
fc1c95fefb | ||
|
|
6a174716af | ||
|
|
defe256f1b | ||
|
|
8a5f154d9e | ||
|
|
fe56a69e8f | ||
|
|
c6d326f973 | ||
|
|
9e5f670feb | ||
|
|
9ebacf8816 | ||
|
|
df2d7ec9c2 | ||
|
|
ddab74582b | ||
|
|
2801079bc8 | ||
|
|
1deb49b524 | ||
|
|
49d550f652 | ||
|
|
1a43ce6ecc | ||
|
|
15a0131587 | ||
|
|
0dca34958c | ||
|
|
4b231e36ea | ||
|
|
52478a00db | ||
|
|
e177766270 | ||
|
|
ff8da7c8f8 | ||
|
|
89c8c5a0c7 | ||
|
|
38c6266f9c | ||
|
|
16f8e7e123 | ||
|
|
7110c7a11f | ||
|
|
6d79f316a6 | ||
|
|
c1b6811b8a | ||
|
|
7d7b76b2e9 | ||
|
|
657aa52fa7 | ||
|
|
8e9ef8db39 | ||
|
|
92a0096b54 | ||
|
|
87338760ad | ||
|
|
28019b0a09 | ||
|
|
248b007f4a | ||
|
|
9e31c59de8 | ||
|
|
269e785888 | ||
|
|
3669aef42d | ||
|
|
1087eb3a06 | ||
|
|
43af80a137 | ||
|
|
0766a27a71 | ||
|
|
a12f049d14 | ||
|
|
6afe2fd9cf | ||
|
|
61f634a21e | ||
|
|
d626fda710 | ||
|
|
b40d1f3463 | ||
|
|
1030d0d748 | ||
|
|
2e52c8124a | ||
|
|
8de5fcdac6 | ||
|
|
4aa9801be4 | ||
|
|
3e58378490 | ||
|
|
2c40db3074 | ||
|
|
fba228fd9d | ||
|
|
ef2b8e88b4 | ||
|
|
c77b270fa8 | ||
|
|
872a4f4650 | ||
|
|
d0f1e7c6a3 | ||
|
|
53e7e383a3 | ||
|
|
c06e1f3135 | ||
|
|
bb73cb8eec | ||
|
|
5acab98025 | ||
|
|
ed6a46e9c0 | ||
|
|
4291e2233d | ||
|
|
6ccbad031f | ||
|
|
d1dfed2833 | ||
|
|
3986de4ebc | ||
|
|
d80fe99609 | ||
|
|
43b6e3ac07 | ||
|
|
58acd53a9a | ||
|
|
05a97a19ab | ||
|
|
db23f5cdef | ||
|
|
85163443e3 | ||
|
|
6ea49405f4 | ||
|
|
4776cef473 | ||
|
|
e8fe9da602 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@
|
||||
/_source/
|
||||
.project
|
||||
.pydevproject
|
||||
node_modules
|
||||
.tmp
|
||||
121
Gruntfile.js
Normal file
121
Gruntfile.js
Normal file
@@ -0,0 +1,121 @@
|
||||
'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
config.rb
Normal file
45
config.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
# 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} : {}
|
||||
@@ -40,6 +40,8 @@ class WebHandler(BaseHandler):
|
||||
return
|
||||
|
||||
try:
|
||||
if route == 'robots.txt':
|
||||
self.set_header('Content-Type', 'text/plain')
|
||||
self.write(views[route]())
|
||||
except:
|
||||
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
|
||||
@@ -60,6 +62,13 @@ def index():
|
||||
addView('', index)
|
||||
|
||||
|
||||
# Web view
|
||||
def robots():
|
||||
return 'User-agent: * \n' \
|
||||
'Disallow: /'
|
||||
addView('robots.txt', robots)
|
||||
|
||||
|
||||
# API docs
|
||||
def apiDocs():
|
||||
routes = list(api.keys())
|
||||
|
||||
@@ -7,6 +7,7 @@ import urllib
|
||||
|
||||
from couchpotato.core.helpers.request import getParams
|
||||
from couchpotato.core.logger import CPLog
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.web import RequestHandler, asynchronous
|
||||
|
||||
|
||||
@@ -50,24 +51,22 @@ class NonBlockHandler(RequestHandler):
|
||||
start, stop = api_nonblock[route]
|
||||
self.stopper = stop
|
||||
|
||||
start(self.onNewMessage, last_id = self.get_argument('last_id', None))
|
||||
start(self.sendData, last_id = self.get_argument('last_id', None))
|
||||
|
||||
def onNewMessage(self, response):
|
||||
if self.request.connection.stream.closed():
|
||||
self.on_connection_close()
|
||||
return
|
||||
def sendData(self, response):
|
||||
if not self.request.connection.stream.closed():
|
||||
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
|
||||
|
||||
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):
|
||||
self.removeStopper()
|
||||
|
||||
def removeStopper(self):
|
||||
if self.stopper:
|
||||
self.stopper(self.onNewMessage)
|
||||
self.stopper(self.sendData)
|
||||
|
||||
self.stopper = None
|
||||
|
||||
@@ -83,10 +82,11 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
|
||||
# Blocking API handler
|
||||
class ApiHandler(RequestHandler):
|
||||
route = None
|
||||
|
||||
@asynchronous
|
||||
def get(self, route, *args, **kwargs):
|
||||
route = route.strip('/')
|
||||
self.route = route = route.strip('/')
|
||||
if not api.get(route):
|
||||
self.write('API call doesn\'t seem to exist')
|
||||
self.finish()
|
||||
@@ -123,11 +123,15 @@ class ApiHandler(RequestHandler):
|
||||
except:
|
||||
log.error('Failed write error "%s": %s', (route, traceback.format_exc()))
|
||||
|
||||
api_locks[route].release()
|
||||
self.unlock()
|
||||
|
||||
post = get
|
||||
|
||||
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():
|
||||
try:
|
||||
@@ -135,14 +139,12 @@ class ApiHandler(RequestHandler):
|
||||
jsonp_callback = self.get_argument('callback_func', default = None)
|
||||
|
||||
if jsonp_callback:
|
||||
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
||||
self.set_header("Content-Type", "text/javascript")
|
||||
self.finish()
|
||||
self.set_header('Content-Type', 'text/javascript')
|
||||
self.finish(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
||||
elif isinstance(result, tuple) and result[0] == 'redirect':
|
||||
self.redirect(result[1])
|
||||
else:
|
||||
self.write(result)
|
||||
self.finish()
|
||||
self.finish(result)
|
||||
except UnicodeDecodeError:
|
||||
log.error('Failed proper encode: %s', traceback.format_exc())
|
||||
except:
|
||||
@@ -150,7 +152,9 @@ class ApiHandler(RequestHandler):
|
||||
try: self.finish({'success': False, 'error': 'Failed returning results'})
|
||||
except: pass
|
||||
|
||||
api_locks[route].release()
|
||||
def unlock(self):
|
||||
try: api_locks[self.route].release()
|
||||
except: pass
|
||||
|
||||
|
||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
@@ -8,8 +7,6 @@ from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from minify.cssmin import cssmin
|
||||
from minify.jsmin import jsmin
|
||||
from tornado.web import StaticFileHandler
|
||||
|
||||
|
||||
@@ -22,30 +19,26 @@ class ClientScript(Plugin):
|
||||
|
||||
core_static = {
|
||||
'style': [
|
||||
'style/main.css',
|
||||
'style/uniform.generic.css',
|
||||
'style/uniform.css',
|
||||
'style/settings.css',
|
||||
'style/combined.min.css',
|
||||
],
|
||||
'script': [
|
||||
'scripts/library/mootools.js',
|
||||
'scripts/library/mootools_more.js',
|
||||
'scripts/vendor/mootools.js',
|
||||
'scripts/vendor/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/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/scrollspy.js',
|
||||
'scripts/library/spin.js',
|
||||
'scripts/library/Array.stableSort.js',
|
||||
'scripts/library/async.js',
|
||||
'scripts/couchpotato.js',
|
||||
'scripts/api.js',
|
||||
'scripts/library/history.js',
|
||||
'scripts/page.js',
|
||||
'scripts/block.js',
|
||||
'scripts/block/navigation.js',
|
||||
'scripts/block/header.js',
|
||||
'scripts/block/footer.js',
|
||||
'scripts/block/menu.js',
|
||||
'scripts/page/home.js',
|
||||
@@ -54,8 +47,9 @@ class ClientScript(Plugin):
|
||||
],
|
||||
}
|
||||
|
||||
urls = {'style': {}, 'script': {}}
|
||||
minified = {'style': {}, 'script': {}}
|
||||
watches = {}
|
||||
|
||||
original_paths = {'style': {}, 'script': {}}
|
||||
paths = {'style': {}, 'script': {}}
|
||||
comment = {
|
||||
'style': '/*** %s:%d ***/\n',
|
||||
@@ -74,8 +68,7 @@ class ClientScript(Plugin):
|
||||
addEvent('clientscript.get_styles', self.getStyles)
|
||||
addEvent('clientscript.get_scripts', self.getScripts)
|
||||
|
||||
if not Env.get('dev'):
|
||||
addEvent('app.load', self.minify)
|
||||
addEvent('app.load', self.compile)
|
||||
|
||||
self.addCore()
|
||||
|
||||
@@ -91,7 +84,7 @@ class ClientScript(Plugin):
|
||||
else:
|
||||
self.registerStyle(core_url, file_path, position = 'front')
|
||||
|
||||
def minify(self):
|
||||
def compile(self):
|
||||
|
||||
# Create cache dir
|
||||
cache = Env.get('cache_dir')
|
||||
@@ -102,47 +95,43 @@ class ClientScript(Plugin):
|
||||
|
||||
for file_type in ['style', 'script']:
|
||||
ext = 'js' if file_type is 'script' else 'css'
|
||||
positions = self.paths.get(file_type, {})
|
||||
positions = self.original_paths.get(file_type, {})
|
||||
for position in positions:
|
||||
files = positions.get(position)
|
||||
self._minify(file_type, files, position, position + '.' + ext)
|
||||
self._compile(file_type, files, position, position + '.' + ext)
|
||||
|
||||
def _minify(self, file_type, files, position, out):
|
||||
def _compile(self, file_type, paths, position, out):
|
||||
|
||||
cache = Env.get('cache_dir')
|
||||
out_name = out
|
||||
out = os.path.join(cache, 'minified', out_name)
|
||||
minified_dir = os.path.join(cache, 'minified')
|
||||
|
||||
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()
|
||||
|
||||
if file_type == 'script':
|
||||
data = jsmin(f)
|
||||
else:
|
||||
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
|
||||
if not Env.get('dev'):
|
||||
data = f
|
||||
|
||||
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
|
||||
data_combined += self.comment.get(file_type) % (ss(file_path), int(os.path.getmtime(file_path)))
|
||||
data_combined += data + '\n\n'
|
||||
else:
|
||||
new_paths.append(x)
|
||||
|
||||
# Combine all files together with some comments
|
||||
data = ''
|
||||
for r in raw:
|
||||
data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
|
||||
data += r.get('data') + '\n\n'
|
||||
if not Env.get('dev'):
|
||||
|
||||
self.createFile(out, data.strip())
|
||||
out_path = os.path.join(minified_dir, out_name)
|
||||
self.createFile(out_path, data_combined.strip())
|
||||
|
||||
if not self.minified.get(file_type):
|
||||
self.minified[file_type] = {}
|
||||
if not self.minified[file_type].get(position):
|
||||
self.minified[file_type][position] = []
|
||||
minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out)))
|
||||
new_paths.append((out_path, {'url': minified_url}))
|
||||
|
||||
minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out)))
|
||||
self.minified[file_type][position].append(minified_url)
|
||||
self.paths[file_type][position] = new_paths
|
||||
|
||||
def getStyles(self, *args, **kwargs):
|
||||
return self.get('style', *args, **kwargs)
|
||||
@@ -150,22 +139,12 @@ class ClientScript(Plugin):
|
||||
def getScripts(self, *args, **kwargs):
|
||||
return self.get('script', *args, **kwargs)
|
||||
|
||||
def get(self, type, as_html = False, location = 'head'):
|
||||
def get(self, type, 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]
|
||||
|
||||
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
|
||||
return []
|
||||
|
||||
def registerStyle(self, api_path, file_path, position = 'head'):
|
||||
self.register(api_path, file_path, 'style', position)
|
||||
@@ -177,36 +156,10 @@ class ClientScript(Plugin):
|
||||
|
||||
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
|
||||
|
||||
if not self.urls[type].get(location):
|
||||
self.urls[type][location] = []
|
||||
self.urls[type][location].append(api_path)
|
||||
if not self.original_paths[type].get(location):
|
||||
self.original_paths[type][location] = []
|
||||
self.original_paths[type][location].append((file_path, api_path))
|
||||
|
||||
if not self.paths[type].get(location):
|
||||
self.paths[type][location] = []
|
||||
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
|
||||
self.paths[type][location].append((file_path, api_path))
|
||||
|
||||
@@ -16,8 +16,8 @@ var DownloadersBase = new Class({
|
||||
|
||||
var setting_page = App.getPage('Settings');
|
||||
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){
|
||||
message = new Element('span.success', {
|
||||
'text': 'Connection successful'
|
||||
}).inject(button, 'after')
|
||||
}).inject(button, 'after');
|
||||
}
|
||||
else {
|
||||
var msg_text = 'Connection failed. Check logs for details.';
|
||||
if(json.hasOwnProperty('msg')) msg_text = json.msg;
|
||||
message = new Element('span.failed', {
|
||||
'text': msg_text
|
||||
}).inject(button, 'after')
|
||||
}).inject(button, 'after');
|
||||
}
|
||||
|
||||
(function(){
|
||||
message.destroy();
|
||||
}).delay(3000)
|
||||
}).delay(3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ var UpdaterBase = new Class({
|
||||
App.trigger('message', ['No updates available']);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
@@ -50,8 +50,8 @@ var UpdaterBase = new Class({
|
||||
self.message.destroy();
|
||||
}
|
||||
}
|
||||
})
|
||||
}, (timeout || 0))
|
||||
});
|
||||
}, (timeout || 0));
|
||||
|
||||
},
|
||||
|
||||
@@ -84,7 +84,7 @@ var UpdaterBase = new Class({
|
||||
'click': self.doUpdate.bind(self)
|
||||
}
|
||||
})
|
||||
).inject(document.body)
|
||||
).inject(document.body);
|
||||
},
|
||||
|
||||
doUpdate: function(){
|
||||
@@ -96,7 +96,7 @@ var UpdaterBase = new Class({
|
||||
if(json.success)
|
||||
self.updating();
|
||||
else
|
||||
App.unBlockPage()
|
||||
App.unBlockPage();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -20,14 +20,31 @@ class Blackhole(DownloaderBase):
|
||||
status_support = False
|
||||
|
||||
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 = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
|
||||
# The folder needs to exist
|
||||
if not directory or not os.path.isdir(directory):
|
||||
log.error('No directory set for blackhole %s download.', data.get('protocol'))
|
||||
else:
|
||||
try:
|
||||
# Filedata can be empty, which probably means it a magnet link
|
||||
if not filedata or len(filedata) < 50:
|
||||
try:
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
@@ -36,13 +53,16 @@ class Blackhole(DownloaderBase):
|
||||
except:
|
||||
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:
|
||||
log.error('No nzb/torrent available: %s', data.get('url'))
|
||||
return False
|
||||
|
||||
# Create filename with imdb id and other nice stuff
|
||||
file_name = self.createFileName(data, filedata, media)
|
||||
full_path = os.path.join(directory, file_name)
|
||||
|
||||
# People want thinks nice and tidy, create a subdir
|
||||
if self.conf('create_subdir'):
|
||||
try:
|
||||
new_path = os.path.splitext(full_path)[0]
|
||||
@@ -53,6 +73,8 @@ class Blackhole(DownloaderBase):
|
||||
log.error('Couldnt create sub dir, reverting to old one: %s', full_path)
|
||||
|
||||
try:
|
||||
|
||||
# Make sure the file doesn't exist yet, no need in overwriting it
|
||||
if not os.path.isfile(full_path):
|
||||
log.info('Downloading %s to %s.', (data.get('protocol'), full_path))
|
||||
with open(full_path, 'wb') as f:
|
||||
@@ -74,6 +96,10 @@ class Blackhole(DownloaderBase):
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
""" Test and see if the directory is writable
|
||||
:return: boolean
|
||||
"""
|
||||
|
||||
directory = self.conf('directory')
|
||||
if directory and os.path.isdir(directory):
|
||||
|
||||
@@ -88,6 +114,10 @@ class Blackhole(DownloaderBase):
|
||||
return False
|
||||
|
||||
def getEnabledProtocol(self):
|
||||
""" What protocols is this downloaded used for
|
||||
:return: list with protocols
|
||||
"""
|
||||
|
||||
if self.conf('use_for') == 'both':
|
||||
return super(Blackhole, self).getEnabledProtocol()
|
||||
elif self.conf('use_for') == 'torrent':
|
||||
@@ -96,6 +126,12 @@ class Blackhole(DownloaderBase):
|
||||
return ['nzb']
|
||||
|
||||
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 = {}
|
||||
for_protocol = ['both']
|
||||
if data and 'torrent' in data.get('protocol'):
|
||||
|
||||
@@ -25,6 +25,11 @@ class Deluge(DownloaderBase):
|
||||
drpc = None
|
||||
|
||||
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.
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
|
||||
@@ -42,6 +47,20 @@ class Deluge(DownloaderBase):
|
||||
return self.drpc
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -96,11 +115,21 @@ class Deluge(DownloaderBase):
|
||||
return self.downloadReturnId(remote_torrent)
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
if self.connect(True) and self.drpc.test():
|
||||
return True
|
||||
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 Deluge download status.')
|
||||
|
||||
|
||||
427
couchpotato/core/downloaders/hadouken.py
Normal file
427
couchpotato/core/downloaders/hadouken.py
Normal file
@@ -0,0 +1,427 @@
|
||||
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.'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -23,6 +23,20 @@ class NZBGet(DownloaderBase):
|
||||
rpc = 'xmlrpc'
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -71,6 +85,10 @@ class NZBGet(DownloaderBase):
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
rpc = self.getRPC()
|
||||
|
||||
try:
|
||||
@@ -91,6 +109,13 @@ class NZBGet(DownloaderBase):
|
||||
return True
|
||||
|
||||
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.')
|
||||
|
||||
@@ -163,12 +188,12 @@ class NZBGet(DownloaderBase):
|
||||
nzb_id = nzb['NZBID']
|
||||
|
||||
if nzb_id in ids:
|
||||
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
|
||||
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']))
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
|
||||
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
|
||||
'status': 'completed' if 'SUCCESS' in nzb['Status'] else 'failed',
|
||||
'original_status': nzb['Status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(nzb['DestDir'])
|
||||
})
|
||||
|
||||
@@ -24,6 +24,20 @@ class NZBVortex(DownloaderBase):
|
||||
session_id = 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 data: data = {}
|
||||
|
||||
@@ -45,6 +59,10 @@ class NZBVortex(DownloaderBase):
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
try:
|
||||
login_result = self.login()
|
||||
except:
|
||||
@@ -53,6 +71,13 @@ class NZBVortex(DownloaderBase):
|
||||
return login_result
|
||||
|
||||
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')
|
||||
|
||||
|
||||
@@ -19,6 +19,20 @@ class Pneumatic(DownloaderBase):
|
||||
status_support = False
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -63,6 +77,10 @@ class Pneumatic(DownloaderBase):
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
directory = self.conf('directory')
|
||||
if directory and os.path.isdir(directory):
|
||||
|
||||
|
||||
68
couchpotato/core/downloaders/putio/__init__.py
Normal file
68
couchpotato/core/downloaders/putio/__init__.py
Normal file
@@ -0,0 +1,68 @@
|
||||
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
couchpotato/core/downloaders/putio/main.py
Normal file
181
couchpotato/core/downloaders/putio/main.py
Normal file
@@ -0,0 +1,181 @@
|
||||
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,
|
||||
}
|
||||
|
||||
68
couchpotato/core/downloaders/putio/static/putio.js
Normal file
68
couchpotato/core/downloaders/putio/static/putio.js
Normal file
@@ -0,0 +1,68 @@
|
||||
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,12 +41,30 @@ class qBittorrent(DownloaderBase):
|
||||
return self.qb
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
if self.connect():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -95,6 +113,14 @@ class qBittorrent(DownloaderBase):
|
||||
return 'busy'
|
||||
|
||||
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.')
|
||||
|
||||
if not self.connect():
|
||||
|
||||
@@ -84,6 +84,10 @@ class rTorrent(DownloaderBase):
|
||||
return self.rt
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
if self.connect(True):
|
||||
return True
|
||||
|
||||
@@ -94,6 +98,20 @@ class rTorrent(DownloaderBase):
|
||||
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -161,6 +179,14 @@ class rTorrent(DownloaderBase):
|
||||
return 'completed'
|
||||
|
||||
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.')
|
||||
|
||||
if not self.connect():
|
||||
|
||||
@@ -21,6 +21,21 @@ class Sabnzbd(DownloaderBase):
|
||||
protocol = ['nzb']
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -69,6 +84,11 @@ class Sabnzbd(DownloaderBase):
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
Return message if an old version of SAB is used
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
try:
|
||||
sab_data = self.call({
|
||||
'mode': 'version',
|
||||
@@ -89,6 +109,13 @@ class Sabnzbd(DownloaderBase):
|
||||
return True
|
||||
|
||||
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.')
|
||||
|
||||
|
||||
@@ -19,6 +19,21 @@ class Synology(DownloaderBase):
|
||||
status_support = False
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -50,6 +65,10 @@ class Synology(DownloaderBase):
|
||||
return self.downloadReturnId('') if response else False
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
try:
|
||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||
@@ -118,7 +137,7 @@ class SynologyRPC(object):
|
||||
def _req(self, url, args, files = None):
|
||||
response = {'success': False}
|
||||
try:
|
||||
req = requests.post(url, data = args, files = files)
|
||||
req = requests.post(url, data = args, files = files, verify = False)
|
||||
req.raise_for_status()
|
||||
response = json.loads(req.text)
|
||||
if response['success']:
|
||||
|
||||
@@ -34,6 +34,21 @@ class Transmission(DownloaderBase):
|
||||
return self.trpc
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -88,11 +103,22 @@ class Transmission(DownloaderBase):
|
||||
return self.downloadReturnId(data['hashString'])
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
if self.connect() and self.trpc.get_session():
|
||||
return True
|
||||
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 Transmission download status.')
|
||||
|
||||
@@ -121,6 +147,8 @@ class Transmission(DownloaderBase):
|
||||
status = 'failed'
|
||||
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
|
||||
status = 'completed'
|
||||
elif torrent['status'] == 16 and torrent['percentDone'] == 1:
|
||||
status = 'completed'
|
||||
elif torrent['status'] in [5, 6]:
|
||||
status = 'seeding'
|
||||
|
||||
|
||||
@@ -51,6 +51,21 @@ class uTorrent(DownloaderBase):
|
||||
return self.utorrent_api
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -120,6 +135,10 @@ class uTorrent(DownloaderBase):
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
if self.connect():
|
||||
build_version = self.utorrent_api.get_build()
|
||||
if not build_version:
|
||||
@@ -131,6 +150,13 @@ class uTorrent(DownloaderBase):
|
||||
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 uTorrent download status.')
|
||||
|
||||
|
||||
@@ -37,15 +37,18 @@ def toUnicode(original, *args):
|
||||
except:
|
||||
try:
|
||||
detected = detect(original)
|
||||
if detected.get('encoding') == 'utf-8':
|
||||
return original.decode('utf-8')
|
||||
try:
|
||||
if detected.get('confidence') > 0.8:
|
||||
return original.decode(detected.get('encoding'))
|
||||
except:
|
||||
pass
|
||||
|
||||
return ek(original, *args)
|
||||
except:
|
||||
raise
|
||||
except:
|
||||
log.error('Unable to decode value "%s..." : %s ', (repr(original)[:20], traceback.format_exc()))
|
||||
ascii_text = str(original).encode('string_escape')
|
||||
return toUnicode(ascii_text)
|
||||
return 'ERROR DECODING STRING'
|
||||
|
||||
|
||||
def ss(original, *args):
|
||||
@@ -92,7 +95,7 @@ def ek(original, *args):
|
||||
if isinstance(original, (str, unicode)):
|
||||
try:
|
||||
from couchpotato.environment import Env
|
||||
return original.decode(Env.get('encoding'))
|
||||
return original.decode(Env.get('encoding'), 'ignore')
|
||||
except UnicodeDecodeError:
|
||||
raise
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from couchpotato import CPLog
|
||||
from couchpotato import CPLog, md5
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import getExt
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import six
|
||||
|
||||
@@ -92,7 +93,15 @@ class MediaBase(Plugin):
|
||||
if not isinstance(image, (str, unicode)):
|
||||
continue
|
||||
|
||||
if file_type not in existing_files or len(existing_files.get(file_type, [])) == 0:
|
||||
# Check if it has top image
|
||||
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)
|
||||
if file_path:
|
||||
existing_files[file_type] = [toUnicode(file_path)]
|
||||
|
||||
@@ -273,6 +273,10 @@ class MediaPlugin(MediaBase):
|
||||
for x in filter_by:
|
||||
media_ids = [n for n in media_ids if n in filter_by[x]]
|
||||
|
||||
total_count = len(media_ids)
|
||||
if total_count == 0:
|
||||
return 0, []
|
||||
|
||||
offset = 0
|
||||
limit = -1
|
||||
if limit_offset:
|
||||
@@ -302,30 +306,11 @@ class MediaPlugin(MediaBase):
|
||||
media_ids.remove(media_id)
|
||||
if len(media_ids) == 0 or len(medias) == limit: break
|
||||
|
||||
# Sort media by type and return result
|
||||
result = {}
|
||||
|
||||
# Create keys for media types we are listing
|
||||
if types:
|
||||
for media_type in types:
|
||||
result['%ss' % media_type] = []
|
||||
else:
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
result['%ss' % media_type] = []
|
||||
|
||||
total_count = len(medias)
|
||||
|
||||
if total_count == 0:
|
||||
return 0, result
|
||||
|
||||
for kind in medias:
|
||||
result['%ss' % kind['type']].append(kind)
|
||||
|
||||
return total_count, result
|
||||
return total_count, medias
|
||||
|
||||
def listView(self, **kwargs):
|
||||
|
||||
total_count, result = self.list(
|
||||
total_movies, movies = self.list(
|
||||
types = splitString(kwargs.get('type')),
|
||||
status = splitString(kwargs.get('status')),
|
||||
release_status = splitString(kwargs.get('release_status')),
|
||||
@@ -336,12 +321,12 @@ class MediaPlugin(MediaBase):
|
||||
search = kwargs.get('search')
|
||||
)
|
||||
|
||||
results = result
|
||||
results['success'] = True
|
||||
results['empty'] = len(result) == 0
|
||||
results['total'] = total_count
|
||||
|
||||
return results
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
}
|
||||
|
||||
def addSingleListView(self):
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ class Provider(Plugin):
|
||||
try:
|
||||
data = XMLTree.fromstring(ss(data))
|
||||
return self.getElements(data, item_path)
|
||||
except XMLTree.ParseError:
|
||||
log.error('Invalid XML returned, check "%s" manually for issues', url)
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
|
||||
@@ -68,8 +68,12 @@ class Base(NZBProvider, RSS):
|
||||
if not date:
|
||||
date = self.getTextElement(nzb, 'pubDate')
|
||||
|
||||
nzb_id = self.getTextElement(nzb, 'guid').split('/')[-1:].pop()
|
||||
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:
|
||||
continue
|
||||
@@ -103,7 +107,7 @@ class Base(NZBProvider, RSS):
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
||||
'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
|
||||
'detail_url': (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id),
|
||||
'detail_url': detail_url,
|
||||
'content': self.getTextElement(nzb, 'description'),
|
||||
'description': description,
|
||||
'score': host['extra_score'],
|
||||
@@ -183,7 +187,7 @@ class Base(NZBProvider, RSS):
|
||||
return 'try_next'
|
||||
|
||||
try:
|
||||
data = self.urlopen(url, show_error = False)
|
||||
data = self.urlopen(url, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
|
||||
self.limits_reached[host] = False
|
||||
return data
|
||||
except HTTPError as e:
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
from urlparse import urlparse, parse_qs
|
||||
import time
|
||||
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
|
||||
from dateutil.parser import parse
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -16,27 +12,19 @@ log = CPLog(__name__)
|
||||
class Base(NZBProvider, RSS):
|
||||
|
||||
urls = {
|
||||
'search': 'https://rss.omgwtfnzbs.org/rss-search.php?%s',
|
||||
'detail_url': 'https://omgwtfnzbs.org/details.php?id=%s',
|
||||
'search': 'https://api.omgwtfnzbs.org/json/?%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
cat_ids = [
|
||||
([15], ['dvdrip']),
|
||||
([15], ['dvdrip', 'scr', 'r5', 'tc', 'ts', 'cam']),
|
||||
([15, 16], ['brrip']),
|
||||
([16], ['720p', '1080p', 'bd50']),
|
||||
([17], ['dvdr']),
|
||||
]
|
||||
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):
|
||||
|
||||
q = '%s %s' % (title, movie['info']['year'])
|
||||
@@ -47,22 +35,20 @@ class Base(NZBProvider, RSS):
|
||||
'api': self.conf('api_key', default = ''),
|
||||
})
|
||||
|
||||
nzbs = self.getRSSData(self.urls['search'] % params)
|
||||
nzbs = self.getJsonData(self.urls['search'] % params)
|
||||
|
||||
for nzb in nzbs:
|
||||
if isinstance(nzbs, list):
|
||||
for nzb in nzbs:
|
||||
|
||||
enclosure = self.getElement(nzb, 'enclosure').attrib
|
||||
nzb_id = parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0]
|
||||
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'name': toUnicode(self.getTextElement(nzb, 'title')),
|
||||
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, 'pubDate')).timetuple()))),
|
||||
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
||||
'url': enclosure['url'],
|
||||
'detail_url': self.urls['detail_url'] % nzb_id,
|
||||
'description': self.getTextElement(nzb, 'description')
|
||||
})
|
||||
results.append({
|
||||
'id': nzb.get('nzbid'),
|
||||
'name': toUnicode(nzb.get('release')),
|
||||
'age': self.calculateAge(tryInt(nzb.get('usenetage'))),
|
||||
'size': tryInt(nzb.get('sizebytes')) / 1024 / 1024,
|
||||
'url': nzb.get('getnzb'),
|
||||
'detail_url': nzb.get('details'),
|
||||
'description': nzb.get('weblink')
|
||||
})
|
||||
|
||||
|
||||
config = [{
|
||||
|
||||
@@ -13,11 +13,11 @@ log = CPLog(__name__)
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://www.bit-hdtv.com/',
|
||||
'login': 'http://www.bit-hdtv.com/takelogin.php',
|
||||
'login_check': 'http://www.bit-hdtv.com/messages.php',
|
||||
'detail': 'http://www.bit-hdtv.com/details.php?id=%s',
|
||||
'search': 'http://www.bit-hdtv.com/torrents.php?',
|
||||
'test': 'https://www.bit-hdtv.com/',
|
||||
'login': 'https://www.bit-hdtv.com/takelogin.php',
|
||||
'login_check': 'https://www.bit-hdtv.com/messages.php',
|
||||
'detail': 'https://www.bit-hdtv.com/details.php?id=%s',
|
||||
'search': 'https://www.bit-hdtv.com/torrents.php?',
|
||||
}
|
||||
|
||||
# Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken
|
||||
@@ -93,7 +93,7 @@ config = [{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'BiT-HDTV',
|
||||
'description': '<a href="http://bit-hdtv.com">BiT-HDTV</a>',
|
||||
'description': '<a href="https://bit-hdtv.com">BiT-HDTV</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABMklEQVR4AZ3Qu0ojcQCF8W9MJcQbJNgEEQUbQVIqWgnaWfkIvoCgggixEAmIhRtY2GV3w7KwU61B0EYIxmiw0YCik84ipaCuc0nmP5dcjIUgOjqDvxf4OAdf9mnMLcUJyPyGSCP+YRdC+Kp8iagJKhuS+InYRhTGgDbeV2uEMand4ZRxizjXHQEimxhraAnUr73BNqQxMiNeV2SwcjTLEVtb4Zl10mXutvOWm2otw5Sxz6TGTbdd6ncuYvVLXAXrvM+ruyBpy1S3JLGDfUQ1O6jn5vTsrJXvqSt4UNfj6vxTRPxBHER5QeSirhLGk/5rWN+ffB1XZuxjnDy1q87m7TS+xOGA+Iv4gfkbaw+nOMXHDHnITGEk0VfRFnn4Po4vNYm6RGukmggR0L08+l+e4HMeASo/i6AJUjLgAAAAAElFTkSuQmCC',
|
||||
'options': [
|
||||
|
||||
130
couchpotato/core/media/_base/providers/torrent/hdaccess.py
Normal file
130
couchpotato/core/media/_base/providers/torrent/hdaccess.py
Normal file
@@ -0,0 +1,130 @@
|
||||
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,6 +29,9 @@ class Base(TorrentProvider):
|
||||
}
|
||||
post_data.update(params)
|
||||
|
||||
if self.conf('internal_only'):
|
||||
post_data.update({'origin': [1]})
|
||||
|
||||
try:
|
||||
result = self.getJsonData(self.urls['api'], data = json.dumps(post_data))
|
||||
|
||||
@@ -110,6 +113,14 @@ config = [{
|
||||
'default': 0,
|
||||
'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):
|
||||
|
||||
urls = {
|
||||
'test': 'https://www.iptorrents.com/',
|
||||
'base_url': 'https://www.iptorrents.com',
|
||||
'login': 'https://www.iptorrents.com/torrents/',
|
||||
'login_check': 'https://www.iptorrents.com/inbox.php',
|
||||
'search': 'https://www.iptorrents.com/torrents/?%s%%s&q=%s&qf=ti&p=%%d',
|
||||
'test': 'https://iptorrents.eu/',
|
||||
'base_url': 'https://iptorrents.eu',
|
||||
'login': 'https://iptorrents.eu/torrents/',
|
||||
'login_check': 'https://iptorrents.eu/inbox.php',
|
||||
'search': 'https://iptorrents.eu/torrents/?%s%%s&q=%s&qf=ti&p=%%d',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
@@ -120,7 +120,7 @@ config = [{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'IPTorrents',
|
||||
'description': '<a href="http://www.iptorrents.com">IPTorrents</a>',
|
||||
'description': '<a href="https://iptorrents.eu">IPTorrents</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABRklEQVR42qWQO0vDUBiG8zeKY3EqQUtNO7g0J6ZJ1+ifKIIFQXAqDYKCyaaYxM3udrZLHdRFhXrZ6liCW6mubfk874EESgqaeOCF7/Y8hEh41aq6yZi2nyZgBGya9XKtZs4No05pAkZV2YbEmyMMsoSxLQeC46wCTdPPY4HruPQyGIhF97qLWsS78Miydn4XdK46NJ9OsQAYBzMIMf8MQ9wtCnTdWCaIDx/u7uljOIQEe0hiIWPamSTLay3+RxOCSPI9+RJAo7Er9r2bnqjBFAqyK+VyK4f5/Cr5ni8OFKVCz49PFI5GdNvvU7ttE1M1zMU+8AMqFksEhrMnQsBDzqmDAwzx2ehRLwT7yyCI+vSC99c3mozH1NxrJgWWtR1BOECfEJSVCm6WCzJGCA7+IWhBsM4zywDPwEp4vCjx2DzBH2ODAfsDb33Ps6dQwJgAAAAASUVORK5CYII=',
|
||||
'options': [
|
||||
|
||||
@@ -42,6 +42,7 @@ class Base(TorrentProvider):
|
||||
|
||||
link = result.find('td', attrs = {'class': 'ttr_name'}).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')
|
||||
torrent_id = link['href'].replace('details?id=', '')
|
||||
|
||||
@@ -51,7 +52,7 @@ class Base(TorrentProvider):
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'size': self.parseSize(result.find('td', attrs = {'class': 'ttr_size'}).contents[0]),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class': 'ttr_seeders'}).find('a').string),
|
||||
'seeders': tryInt(seeders.string) if seeders else 0,
|
||||
'leechers': tryInt(leechers.string) if leechers else 0,
|
||||
'get_more_info': self.getMoreInfo,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
@@ -56,11 +56,12 @@ class Base(TorrentProvider):
|
||||
|
||||
full_id = link['href'].replace('details.php?id=', '')
|
||||
torrent_id = full_id[:6]
|
||||
name = toUnicode(link.get('title', link.contents[0]).encode('ISO-8859-1')).strip()
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': link.contents[0],
|
||||
'url': self.urls['download'] % (torrent_id, link.contents[0]),
|
||||
'name': name,
|
||||
'url': self.urls['download'] % (torrent_id, name),
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]),
|
||||
'seeders': tryInt(cells[8].find('span').contents[0]),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
@@ -8,12 +9,12 @@ log = CPLog(__name__)
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://www.td.af/',
|
||||
'login': 'http://www.td.af/torrents/',
|
||||
'login_check': 'http://www.torrentday.com/userdetails.php',
|
||||
'detail': 'http://www.td.af/details.php?id=%s',
|
||||
'search': 'http://www.td.af/V3/API/API.php',
|
||||
'download': 'http://www.td.af/download.php/%s/%s',
|
||||
'test': 'https://torrentday.eu/',
|
||||
'login': 'https://torrentday.eu/torrents/',
|
||||
'login_check': 'https://torrentday.eu/userdetails.php',
|
||||
'detail': 'https://torrentday.eu/details.php?id=%s',
|
||||
'search': 'https://torrentday.eu/V3/API/API.php',
|
||||
'download': 'https://torrentday.eu/download.php/%s/%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
@@ -55,6 +56,10 @@ class Base(TorrentProvider):
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
def loginCheckSuccess(self, output):
|
||||
@@ -68,7 +73,7 @@ config = [{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentDay',
|
||||
'description': '<a href="http://www.td.af/">TorrentDay</a>',
|
||||
'description': '<a href="https://torrentday.eu/">TorrentDay</a>',
|
||||
'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=',
|
||||
'options': [
|
||||
|
||||
126
couchpotato/core/media/_base/providers/torrent/torrentleech.py
Normal file
126
couchpotato/core/media/_base/providers/torrent/torrentleech.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://www.torrentleech.org/',
|
||||
'login': 'https://www.torrentleech.org/user/account/login/',
|
||||
'login_check': 'https://torrentleech.org/user/messages',
|
||||
'detail': 'https://www.torrentleech.org/torrent/%s',
|
||||
'search': 'https://www.torrentleech.org/torrents/browse/index/query/%s/categories/%s',
|
||||
'download': 'https://www.torrentleech.org%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
cat_backup_id = None
|
||||
|
||||
def _searchOnTitle(self, title, media, quality, results):
|
||||
|
||||
url = self.urls['search'] % self.buildUrl(title, media, quality)
|
||||
|
||||
data = self.getHTMLData(url)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'id': 'torrenttable'})
|
||||
if not result_table:
|
||||
return
|
||||
|
||||
entries = result_table.find_all('tr')
|
||||
|
||||
for result in entries[1:]:
|
||||
|
||||
link = result.find('td', attrs = {'class': 'name'}).find('a')
|
||||
url = result.find('td', attrs = {'class': 'quickdownload'}).find('a')
|
||||
details = result.find('td', attrs = {'class': 'name'}).find('a')
|
||||
|
||||
results.append({
|
||||
'id': link['href'].replace('/torrent/', ''),
|
||||
'name': six.text_type(link.string),
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % details['href'],
|
||||
'size': self.parseSize(result.find_all('td')[4].string),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class': 'seeders'}).string),
|
||||
'leechers': tryInt(result.find('td', attrs = {'class': 'leechers'}).string),
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return {
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'remember_me': 'on',
|
||||
'login': 'submit',
|
||||
}
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'torrentleech',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentLeech',
|
||||
'description': '<a href="http://torrentleech.org">TorrentLeech</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACHUlEQVR4AZVSO48SYRSdGTCBEMKzILLAWiybkKAGMZRUUJEoDZX7B9zsbuQPYEEjNLTQkYgJDwsoSaxspEBsCITXjjNAIKi8AkzceXgmbHQ1NJ5iMufmO9/9zrmXlCSJ+B8o75J8Pp/NZj0eTzweBy0Wi4PBYD6f12o1r9ebTCZx+22HcrnMsuxms7m6urTZ7LPZDMVYLBZ8ZV3yo8aq9Pq0wzCMTqe77dDv9y8uLyAWBH6xWOyL0K/56fcb+rrPgPZ6PZfLRe1fsl6vCUmGKIqoqNXqdDr9Dbjps9znUV0uTqdTjuPkDoVCIfcuJ4gizjMMm8u9vW+1nr04czqdK56c37CbKY9j2+1WEARZ0Gq1RFHAz2q1qlQqXxoN69HRcDjUarW8ZD6QUigUOnY8uKYH8N1sNkul9yiGw+F6vS4Rxn8EsodEIqHRaOSnq9T7ajQazWQycEIR1AEBYDabSZJyHDucJyegwWBQr9ebTCaKvHd4cCQANUU9evwQ1Ofz4YvUKUI43GE8HouSiFiNRhOowWBIpVLyHITJkuW3PwgAEf3pgIwxF5r+OplMEsk3CPT5szCMnY7EwUdhwUh/CXiej0Qi3idPz89fdrpdbsfBzH7S3Q9K5pP4c0sAKpVKoVAQGO1ut+t0OoFAQHkH2Da/3/+but3uarWK0ZMQoNdyucRutdttmqZxMTzY7XaYxsrgtUjEZrNhkSwWyy/0NCatZumrNQAAAABJRU5ErkJggg==',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 20,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -13,12 +13,12 @@ log = CPLog(__name__)
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://torrentshack.eu/',
|
||||
'login': 'http://torrentshack.eu/login.php',
|
||||
'login_check': 'http://torrentshack.eu/inbox.php',
|
||||
'detail': 'http://torrentshack.eu/torrent/%s',
|
||||
'search': 'http://torrentshack.eu/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1',
|
||||
'download': 'http://torrentshack.eu/%s',
|
||||
'test': 'https://theshack.us.to/',
|
||||
'login': 'https://theshack.us.to/login.php',
|
||||
'login_check': 'https://theshack.us.to/inbox.php',
|
||||
'detail': 'https://theshack.us.to/torrent/%s',
|
||||
'search': 'https://theshack.us.to/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1',
|
||||
'download': 'https://theshack.us.to/%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
@@ -42,6 +42,7 @@ class Base(TorrentProvider):
|
||||
|
||||
link = result.find('span', attrs = {'class': 'torrent_name_link'}).parent
|
||||
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')
|
||||
|
||||
results.append({
|
||||
@@ -49,7 +50,7 @@ class Base(TorrentProvider):
|
||||
'name': six.text_type(link.span.string).translate({ord(six.u('\xad')): None}),
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % link['href'],
|
||||
'size': self.parseSize(result.find_all('td')[5].string),
|
||||
'size': self.parseSize(size),
|
||||
'seeders': tryInt(tds[len(tds)-2].string),
|
||||
'leechers': tryInt(tds[len(tds)-1].string),
|
||||
})
|
||||
|
||||
@@ -22,12 +22,12 @@ class Base(TorrentMagnetProvider, RSS):
|
||||
|
||||
http_time_between_calls = 0
|
||||
|
||||
def _search(self, media, quality, results):
|
||||
def _searchOnTitle(self, title, media, quality, results):
|
||||
|
||||
search_url = self.urls['verified_search'] if self.conf('verified_only') else self.urls['search']
|
||||
|
||||
# Create search parameters
|
||||
search_params = self.buildUrl(media)
|
||||
search_params = self.buildUrl(title, media, quality)
|
||||
|
||||
smin = quality.get('size_min')
|
||||
smax = quality.get('size_max')
|
||||
|
||||
@@ -2,28 +2,25 @@ 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 TorrentMagnetProvider
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentMagnetProvider):
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': '%s/api',
|
||||
'search': '%s/api/list.json?keywords=%s&quality=%s',
|
||||
'detail': '%s/api/movie.json?id=%s'
|
||||
'test': '%s/api/v2',
|
||||
'search': '%s/api/v2/list_movies.json?limit=50&query_term=%s'
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # seconds
|
||||
|
||||
proxy_list = [
|
||||
'http://yify.unlocktorrent.com',
|
||||
'http://yify-torrents.com.come.in',
|
||||
'http://yts.re',
|
||||
'http://yts.im'
|
||||
'http://yify-torrents.im',
|
||||
'https://yts.re',
|
||||
'https://yts.wf',
|
||||
'https://yts.im',
|
||||
]
|
||||
|
||||
def search(self, movie, quality):
|
||||
@@ -39,28 +36,31 @@ class Base(TorrentMagnetProvider):
|
||||
if not domain:
|
||||
return
|
||||
|
||||
search_url = self.urls['search'] % (domain, getIdentifier(movie), quality['identifier'])
|
||||
search_url = self.urls['search'] % (domain, getIdentifier(movie))
|
||||
|
||||
data = self.getJsonData(search_url)
|
||||
data = data.get('data')
|
||||
|
||||
if data and data.get('MovieList'):
|
||||
if isinstance(data, dict) and data.get('movies'):
|
||||
try:
|
||||
for result in data.get('MovieList'):
|
||||
for result in data.get('movies'):
|
||||
|
||||
if result['Quality'] and result['Quality'] not in result['MovieTitle']:
|
||||
title = result['MovieTitle'] + ' BrRip ' + result['Quality']
|
||||
else:
|
||||
title = result['MovieTitle'] + ' BrRip'
|
||||
for release in result.get('torrents', []):
|
||||
|
||||
results.append({
|
||||
'id': result['MovieID'],
|
||||
'name': title,
|
||||
'url': result['TorrentMagnetUrl'],
|
||||
'detail_url': self.urls['detail'] % (domain, result['MovieID']),
|
||||
'size': self.parseSize(result['Size']),
|
||||
'seeders': tryInt(result['TorrentSeeds']),
|
||||
'leechers': tryInt(result['TorrentPeers']),
|
||||
})
|
||||
if release['quality'] and release['quality'] not in result['title_long']:
|
||||
title = result['title_long'] + ' BRRip ' + release['quality']
|
||||
else:
|
||||
title = result['title_long'] + ' BRRip'
|
||||
|
||||
results.append({
|
||||
'id': release['hash'],
|
||||
'name': title,
|
||||
'url': release['url'],
|
||||
'detail_url': result['url'],
|
||||
'size': self.parseSize(release['size']),
|
||||
'seeders': tryInt(release['seeds']),
|
||||
'leechers': tryInt(release['peers']),
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
.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 @@
|
||||
Block.Search = new Class({
|
||||
var BlockSearch = new Class({
|
||||
|
||||
Extends: BlockBase,
|
||||
|
||||
@@ -9,45 +9,46 @@ Block.Search = new Class({
|
||||
|
||||
var focus_timer = 0;
|
||||
self.el = new Element('div.search_form').adopt(
|
||||
new Element('div.input').adopt(
|
||||
self.input = new Element('input', {
|
||||
'placeholder': 'Search & add a new media',
|
||||
new Element('a.icon-search', {
|
||||
'events': {
|
||||
'click': self.clear.bind(self),
|
||||
'touchend': self.clear.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('div.wrapper').adopt(
|
||||
self.result_container = new Element('div.results_container', {
|
||||
'tween': {
|
||||
'duration': 200
|
||||
},
|
||||
'events': {
|
||||
'input': self.keyup.bind(self),
|
||||
'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);
|
||||
'mousewheel': function(e){
|
||||
(e).stopPropagation();
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('a.icon2', {
|
||||
'events': {
|
||||
'click': self.clear.bind(self),
|
||||
'touchend': self.clear.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
self.result_container = new Element('div.results_container', {
|
||||
'tween': {
|
||||
'duration': 200
|
||||
},
|
||||
'events': {
|
||||
'mousewheel': function(e){
|
||||
(e).stopPropagation();
|
||||
}
|
||||
}
|
||||
}).adopt(
|
||||
self.results = new Element('div.results')
|
||||
}).grab(
|
||||
self.results = new Element('div.results')
|
||||
),
|
||||
new Element('div.input').grab(
|
||||
self.input = new Element('input', {
|
||||
'placeholder': 'Search & add a new media',
|
||||
'events': {
|
||||
'input': self.keyup.bind(self),
|
||||
'paste': self.keyup.bind(self),
|
||||
'change': self.keyup.bind(self),
|
||||
'keyup': self.keyup.bind(self),
|
||||
'focus': function(){
|
||||
if(focus_timer) clearTimeout(focus_timer);
|
||||
if(this.get('value'))
|
||||
self.hideResults(false);
|
||||
},
|
||||
'blur': function(){
|
||||
focus_timer = (function(){
|
||||
self.el.removeClass('focused');
|
||||
}).delay(100);
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -67,11 +68,12 @@ Block.Search = new Class({
|
||||
|
||||
self.last_q = '';
|
||||
self.input.set('value', '');
|
||||
self.el.addClass('focused');
|
||||
self.input.focus();
|
||||
|
||||
self.media = {};
|
||||
self.results.empty();
|
||||
self.el.removeClass('filled')
|
||||
self.el.removeClass('filled');
|
||||
|
||||
}
|
||||
},
|
||||
@@ -105,7 +107,7 @@ Block.Search = new Class({
|
||||
self.api_request.cancel();
|
||||
|
||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer);
|
||||
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
||||
self.autocomplete_timer = self.autocomplete.delay(300, self);
|
||||
}
|
||||
|
||||
},
|
||||
@@ -115,10 +117,10 @@ Block.Search = new Class({
|
||||
|
||||
if(!self.q()){
|
||||
self.hideResults(true);
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
self.list()
|
||||
self.list();
|
||||
},
|
||||
|
||||
list: function(){
|
||||
@@ -139,7 +141,7 @@ Block.Search = new Class({
|
||||
'q': q
|
||||
},
|
||||
'onComplete': self.fill.bind(self, q)
|
||||
})
|
||||
});
|
||||
}
|
||||
else
|
||||
self.fill(q, cache);
|
||||
@@ -158,30 +160,25 @@ Block.Search = new Class({
|
||||
|
||||
Object.each(json, function(media){
|
||||
if(typeOf(media) == 'array'){
|
||||
Object.each(media, function(m){
|
||||
Object.each(media, function(me){
|
||||
|
||||
var m = new Block.Search[m.type.capitalize() + 'Item'](m);
|
||||
var m = new window['BlockSearch' + me.type.capitalize() + 'Item'](me);
|
||||
$(m).inject(self.results);
|
||||
self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m;
|
||||
|
||||
if(q == m.imdb)
|
||||
m.showOptions()
|
||||
m.showOptions();
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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')
|
||||
self.mask.fade('out');
|
||||
|
||||
},
|
||||
|
||||
loading: function(bool){
|
||||
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
||||
this.el[bool ? 'addClass' : 'removeClass']('loading');
|
||||
},
|
||||
|
||||
q: function(){
|
||||
|
||||
242
couchpotato/core/media/_base/search/static/search.scss
Normal file
242
couchpotato/core/media/_base/search/static/search.scss
Normal file
@@ -0,0 +1,242 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -65,7 +65,7 @@ class MovieBase(MovieTypeBase):
|
||||
return False
|
||||
elif not params.get('info'):
|
||||
try:
|
||||
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True)
|
||||
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), adding = True, single = True)
|
||||
if not is_movie:
|
||||
msg = 'Can\'t add movie, seems to be a TV show.'
|
||||
log.error(msg)
|
||||
|
||||
52
couchpotato/core/media/movie/_base/static/details.js
Normal file
52
couchpotato/core/media/movie/_base/static/details.js
Normal file
@@ -0,0 +1,52 @@
|
||||
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,15 +45,16 @@ var MovieList = new Class({
|
||||
}) : null
|
||||
);
|
||||
|
||||
if($(window).getSize().x <= 480 && !self.options.force_view)
|
||||
self.changeView('list');
|
||||
else
|
||||
self.changeView(self.getSavedView() || self.options.view || 'details');
|
||||
self.changeView(self.getSavedView() || self.options.view || 'thumb');
|
||||
|
||||
// Create the alphabet nav
|
||||
if(self.options.navigation)
|
||||
self.createNavigation();
|
||||
|
||||
self.getMovies();
|
||||
|
||||
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){
|
||||
@@ -67,7 +68,7 @@ var MovieList = new Class({
|
||||
self.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
self.checkIfEmpty();
|
||||
@@ -89,15 +90,11 @@ var MovieList = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
// Create the alphabet nav
|
||||
if(self.options.navigation)
|
||||
self.createNavigation();
|
||||
|
||||
if(self.options.load_more)
|
||||
self.scrollspy = new ScrollSpy({
|
||||
min: function(){
|
||||
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)
|
||||
});
|
||||
@@ -138,7 +135,7 @@ var MovieList = new Class({
|
||||
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+'"' : '') +
|
||||
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
|
||||
|
||||
@@ -230,30 +227,33 @@ var MovieList = new Class({
|
||||
),
|
||||
new Element('div.menus').adopt(
|
||||
self.navigation_counter = new Element('span.counter[title=Total]'),
|
||||
self.filter_menu = new Block.Menu(self, {
|
||||
'class': 'filter'
|
||||
self.filter_menu = new BlockMenu(self, {
|
||||
'class': 'filter',
|
||||
'button_class': 'icon-filter'
|
||||
}),
|
||||
self.navigation_actions = new Element('ul.actions', {
|
||||
self.navigation_actions = new Element('div.actions', {
|
||||
'events': {
|
||||
'click:relay(li)': function(e, el){
|
||||
'click': function(e, el){
|
||||
(e).stop();
|
||||
|
||||
var new_view = self.current_view == 'list' ? 'thumb' : 'list';
|
||||
|
||||
var a = 'active';
|
||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||
self.changeView(el.get('data-view'));
|
||||
this.addClass(a);
|
||||
self.changeView(new_view);
|
||||
|
||||
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 Block.Menu(self, {
|
||||
'class': 'extra'
|
||||
self.navigation_menu = new BlockMenu(self, {
|
||||
'class': 'extra',
|
||||
'button_class': 'icon-dots'
|
||||
})
|
||||
)
|
||||
).inject(self.el, 'top');
|
||||
);
|
||||
|
||||
// Mass edit
|
||||
self.mass_edit_select_class = new Form.Check(self.mass_edit_select);
|
||||
@@ -261,7 +261,7 @@ var MovieList = new Class({
|
||||
new Element('option', {
|
||||
'value': profile.get('_id'),
|
||||
'text': profile.get('label')
|
||||
}).inject(self.mass_edit_quality)
|
||||
}).inject(self.mass_edit_quality);
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
@@ -273,7 +273,7 @@ var MovieList = new Class({
|
||||
'change': self.search.bind(self)
|
||||
}
|
||||
})
|
||||
).addClass('search');
|
||||
).addClass('search icon-search');
|
||||
|
||||
var available_chars;
|
||||
self.filter_menu.addEvent('open', function(){
|
||||
@@ -289,8 +289,8 @@ var MovieList = new Class({
|
||||
available_chars = json.chars;
|
||||
|
||||
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': {
|
||||
'click:relay(li.available)': function(e, el){
|
||||
self.activateLetter(el.get('data-letter'));
|
||||
self.getMovies(true)
|
||||
self.getMovies(true);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Actions
|
||||
['mass_edit', 'details', 'list'].each(function(view){
|
||||
['thumb', 'list'].each(function(view){
|
||||
var current = self.current_view == view;
|
||||
new Element('li', {
|
||||
'class': 'icon2 ' + view + (current ? ' active ' : ''),
|
||||
new Element('a', {
|
||||
'class': 'icon-' + view + (current ? ' active ' : ''),
|
||||
'data-view': view
|
||||
}).inject(self.navigation_actions, current ? 'top' : 'bottom');
|
||||
});
|
||||
|
||||
// All
|
||||
self.letters['all'] = new Element('li.letter_all.available.active', {
|
||||
self.letters.all = new Element('li.letter_all.available.active', {
|
||||
'text': 'ALL'
|
||||
}).inject(self.navigation_alpha);
|
||||
|
||||
@@ -346,7 +346,7 @@ var MovieList = new Class({
|
||||
var selected = 0,
|
||||
movies = self.movies.length;
|
||||
self.movies.each(function(movie){
|
||||
selected += movie.isSelected() ? 1 : 0
|
||||
selected += movie.isSelected() ? 1 : 0;
|
||||
});
|
||||
|
||||
var indeterminate = selected > 0 && selected < movies,
|
||||
@@ -441,10 +441,10 @@ var MovieList = new Class({
|
||||
var ids = [];
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected())
|
||||
ids.include(movie.get('_id'))
|
||||
ids.include(movie.get('_id'));
|
||||
});
|
||||
|
||||
return ids
|
||||
return ids;
|
||||
},
|
||||
|
||||
massEditToggleAll: function(){
|
||||
@@ -453,10 +453,10 @@ var MovieList = new Class({
|
||||
var select = self.mass_edit_select.get('checked');
|
||||
|
||||
self.movies.each(function(movie){
|
||||
movie.select(select)
|
||||
movie.select(select);
|
||||
});
|
||||
|
||||
self.calculateSelected()
|
||||
self.calculateSelected();
|
||||
},
|
||||
|
||||
reset: function(){
|
||||
@@ -493,12 +493,12 @@ var MovieList = new Class({
|
||||
.addClass(new_view+'_list');
|
||||
|
||||
self.current_view = new_view;
|
||||
Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000});
|
||||
Cookie.write(self.options.identifier+'_view3', new_view, {duration: 1000});
|
||||
},
|
||||
|
||||
getSavedView: function(){
|
||||
var self = this;
|
||||
return Cookie.read(self.options.identifier+'_view2');
|
||||
return Cookie.read(self.options.identifier+'_view3');
|
||||
},
|
||||
|
||||
search: function(){
|
||||
@@ -537,7 +537,7 @@ var MovieList = new Class({
|
||||
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(
|
||||
new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
|
||||
@@ -590,7 +590,7 @@ var MovieList = new Class({
|
||||
loadMore: function(){
|
||||
var self = this;
|
||||
if(self.offset >= self.options.limit)
|
||||
self.getMovies()
|
||||
self.getMovies();
|
||||
},
|
||||
|
||||
store: function(movies){
|
||||
@@ -603,7 +603,7 @@ var MovieList = new Class({
|
||||
checkIfEmpty: function(){
|
||||
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)
|
||||
self.title[is_empty ? 'hide' : 'show']();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Page.Manage = new Class({
|
||||
var MoviesManage = new Class({
|
||||
|
||||
Extends: PageBase,
|
||||
|
||||
@@ -126,12 +126,12 @@ Page.Manage = new Class({
|
||||
(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%'})
|
||||
).inject(self.progress_container)
|
||||
).inject(self.progress_container);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
}, 1000);
|
||||
},
|
||||
@@ -141,10 +141,10 @@ Page.Manage = new Class({
|
||||
|
||||
for (folder in progress_object) {
|
||||
if (progress_object.hasOwnProperty(folder)) {
|
||||
temp_array.push(folder)
|
||||
temp_array.push(folder);
|
||||
}
|
||||
}
|
||||
return temp_array.stableSort()
|
||||
return temp_array.stableSort();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -2,7 +2,10 @@ var MovieAction = new Class({
|
||||
|
||||
Implements: [Options],
|
||||
|
||||
class_name: 'action icon2',
|
||||
class_name: 'action',
|
||||
label: 'UNKNOWN',
|
||||
button: null,
|
||||
details: null,
|
||||
|
||||
initialize: function(movie, options){
|
||||
var self = this;
|
||||
@@ -11,20 +14,33 @@ var MovieAction = new Class({
|
||||
self.movie = movie;
|
||||
|
||||
self.create();
|
||||
if(self.el)
|
||||
self.el.addClass(self.class_name)
|
||||
|
||||
if(self.button)
|
||||
self.button.addClass(self.class_name);
|
||||
},
|
||||
|
||||
create: function(){},
|
||||
|
||||
getButton: function(){
|
||||
return this.button || null;
|
||||
},
|
||||
|
||||
getDetails: function(){
|
||||
return this.details || null;
|
||||
},
|
||||
|
||||
getLabel: function(){
|
||||
return this.label;
|
||||
},
|
||||
|
||||
disable: function(){
|
||||
if(this.el)
|
||||
this.el.addClass('disable')
|
||||
this.el.addClass('disable');
|
||||
},
|
||||
|
||||
enable: function(){
|
||||
if(this.el)
|
||||
this.el.removeClass('disable')
|
||||
this.el.removeClass('disable');
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
@@ -37,7 +53,7 @@ var MovieAction = new Class({
|
||||
try {
|
||||
return self.movie.original_title ? self.movie.original_title : self.movie.titles[0];
|
||||
}
|
||||
catch(e){
|
||||
catch(e2){
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
@@ -46,10 +62,10 @@ var MovieAction = new Class({
|
||||
get: function(key){
|
||||
var self = this;
|
||||
try {
|
||||
return self.movie.get(key)
|
||||
return self.movie.get(key);
|
||||
}
|
||||
catch(e){
|
||||
return self.movie[key]
|
||||
return self.movie[key];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -63,7 +79,7 @@ var MovieAction = new Class({
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el || null
|
||||
return this.el || null;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -80,7 +96,8 @@ MA.IMDB = new Class({
|
||||
|
||||
self.id = self.movie.getIdentifier ? self.movie.getIdentifier() : self.get('imdb');
|
||||
|
||||
self.el = new Element('a.imdb', {
|
||||
self.button = new Element('a.imdb', {
|
||||
'text': 'IMDB',
|
||||
'title': 'Go to the IMDB page of ' + self.getTitle(),
|
||||
'href': 'http://www.imdb.com/title/'+self.id+'/',
|
||||
'target': '_blank'
|
||||
@@ -94,22 +111,11 @@ MA.IMDB = new Class({
|
||||
MA.Release = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
label: 'Releases',
|
||||
|
||||
create: function(){
|
||||
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){
|
||||
if(self.movie.data._id != notification.data._id) return;
|
||||
|
||||
@@ -118,7 +124,7 @@ MA.Release = new Class({
|
||||
// Releases are currently displayed
|
||||
if(self.options_container.isDisplayed()){
|
||||
self.options_container.destroy();
|
||||
self.createReleases();
|
||||
self.getDetails();
|
||||
}
|
||||
else {
|
||||
self.options_container.destroy();
|
||||
@@ -129,16 +135,7 @@ MA.Release = new Class({
|
||||
|
||||
},
|
||||
|
||||
show: function(e){
|
||||
var self = this;
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.createReleases();
|
||||
|
||||
},
|
||||
|
||||
createReleases: function(refresh){
|
||||
getDetails: function(refresh){
|
||||
var self = this;
|
||||
|
||||
if(!self.options_container || refresh){
|
||||
@@ -162,14 +159,14 @@ MA.Release = new Class({
|
||||
|
||||
var quality = Quality.getQuality(release.quality) || {},
|
||||
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');
|
||||
if(release.files && release.files.length > 0){
|
||||
try {
|
||||
var movie_file = release.files.filter(function(file){
|
||||
var type = File.Type.get(file.type_id);
|
||||
return type && type.identifier == 'movie'
|
||||
return type && type.identifier == 'movie';
|
||||
}).pick();
|
||||
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
|
||||
}
|
||||
@@ -177,19 +174,19 @@ MA.Release = new Class({
|
||||
}
|
||||
|
||||
// Create release
|
||||
release['el'] = new Element('div', {
|
||||
release.el = new Element('div', {
|
||||
'class': 'item '+release.status,
|
||||
'id': 'release_'+release._id
|
||||
}).adopt(
|
||||
new Element('span.name', {'text': release_name, 'title': release_name}),
|
||||
new Element('span.status', {'text': release.status, 'class': 'release_status '+release.status}),
|
||||
new Element('span.status', {'text': release.status, 'class': 'status '+release.status}),
|
||||
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.score', {'text': self.get(release, 'score')}),
|
||||
new Element('span.provider', { 'text': provider, 'title': provider }),
|
||||
info['detail_url'] ? new Element('a.info.icon2', {
|
||||
'href': info['detail_url'],
|
||||
info.detail_url ? new Element('a.info.icon2', {
|
||||
'href': info.detail_url,
|
||||
'target': '_blank'
|
||||
}) : new Element('a'),
|
||||
new Element('a.download.icon2', {
|
||||
@@ -283,7 +280,7 @@ MA.Release = new Class({
|
||||
new Element('span.or', {
|
||||
'text': 'or pick one below'
|
||||
})] : null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
self.last_release = null;
|
||||
@@ -291,9 +288,7 @@ MA.Release = new Class({
|
||||
|
||||
}
|
||||
|
||||
// Show it
|
||||
self.options_container.inject(self.movie, 'top');
|
||||
self.movie.slide('in', self.options_container);
|
||||
return self.options_container;
|
||||
|
||||
},
|
||||
|
||||
@@ -342,13 +337,13 @@ MA.Release = new Class({
|
||||
'click': self.markMovieDone.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
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){
|
||||
@@ -386,7 +381,7 @@ MA.Release = new Class({
|
||||
'data': {
|
||||
'id': release._id
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
@@ -403,7 +398,7 @@ MA.Release = new Class({
|
||||
movie.set('tween', {
|
||||
'duration': 300,
|
||||
'onComplete': function(){
|
||||
self.movie.destroy()
|
||||
self.movie.destroy();
|
||||
}
|
||||
});
|
||||
movie.tween('height', 0);
|
||||
@@ -429,49 +424,35 @@ MA.Trailer = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
id: null,
|
||||
label: 'Trailer',
|
||||
|
||||
create: function(){
|
||||
getDetails: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.trailer', {
|
||||
'title': 'Watch the trailer of ' + self.getTitle(),
|
||||
'events': {
|
||||
'click': self.watch.bind(self)
|
||||
}
|
||||
});
|
||||
if(!self.player_container){
|
||||
var id = 'trailer-'+randomString();
|
||||
self.player_container = new Element('div.icon-play[id='+id+']', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
self.watch(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.container = new Element('div.trailer_container')
|
||||
.grab(self.player_container);
|
||||
}
|
||||
|
||||
return self.player_container;
|
||||
},
|
||||
|
||||
watch: function(offset){
|
||||
watch: function(){
|
||||
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 url = data_url.substitute({
|
||||
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({
|
||||
'title': encodeURI(self.getTitle()),
|
||||
'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);
|
||||
'year': self.get('year')
|
||||
});
|
||||
|
||||
new Request.JSONP({
|
||||
'url': url,
|
||||
@@ -491,8 +472,6 @@ MA.Trailer = new Class({
|
||||
}
|
||||
});
|
||||
|
||||
self.close_button.removeClass('hide');
|
||||
|
||||
var quality_set = false;
|
||||
var change_quality = function(state){
|
||||
if(!quality_set && (state.data == 1 || state.data || 2)){
|
||||
@@ -508,7 +487,9 @@ MA.Trailer = new Class({
|
||||
self.player.addEventListener('onStateChange', change_quality);
|
||||
|
||||
}
|
||||
}).send()
|
||||
}).send();
|
||||
|
||||
return self.container;
|
||||
|
||||
},
|
||||
|
||||
@@ -523,7 +504,7 @@ MA.Trailer = new Class({
|
||||
setTimeout(function(){
|
||||
self.container.destroy();
|
||||
self.close_button.destroy();
|
||||
}, 1800)
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
|
||||
@@ -536,7 +517,8 @@ MA.Edit = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.edit', {
|
||||
self.button = new Element('a.edit', {
|
||||
'text': 'Edit',
|
||||
'title': 'Change movie information, like title and quality.',
|
||||
'events': {
|
||||
'click': self.editMovie.bind(self)
|
||||
@@ -585,7 +567,7 @@ MA.Edit = new Class({
|
||||
// Fill categories
|
||||
var categories = CategoryList.getAll();
|
||||
|
||||
if(categories.length == 0)
|
||||
if(categories.length === 0)
|
||||
self.category_select.hide();
|
||||
else {
|
||||
self.category_select.show();
|
||||
@@ -659,7 +641,8 @@ MA.Refresh = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.refresh', {
|
||||
self.button = new Element('a.refresh', {
|
||||
'text': 'Refresh',
|
||||
'title': 'Refresh the movie info and do a forced search',
|
||||
'events': {
|
||||
'click': self.doRefresh.bind(self)
|
||||
@@ -670,7 +653,7 @@ MA.Refresh = new Class({
|
||||
|
||||
doRefresh: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
(e).stop();
|
||||
|
||||
Api.request('media.refresh', {
|
||||
'data': {
|
||||
@@ -686,17 +669,18 @@ MA.Readd = new Class({
|
||||
Extends: MovieAction,
|
||||
|
||||
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)
|
||||
var snatched = self.movie.data.releases.filter(function(release){
|
||||
snatched = self.movie.data.releases.filter(function(release){
|
||||
return release.status && (release.status == 'snatched' || release.status == 'seeding' || release.status == 'downloaded' || release.status == 'done');
|
||||
}).length;
|
||||
|
||||
if(movie_done || snatched && snatched > 0)
|
||||
self.el = new Element('a.readd', {
|
||||
'title': 'Readd the movie and mark all previous snatched/downloaded as ignored',
|
||||
'title': 'Re-add the movie and mark all previous snatched/downloaded as ignored',
|
||||
'events': {
|
||||
'click': self.doReadd.bind(self)
|
||||
}
|
||||
@@ -792,7 +776,7 @@ MA.Delete = new Class({
|
||||
movie.set('tween', {
|
||||
'duration': 300,
|
||||
'onComplete': function(){
|
||||
self.movie.destroy()
|
||||
self.movie.destroy();
|
||||
}
|
||||
});
|
||||
movie.tween('height', 0);
|
||||
@@ -847,7 +831,7 @@ MA.Files = new Class({
|
||||
new Element('div.file.item').adopt(
|
||||
new Element('span.name', {'text': file}),
|
||||
new Element('span.type', {'text': type})
|
||||
).inject(rel)
|
||||
).inject(rel);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,22 +2,51 @@ var Movie = new Class({
|
||||
|
||||
Extends: BlockBase,
|
||||
|
||||
action: {},
|
||||
actions: [],
|
||||
details: null,
|
||||
|
||||
initialize: function(list, options, data){
|
||||
var self = this;
|
||||
|
||||
self.data = data;
|
||||
self.view = options.view || 'details';
|
||||
self.list = list;
|
||||
|
||||
self.el = new Element('div.movie');
|
||||
self.el = new Element('a.movie', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).stop();
|
||||
self.openDetails();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||
self.category = CategoryList.getCategory(data.category_id) || {};
|
||||
self.parent(self, options);
|
||||
|
||||
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(){
|
||||
@@ -30,7 +59,6 @@ var Movie = new Class({
|
||||
if(self.data._id != notification.data._id) return;
|
||||
|
||||
self.busy(false);
|
||||
self.removeView();
|
||||
self.update.delay(2000, self, notification);
|
||||
};
|
||||
App.on('movie.update', self.global_events['movie.update']);
|
||||
@@ -47,7 +75,7 @@ var Movie = new Class({
|
||||
// Remove spinner
|
||||
self.global_events['movie.searcher.ended'] = function(notification){
|
||||
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']);
|
||||
|
||||
@@ -62,7 +90,7 @@ var Movie = new Class({
|
||||
var updated = false;
|
||||
self.data.releases.each(function(release){
|
||||
if(release._id == data._id){
|
||||
release['status'] = data.status;
|
||||
release.status = data.status;
|
||||
updated = true;
|
||||
}
|
||||
});
|
||||
@@ -102,12 +130,12 @@ var Movie = new Class({
|
||||
if(self.mask)
|
||||
self.mask.destroy();
|
||||
if(self.spinner)
|
||||
self.spinner.el.destroy();
|
||||
self.spinner.destroy();
|
||||
self.spinner = null;
|
||||
self.mask = null;
|
||||
}, timeout || 400);
|
||||
}
|
||||
}, timeout || 1000)
|
||||
}, timeout || 1000);
|
||||
}
|
||||
else if(!self.spinner) {
|
||||
self.createMask();
|
||||
@@ -130,7 +158,6 @@ var Movie = new Class({
|
||||
|
||||
self.data = notification.data;
|
||||
self.el.empty();
|
||||
self.removeView();
|
||||
|
||||
self.profile = Quality.getProfile(self.data.profile_id) || {};
|
||||
self.category = CategoryList.getCategory(self.data.category_id) || {};
|
||||
@@ -150,7 +177,7 @@ var Movie = new Class({
|
||||
|
||||
if(self.data.info.release_date)
|
||||
[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;
|
||||
});
|
||||
|
||||
@@ -163,7 +190,7 @@ var Movie = new Class({
|
||||
self.select_checkbox = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': function(){
|
||||
self.fireEvent('select')
|
||||
self.fireEvent('select');
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -181,9 +208,6 @@ var Movie = new Class({
|
||||
'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', {
|
||||
'text': eta_date,
|
||||
'title': 'ETA'
|
||||
@@ -193,19 +217,24 @@ var Movie = new Class({
|
||||
'click': function(e){
|
||||
var releases = self.el.getElement('.actions .releases');
|
||||
if(releases.isVisible())
|
||||
releases.fireEvent('click', [e])
|
||||
releases.fireEvent('click', [e]);
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
self.actions = new Element('div.actions')
|
||||
self.actions_el = new Element('div.actions', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).stopPropagation();
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if(!self.thumbnail)
|
||||
self.el.addClass('no_thumbnail');
|
||||
|
||||
//self.changeView(self.view);
|
||||
self.select_checkbox_class = new Form.Check(self.select_checkbox);
|
||||
|
||||
// Add profile
|
||||
@@ -213,9 +242,9 @@ var Movie = new Class({
|
||||
self.profile.getTypes().each(function(type){
|
||||
|
||||
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.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.');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -223,17 +252,20 @@ var Movie = new Class({
|
||||
// Add releases
|
||||
self.updateReleases();
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
|
||||
if(action.el)
|
||||
self.actions.adopt(action)
|
||||
self.options.actions.each(function(action){
|
||||
var action = new action(self),
|
||||
button = action.getButton();
|
||||
if(button)
|
||||
self.actions_el.grab(button);
|
||||
|
||||
self.actions.push(action);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
updateReleases: function(){
|
||||
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){
|
||||
|
||||
@@ -245,7 +277,7 @@ var Movie = new Class({
|
||||
|
||||
if (q && !q.hasClass(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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -271,7 +303,7 @@ var Movie = new Class({
|
||||
else if(self.data.info.titles.length > 0)
|
||||
return self.getUnprefixedTitle(self.data.info.titles[0]);
|
||||
|
||||
return 'Unknown movie'
|
||||
return 'Unknown movie';
|
||||
},
|
||||
|
||||
getUnprefixedTitle: function(t){
|
||||
@@ -284,49 +316,6 @@ var Movie = new Class({
|
||||
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(){
|
||||
var self = this;
|
||||
|
||||
@@ -339,12 +328,12 @@ var Movie = new Class({
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr] || this.data.info[attr]
|
||||
return this.data[attr] || this.data.info[attr];
|
||||
},
|
||||
|
||||
select: function(bool){
|
||||
var self = this;
|
||||
self.select_checkbox_class[bool ? 'check' : 'uncheck']()
|
||||
self.select_checkbox_class[bool ? 'check' : 'uncheck']();
|
||||
},
|
||||
|
||||
isSelected: function(){
|
||||
|
||||
367
couchpotato/core/media/movie/_base/static/movie.scss
Normal file
367
couchpotato/core/media/movie/_base/static/movie.scss
Normal file
@@ -0,0 +1,367 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
49
couchpotato/core/media/movie/_base/static/page.js
Normal file
49
couchpotato/core/media/movie/_base/static/page.js
Normal file
@@ -0,0 +1,49 @@
|
||||
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 @@
|
||||
Block.Search.MovieItem = new Class({
|
||||
var BlockSearchMovieItem = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
@@ -31,9 +31,11 @@ Block.Search.MovieItem = new Class({
|
||||
}
|
||||
}).adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
new Element('h2').adopt(
|
||||
new Element('h2', {
|
||||
'title': self.getTitle()
|
||||
}).adopt(
|
||||
self.title = new Element('span.title', {
|
||||
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
|
||||
'text': self.getTitle()
|
||||
}),
|
||||
self.year = info.year ? new Element('span.year', {
|
||||
'text': info.year
|
||||
@@ -48,7 +50,7 @@ Block.Search.MovieItem = new Class({
|
||||
self.alternativeTitle({
|
||||
'title': title
|
||||
});
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
alternativeTitle: function(alternative){
|
||||
@@ -68,7 +70,7 @@ Block.Search.MovieItem = new Class({
|
||||
},
|
||||
|
||||
get: function(key){
|
||||
return this.info[key]
|
||||
return this.info[key];
|
||||
},
|
||||
|
||||
showOptions: function(){
|
||||
@@ -77,7 +79,7 @@ Block.Search.MovieItem = new Class({
|
||||
self.createOptions();
|
||||
|
||||
self.data_container.addClass('open');
|
||||
self.el.addEvent('outerClick', self.closeOptions.bind(self))
|
||||
self.el.addEvent('outerClick', self.closeOptions.bind(self));
|
||||
|
||||
},
|
||||
|
||||
@@ -85,7 +87,7 @@ Block.Search.MovieItem = new Class({
|
||||
var self = this;
|
||||
|
||||
self.data_container.removeClass('open');
|
||||
self.el.removeEvents('outerClick')
|
||||
self.el.removeEvents('outerClick');
|
||||
},
|
||||
|
||||
add: function(e){
|
||||
@@ -132,10 +134,11 @@ Block.Search.MovieItem = new Class({
|
||||
|
||||
if(!self.options_el.hasClass('set')){
|
||||
|
||||
var in_library;
|
||||
if(info.in_library){
|
||||
var in_library = [];
|
||||
in_library = [];
|
||||
(info.in_library.releases || []).each(function(release){
|
||||
in_library.include(release.quality)
|
||||
in_library.include(release.quality);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,14 +174,14 @@ Block.Search.MovieItem = new Class({
|
||||
Array.each(self.alternative_titles, function(alt){
|
||||
new Element('option', {
|
||||
'text': alt.title
|
||||
}).inject(self.title_select)
|
||||
}).inject(self.title_select);
|
||||
});
|
||||
|
||||
|
||||
// Fill categories
|
||||
var categories = CategoryList.getAll();
|
||||
|
||||
if(categories.length == 0)
|
||||
if(categories.length === 0)
|
||||
self.category_select.hide();
|
||||
else {
|
||||
self.category_select.show();
|
||||
@@ -199,12 +202,12 @@ Block.Search.MovieItem = new Class({
|
||||
new Element('option', {
|
||||
'value': profile.get('_id'),
|
||||
'text': profile.get('label')
|
||||
}).inject(self.profile_select)
|
||||
}).inject(self.profile_select);
|
||||
});
|
||||
|
||||
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.add();
|
||||
|
||||
@@ -218,12 +221,12 @@ Block.Search.MovieItem = new Class({
|
||||
self.mask = new Element('div.mask').inject(self.el).fade('hide');
|
||||
|
||||
createSpinner(self.mask);
|
||||
self.mask.fade('in')
|
||||
self.mask.fade('in');
|
||||
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el
|
||||
return this.el;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Page.Wanted = new Class({
|
||||
var MoviesWanted = new Class({
|
||||
|
||||
Extends: PageBase,
|
||||
|
||||
@@ -10,7 +10,7 @@ Page.Wanted = new Class({
|
||||
indexAction: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.wanted){
|
||||
if(!self.list){
|
||||
|
||||
self.manual_search = new Element('a', {
|
||||
'title': 'Force a search for the full wanted list',
|
||||
@@ -20,7 +20,6 @@ Page.Wanted = new Class({
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
self.scan_folder = new Element('a', {
|
||||
'title': 'Scan a folder and rename all movies in it',
|
||||
'text': 'Manual folder scan',
|
||||
@@ -30,7 +29,7 @@ Page.Wanted = new Class({
|
||||
});
|
||||
|
||||
// Wanted movies
|
||||
self.wanted = new MovieList({
|
||||
self.list = new MovieList({
|
||||
'identifier': 'wanted',
|
||||
'status': 'active',
|
||||
'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Edit, MA.Refresh, MA.Readd, MA.Delete],
|
||||
@@ -38,7 +37,7 @@ Page.Wanted = new Class({
|
||||
'menu': [self.manual_search, self.scan_folder],
|
||||
'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted')
|
||||
});
|
||||
$(self.wanted).inject(self.el);
|
||||
$(self.list).inject(self.el);
|
||||
|
||||
// Check if search is in progress
|
||||
self.startProgressInterval.delay(4000, self);
|
||||
@@ -91,7 +90,7 @@ Page.Wanted = new Class({
|
||||
};
|
||||
|
||||
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() {
|
||||
var folder = self.folder_browser.getValue();
|
||||
|
||||
@@ -44,11 +44,12 @@ var Charts = new Class({
|
||||
|
||||
if( Cookie.read('suggestions_charts_menu_selected') === 'charts'){
|
||||
self.show();
|
||||
self.fireEvent.delay(0, self, 'created');
|
||||
}
|
||||
else
|
||||
self.el.hide();
|
||||
|
||||
self.fireEvent.delay(0, self, 'created');
|
||||
|
||||
},
|
||||
|
||||
fill: function(json){
|
||||
@@ -58,7 +59,7 @@ var Charts = new Class({
|
||||
self.el_refreshing_text.hide();
|
||||
self.el_refresh_link.show();
|
||||
|
||||
if(!json || json.count == 0){
|
||||
if(!json || json.count === 0){
|
||||
self.el_no_charts_enabled.show();
|
||||
self.el_refresh_link.show();
|
||||
self.el_refreshing_text.hide();
|
||||
@@ -83,17 +84,16 @@ var Charts = new Class({
|
||||
|
||||
Object.each(chart.list, function(movie){
|
||||
|
||||
var m = new Block.Search.MovieItem(movie, {
|
||||
var m = new BlockSearchMovieItem(movie, {
|
||||
'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' : ''))),
|
||||
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(
|
||||
new Element('div.chart_number', {
|
||||
'text': it++,
|
||||
@@ -135,7 +135,7 @@ var Charts = new Class({
|
||||
'text': plot,
|
||||
'events': {
|
||||
'click': function(){
|
||||
this.toggleClass('full')
|
||||
this.toggleClass('full');
|
||||
}
|
||||
}
|
||||
}) : null
|
||||
|
||||
89
couchpotato/core/media/movie/providers/automation/crowdai.py
Normal file
89
couchpotato/core/media/movie/providers/automation/crowdai.py
Normal file
@@ -0,0 +1,89 @@
|
||||
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,11 +48,12 @@ class Letterboxd(Automation):
|
||||
|
||||
soup = BeautifulSoup(self.getHTMLData(self.url % username))
|
||||
|
||||
for movie in soup.find_all('a', attrs = {'class': 'frame'}):
|
||||
match = removeEmpty(self.pattern.split(movie['title']))
|
||||
for movie in soup.find_all('li', attrs = {'class': 'poster-container'}):
|
||||
img = movie.find('img', movie)
|
||||
title = img.get('alt')
|
||||
|
||||
movies.append({
|
||||
'title': match[0],
|
||||
'year': match[1]
|
||||
'title': title
|
||||
})
|
||||
|
||||
return movies
|
||||
|
||||
@@ -39,15 +39,14 @@ class Rottentomatoes(Automation, RSS):
|
||||
|
||||
if result:
|
||||
|
||||
log.info2('Something smells...')
|
||||
rating = tryInt(self.getTextElement(movie, rating_tag))
|
||||
name = result.group(0)
|
||||
|
||||
print rating, tryInt(self.conf('tomatometer_percent'))
|
||||
if rating < tryInt(self.conf('tomatometer_percent')):
|
||||
log.info2('%s seems to be rotten...', name)
|
||||
else:
|
||||
|
||||
log.info2('Found %s fresh enough movies, enqueuing: %s', (rating, name))
|
||||
log.info2('Found %s with fresh rating %s', (name, rating))
|
||||
year = datetime.datetime.now().strftime("%Y")
|
||||
imdb = self.search(name, year)
|
||||
|
||||
|
||||
@@ -69,12 +69,15 @@ class CouchPotatoApi(MovieProvider):
|
||||
name_enc = base64.b64encode(ss(name))
|
||||
return self.getJsonData(self.urls['validate'] % name_enc, headers = self.getRequestHeaders())
|
||||
|
||||
def isMovie(self, identifier = None):
|
||||
def isMovie(self, identifier = None, adding = False):
|
||||
|
||||
if not identifier:
|
||||
return
|
||||
|
||||
data = self.getJsonData(self.urls['is_movie'] % identifier, headers = self.getRequestHeaders())
|
||||
url = self.urls['is_movie'] % identifier
|
||||
url += '?adding=1' if adding else ''
|
||||
|
||||
data = self.getJsonData(url, headers = self.getRequestHeaders())
|
||||
if data:
|
||||
return data.get('is_movie', True)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from couchpotato import Env
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString
|
||||
@@ -17,8 +18,8 @@ autoload = 'OMDBAPI'
|
||||
class OMDBAPI(MovieProvider):
|
||||
|
||||
urls = {
|
||||
'search': 'http://www.omdbapi.com/?%s',
|
||||
'info': 'http://www.omdbapi.com/?i=%s',
|
||||
'search': 'http://www.omdbapi.com/?type=movie&%s',
|
||||
'info': 'http://www.omdbapi.com/?type=movie&i=%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 0
|
||||
@@ -38,7 +39,8 @@ class OMDBAPI(MovieProvider):
|
||||
}
|
||||
|
||||
cache_key = 'omdbapi.cache.%s' % q
|
||||
cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')}), timeout = 3)
|
||||
url = self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')})
|
||||
cached = self.getCache(cache_key, url, timeout = 3, headers = {'User-Agent': Env.getIdentifier()})
|
||||
|
||||
if cached:
|
||||
result = self.parseMovie(cached)
|
||||
@@ -56,7 +58,7 @@ class OMDBAPI(MovieProvider):
|
||||
return {}
|
||||
|
||||
cache_key = 'omdbapi.cache.%s' % identifier
|
||||
cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3)
|
||||
cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3, headers = {'User-Agent': Env.getIdentifier()})
|
||||
|
||||
if cached:
|
||||
result = self.parseMovie(cached)
|
||||
|
||||
@@ -11,7 +11,7 @@ autoload = 'Bitsoup'
|
||||
class Bitsoup(MovieProvider, Base):
|
||||
cat_ids = [
|
||||
([17], ['3d']),
|
||||
([41], ['720p', '1080p']),
|
||||
([80], ['720p', '1080p']),
|
||||
([20], ['dvdr']),
|
||||
([19], ['brrip', 'dvdrip']),
|
||||
]
|
||||
|
||||
11
couchpotato/core/media/movie/providers/torrent/hdaccess.py
Normal file
11
couchpotato/core/media/movie/providers/torrent/hdaccess.py
Normal file
@@ -0,0 +1,11 @@
|
||||
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']),
|
||||
([48], ['720p', '1080p', 'bd50']),
|
||||
([72], ['cam', 'ts', 'tc', 'r5', 'scr']),
|
||||
([7,48], ['dvdrip', 'brrip']),
|
||||
([7, 48, 20], ['dvdrip', 'brrip']),
|
||||
([6], ['dvdr']),
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.torrentleech import Base
|
||||
from couchpotato.core.media.movie.providers.base import MovieProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'TorrentLeech'
|
||||
|
||||
|
||||
class TorrentLeech(MovieProvider, Base):
|
||||
|
||||
cat_ids = [
|
||||
([13], ['720p', '1080p', 'bd50']),
|
||||
([8], ['cam']),
|
||||
([9], ['ts', 'tc']),
|
||||
([10], ['r5', 'scr']),
|
||||
([11], ['dvdrip']),
|
||||
([13, 14], ['brrip']),
|
||||
([12], ['dvdr']),
|
||||
]
|
||||
|
||||
def buildUrl(self, title, media, quality):
|
||||
return (
|
||||
tryUrlencode(title.replace(':', '')),
|
||||
','.join([str(x) for x in self.getCatId(quality)])
|
||||
)
|
||||
@@ -22,8 +22,8 @@ class TorrentShack(MovieProvider, Base):
|
||||
# Movies-SD Pack - 983 (not included)
|
||||
|
||||
cat_ids = [
|
||||
([970], ['bd50']),
|
||||
([300], ['720p', '1080p']),
|
||||
([970, 320], ['bd50']),
|
||||
([300, 320], ['720p', '1080p']),
|
||||
([350], ['dvdr']),
|
||||
([400], ['brrip', 'dvdrip']),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
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.movie.providers.base import MovieProvider
|
||||
|
||||
@@ -11,5 +10,5 @@ autoload = 'Torrentz'
|
||||
|
||||
class Torrentz(MovieProvider, Base):
|
||||
|
||||
def buildUrl(self, media):
|
||||
return tryUrlencode('"%s"' % fireEvent('library.query', media, single = True))
|
||||
def buildUrl(self, title, media, quality):
|
||||
return tryUrlencode('"%s %s"' % (title, media['info']['year']))
|
||||
@@ -12,7 +12,7 @@ autoload = 'RottenTomatoes'
|
||||
|
||||
class RottenTomatoes(UserscriptBase):
|
||||
|
||||
includes = ['*://www.rottentomatoes.com/m/*/']
|
||||
includes = ['*://www.rottentomatoes.com/m/*']
|
||||
excludes = ['*://www.rottentomatoes.com/m/*/*/']
|
||||
|
||||
version = 2
|
||||
|
||||
@@ -394,8 +394,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
log.info('Trying next release for: %s', getTitle(media))
|
||||
self.single(media, manual = manual, force_download = force_download)
|
||||
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
except:
|
||||
log.error('Failed searching for next release: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
@@ -51,8 +51,8 @@ var SuggestList = new Class({
|
||||
self.show();
|
||||
else
|
||||
self.hide();
|
||||
|
||||
self.fireEvent('created');
|
||||
|
||||
self.fireEvent.delay(0, self, 'created');
|
||||
|
||||
},
|
||||
|
||||
@@ -60,16 +60,16 @@ var SuggestList = new Class({
|
||||
|
||||
var self = this;
|
||||
|
||||
if(!json || json.count == 0){
|
||||
if(!json || json.count === 0){
|
||||
self.el.hide();
|
||||
}
|
||||
else {
|
||||
|
||||
Object.each(json.suggestions, function(movie){
|
||||
|
||||
var m = new Block.Search.MovieItem(movie, {
|
||||
var m = new BlockSearchMovieItem(movie, {
|
||||
'onAdded': function(){
|
||||
self.afterAdded(m, movie)
|
||||
self.afterAdded(m, movie);
|
||||
}
|
||||
});
|
||||
m.data_container.grab(
|
||||
@@ -114,7 +114,7 @@ var SuggestList = new Class({
|
||||
'text': plot,
|
||||
'events': {
|
||||
'click': function(){
|
||||
this.toggleClass('full')
|
||||
this.toggleClass('full');
|
||||
}
|
||||
}
|
||||
}) : null
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
from couchpotato.core.media import MediaBase
|
||||
|
||||
|
||||
class ShowTypeBase(MediaBase):
|
||||
_type = 'show'
|
||||
|
||||
def getType(self):
|
||||
if hasattr(self, 'type') and self.type != self._type:
|
||||
return '%s.%s' % (self._type, self.type)
|
||||
|
||||
return self._type
|
||||
@@ -1,4 +0,0 @@
|
||||
from .main import ShowBase
|
||||
|
||||
def autoload():
|
||||
return ShowBase()
|
||||
@@ -1,109 +0,0 @@
|
||||
from couchpotato import get_db
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.media import MediaBase
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Episode'
|
||||
|
||||
|
||||
class Episode(MediaBase):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('show.episode.add', self.add)
|
||||
addEvent('show.episode.update', self.update)
|
||||
addEvent('show.episode.update_extras', self.updateExtras)
|
||||
|
||||
def add(self, parent_id, info = None, update_after = True, status = None):
|
||||
if not info: info = {}
|
||||
|
||||
identifiers = info.pop('identifiers', None)
|
||||
|
||||
if not identifiers:
|
||||
log.warning('Unable to add episode, missing identifiers (info provider mismatch?)')
|
||||
return
|
||||
|
||||
# Add Season
|
||||
episode_info = {
|
||||
'_t': 'media',
|
||||
'type': 'show.episode',
|
||||
'identifiers': identifiers,
|
||||
'status': status if status else 'active',
|
||||
'parent_id': parent_id,
|
||||
'info': info, # Returned dict by providers
|
||||
}
|
||||
|
||||
# Check if season already exists
|
||||
existing_episode = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True)
|
||||
|
||||
db = get_db()
|
||||
|
||||
if existing_episode:
|
||||
s = existing_episode['doc']
|
||||
s.update(episode_info)
|
||||
|
||||
episode = db.update(s)
|
||||
else:
|
||||
episode = db.insert(episode_info)
|
||||
|
||||
# Update library info
|
||||
if update_after is not False:
|
||||
handle = fireEventAsync if update_after is 'async' else fireEvent
|
||||
handle('show.episode.update_extras', episode, info, store = True, single = True)
|
||||
|
||||
return episode
|
||||
|
||||
def update(self, media_id = None, identifiers = None, info = None):
|
||||
if not info: info = {}
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
db = get_db()
|
||||
|
||||
episode = db.get('id', media_id)
|
||||
|
||||
# Get new info
|
||||
if not info:
|
||||
season = db.get('id', episode['parent_id'])
|
||||
show = db.get('id', season['parent_id'])
|
||||
|
||||
info = fireEvent(
|
||||
'episode.info', show.get('identifiers'), {
|
||||
'season_identifiers': season.get('identifiers'),
|
||||
'season_number': season.get('info', {}).get('number'),
|
||||
|
||||
'episode_identifiers': episode.get('identifiers'),
|
||||
'episode_number': episode.get('info', {}).get('number'),
|
||||
|
||||
'absolute_number': episode.get('info', {}).get('absolute_number')
|
||||
},
|
||||
merge = True
|
||||
)
|
||||
|
||||
info['season_number'] = season.get('info', {}).get('number')
|
||||
|
||||
identifiers = info.pop('identifiers', None) or identifiers
|
||||
|
||||
# Update/create media
|
||||
episode['identifiers'].update(identifiers)
|
||||
episode.update({'info': info})
|
||||
|
||||
self.updateExtras(episode, info)
|
||||
|
||||
db.update(episode)
|
||||
return episode
|
||||
|
||||
def updateExtras(self, episode, info, store=False):
|
||||
db = get_db()
|
||||
|
||||
# Get images
|
||||
image_urls = info.get('images', [])
|
||||
existing_files = episode.get('files', {})
|
||||
self.getPoster(image_urls, existing_files)
|
||||
|
||||
if store:
|
||||
db.update(episode)
|
||||
@@ -1,291 +0,0 @@
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from couchpotato import get_db
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.variable import getTitle, find
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media import MediaBase
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class ShowBase(MediaBase):
|
||||
|
||||
_type = 'show'
|
||||
|
||||
def __init__(self):
|
||||
super(ShowBase, self).__init__()
|
||||
self.initType()
|
||||
|
||||
addApiView('show.add', self.addView, docs = {
|
||||
'desc': 'Add new show to the wanted list',
|
||||
'params': {
|
||||
'identifier': {'desc': 'IMDB id of the show your want to add.'},
|
||||
'profile_id': {'desc': 'ID of quality profile you want the add the show in. If empty will use the default profile.'},
|
||||
'category_id': {'desc': 'ID of category you want the add the show in.'},
|
||||
'title': {'desc': 'Title of the show to use for search and renaming'},
|
||||
}
|
||||
})
|
||||
|
||||
addEvent('show.add', self.add)
|
||||
addEvent('show.update', self.update)
|
||||
addEvent('show.update_extras', self.updateExtras)
|
||||
|
||||
def addView(self, **kwargs):
|
||||
add_dict = self.add(params = kwargs)
|
||||
|
||||
return {
|
||||
'success': True if add_dict else False,
|
||||
'show': add_dict,
|
||||
}
|
||||
|
||||
def add(self, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None):
|
||||
if not params: params = {}
|
||||
|
||||
# Identifiers
|
||||
if not params.get('identifiers'):
|
||||
msg = 'Can\'t add show without at least 1 identifier.'
|
||||
log.error(msg)
|
||||
fireEvent('notify.frontend', type = 'show.no_identifier', message = msg)
|
||||
return False
|
||||
|
||||
info = params.get('info')
|
||||
if not info or (info and len(info.get('titles', [])) == 0):
|
||||
info = fireEvent('show.info', merge = True, identifiers = params.get('identifiers'))
|
||||
|
||||
# Add Show
|
||||
try:
|
||||
m, added = self.create(info, params, force_readd, search_after, update_after)
|
||||
|
||||
result = fireEvent('media.get', m['_id'], single = True)
|
||||
|
||||
if added and notify_after:
|
||||
if params.get('title'):
|
||||
message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')
|
||||
else:
|
||||
title = getTitle(m)
|
||||
if title:
|
||||
message = 'Successfully added "%s" to your wanted list.' % title
|
||||
else:
|
||||
message = 'Successfully added to your wanted list.'
|
||||
|
||||
fireEvent('notify.frontend', type = 'show.added', data = result, message = message)
|
||||
|
||||
return result
|
||||
except:
|
||||
log.error('Failed adding media: %s', traceback.format_exc())
|
||||
|
||||
def create(self, info, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None):
|
||||
# Set default title
|
||||
def_title = self.getDefaultTitle(info)
|
||||
|
||||
# Default profile and category
|
||||
default_profile = {}
|
||||
if not params.get('profile_id'):
|
||||
default_profile = fireEvent('profile.default', single = True)
|
||||
|
||||
cat_id = params.get('category_id')
|
||||
|
||||
media = {
|
||||
'_t': 'media',
|
||||
'type': 'show',
|
||||
'title': def_title,
|
||||
'identifiers': info.get('identifiers'),
|
||||
'status': status if status else 'active',
|
||||
'profile_id': params.get('profile_id', default_profile.get('_id')),
|
||||
'category_id': cat_id if cat_id is not None and len(cat_id) > 0 and cat_id != '-1' else None
|
||||
}
|
||||
|
||||
identifiers = info.pop('identifiers', {})
|
||||
seasons = info.pop('seasons', {})
|
||||
|
||||
# Update media with info
|
||||
self.updateInfo(media, info)
|
||||
|
||||
existing_show = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True)
|
||||
|
||||
db = get_db()
|
||||
|
||||
if existing_show:
|
||||
s = existing_show['doc']
|
||||
s.update(media)
|
||||
|
||||
show = db.update(s)
|
||||
else:
|
||||
show = db.insert(media)
|
||||
|
||||
# Update dict to be usable
|
||||
show.update(media)
|
||||
|
||||
added = True
|
||||
do_search = False
|
||||
search_after = search_after and self.conf('search_on_add', section = 'showsearcher')
|
||||
onComplete = None
|
||||
|
||||
if existing_show:
|
||||
if search_after:
|
||||
onComplete = self.createOnComplete(show['_id'])
|
||||
|
||||
search_after = False
|
||||
elif force_readd:
|
||||
# Clean snatched history
|
||||
for release in fireEvent('release.for_media', show['_id'], single = True):
|
||||
if release.get('status') in ['downloaded', 'snatched', 'done']:
|
||||
if params.get('ignore_previous', False):
|
||||
release['status'] = 'ignored'
|
||||
db.update(release)
|
||||
else:
|
||||
fireEvent('release.delete', release['_id'], single = True)
|
||||
|
||||
show['profile_id'] = params.get('profile_id', default_profile.get('id'))
|
||||
show['category_id'] = media.get('category_id')
|
||||
show['last_edit'] = int(time.time())
|
||||
|
||||
do_search = True
|
||||
db.update(show)
|
||||
else:
|
||||
params.pop('info', None)
|
||||
log.debug('Show already exists, not updating: %s', params)
|
||||
added = False
|
||||
|
||||
# Create episodes
|
||||
self.createEpisodes(show, seasons)
|
||||
|
||||
# Trigger update info
|
||||
if added and update_after:
|
||||
# Do full update to get images etc
|
||||
fireEventAsync('show.update_extras', show.copy(), info, store = True, on_complete = onComplete)
|
||||
|
||||
# Remove releases
|
||||
for rel in fireEvent('release.for_media', show['_id'], single = True):
|
||||
if rel['status'] is 'available':
|
||||
db.delete(rel)
|
||||
|
||||
if do_search and search_after:
|
||||
onComplete = self.createOnComplete(show['_id'])
|
||||
onComplete()
|
||||
|
||||
return show, added
|
||||
|
||||
def createEpisodes(self, m, seasons_info):
|
||||
# Add Seasons
|
||||
for season_nr in seasons_info:
|
||||
season_info = seasons_info[season_nr]
|
||||
episodes = season_info.get('episodes', {})
|
||||
|
||||
season = fireEvent('show.season.add', m.get('_id'), season_info, update_after = False, single = True)
|
||||
|
||||
# Add Episodes
|
||||
for episode_nr in episodes:
|
||||
episode_info = episodes[episode_nr]
|
||||
episode_info['season_number'] = season_nr
|
||||
|
||||
fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True)
|
||||
|
||||
def update(self, media_id = None, media = None, identifiers = None, info = None):
|
||||
"""
|
||||
Update movie information inside media['doc']['info']
|
||||
|
||||
@param media_id: document id
|
||||
@param identifiers: identifiers from multiple providers
|
||||
{
|
||||
'thetvdb': 123,
|
||||
'imdb': 'tt123123',
|
||||
..
|
||||
}
|
||||
@param extended: update with extended info (parses more info, actors, images from some info providers)
|
||||
@return: dict, with media
|
||||
"""
|
||||
|
||||
if not info: info = {}
|
||||
if not identifiers: identifiers = {}
|
||||
|
||||
db = get_db()
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
if media is None and media_id:
|
||||
media = db.get('id', media_id)
|
||||
else:
|
||||
log.error('missing "media" and "media_id" parameters, unable to update')
|
||||
return
|
||||
|
||||
if not info:
|
||||
info = fireEvent('show.info', identifiers = media.get('identifiers'), merge = True)
|
||||
|
||||
try:
|
||||
identifiers = info.pop('identifiers', {})
|
||||
seasons = info.pop('seasons', {})
|
||||
|
||||
self.updateInfo(media, info)
|
||||
self.updateEpisodes(media, seasons)
|
||||
self.updateExtras(media, info)
|
||||
|
||||
db.update(media)
|
||||
return media
|
||||
except:
|
||||
log.error('Failed update media: %s', traceback.format_exc())
|
||||
|
||||
return {}
|
||||
|
||||
def updateInfo(self, media, info):
|
||||
db = get_db()
|
||||
|
||||
# Remove season info for later use (save separately)
|
||||
info.pop('in_wanted', None)
|
||||
info.pop('in_library', None)
|
||||
|
||||
if not info or len(info) == 0:
|
||||
log.error('Could not update, no show info to work with: %s', media.get('identifier'))
|
||||
return False
|
||||
|
||||
# Update basic info
|
||||
media['info'] = info
|
||||
|
||||
def updateEpisodes(self, media, seasons):
|
||||
# Fetch current season/episode tree
|
||||
show_tree = fireEvent('library.tree', media_id = media['_id'], single = True)
|
||||
|
||||
# Update seasons
|
||||
for season_num in seasons:
|
||||
season_info = seasons[season_num]
|
||||
episodes = season_info.get('episodes', {})
|
||||
|
||||
# Find season that matches number
|
||||
season = find(lambda s: s.get('info', {}).get('number', 0) == season_num, show_tree.get('seasons', []))
|
||||
|
||||
if not season:
|
||||
log.warning('Unable to find season "%s"', season_num)
|
||||
continue
|
||||
|
||||
# Update season
|
||||
fireEvent('show.season.update', season['_id'], info = season_info, single = True)
|
||||
|
||||
# Update episodes
|
||||
for episode_num in episodes:
|
||||
episode_info = episodes[episode_num]
|
||||
episode_info['season_number'] = season_num
|
||||
|
||||
# Find episode that matches number
|
||||
episode = find(lambda s: s.get('info', {}).get('number', 0) == episode_num, season.get('episodes', []))
|
||||
|
||||
if not episode:
|
||||
log.debug('Creating new episode %s in season %s', (episode_num, season_num))
|
||||
fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True)
|
||||
continue
|
||||
|
||||
fireEvent('show.episode.update', episode['_id'], info = episode_info, single = True)
|
||||
|
||||
def updateExtras(self, media, info, store=False):
|
||||
db = get_db()
|
||||
|
||||
# Update image file
|
||||
image_urls = info.get('images', [])
|
||||
self.getPoster(media, image_urls)
|
||||
|
||||
if store:
|
||||
db.update(media)
|
||||
@@ -1,94 +0,0 @@
|
||||
from couchpotato import get_db
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.media import MediaBase
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Season'
|
||||
|
||||
|
||||
class Season(MediaBase):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('show.season.add', self.add)
|
||||
addEvent('show.season.update', self.update)
|
||||
addEvent('show.season.update_extras', self.updateExtras)
|
||||
|
||||
def add(self, parent_id, info = None, update_after = True, status = None):
|
||||
if not info: info = {}
|
||||
|
||||
identifiers = info.pop('identifiers', None)
|
||||
info.pop('episodes', None)
|
||||
|
||||
# Add Season
|
||||
season_info = {
|
||||
'_t': 'media',
|
||||
'type': 'show.season',
|
||||
'identifiers': identifiers,
|
||||
'status': status if status else 'active',
|
||||
'parent_id': parent_id,
|
||||
'info': info, # Returned dict by providers
|
||||
}
|
||||
|
||||
# Check if season already exists
|
||||
existing_season = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True)
|
||||
|
||||
db = get_db()
|
||||
|
||||
if existing_season:
|
||||
s = existing_season['doc']
|
||||
s.update(season_info)
|
||||
|
||||
season = db.update(s)
|
||||
else:
|
||||
season = db.insert(season_info)
|
||||
|
||||
# Update library info
|
||||
if update_after is not False:
|
||||
handle = fireEventAsync if update_after is 'async' else fireEvent
|
||||
handle('show.season.update_extras', season, info, store = True, single = True)
|
||||
|
||||
return season
|
||||
|
||||
def update(self, media_id = None, identifiers = None, info = None):
|
||||
if not info: info = {}
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
db = get_db()
|
||||
|
||||
season = db.get('id', media_id)
|
||||
show = db.get('id', season['parent_id'])
|
||||
|
||||
# Get new info
|
||||
if not info:
|
||||
info = fireEvent('season.info', show.get('identifiers'), {
|
||||
'season_number': season.get('info', {}).get('number', 0)
|
||||
}, merge = True)
|
||||
|
||||
identifiers = info.pop('identifiers', None) or identifiers
|
||||
info.pop('episodes', None)
|
||||
|
||||
# Update/create media
|
||||
season['identifiers'].update(identifiers)
|
||||
season.update({'info': info})
|
||||
|
||||
self.updateExtras(season, info)
|
||||
|
||||
db.update(season)
|
||||
return season
|
||||
|
||||
def updateExtras(self, season, info, store=False):
|
||||
db = get_db()
|
||||
|
||||
# Get images
|
||||
image_urls = info.get('images', [])
|
||||
existing_files = season.get('files', {})
|
||||
self.getPoster(image_urls, existing_files)
|
||||
|
||||
if store:
|
||||
db.update(season)
|
||||
@@ -1,28 +0,0 @@
|
||||
Page.Shows = new Class({
|
||||
|
||||
Extends: PageBase,
|
||||
|
||||
name: 'shows',
|
||||
title: 'Gimmy gimmy gimmy!',
|
||||
folder_browser: null,
|
||||
|
||||
indexAction: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.wanted){
|
||||
|
||||
// Wanted movies
|
||||
self.wanted = new ShowList({
|
||||
'identifier': 'wanted',
|
||||
'status': 'active',
|
||||
'type': 'show',
|
||||
'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Edit, MA.Refresh, MA.Readd, MA.Delete],
|
||||
'add_new': true,
|
||||
'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted')
|
||||
});
|
||||
$(self.wanted).inject(self.el);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,474 +0,0 @@
|
||||
var EpisodeAction = new Class({
|
||||
|
||||
Implements: [Options],
|
||||
|
||||
class_name: 'item-action icon2',
|
||||
|
||||
initialize: function(episode, options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.show = episode.show;
|
||||
self.episode = episode;
|
||||
|
||||
self.create();
|
||||
if(self.el)
|
||||
self.el.addClass(self.class_name)
|
||||
},
|
||||
|
||||
create: function(){},
|
||||
|
||||
disable: function(){
|
||||
if(this.el)
|
||||
this.el.addClass('disable')
|
||||
},
|
||||
|
||||
enable: function(){
|
||||
if(this.el)
|
||||
this.el.removeClass('disable')
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
return self.show.getTitle();
|
||||
}
|
||||
catch(e){
|
||||
try {
|
||||
return self.show.original_title ? self.show.original_title : self.show.titles[0];
|
||||
}
|
||||
catch(e){
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key){
|
||||
var self = this;
|
||||
try {
|
||||
return self.show.get(key)
|
||||
}
|
||||
catch(e){
|
||||
return self.show[key]
|
||||
}
|
||||
},
|
||||
|
||||
createMask: function(){
|
||||
var self = this;
|
||||
self.mask = new Element('div.mask', {
|
||||
'styles': {
|
||||
'z-index': '1'
|
||||
}
|
||||
}).inject(self.show, 'top').fade('hide');
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el || null
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var EA = {};
|
||||
|
||||
EA.IMDB = new Class({
|
||||
|
||||
Extends: EpisodeAction,
|
||||
id: null,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.id = self.show.getIdentifier ? self.show.getIdentifier() : self.get('imdb');
|
||||
|
||||
self.el = new Element('a.imdb', {
|
||||
'title': 'Go to the IMDB page of ' + self.getTitle(),
|
||||
'href': 'http://www.imdb.com/title/'+self.id+'/',
|
||||
'target': '_blank'
|
||||
});
|
||||
|
||||
if(!self.id) self.disable();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
EA.Release = new Class({
|
||||
|
||||
Extends: EpisodeAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.releases.download', {
|
||||
'title': 'Show the releases that are available for ' + self.getTitle(),
|
||||
'events': {
|
||||
'click': self.toggle.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
self.options = new Element('div.episode-options').inject(self.episode.el);
|
||||
|
||||
if(!self.episode.data.releases || self.episode.data.releases.length == 0)
|
||||
self.el.hide();
|
||||
else
|
||||
self.showHelper();
|
||||
|
||||
App.on('show.searcher.ended', function(notification){
|
||||
if(self.show.data._id != notification.data._id) return;
|
||||
|
||||
self.releases = null;
|
||||
if(self.options_container){
|
||||
self.options_container.destroy();
|
||||
self.options_container = null;
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
toggle: function(e){
|
||||
var self = this;
|
||||
|
||||
if(self.options && self.options.hasClass('expanded')) {
|
||||
self.close();
|
||||
} else {
|
||||
self.open();
|
||||
}
|
||||
},
|
||||
|
||||
open: function(e){
|
||||
var self = this;
|
||||
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.createReleases();
|
||||
|
||||
},
|
||||
|
||||
close: function(e) {
|
||||
var self = this;
|
||||
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.options.setStyle('height', 0)
|
||||
.removeClass('expanded');
|
||||
},
|
||||
|
||||
createReleases: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.releases_table){
|
||||
self.options.adopt(
|
||||
self.releases_table = new Element('div.releases.table')
|
||||
);
|
||||
|
||||
// Header
|
||||
new Element('div.item.head').adopt(
|
||||
new Element('span.name', {'text': 'Release name'}),
|
||||
new Element('span.status', {'text': 'Status'}),
|
||||
new Element('span.quality', {'text': 'Quality'}),
|
||||
new Element('span.size', {'text': 'Size'}),
|
||||
new Element('span.age', {'text': 'Age'}),
|
||||
new Element('span.score', {'text': 'Score'}),
|
||||
new Element('span.provider', {'text': 'Provider'})
|
||||
).inject(self.releases_table);
|
||||
|
||||
if(self.episode.data.releases)
|
||||
self.episode.data.releases.each(function(release){
|
||||
|
||||
var quality = Quality.getQuality(release.quality) || {},
|
||||
info = release.info || {},
|
||||
provider = self.get(release, 'provider') + (info['provider_extra'] ? self.get(release, 'provider_extra') : '');
|
||||
|
||||
var release_name = self.get(release, 'name');
|
||||
if(release.files && release.files.length > 0){
|
||||
try {
|
||||
var movie_file = release.files.filter(function(file){
|
||||
var type = File.Type.get(file.type_id);
|
||||
return type && type.identifier == 'movie'
|
||||
}).pick();
|
||||
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
|
||||
}
|
||||
catch(e){}
|
||||
}
|
||||
|
||||
// Create release
|
||||
release['el'] = new Element('div', {
|
||||
'class': 'item '+release.status,
|
||||
'id': 'release_'+release._id
|
||||
}).adopt(
|
||||
new Element('span.name', {'text': release_name, 'title': release_name}),
|
||||
new Element('span.status', {'text': release.status, 'class': 'status '+release.status}),
|
||||
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.age', {'text': self.get(release, 'age')}),
|
||||
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||
new Element('span.provider', { 'text': provider, 'title': provider }),
|
||||
info['detail_url'] ? new Element('a.info.icon2', {
|
||||
'href': info['detail_url'],
|
||||
'target': '_blank'
|
||||
}) : new Element('a'),
|
||||
new Element('a.download.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
if(!this.hasClass('completed'))
|
||||
self.download(release);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('a.delete.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
self.ignore(release);
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.releases_table);
|
||||
|
||||
if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){
|
||||
if(!self.last_release || (self.last_release && self.last_release.status != 'snatched' && release.status == 'snatched'))
|
||||
self.last_release = release;
|
||||
}
|
||||
else if(!self.next_release && release.status == 'available'){
|
||||
self.next_release = release;
|
||||
}
|
||||
|
||||
var update_handle = function(notification) {
|
||||
if(notification.data._id != release._id) return;
|
||||
|
||||
var q = self.show.quality.getElement('.q_' + release.quality),
|
||||
new_status = notification.data.status;
|
||||
|
||||
release.el.set('class', 'item ' + new_status);
|
||||
|
||||
var status_el = release.el.getElement('.release_status');
|
||||
status_el.set('class', 'release_status ' + new_status);
|
||||
status_el.set('text', new_status);
|
||||
|
||||
if(!q && (new_status == 'snatched' || new_status == 'seeding' || new_status == 'done'))
|
||||
q = self.addQuality(release.quality_id);
|
||||
|
||||
if(q && !q.hasClass(new_status)) {
|
||||
q.removeClass(release.status).addClass(new_status);
|
||||
q.set('title', q.get('title').replace(release.status, new_status));
|
||||
}
|
||||
};
|
||||
|
||||
App.on('release.update_status', update_handle);
|
||||
|
||||
});
|
||||
|
||||
if(self.last_release)
|
||||
self.releases_table.getElements('#release_'+self.last_release._id).addClass('last_release');
|
||||
|
||||
if(self.next_release)
|
||||
self.releases_table.getElements('#release_'+self.next_release._id).addClass('next_release');
|
||||
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status) === false)){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.try_container').inject(self.releases_table, 'top');
|
||||
|
||||
var nr = self.next_release,
|
||||
lr = self.last_release;
|
||||
|
||||
self.trynext_container.adopt(
|
||||
new Element('span.or', {
|
||||
'text': 'If anything went wrong, download'
|
||||
}),
|
||||
lr ? new Element('a.button.orange', {
|
||||
'text': 'the same release again',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.download(lr);
|
||||
}
|
||||
}
|
||||
}) : null,
|
||||
nr && lr ? new Element('span.or', {
|
||||
'text': ','
|
||||
}) : null,
|
||||
nr ? [new Element('a.button.green', {
|
||||
'text': lr ? 'another release' : 'the best release',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.download(nr);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('span.or', {
|
||||
'text': 'or pick one below'
|
||||
})] : null
|
||||
)
|
||||
}
|
||||
|
||||
self.last_release = null;
|
||||
self.next_release = null;
|
||||
|
||||
self.episode.el.addEvent('outerClick', function(){
|
||||
self.close();
|
||||
});
|
||||
}
|
||||
|
||||
self.options.setStyle('height', self.releases_table.getSize().y)
|
||||
.addClass('expanded');
|
||||
|
||||
},
|
||||
|
||||
showHelper: function(e){
|
||||
var self = this;
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
var has_available = false,
|
||||
has_snatched = false;
|
||||
|
||||
if(self.episode.data.releases)
|
||||
self.episode.data.releases.each(function(release){
|
||||
if(has_available && has_snatched) return;
|
||||
|
||||
if(['snatched', 'downloaded', 'seeding'].contains(release.status))
|
||||
has_snatched = true;
|
||||
|
||||
if(['available'].contains(release.status))
|
||||
has_available = true;
|
||||
|
||||
});
|
||||
|
||||
if(has_available || has_snatched){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.trynext').inject(self.show.info_container);
|
||||
|
||||
self.trynext_container.adopt(
|
||||
has_available ? [new Element('a.icon2.readd', {
|
||||
'text': has_snatched ? 'Download another release' : 'Download the best release',
|
||||
'events': {
|
||||
'click': self.tryNextRelease.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('a.icon2.download', {
|
||||
'text': 'pick one yourself',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.show.quality.fireEvent('click');
|
||||
}
|
||||
}
|
||||
})] : null,
|
||||
new Element('a.icon2.completed', {
|
||||
'text': 'mark this movie done',
|
||||
'events': {
|
||||
'click': self.markMovieDone.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
get: function(release, type){
|
||||
return (release.info && release.info[type] !== undefined) ? release.info[type] : 'n/a'
|
||||
},
|
||||
|
||||
download: function(release){
|
||||
var self = this;
|
||||
|
||||
var release_el = self.releases_table.getElement('#release_'+release._id),
|
||||
icon = release_el.getElement('.download.icon2');
|
||||
|
||||
if(icon)
|
||||
icon.addClass('icon spinner').removeClass('download');
|
||||
|
||||
Api.request('release.manual_download', {
|
||||
'data': {
|
||||
'id': release._id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
if(icon)
|
||||
icon.removeClass('icon spinner');
|
||||
|
||||
if(json.success){
|
||||
if(icon)
|
||||
icon.addClass('completed');
|
||||
release_el.getElement('.release_status').set('text', 'snatched');
|
||||
}
|
||||
else
|
||||
if(icon)
|
||||
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
ignore: function(release){
|
||||
|
||||
Api.request('release.ignore', {
|
||||
'data': {
|
||||
'id': release._id
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
markMovieDone: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('media.delete', {
|
||||
'data': {
|
||||
'id': self.show.get('_id'),
|
||||
'delete_from': 'wanted'
|
||||
},
|
||||
'onComplete': function(){
|
||||
var movie = $(self.show);
|
||||
movie.set('tween', {
|
||||
'duration': 300,
|
||||
'onComplete': function(){
|
||||
self.show.destroy()
|
||||
}
|
||||
});
|
||||
movie.tween('height', 0);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
tryNextRelease: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('movie.searcher.try_next', {
|
||||
'data': {
|
||||
'media_id': self.show.get('_id')
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
EA.Refresh = new Class({
|
||||
|
||||
Extends: EpisodeAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.refresh', {
|
||||
'title': 'Refresh the movie info and do a forced search',
|
||||
'events': {
|
||||
'click': self.doRefresh.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
doRefresh: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
Api.request('media.refresh', {
|
||||
'data': {
|
||||
'id': self.episode.get('_id')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
var Episode = new Class({
|
||||
|
||||
Extends: BlockBase,
|
||||
|
||||
action: {},
|
||||
|
||||
initialize: function(show, options, data){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.show = show;
|
||||
self.options = options;
|
||||
self.data = data;
|
||||
|
||||
self.profile = self.show.profile;
|
||||
|
||||
self.el = new Element('div.item.episode').adopt(
|
||||
self.detail = new Element('div.item.data')
|
||||
);
|
||||
|
||||
self.create();
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.detail.set('id', 'episode_'+self.data._id);
|
||||
|
||||
self.detail.adopt(
|
||||
new Element('span.episode', {'text': (self.data.info.number || 0)}),
|
||||
new Element('span.name', {'text': self.getTitle()}),
|
||||
new Element('span.firstaired', {'text': self.data.info.firstaired}),
|
||||
|
||||
self.quality = new Element('span.quality', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.detail.getElement('.item-actions .releases');
|
||||
|
||||
if(releases.isVisible())
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.actions = new Element('div.item-actions')
|
||||
);
|
||||
|
||||
// Add profile
|
||||
if(self.profile.data) {
|
||||
self.profile.getTypes().each(function(type){
|
||||
var q = self.addQuality(type.get('quality'), type.get('3d'));
|
||||
|
||||
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
|
||||
q.addClass('finish');
|
||||
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add releases
|
||||
self.updateReleases();
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
|
||||
if(action.el)
|
||||
self.actions.adopt(action)
|
||||
});
|
||||
},
|
||||
|
||||
updateReleases: function(){
|
||||
var self = this;
|
||||
if(!self.data.releases || self.data.releases.length == 0) return;
|
||||
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
|
||||
status = release.status;
|
||||
|
||||
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
|
||||
q = self.addQuality(release.quality, release.is_3d || false);
|
||||
|
||||
if (q && !q.hasClass(status)){
|
||||
q.addClass(status);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
addQuality: function(quality, is_3d){
|
||||
var self = this,
|
||||
q = Quality.getQuality(quality);
|
||||
|
||||
return new Element('span', {
|
||||
'text': q.label + (is_3d ? ' 3D' : ''),
|
||||
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
|
||||
'title': ''
|
||||
}).inject(self.quality);
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
var title = '';
|
||||
|
||||
if(self.data.info.titles && self.data.info.titles.length > 0) {
|
||||
title = self.data.info.titles[0];
|
||||
} else {
|
||||
title = 'Episode ' + self.data.info.number;
|
||||
}
|
||||
|
||||
return title;
|
||||
},
|
||||
|
||||
getIdentifier: function(){
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
return self.get('identifiers').imdb;
|
||||
}
|
||||
catch (e){ }
|
||||
|
||||
return self.get('imdb');
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr] || this.data.info[attr]
|
||||
}
|
||||
});
|
||||
@@ -1,636 +0,0 @@
|
||||
var ShowList = new Class({
|
||||
|
||||
Implements: [Events, Options],
|
||||
|
||||
options: {
|
||||
navigation: true,
|
||||
limit: 50,
|
||||
load_more: true,
|
||||
loader: true,
|
||||
menu: [],
|
||||
add_new: false,
|
||||
force_view: false
|
||||
},
|
||||
|
||||
movies: [],
|
||||
movies_added: {},
|
||||
total_movies: 0,
|
||||
letters: {},
|
||||
filter: null,
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.offset = 0;
|
||||
self.filter = self.options.filter || {
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
};
|
||||
|
||||
self.el = new Element('div.shows').adopt(
|
||||
self.title = self.options.title ? new Element('h2', {
|
||||
'text': self.options.title,
|
||||
'styles': {'display': 'none'}
|
||||
}) : null,
|
||||
self.description = self.options.description ? new Element('div.description', {
|
||||
'html': self.options.description,
|
||||
'styles': {'display': 'none'}
|
||||
}) : null,
|
||||
self.movie_list = new Element('div.list'),
|
||||
self.load_more = self.options.load_more ? new Element('a.load_more', {
|
||||
'events': {
|
||||
'click': self.loadMore.bind(self)
|
||||
}
|
||||
}) : null
|
||||
);
|
||||
|
||||
if($(window).getSize().x <= 480 && !self.options.force_view)
|
||||
self.changeView('list');
|
||||
else
|
||||
self.changeView(self.getSavedView() || self.options.view || 'details');
|
||||
|
||||
self.getMovies();
|
||||
|
||||
App.on('movie.added', self.movieAdded.bind(self));
|
||||
App.on('movie.deleted', self.movieDeleted.bind(self))
|
||||
},
|
||||
|
||||
movieDeleted: function(notification){
|
||||
var self = this;
|
||||
|
||||
if(self.movies_added[notification.data._id]){
|
||||
self.movies.each(function(movie){
|
||||
if(movie.get('_id') == notification.data._id){
|
||||
movie.destroy();
|
||||
delete self.movies_added[notification.data._id];
|
||||
self.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.checkIfEmpty();
|
||||
},
|
||||
|
||||
movieAdded: function(notification){
|
||||
var self = this;
|
||||
|
||||
self.fireEvent('movieAdded', notification);
|
||||
if(self.options.add_new && !self.movies_added[notification.data._id] && notification.data.status == self.options.status){
|
||||
window.scroll(0,0);
|
||||
self.createShow(notification.data, 'top');
|
||||
self.setCounter(self.counter_count+1);
|
||||
|
||||
self.checkIfEmpty();
|
||||
}
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
// Create the alphabet nav
|
||||
if(self.options.navigation)
|
||||
self.createNavigation();
|
||||
|
||||
if(self.options.load_more)
|
||||
self.scrollspy = new ScrollSpy({
|
||||
min: function(){
|
||||
var c = self.load_more.getCoordinates();
|
||||
return c.top - window.document.getSize().y - 300
|
||||
},
|
||||
onEnter: self.loadMore.bind(self)
|
||||
});
|
||||
|
||||
self.created = true;
|
||||
},
|
||||
|
||||
addMovies: function(movies, total){
|
||||
var self = this;
|
||||
|
||||
if(!self.created) self.create();
|
||||
|
||||
// do scrollspy
|
||||
if(movies.length < self.options.limit && self.scrollspy){
|
||||
self.load_more.hide();
|
||||
self.scrollspy.stop();
|
||||
}
|
||||
|
||||
Object.each(movies, function(movie){
|
||||
self.createShow(movie);
|
||||
});
|
||||
|
||||
self.total_movies += total;
|
||||
self.setCounter(total);
|
||||
|
||||
},
|
||||
|
||||
setCounter: function(count){
|
||||
var self = this;
|
||||
|
||||
if(!self.navigation_counter) return;
|
||||
|
||||
self.counter_count = count;
|
||||
self.navigation_counter.set('text', (count || 0) + ' shows');
|
||||
|
||||
if (self.empty_message) {
|
||||
self.empty_message.destroy();
|
||||
self.empty_message = null;
|
||||
}
|
||||
|
||||
if(self.total_movies && count == 0 && !self.empty_message){
|
||||
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
|
||||
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
|
||||
|
||||
self.empty_message = new Element('.message', {
|
||||
'html': 'No shows found ' + message + '.<br/>'
|
||||
}).grab(
|
||||
new Element('a', {
|
||||
'text': 'Reset filter',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.filter = {
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
};
|
||||
self.navigation_search_input.set('value', '');
|
||||
self.reset();
|
||||
self.activateLetter();
|
||||
self.getMovies(true);
|
||||
self.last_search_value = '';
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.movie_list);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
createShow: function(show, inject_at){
|
||||
var self = this;
|
||||
var m = new Show(self, {
|
||||
'actions': self.options.actions,
|
||||
'view': self.current_view,
|
||||
'onSelect': self.calculateSelected.bind(self)
|
||||
}, show);
|
||||
|
||||
$(m).inject(self.movie_list, inject_at || 'bottom');
|
||||
|
||||
m.fireEvent('injected');
|
||||
|
||||
self.movies.include(m);
|
||||
self.movies_added[show._id] = true;
|
||||
},
|
||||
|
||||
createNavigation: function(){
|
||||
var self = this;
|
||||
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
self.el.addClass('with_navigation');
|
||||
|
||||
self.navigation = new Element('div.alph_nav').adopt(
|
||||
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
|
||||
new Element('span.select').adopt(
|
||||
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': self.massEditToggleAll.bind(self)
|
||||
}
|
||||
}),
|
||||
self.mass_edit_selected = new Element('span.count', {'text': 0}),
|
||||
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
|
||||
),
|
||||
new Element('div.quality').adopt(
|
||||
self.mass_edit_quality = new Element('select'),
|
||||
new Element('a.button.orange', {
|
||||
'text': 'Change quality',
|
||||
'events': {
|
||||
'click': self.changeQualitySelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.delete').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.red', {
|
||||
'text': 'Delete',
|
||||
'events': {
|
||||
'click': self.deleteSelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.refresh').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.green', {
|
||||
'text': 'Refresh',
|
||||
'events': {
|
||||
'click': self.refreshSelected.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
new Element('div.menus').adopt(
|
||||
self.navigation_counter = new Element('span.counter[title=Total]'),
|
||||
self.filter_menu = new Block.Menu(self, {
|
||||
'class': 'filter'
|
||||
}),
|
||||
self.navigation_actions = new Element('ul.actions', {
|
||||
'events': {
|
||||
'click:relay(li)': function(e, el){
|
||||
var a = 'active';
|
||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||
self.changeView(el.get('data-view'));
|
||||
this.addClass(a);
|
||||
|
||||
el.inject(el.getParent(), 'top');
|
||||
el.getSiblings().hide();
|
||||
setTimeout(function(){
|
||||
el.getSiblings().setStyle('display', null);
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.navigation_menu = new Block.Menu(self, {
|
||||
'class': 'extra'
|
||||
})
|
||||
)
|
||||
).inject(self.el, 'top');
|
||||
|
||||
// Mass edit
|
||||
self.mass_edit_select_class = new Form.Check(self.mass_edit_select);
|
||||
Quality.getActiveProfiles().each(function(profile){
|
||||
new Element('option', {
|
||||
'value': profile.get('_id'),
|
||||
'text': profile.get('label')
|
||||
}).inject(self.mass_edit_quality)
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
self.navigation_search_input = new Element('input', {
|
||||
'title': 'Search through ' + self.options.identifier,
|
||||
'placeholder': 'Search through ' + self.options.identifier,
|
||||
'events': {
|
||||
'keyup': self.search.bind(self),
|
||||
'change': self.search.bind(self)
|
||||
}
|
||||
})
|
||||
).addClass('search');
|
||||
|
||||
var available_chars;
|
||||
self.filter_menu.addEvent('open', function(){
|
||||
self.navigation_search_input.focus();
|
||||
|
||||
// Get available chars and highlight
|
||||
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
|
||||
Api.request('media.available_chars', {
|
||||
'data': Object.merge({
|
||||
'type': 'show',
|
||||
'status': self.options.status
|
||||
}, self.filter),
|
||||
'onSuccess': function(json){
|
||||
available_chars = json.chars;
|
||||
|
||||
available_chars.each(function(c){
|
||||
self.letters[c.capitalize()].addClass('available')
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
self.navigation_alpha = new Element('ul.numbers', {
|
||||
'events': {
|
||||
'click:relay(li.available)': function(e, el){
|
||||
self.activateLetter(el.get('data-letter'));
|
||||
self.getMovies(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Actions
|
||||
['mass_edit', 'details', 'list'].each(function(view){
|
||||
var current = self.current_view == view;
|
||||
new Element('li', {
|
||||
'class': 'icon2 ' + view + (current ? ' active ' : ''),
|
||||
'data-view': view
|
||||
}).inject(self.navigation_actions, current ? 'top' : 'bottom');
|
||||
});
|
||||
|
||||
// All
|
||||
self.letters['all'] = new Element('li.letter_all.available.active', {
|
||||
'text': 'ALL'
|
||||
}).inject(self.navigation_alpha);
|
||||
|
||||
// Chars
|
||||
chars.split('').each(function(c){
|
||||
self.letters[c] = new Element('li', {
|
||||
'text': c,
|
||||
'class': 'letter_'+c,
|
||||
'data-letter': c
|
||||
}).inject(self.navigation_alpha);
|
||||
});
|
||||
|
||||
// Add menu or hide
|
||||
if (self.options.menu.length > 0)
|
||||
self.options.menu.each(function(menu_item){
|
||||
self.navigation_menu.addLink(menu_item);
|
||||
});
|
||||
else
|
||||
self.navigation_menu.hide();
|
||||
|
||||
},
|
||||
|
||||
calculateSelected: function(){
|
||||
var self = this;
|
||||
|
||||
var selected = 0,
|
||||
movies = self.movies.length;
|
||||
self.movies.each(function(movie){
|
||||
selected += movie.isSelected() ? 1 : 0
|
||||
});
|
||||
|
||||
var indeterminate = selected > 0 && selected < movies,
|
||||
checked = selected == movies && selected > 0;
|
||||
|
||||
self.mass_edit_select.set('indeterminate', indeterminate);
|
||||
|
||||
self.mass_edit_select_class[checked ? 'check' : 'uncheck']();
|
||||
self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate');
|
||||
|
||||
self.mass_edit_selected.set('text', selected);
|
||||
},
|
||||
|
||||
deleteSelected: function(){
|
||||
var self = this,
|
||||
ids = self.getSelectedMovies(),
|
||||
help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list';
|
||||
|
||||
var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{
|
||||
'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'),
|
||||
'class': 'delete',
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
this.set('text', 'Deleting..');
|
||||
Api.request('media.delete', {
|
||||
'method': 'post',
|
||||
'data': {
|
||||
'id': ids.join(','),
|
||||
'delete_from': self.options.identifier
|
||||
},
|
||||
'onSuccess': function(){
|
||||
qObj.close();
|
||||
|
||||
var erase_movies = [];
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected()){
|
||||
$(movie).destroy();
|
||||
erase_movies.include(movie);
|
||||
}
|
||||
});
|
||||
|
||||
erase_movies.each(function(movie){
|
||||
self.movies.erase(movie);
|
||||
movie.destroy();
|
||||
self.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
});
|
||||
|
||||
self.calculateSelected();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}, {
|
||||
'text': 'Cancel',
|
||||
'cancel': true
|
||||
}]);
|
||||
|
||||
},
|
||||
|
||||
changeQualitySelected: function(){
|
||||
var self = this;
|
||||
var ids = self.getSelectedMovies();
|
||||
|
||||
Api.request('movie.edit', {
|
||||
'method': 'post',
|
||||
'data': {
|
||||
'id': ids.join(','),
|
||||
'profile_id': self.mass_edit_quality.get('value')
|
||||
},
|
||||
'onSuccess': self.search.bind(self)
|
||||
});
|
||||
},
|
||||
|
||||
refreshSelected: function(){
|
||||
var self = this;
|
||||
var ids = self.getSelectedMovies();
|
||||
|
||||
Api.request('media.refresh', {
|
||||
'method': 'post',
|
||||
'data': {
|
||||
'id': ids.join(',')
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getSelectedMovies: function(){
|
||||
var self = this;
|
||||
|
||||
var ids = [];
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected())
|
||||
ids.include(movie.get('_id'))
|
||||
});
|
||||
|
||||
return ids
|
||||
},
|
||||
|
||||
massEditToggleAll: function(){
|
||||
var self = this;
|
||||
|
||||
var select = self.mass_edit_select.get('checked');
|
||||
|
||||
self.movies.each(function(movie){
|
||||
movie.select(select)
|
||||
});
|
||||
|
||||
self.calculateSelected()
|
||||
},
|
||||
|
||||
reset: function(){
|
||||
var self = this;
|
||||
|
||||
self.movies = [];
|
||||
if(self.mass_edit_select)
|
||||
self.calculateSelected();
|
||||
if(self.navigation_alpha)
|
||||
self.navigation_alpha.getElements('.active').removeClass('active');
|
||||
|
||||
self.offset = 0;
|
||||
if(self.scrollspy){
|
||||
//self.load_more.show();
|
||||
self.scrollspy.start();
|
||||
}
|
||||
},
|
||||
|
||||
activateLetter: function(letter){
|
||||
var self = this;
|
||||
|
||||
self.reset();
|
||||
|
||||
self.letters[letter || 'all'].addClass('active');
|
||||
self.filter.starts_with = letter;
|
||||
|
||||
},
|
||||
|
||||
changeView: function(new_view){
|
||||
var self = this;
|
||||
|
||||
self.el
|
||||
.removeClass(self.current_view+'_list')
|
||||
.addClass(new_view+'_list');
|
||||
|
||||
self.current_view = new_view;
|
||||
Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000});
|
||||
},
|
||||
|
||||
getSavedView: function(){
|
||||
var self = this;
|
||||
return Cookie.read(self.options.identifier+'_view2');
|
||||
},
|
||||
|
||||
search: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.search_timer) clearTimeout(self.search_timer);
|
||||
self.search_timer = (function(){
|
||||
var search_value = self.navigation_search_input.get('value');
|
||||
if (search_value == self.last_search_value) return;
|
||||
|
||||
self.reset();
|
||||
|
||||
self.activateLetter();
|
||||
self.filter.search = search_value;
|
||||
|
||||
self.getMovies(true);
|
||||
|
||||
self.last_search_value = search_value;
|
||||
|
||||
}).delay(250);
|
||||
|
||||
},
|
||||
|
||||
update: function(){
|
||||
var self = this;
|
||||
|
||||
self.reset();
|
||||
self.getMovies(true);
|
||||
},
|
||||
|
||||
getMovies: function(reset){
|
||||
var self = this;
|
||||
|
||||
if(self.scrollspy){
|
||||
self.scrollspy.stop();
|
||||
self.load_more.set('text', 'loading...');
|
||||
}
|
||||
|
||||
if(self.movies.length == 0 && self.options.loader){
|
||||
|
||||
self.loader_first = new Element('div.loading').adopt(
|
||||
new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
|
||||
).inject(self.el, 'top');
|
||||
|
||||
createSpinner(self.loader_first, {
|
||||
radius: 4,
|
||||
length: 4,
|
||||
width: 1
|
||||
});
|
||||
|
||||
self.el.setStyle('min-height', 93);
|
||||
|
||||
}
|
||||
|
||||
Api.request(self.options.api_call || 'media.list', {
|
||||
'data': Object.merge({
|
||||
'type': self.options.type || 'movie',
|
||||
'status': self.options.status,
|
||||
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
|
||||
}, self.filter),
|
||||
'onSuccess': function(json){
|
||||
|
||||
if(reset)
|
||||
self.movie_list.empty();
|
||||
|
||||
if(self.loader_first){
|
||||
var lf = self.loader_first;
|
||||
self.loader_first.addClass('hide');
|
||||
self.loader_first = null;
|
||||
setTimeout(function(){
|
||||
lf.destroy();
|
||||
}, 20000);
|
||||
self.el.setStyle('min-height', null);
|
||||
}
|
||||
|
||||
self.store(json.shows);
|
||||
self.addMovies(json.shows, json.total || json.shows.length);
|
||||
if(self.scrollspy) {
|
||||
self.load_more.set('text', 'load more movies');
|
||||
self.scrollspy.start();
|
||||
}
|
||||
|
||||
self.checkIfEmpty();
|
||||
self.fireEvent('loaded');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadMore: function(){
|
||||
var self = this;
|
||||
if(self.offset >= self.options.limit)
|
||||
self.getMovies()
|
||||
},
|
||||
|
||||
store: function(movies){
|
||||
var self = this;
|
||||
|
||||
self.offset += movies.length;
|
||||
|
||||
},
|
||||
|
||||
checkIfEmpty: function(){
|
||||
var self = this;
|
||||
|
||||
var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
|
||||
|
||||
if(self.title)
|
||||
self.title[is_empty ? 'hide' : 'show']();
|
||||
|
||||
if(self.description)
|
||||
self.description.setStyle('display', [is_empty ? 'none' : '']);
|
||||
|
||||
if(is_empty && self.options.on_empty_element){
|
||||
self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after');
|
||||
|
||||
if(self.navigation)
|
||||
self.navigation.hide();
|
||||
|
||||
self.empty_element = self.options.on_empty_element;
|
||||
}
|
||||
else if(self.empty_element){
|
||||
self.empty_element.destroy();
|
||||
|
||||
if(self.navigation)
|
||||
self.navigation.show();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,230 +0,0 @@
|
||||
Block.Search.ShowItem = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
initialize: function(info, options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.info = info;
|
||||
self.alternative_titles = [];
|
||||
|
||||
self.create();
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this,
|
||||
info = self.info;
|
||||
|
||||
self.el = new Element('div.media_result', {
|
||||
'id': info.id
|
||||
}).adopt(
|
||||
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||
'src': info.images.poster[0],
|
||||
'height': null,
|
||||
'width': null
|
||||
}) : null,
|
||||
self.options_el = new Element('div.options.inlay'),
|
||||
self.data_container = new Element('div.data', {
|
||||
'events': {
|
||||
'click': self.showOptions.bind(self)
|
||||
}
|
||||
}).adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
new Element('h2').adopt(
|
||||
self.title = new Element('span.title', {
|
||||
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
|
||||
}),
|
||||
self.year = info.year ? new Element('span.year', {
|
||||
'text': info.year
|
||||
}) : null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if(info.titles)
|
||||
info.titles.each(function(title){
|
||||
self.alternativeTitle({
|
||||
'title': title
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
alternativeTitle: function(alternative){
|
||||
var self = this;
|
||||
|
||||
self.alternative_titles.include(alternative);
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
try {
|
||||
return self.info.original_title ? self.info.original_title : self.info.titles[0];
|
||||
}
|
||||
catch(e){
|
||||
return 'Unknown';
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key){
|
||||
return this.info[key]
|
||||
},
|
||||
|
||||
showOptions: function(){
|
||||
var self = this;
|
||||
|
||||
self.createOptions();
|
||||
|
||||
self.data_container.addClass('open');
|
||||
self.el.addEvent('outerClick', self.closeOptions.bind(self))
|
||||
|
||||
},
|
||||
|
||||
closeOptions: function(){
|
||||
var self = this;
|
||||
|
||||
self.data_container.removeClass('open');
|
||||
self.el.removeEvents('outerClick')
|
||||
},
|
||||
|
||||
add: function(e){
|
||||
var self = this;
|
||||
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.loadingMask();
|
||||
|
||||
Api.request('show.add', {
|
||||
'data': {
|
||||
'identifiers': self.info.identifiers,
|
||||
'type': self.info.type,
|
||||
'title': self.title_select.get('value'),
|
||||
'profile_id': self.profile_select.get('value'),
|
||||
'category_id': self.category_select.get('value')
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.options_el.empty();
|
||||
self.options_el.adopt(
|
||||
new Element('div.message', {
|
||||
'text': json.success ? 'Show successfully added.' : 'Show didn\'t add properly. Check logs'
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
|
||||
self.fireEvent('added');
|
||||
},
|
||||
'onFailure': function(){
|
||||
self.options_el.empty();
|
||||
self.options_el.adopt(
|
||||
new Element('div.message', {
|
||||
'text': 'Something went wrong, check the logs for more info.'
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createOptions: function(){
|
||||
var self = this,
|
||||
info = self.info;
|
||||
|
||||
if(!self.options_el.hasClass('set')){
|
||||
|
||||
if(self.info.in_library){
|
||||
var in_library = [];
|
||||
self.info.in_library.releases.each(function(release){
|
||||
in_library.include(release.quality.label)
|
||||
});
|
||||
}
|
||||
|
||||
self.options_el.grab(
|
||||
new Element('div', {
|
||||
'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
|
||||
}).adopt(
|
||||
self.info.in_wanted && self.info.in_wanted.profile_id ? new Element('span.in_wanted', {
|
||||
'text': 'Already in wanted list: ' + Quality.getProfile(self.info.in_wanted.profile_id).get('label')
|
||||
}) : (in_library ? new Element('span.in_library', {
|
||||
'text': 'Already in library: ' + in_library.join(', ')
|
||||
}) : null),
|
||||
self.title_select = new Element('select', {
|
||||
'name': 'title'
|
||||
}),
|
||||
self.profile_select = new Element('select', {
|
||||
'name': 'profile'
|
||||
}),
|
||||
self.category_select = new Element('select', {
|
||||
'name': 'category'
|
||||
}).grab(
|
||||
new Element('option', {'value': -1, 'text': 'None'})
|
||||
),
|
||||
self.add_button = new Element('a.button', {
|
||||
'text': 'Add',
|
||||
'events': {
|
||||
'click': self.add.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
Array.each(self.alternative_titles, function(alt){
|
||||
new Element('option', {
|
||||
'text': alt.title
|
||||
}).inject(self.title_select)
|
||||
})
|
||||
|
||||
|
||||
// Fill categories
|
||||
var categories = CategoryList.getAll();
|
||||
|
||||
if(categories.length == 0)
|
||||
self.category_select.hide();
|
||||
else {
|
||||
self.category_select.show();
|
||||
categories.each(function(category){
|
||||
new Element('option', {
|
||||
'value': category.data._id,
|
||||
'text': category.data.label
|
||||
}).inject(self.category_select);
|
||||
});
|
||||
}
|
||||
|
||||
// Fill profiles
|
||||
var profiles = Quality.getActiveProfiles();
|
||||
if(profiles.length == 1)
|
||||
self.profile_select.hide();
|
||||
|
||||
profiles.each(function(profile){
|
||||
new Element('option', {
|
||||
'value': profile.get('_id'),
|
||||
'text': profile.get('label')
|
||||
}).inject(self.profile_select)
|
||||
});
|
||||
|
||||
self.options_el.addClass('set');
|
||||
|
||||
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.add();
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
loadingMask: function(){
|
||||
var self = this;
|
||||
|
||||
self.mask = new Element('div.mask').inject(self.el).fade('hide')
|
||||
|
||||
createSpinner(self.mask)
|
||||
self.mask.fade('in')
|
||||
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
var Season = new Class({
|
||||
|
||||
Extends: BlockBase,
|
||||
|
||||
action: {},
|
||||
|
||||
initialize: function(show, options, data){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.show = show;
|
||||
self.options = options;
|
||||
self.data = data;
|
||||
|
||||
self.profile = self.show.profile;
|
||||
|
||||
self.el = new Element('div.item.season').adopt(
|
||||
self.detail = new Element('div.item.data')
|
||||
);
|
||||
|
||||
self.create();
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.detail.set('id', 'season_'+self.data._id);
|
||||
|
||||
self.detail.adopt(
|
||||
new Element('span.name', {'text': self.getTitle()}),
|
||||
|
||||
self.quality = new Element('span.quality', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.detail.getElement('.item-actions .releases');
|
||||
|
||||
if(releases.isVisible())
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.actions = new Element('div.item-actions')
|
||||
);
|
||||
|
||||
// Add profile
|
||||
if(self.profile.data) {
|
||||
self.profile.getTypes().each(function(type){
|
||||
var q = self.addQuality(type.get('quality'), type.get('3d'));
|
||||
|
||||
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
|
||||
q.addClass('finish');
|
||||
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add releases
|
||||
self.updateReleases();
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
|
||||
if(action.el)
|
||||
self.actions.adopt(action)
|
||||
});
|
||||
},
|
||||
|
||||
updateReleases: function(){
|
||||
var self = this;
|
||||
if(!self.data.releases || self.data.releases.length == 0) return;
|
||||
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
|
||||
status = release.status;
|
||||
|
||||
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
|
||||
q = self.addQuality(release.quality, release.is_3d || false);
|
||||
|
||||
if (q && !q.hasClass(status)){
|
||||
q.addClass(status);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
addQuality: function(quality, is_3d){
|
||||
var self = this,
|
||||
q = Quality.getQuality(quality);
|
||||
|
||||
return new Element('span', {
|
||||
'text': q.label + (is_3d ? ' 3D' : ''),
|
||||
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
|
||||
'title': ''
|
||||
}).inject(self.quality);
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
var title = '';
|
||||
|
||||
if(self.data.info.number) {
|
||||
title = 'Season ' + self.data.info.number;
|
||||
} else {
|
||||
// Season 0 / Specials
|
||||
title = 'Specials';
|
||||
}
|
||||
|
||||
return title;
|
||||
},
|
||||
|
||||
getIdentifier: function(){
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
return self.get('identifiers').imdb;
|
||||
}
|
||||
catch (e){ }
|
||||
|
||||
return self.get('imdb');
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr] || this.data.info[attr]
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,92 +0,0 @@
|
||||
var Episodes = new Class({
|
||||
initialize: function(show, options) {
|
||||
var self = this;
|
||||
|
||||
self.show = show;
|
||||
self.options = options;
|
||||
},
|
||||
|
||||
open: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.container){
|
||||
self.container = new Element('div.options').grab(
|
||||
self.episodes_container = new Element('div.episodes.table')
|
||||
);
|
||||
|
||||
self.container.inject(self.show, 'top');
|
||||
|
||||
Api.request('library.tree', {
|
||||
'data': {
|
||||
'media_id': self.show.data._id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.data = json.result;
|
||||
|
||||
self.createEpisodes();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.show.slide('in', self.container, true);
|
||||
},
|
||||
|
||||
createEpisodes: function() {
|
||||
var self = this;
|
||||
|
||||
self.data.seasons.sort(self.sortSeasons);
|
||||
self.data.seasons.each(function(season) {
|
||||
self.createSeason(season);
|
||||
|
||||
season.episodes.sort(self.sortEpisodes);
|
||||
season.episodes.each(function(episode) {
|
||||
self.createEpisode(episode);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
createSeason: function(season) {
|
||||
var self = this,
|
||||
s = new Season(self.show, self.options, season);
|
||||
|
||||
$(s).inject(self.episodes_container);
|
||||
},
|
||||
|
||||
createEpisode: function(episode){
|
||||
var self = this,
|
||||
e = new Episode(self.show, self.options, episode);
|
||||
|
||||
$(e).inject(self.episodes_container);
|
||||
},
|
||||
|
||||
sortSeasons: function(a, b) {
|
||||
// Move "Specials" to the bottom of the list
|
||||
if(!a.info.number) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(!b.info.number) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Order seasons descending
|
||||
if(a.info.number < b.info.number)
|
||||
return -1;
|
||||
|
||||
if(a.info.number > b.info.number)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
sortEpisodes: function(a, b) {
|
||||
// Order episodes descending
|
||||
if(a.info.number < b.info.number)
|
||||
return -1;
|
||||
|
||||
if(a.info.number > b.info.number)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
@@ -1,370 +0,0 @@
|
||||
var Show = new Class({
|
||||
|
||||
Extends: BlockBase,
|
||||
|
||||
action: {},
|
||||
|
||||
initialize: function(list, options, data){
|
||||
var self = this;
|
||||
|
||||
self.data = data;
|
||||
self.view = options.view || 'details';
|
||||
self.list = list;
|
||||
|
||||
self.el = new Element('div.show');
|
||||
|
||||
self.episodes = new Episodes(self, {
|
||||
'actions': [EA.IMDB, EA.Release, EA.Refresh]
|
||||
});
|
||||
|
||||
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||
self.category = CategoryList.getCategory(data.category_id) || {};
|
||||
self.parent(self, options);
|
||||
|
||||
self.addEvents();
|
||||
},
|
||||
|
||||
addEvents: function(){
|
||||
var self = this;
|
||||
|
||||
self.global_events = {};
|
||||
|
||||
// Do refresh with new data
|
||||
self.global_events['movie.update'] = function(notification){
|
||||
if(self.data._id != notification.data._id) return;
|
||||
|
||||
self.busy(false);
|
||||
self.removeView();
|
||||
self.update.delay(2000, self, notification);
|
||||
};
|
||||
App.on('movie.update', self.global_events['movie.update']);
|
||||
|
||||
// Add spinner on load / search
|
||||
['media.busy', 'movie.searcher.started'].each(function(listener){
|
||||
self.global_events[listener] = function(notification){
|
||||
if(notification.data && (self.data._id == notification.data._id || (typeOf(notification.data._id) == 'array' && notification.data._id.indexOf(self.data._id) > -1)))
|
||||
self.busy(true);
|
||||
};
|
||||
App.on(listener, self.global_events[listener]);
|
||||
});
|
||||
|
||||
// Remove spinner
|
||||
self.global_events['movie.searcher.ended'] = function(notification){
|
||||
if(notification.data && self.data._id == notification.data._id)
|
||||
self.busy(false)
|
||||
};
|
||||
App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
|
||||
|
||||
// Reload when releases have updated
|
||||
self.global_events['release.update_status'] = function(notification){
|
||||
var data = notification.data;
|
||||
if(data && self.data._id == data.movie_id){
|
||||
|
||||
if(!self.data.releases)
|
||||
self.data.releases = [];
|
||||
|
||||
self.data.releases.push({'quality': data.quality, 'status': data.status});
|
||||
self.updateReleases();
|
||||
}
|
||||
};
|
||||
|
||||
App.on('release.update_status', self.global_events['release.update_status']);
|
||||
|
||||
},
|
||||
|
||||
destroy: function(){
|
||||
var self = this;
|
||||
|
||||
self.el.destroy();
|
||||
delete self.list.movies_added[self.get('id')];
|
||||
self.list.movies.erase(self);
|
||||
|
||||
self.list.checkIfEmpty();
|
||||
|
||||
// Remove events
|
||||
Object.each(self.global_events, function(handle, listener){
|
||||
App.off(listener, handle);
|
||||
});
|
||||
},
|
||||
|
||||
busy: function(set_busy, timeout){
|
||||
var self = this;
|
||||
|
||||
if(!set_busy){
|
||||
setTimeout(function(){
|
||||
if(self.spinner){
|
||||
self.mask.fade('out');
|
||||
setTimeout(function(){
|
||||
if(self.mask)
|
||||
self.mask.destroy();
|
||||
if(self.spinner)
|
||||
self.spinner.el.destroy();
|
||||
self.spinner = null;
|
||||
self.mask = null;
|
||||
}, timeout || 400);
|
||||
}
|
||||
}, timeout || 1000)
|
||||
}
|
||||
else if(!self.spinner) {
|
||||
self.createMask();
|
||||
self.spinner = createSpinner(self.mask);
|
||||
self.mask.fade('in');
|
||||
}
|
||||
},
|
||||
|
||||
createMask: function(){
|
||||
var self = this;
|
||||
self.mask = new Element('div.mask', {
|
||||
'styles': {
|
||||
'z-index': 4
|
||||
}
|
||||
}).inject(self.el, 'top').fade('hide');
|
||||
},
|
||||
|
||||
update: function(notification){
|
||||
var self = this;
|
||||
|
||||
self.data = notification.data;
|
||||
self.el.empty();
|
||||
self.removeView();
|
||||
|
||||
self.profile = Quality.getProfile(self.data.profile_id) || {};
|
||||
self.category = CategoryList.getCategory(self.data.category_id) || {};
|
||||
self.create();
|
||||
|
||||
self.busy(false);
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el.addClass('status_'+self.get('status'));
|
||||
|
||||
var eta = null,
|
||||
eta_date = null,
|
||||
now = Math.round(+new Date()/1000);
|
||||
|
||||
if(self.data.info.release_date)
|
||||
[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)))
|
||||
eta = timestamp;
|
||||
});
|
||||
|
||||
if(eta){
|
||||
eta_date = new Date(eta * 1000);
|
||||
eta_date = eta_date.toLocaleString('en-us', { month: "long" }) + ' ' + eta_date.getFullYear();
|
||||
}
|
||||
|
||||
self.el.adopt(
|
||||
self.select_checkbox = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': function(){
|
||||
self.fireEvent('select')
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.thumbnail = (self.data.files && self.data.files.image_poster) ? new Element('img', {
|
||||
'class': 'type_image poster',
|
||||
'src': Api.createUrl('file.cache') + self.data.files.image_poster[0].split(Api.getOption('path_sep')).pop()
|
||||
}): null,
|
||||
self.data_container = new Element('div.data.inlay.light').adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
new Element('div.title').adopt(
|
||||
self.title = new Element('a', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
self.episodes.open();
|
||||
}
|
||||
},
|
||||
'text': self.getTitle() || 'n/a'
|
||||
}),
|
||||
self.year = new Element('div.year', {
|
||||
'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', {
|
||||
'text': eta_date,
|
||||
'title': 'ETA'
|
||||
}) : null,
|
||||
self.quality = new Element('div.quality', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.el.getElement('.actions .releases');
|
||||
if(releases.isVisible())
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
self.actions = new Element('div.actions')
|
||||
)
|
||||
);
|
||||
|
||||
if(!self.thumbnail)
|
||||
self.el.addClass('no_thumbnail');
|
||||
|
||||
//self.changeView(self.view);
|
||||
self.select_checkbox_class = new Form.Check(self.select_checkbox);
|
||||
|
||||
// Add profile
|
||||
if(self.profile.data)
|
||||
self.profile.getTypes().each(function(type){
|
||||
|
||||
var q = self.addQuality(type.get('quality'), type.get('3d'));
|
||||
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
|
||||
q.addClass('finish');
|
||||
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Add releases
|
||||
self.updateReleases();
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
|
||||
if(action.el)
|
||||
self.actions.adopt(action)
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
updateReleases: function(){
|
||||
var self = this;
|
||||
if(!self.data.releases || self.data.releases.length == 0) return;
|
||||
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
|
||||
status = release.status;
|
||||
|
||||
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
|
||||
q = self.addQuality(release.quality, release.is_3d || false);
|
||||
|
||||
if (q && !q.hasClass(status)){
|
||||
q.addClass(status);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
addQuality: function(quality, is_3d){
|
||||
var self = this;
|
||||
|
||||
var q = Quality.getQuality(quality);
|
||||
return new Element('span', {
|
||||
'text': q.label + (is_3d ? ' 3D' : ''),
|
||||
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
|
||||
'title': ''
|
||||
}).inject(self.quality);
|
||||
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.data.title)
|
||||
return self.getUnprefixedTitle(self.data.title);
|
||||
else if(self.data.info.titles.length > 0)
|
||||
return self.getUnprefixedTitle(self.data.info.titles[0]);
|
||||
|
||||
return 'Unknown movie'
|
||||
},
|
||||
|
||||
getUnprefixedTitle: function(t){
|
||||
if(t.substr(0, 4).toLowerCase() == 'the ')
|
||||
t = t.substr(4) + ', The';
|
||||
else if(t.substr(0, 3).toLowerCase() == 'an ')
|
||||
t = t.substr(3) + ', An';
|
||||
else if(t.substr(0, 2).toLowerCase() == 'a ')
|
||||
t = t.substr(2) + ', A';
|
||||
return t;
|
||||
},
|
||||
|
||||
slide: function(direction, el, expand){
|
||||
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();
|
||||
|
||||
|
||||
if(expand === true) {
|
||||
self.el.addClass('expanded');
|
||||
self.el.getElements('.table').addClass('expanded');
|
||||
}
|
||||
|
||||
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();
|
||||
self.el.getElements('.table').removeClass('expanded');
|
||||
}
|
||||
}, 600);
|
||||
|
||||
self.el.removeClass('expanded');
|
||||
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(){
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
return self.get('identifiers').imdb;
|
||||
}
|
||||
catch (e){ }
|
||||
|
||||
return self.get('imdb');
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr] || this.data.info[attr]
|
||||
},
|
||||
|
||||
select: function(bool){
|
||||
var self = this;
|
||||
self.select_checkbox_class[bool ? 'check' : 'uncheck']()
|
||||
},
|
||||
|
||||
isSelected: function(){
|
||||
return this.select_checkbox.get('checked');
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'EpisodeLibraryPlugin'
|
||||
|
||||
|
||||
class EpisodeLibraryPlugin(LibraryBase):
|
||||
def __init__(self):
|
||||
addEvent('library.query', self.query)
|
||||
addEvent('library.identifier', self.identifier)
|
||||
|
||||
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
|
||||
if media.get('type') != 'show.episode':
|
||||
return
|
||||
|
||||
related = fireEvent('library.related', media, single = True)
|
||||
|
||||
# Get season titles
|
||||
titles = fireEvent(
|
||||
'library.query', related['season'],
|
||||
|
||||
first = False,
|
||||
include_identifier = include_identifier,
|
||||
condense = condense,
|
||||
|
||||
single = True
|
||||
)
|
||||
|
||||
# Add episode identifier to titles
|
||||
if include_identifier:
|
||||
identifier = fireEvent('library.identifier', media, single = True)
|
||||
|
||||
if identifier and identifier.get('episode'):
|
||||
titles = [title + ('E%02d' % identifier['episode']) for title in titles]
|
||||
|
||||
if first:
|
||||
return titles[0] if titles else None
|
||||
|
||||
return titles
|
||||
|
||||
def identifier(self, media):
|
||||
if media.get('type') != 'show.episode':
|
||||
return
|
||||
|
||||
identifier = {
|
||||
'season': None,
|
||||
'episode': None
|
||||
}
|
||||
|
||||
# TODO identifier mapping
|
||||
# scene_map = media['info'].get('map_episode', {}).get('scene')
|
||||
|
||||
# if scene_map:
|
||||
# # Use scene mappings if they are available
|
||||
# identifier['season'] = scene_map.get('season_nr')
|
||||
# identifier['episode'] = scene_map.get('episode_nr')
|
||||
# else:
|
||||
# Fallback to normal season/episode numbers
|
||||
identifier['season'] = media['info'].get('season_number')
|
||||
identifier['episode'] = media['info'].get('number')
|
||||
|
||||
# Cast identifiers to integers
|
||||
# TODO this will need changing to support identifiers with trailing 'a', 'b' characters
|
||||
identifier['season'] = tryInt(identifier['season'], None)
|
||||
identifier['episode'] = tryInt(identifier['episode'], None)
|
||||
|
||||
return identifier
|
||||
@@ -1,52 +0,0 @@
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'SeasonLibraryPlugin'
|
||||
|
||||
|
||||
class SeasonLibraryPlugin(LibraryBase):
|
||||
def __init__(self):
|
||||
addEvent('library.query', self.query)
|
||||
addEvent('library.identifier', self.identifier)
|
||||
|
||||
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
|
||||
if media.get('type') != 'show.season':
|
||||
return
|
||||
|
||||
related = fireEvent('library.related', media, single = True)
|
||||
|
||||
# Get show titles
|
||||
titles = fireEvent(
|
||||
'library.query', related['show'],
|
||||
|
||||
first = False,
|
||||
condense = condense,
|
||||
|
||||
single = True
|
||||
)
|
||||
|
||||
# TODO map_names
|
||||
|
||||
# Add season identifier to titles
|
||||
if include_identifier:
|
||||
identifier = fireEvent('library.identifier', media, single = True)
|
||||
|
||||
if identifier and identifier.get('season') is not None:
|
||||
titles = [title + (' S%02d' % identifier['season']) for title in titles]
|
||||
|
||||
if first:
|
||||
return titles[0] if titles else None
|
||||
|
||||
return titles
|
||||
|
||||
def identifier(self, media):
|
||||
if media.get('type') != 'show.season':
|
||||
return
|
||||
|
||||
return {
|
||||
'season': tryInt(media['info']['number'], None)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
from qcond import QueryCondenser
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'ShowLibraryPlugin'
|
||||
|
||||
|
||||
class ShowLibraryPlugin(LibraryBase):
|
||||
query_condenser = QueryCondenser()
|
||||
|
||||
def __init__(self):
|
||||
addEvent('library.query', self.query)
|
||||
|
||||
def query(self, media, first = True, condense = True, include_identifier = True, **kwargs):
|
||||
if media.get('type') != 'show':
|
||||
return
|
||||
|
||||
titles = media['info']['titles']
|
||||
|
||||
if condense:
|
||||
# Use QueryCondenser to build a list of optimal search titles
|
||||
condensed_titles = self.query_condenser.distinct(titles)
|
||||
|
||||
if condensed_titles:
|
||||
# Use condensed titles if we got a valid result
|
||||
titles = condensed_titles
|
||||
else:
|
||||
# Fallback to simplifying titles
|
||||
titles = [simplifyString(title) for title in titles]
|
||||
|
||||
if first:
|
||||
return titles[0] if titles else None
|
||||
|
||||
return titles
|
||||
@@ -1,7 +0,0 @@
|
||||
from .main import ShowMatcher
|
||||
|
||||
|
||||
def autoload():
|
||||
return ShowMatcher()
|
||||
|
||||
config = []
|
||||
@@ -1,72 +0,0 @@
|
||||
from couchpotato import fireEvent, CPLog, tryInt
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.media._base.matcher.base import MatcherBase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(MatcherBase):
|
||||
|
||||
# TODO come back to this later, think this could be handled better, this is starting to get out of hand....
|
||||
quality_map = {
|
||||
'bluray_1080p': {'resolution': ['1080p'], 'source': ['bluray']},
|
||||
'bluray_720p': {'resolution': ['720p'], 'source': ['bluray']},
|
||||
|
||||
'bdrip_1080p': {'resolution': ['1080p'], 'source': ['BDRip']},
|
||||
'bdrip_720p': {'resolution': ['720p'], 'source': ['BDRip']},
|
||||
|
||||
'brrip_1080p': {'resolution': ['1080p'], 'source': ['BRRip']},
|
||||
'brrip_720p': {'resolution': ['720p'], 'source': ['BRRip']},
|
||||
|
||||
'webdl_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]},
|
||||
'webdl_720p': {'resolution': ['720p'], 'source': ['webdl', ['web', 'dl']]},
|
||||
'webdl_480p': {'resolution': ['480p'], 'source': ['webdl', ['web', 'dl']]},
|
||||
|
||||
'hdtv_720p': {'resolution': ['720p'], 'source': ['hdtv']},
|
||||
'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super(Base, self).__init__()
|
||||
|
||||
addEvent('%s.matcher.correct_identifier' % self.type, self.correctIdentifier)
|
||||
|
||||
def correct(self, chain, release, media, quality):
|
||||
log.info("Checking if '%s' is valid", release['name'])
|
||||
log.info2('Release parsed as: %s', chain.info)
|
||||
|
||||
if not fireEvent('matcher.correct_quality', chain, quality, self.quality_map, single = True):
|
||||
log.info('Wrong: %s, quality does not match', release['name'])
|
||||
return False
|
||||
|
||||
if not fireEvent('%s.matcher.correct_identifier' % self.type, chain, media):
|
||||
log.info('Wrong: %s, identifier does not match', release['name'])
|
||||
return False
|
||||
|
||||
if not fireEvent('matcher.correct_title', chain, media):
|
||||
log.info("Wrong: '%s', undetermined naming.", (' '.join(chain.info['show_name'])))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def correctIdentifier(self, chain, media):
|
||||
raise NotImplementedError()
|
||||
|
||||
def getChainIdentifier(self, chain):
|
||||
if 'identifier' not in chain.info:
|
||||
return None
|
||||
|
||||
identifier = self.flattenInfo(chain.info['identifier'])
|
||||
|
||||
# Try cast values to integers
|
||||
for key, value in identifier.items():
|
||||
if isinstance(value, list):
|
||||
if len(value) <= 1:
|
||||
value = value[0]
|
||||
else:
|
||||
log.warning('Wrong: identifier contains multiple season or episode values, unsupported')
|
||||
return None
|
||||
|
||||
identifier[key] = tryInt(value, value)
|
||||
|
||||
return identifier
|
||||
@@ -1,30 +0,0 @@
|
||||
from couchpotato import fireEvent, CPLog
|
||||
from couchpotato.core.media.show.matcher.base import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Episode(Base):
|
||||
type = 'show.episode'
|
||||
|
||||
def correctIdentifier(self, chain, media):
|
||||
identifier = self.getChainIdentifier(chain)
|
||||
if not identifier:
|
||||
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
|
||||
return False
|
||||
|
||||
# TODO - Parse episode ranges from identifier to determine if they are multi-part episodes
|
||||
if any([x in identifier for x in ['episode_from', 'episode_to']]):
|
||||
log.info2('Wrong: releases with identifier ranges are not supported yet')
|
||||
return False
|
||||
|
||||
required = fireEvent('library.identifier', media, single = True)
|
||||
|
||||
# TODO - Support air by date episodes
|
||||
# TODO - Support episode parts
|
||||
|
||||
if identifier != required:
|
||||
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,9 +0,0 @@
|
||||
from couchpotato.core.media._base.providers.base import MultiProvider
|
||||
from couchpotato.core.media.show.matcher.episode import Episode
|
||||
from couchpotato.core.media.show.matcher.season import Season
|
||||
|
||||
|
||||
class ShowMatcher(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
@@ -1,27 +0,0 @@
|
||||
from couchpotato import fireEvent, CPLog
|
||||
from couchpotato.core.media.show.matcher.base import Base
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Season(Base):
|
||||
type = 'show.season'
|
||||
|
||||
def correctIdentifier(self, chain, media):
|
||||
identifier = self.getChainIdentifier(chain)
|
||||
if not identifier:
|
||||
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
|
||||
return False
|
||||
|
||||
# TODO - Parse episode ranges from identifier to determine if they are season packs
|
||||
if any([x in identifier for x in ['episode_from', 'episode_to']]):
|
||||
log.info2('Wrong: releases with identifier ranges are not supported yet')
|
||||
return False
|
||||
|
||||
required = fireEvent('library.identifier', media, single = True)
|
||||
|
||||
if identifier != required:
|
||||
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,13 +0,0 @@
|
||||
from couchpotato.core.media._base.providers.info.base import BaseInfoProvider
|
||||
|
||||
|
||||
class ShowProvider(BaseInfoProvider):
|
||||
type = 'show'
|
||||
|
||||
|
||||
class SeasonProvider(BaseInfoProvider):
|
||||
type = 'show.season'
|
||||
|
||||
|
||||
class EpisodeProvider(BaseInfoProvider):
|
||||
type = 'show.episode'
|
||||
@@ -1,372 +0,0 @@
|
||||
from datetime import datetime
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from couchpotato import Env
|
||||
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.show.providers.base import ShowProvider
|
||||
from tvdb_api import tvdb_exceptions
|
||||
from tvdb_api.tvdb_api import Tvdb, Show
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'TheTVDb'
|
||||
|
||||
|
||||
class TheTVDb(ShowProvider):
|
||||
|
||||
# TODO: Consider grabbing zips to put less strain on tvdb
|
||||
# TODO: Unicode stuff (check)
|
||||
# TODO: Notigy frontend on error (tvdb down at monent)
|
||||
# TODO: Expose apikey in setting so it can be changed by user
|
||||
|
||||
def __init__(self):
|
||||
addEvent('show.info', self.getShowInfo, priority = 1)
|
||||
addEvent('season.info', self.getSeasonInfo, priority = 1)
|
||||
addEvent('episode.info', self.getEpisodeInfo, priority = 1)
|
||||
|
||||
self.tvdb_api_parms = {
|
||||
'apikey': self.conf('api_key'),
|
||||
'banners': True,
|
||||
'language': 'en',
|
||||
'cache': os.path.join(Env.get('cache_dir'), 'thetvdb_api'),
|
||||
}
|
||||
self._setup()
|
||||
|
||||
def _setup(self):
|
||||
self.tvdb = Tvdb(**self.tvdb_api_parms)
|
||||
self.valid_languages = self.tvdb.config['valid_languages']
|
||||
|
||||
def getShow(self, identifier = None):
|
||||
show = None
|
||||
try:
|
||||
log.debug('Getting show: %s', identifier)
|
||||
show = self.tvdb[int(identifier)]
|
||||
except (tvdb_exceptions.tvdb_error, IOError), e:
|
||||
log.error('Failed to getShowInfo for show id "%s": %s', (identifier, traceback.format_exc()))
|
||||
return None
|
||||
|
||||
return show
|
||||
|
||||
def getShowInfo(self, identifiers = None):
|
||||
"""
|
||||
|
||||
@param identifiers: dict with identifiers per provider
|
||||
@return: Full show info including season and episode info
|
||||
"""
|
||||
|
||||
if not identifiers or not identifiers.get('thetvdb'):
|
||||
return None
|
||||
|
||||
identifier = tryInt(identifiers.get('thetvdb'))
|
||||
|
||||
cache_key = 'thetvdb.cache.show.%s' % identifier
|
||||
result = None #self.getCache(cache_key)
|
||||
if result:
|
||||
return result
|
||||
|
||||
show = self.getShow(identifier = identifier)
|
||||
if show:
|
||||
result = self._parseShow(show)
|
||||
self.setCache(cache_key, result)
|
||||
|
||||
return result or {}
|
||||
|
||||
def getSeasonInfo(self, identifiers = None, params = {}):
|
||||
"""Either return a list of all seasons or a single season by number.
|
||||
identifier is the show 'id'
|
||||
"""
|
||||
if not identifiers or not identifiers.get('thetvdb'):
|
||||
return None
|
||||
|
||||
season_number = params.get('season_number', None)
|
||||
identifier = tryInt(identifiers.get('thetvdb'))
|
||||
|
||||
cache_key = 'thetvdb.cache.%s.%s' % (identifier, season_number)
|
||||
log.debug('Getting SeasonInfo: %s', cache_key)
|
||||
result = self.getCache(cache_key) or {}
|
||||
if result:
|
||||
return result
|
||||
|
||||
try:
|
||||
show = self.tvdb[int(identifier)]
|
||||
except (tvdb_exceptions.tvdb_error, IOError), e:
|
||||
log.error('Failed parsing TheTVDB SeasonInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
|
||||
return False
|
||||
|
||||
result = []
|
||||
for number, season in show.items():
|
||||
if season_number is not None and number == season_number:
|
||||
result = self._parseSeason(show, number, season)
|
||||
self.setCache(cache_key, result)
|
||||
return result
|
||||
else:
|
||||
result.append(self._parseSeason(show, number, season))
|
||||
|
||||
self.setCache(cache_key, result)
|
||||
return result
|
||||
|
||||
def getEpisodeInfo(self, identifier = None, params = {}):
|
||||
"""Either return a list of all episodes or a single episode.
|
||||
If episode_identifer contains an episode number to search for
|
||||
"""
|
||||
season_number = self.getIdentifier(params.get('season_number', None))
|
||||
episode_identifier = self.getIdentifier(params.get('episode_identifiers', None))
|
||||
identifier = self.getIdentifier(identifier)
|
||||
|
||||
if not identifier and season_number is None:
|
||||
return False
|
||||
|
||||
# season_identifier must contain the 'show id : season number' since there is no tvdb id
|
||||
# for season and we need a reference to both the show id and season number
|
||||
if not identifier and season_number:
|
||||
try:
|
||||
identifier, season_number = season_number.split(':')
|
||||
season_number = int(season_number)
|
||||
except: return None
|
||||
|
||||
cache_key = 'thetvdb.cache.%s.%s.%s' % (identifier, episode_identifier, season_number)
|
||||
log.debug('Getting EpisodeInfo: %s', cache_key)
|
||||
result = self.getCache(cache_key) or {}
|
||||
if result:
|
||||
return result
|
||||
|
||||
try:
|
||||
show = self.tvdb[int(identifier)]
|
||||
except (tvdb_exceptions.tvdb_error, IOError), e:
|
||||
log.error('Failed parsing TheTVDB EpisodeInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
|
||||
return False
|
||||
|
||||
result = []
|
||||
for number, season in show.items():
|
||||
if season_number is not None and number != season_number:
|
||||
continue
|
||||
|
||||
for episode in season.values():
|
||||
if episode_identifier is not None and episode['id'] == toUnicode(episode_identifier):
|
||||
result = self._parseEpisode(episode)
|
||||
self.setCache(cache_key, result)
|
||||
return result
|
||||
else:
|
||||
result.append(self._parseEpisode(episode))
|
||||
|
||||
self.setCache(cache_key, result)
|
||||
return result
|
||||
|
||||
def getIdentifier(self, value):
|
||||
if type(value) is dict:
|
||||
return value.get('thetvdb')
|
||||
|
||||
return value
|
||||
|
||||
def _parseShow(self, show):
|
||||
|
||||
#
|
||||
# NOTE: show object only allows direct access via
|
||||
# show['id'], not show.get('id')
|
||||
#
|
||||
def get(name):
|
||||
return show.get(name) if not hasattr(show, 'search') else show[name]
|
||||
|
||||
## Images
|
||||
poster = get('poster')
|
||||
backdrop = get('fanart')
|
||||
|
||||
genres = splitString(get('genre'), '|')
|
||||
if get('firstaired') is not None:
|
||||
try: year = datetime.strptime(get('firstaired'), '%Y-%m-%d').year
|
||||
except: year = None
|
||||
else:
|
||||
year = None
|
||||
|
||||
show_data = {
|
||||
'identifiers': {
|
||||
'thetvdb': tryInt(get('id')),
|
||||
'imdb': get('imdb_id'),
|
||||
'zap2it': get('zap2it_id'),
|
||||
},
|
||||
'type': 'show',
|
||||
'titles': [get('seriesname')],
|
||||
'images': {
|
||||
'poster': [poster] if poster else [],
|
||||
'backdrop': [backdrop] if backdrop else [],
|
||||
'poster_original': [],
|
||||
'backdrop_original': [],
|
||||
},
|
||||
'year': year,
|
||||
'genres': genres,
|
||||
'network': get('network'),
|
||||
'plot': get('overview'),
|
||||
'networkid': get('networkid'),
|
||||
'air_day': (get('airs_dayofweek') or '').lower(),
|
||||
'air_time': self.parseTime(get('airs_time')),
|
||||
'firstaired': get('firstaired'),
|
||||
'runtime': tryInt(get('runtime')),
|
||||
'contentrating': get('contentrating'),
|
||||
'rating': {},
|
||||
'actors': splitString(get('actors'), '|'),
|
||||
'status': get('status'),
|
||||
'language': get('language'),
|
||||
}
|
||||
|
||||
if tryFloat(get('rating')):
|
||||
show_data['rating']['thetvdb'] = [tryFloat(get('rating')), tryInt(get('ratingcount'))],
|
||||
|
||||
show_data = dict((k, v) for k, v in show_data.iteritems() if v)
|
||||
|
||||
# Only load season info when available
|
||||
if type(show) == Show:
|
||||
|
||||
# Parse season and episode data
|
||||
show_data['seasons'] = {}
|
||||
|
||||
for season_nr in show:
|
||||
season = self._parseSeason(show, season_nr, show[season_nr])
|
||||
season['episodes'] = {}
|
||||
|
||||
for episode_nr in show[season_nr]:
|
||||
season['episodes'][episode_nr] = self._parseEpisode(show[season_nr][episode_nr])
|
||||
|
||||
show_data['seasons'][season_nr] = season
|
||||
|
||||
# Add alternative titles
|
||||
# try:
|
||||
# raw = self.tvdb.search(show['seriesname'])
|
||||
# if raw:
|
||||
# for show_info in raw:
|
||||
# print show_info
|
||||
# if show_info['id'] == show_data['id'] and show_info.get('aliasnames', None):
|
||||
# for alt_name in show_info['aliasnames'].split('|'):
|
||||
# show_data['titles'].append(toUnicode(alt_name))
|
||||
# except (tvdb_exceptions.tvdb_error, IOError), e:
|
||||
# log.error('Failed searching TheTVDB for "%s": %s', (show['seriesname'], traceback.format_exc()))
|
||||
|
||||
return show_data
|
||||
|
||||
def _parseSeason(self, show, number, season):
|
||||
"""
|
||||
contains no data
|
||||
"""
|
||||
|
||||
poster = []
|
||||
try:
|
||||
temp_poster = {}
|
||||
for id, data in show.data['_banners']['season']['season'].items():
|
||||
if data.get('season') == str(number) and data.get('language') == self.tvdb_api_parms['language']:
|
||||
temp_poster[tryFloat(data.get('rating')) * tryInt(data.get('ratingcount'))] = data.get('_bannerpath')
|
||||
#break
|
||||
poster.append(temp_poster[sorted(temp_poster, reverse = True)[0]])
|
||||
except:
|
||||
pass
|
||||
|
||||
season_data = {
|
||||
'identifiers': {
|
||||
'thetvdb': show['id'] if show.get('id') else show[number][1]['seasonid']
|
||||
},
|
||||
'number': tryInt(number),
|
||||
'images': {
|
||||
'poster': poster,
|
||||
},
|
||||
}
|
||||
|
||||
season_data = dict((k, v) for k, v in season_data.iteritems() if v)
|
||||
return season_data
|
||||
|
||||
def _parseEpisode(self, episode):
|
||||
"""
|
||||
('episodenumber', u'1'),
|
||||
('thumb_added', None),
|
||||
('rating', u'7.7'),
|
||||
('overview',
|
||||
u'Experienced waitress Max Black meets her new co-worker, former rich-girl Caroline Channing, and puts her skills to the test at an old but re-emerging Brooklyn diner. Despite her initial distaste for Caroline, Max eventually softens and the two team up for a new business venture.'),
|
||||
('dvd_episodenumber', None),
|
||||
('dvd_discid', None),
|
||||
('combined_episodenumber', u'1'),
|
||||
('epimgflag', u'7'),
|
||||
('id', u'4099506'),
|
||||
('seasonid', u'465948'),
|
||||
('thumb_height', u'225'),
|
||||
('tms_export', u'1374789754'),
|
||||
('seasonnumber', u'1'),
|
||||
('writer', u'|Michael Patrick King|Whitney Cummings|'),
|
||||
('lastupdated', u'1371420338'),
|
||||
('filename', u'http://thetvdb.com/banners/episodes/248741/4099506.jpg'),
|
||||
('absolute_number', u'1'),
|
||||
('ratingcount', u'102'),
|
||||
('combined_season', u'1'),
|
||||
('thumb_width', u'400'),
|
||||
('imdb_id', u'tt1980319'),
|
||||
('director', u'James Burrows'),
|
||||
('dvd_chapter', None),
|
||||
('dvd_season', None),
|
||||
('gueststars',
|
||||
u'|Brooke Lyons|Noah Mills|Shoshana Bush|Cale Hartmann|Adam Korson|Alex Enriquez|Matt Cook|Bill Parks|Eugene Shaw|Sergey Brusilovsky|Greg Lewis|Cocoa Brown|Nick Jameson|'),
|
||||
('seriesid', u'248741'),
|
||||
('language', u'en'),
|
||||
('productioncode', u'296793'),
|
||||
('firstaired', u'2011-09-19'),
|
||||
('episodename', u'Pilot')]
|
||||
"""
|
||||
|
||||
def get(name, default = None):
|
||||
return episode.get(name, default)
|
||||
|
||||
poster = get('filename', [])
|
||||
|
||||
episode_data = {
|
||||
'number': tryInt(get('episodenumber')),
|
||||
'absolute_number': tryInt(get('absolute_number')),
|
||||
'identifiers': {
|
||||
'thetvdb': tryInt(episode['id'])
|
||||
},
|
||||
'type': 'episode',
|
||||
'titles': [get('episodename')] if get('episodename') else [],
|
||||
'images': {
|
||||
'poster': [poster] if poster else [],
|
||||
},
|
||||
'released': get('firstaired'),
|
||||
'plot': get('overview'),
|
||||
'firstaired': get('firstaired'),
|
||||
'language': get('language'),
|
||||
}
|
||||
|
||||
if get('imdb_id'):
|
||||
episode_data['identifiers']['imdb'] = get('imdb_id')
|
||||
|
||||
episode_data = dict((k, v) for k, v in episode_data.iteritems() if v)
|
||||
return episode_data
|
||||
|
||||
def parseTime(self, time):
|
||||
return time
|
||||
|
||||
def isDisabled(self):
|
||||
if self.conf('api_key') == '':
|
||||
log.error('No API key provided.')
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'thetvdb',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'name': 'tmdb',
|
||||
'label': 'TheTVDB',
|
||||
'hidden': True,
|
||||
'description': 'Used for all calls to TheTVDB.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'api_key',
|
||||
'default': '7966C02F860586D2',
|
||||
'label': 'Api Key',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,86 +0,0 @@
|
||||
import urllib
|
||||
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.show.providers.base import ShowProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Trakt'
|
||||
|
||||
|
||||
class Trakt(ShowProvider):
|
||||
api_key = 'c043de5ada9d180028c10229d2a3ea5b'
|
||||
base_url = 'http://api.trakt.tv/%%s.json/%s' % api_key
|
||||
|
||||
def __init__(self):
|
||||
addEvent('info.search', self.search, priority = 1)
|
||||
addEvent('show.search', self.search, priority = 1)
|
||||
|
||||
def search(self, q, limit = 12):
|
||||
if self.isDisabled():
|
||||
return False
|
||||
|
||||
# Check for cached result
|
||||
cache_key = 'trakt.cache.search.%s.%s' % (q, limit)
|
||||
results = self.getCache(cache_key) or []
|
||||
|
||||
if results:
|
||||
return results
|
||||
|
||||
# Search
|
||||
log.debug('Searching for show: "%s"', q)
|
||||
response = self._request('search/shows', query=q, limit=limit)
|
||||
|
||||
if not response:
|
||||
return []
|
||||
|
||||
# Parse search results
|
||||
for show in response:
|
||||
results.append(self._parseShow(show))
|
||||
|
||||
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
|
||||
|
||||
self.setCache(cache_key, results)
|
||||
return results
|
||||
|
||||
def _request(self, action, **kwargs):
|
||||
url = self.base_url % action
|
||||
|
||||
if kwargs:
|
||||
url += '?' + urllib.urlencode(kwargs)
|
||||
|
||||
return self.getJsonData(url)
|
||||
|
||||
def _parseShow(self, show):
|
||||
# Images
|
||||
images = show.get('images', {})
|
||||
|
||||
poster = images.get('poster')
|
||||
backdrop = images.get('backdrop')
|
||||
|
||||
# Rating
|
||||
rating = show.get('ratings', {}).get('percentage')
|
||||
|
||||
# Build show dict
|
||||
show_data = {
|
||||
'identifiers': {
|
||||
'thetvdb': show.get('tvdb_id'),
|
||||
'imdb': show.get('imdb_id'),
|
||||
'tvrage': show.get('tvrage_id'),
|
||||
},
|
||||
'type': 'show',
|
||||
'titles': [show.get('title')],
|
||||
'images': {
|
||||
'poster': [poster] if poster else [],
|
||||
'backdrop': [backdrop] if backdrop else [],
|
||||
'poster_original': [],
|
||||
'backdrop_original': [],
|
||||
},
|
||||
'year': show.get('year'),
|
||||
'rating': {
|
||||
'trakt': float(rating) / 10
|
||||
},
|
||||
}
|
||||
|
||||
return dict((k, v) for k, v in show_data.iteritems() if v)
|
||||
@@ -1,216 +0,0 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.media.show.providers.base import ShowProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Xem'
|
||||
|
||||
|
||||
class Xem(ShowProvider):
|
||||
'''
|
||||
Mapping Information
|
||||
===================
|
||||
|
||||
Single
|
||||
------
|
||||
You will need the id / identifier of the show e.g. tvdb-id for American Dad! is 73141
|
||||
the origin is the name of the site/entity the episode, season (and/or absolute) numbers are based on
|
||||
|
||||
http://thexem.de/map/single?id=&origin=&episode=&season=&absolute=
|
||||
|
||||
episode, season and absolute are all optional but it wont work if you don't provide either episode and season OR absolute in
|
||||
addition you can provide destination as the name of the wished destination, if not provided it will output all available
|
||||
|
||||
When a destination has two or more addresses another entry will be added as _ ... for now the second address gets the index "2"
|
||||
(the first index is omitted) and so on
|
||||
|
||||
http://thexem.de/map/single?id=7529&origin=anidb&season=1&episode=2&destination=trakt
|
||||
{
|
||||
"result":"success",
|
||||
"data":{
|
||||
"trakt": {"season":1,"episode":3,"absolute":3},
|
||||
"trakt_2":{"season":1,"episode":4,"absolute":4}
|
||||
},
|
||||
"message":"single mapping for 7529 on anidb."
|
||||
}
|
||||
|
||||
All
|
||||
---
|
||||
Basically same as "single" just a little easier
|
||||
The origin address is added into the output too!!
|
||||
|
||||
http://thexem.de/map/all?id=7529&origin=anidb
|
||||
|
||||
All Names
|
||||
---------
|
||||
Get all names xem has to offer
|
||||
non optional params: origin(an entity string like 'tvdb')
|
||||
optional params: season, language
|
||||
- season: a season number or a list like: 1,3,5 or a compare operator like ne,gt,ge,lt,le,eq and a season number. default would
|
||||
return all
|
||||
- language: a language string like 'us' or 'jp' default is all
|
||||
- defaultNames: 1(yes) or 0(no) should the default names be added to the list ? default is 0(no)
|
||||
|
||||
http://thexem.de/map/allNames?origin=tvdb&season=le1
|
||||
|
||||
{
|
||||
"result": "success",
|
||||
"data": {
|
||||
"248812": ["Dont Trust the Bitch in Apartment 23", "Don't Trust the Bitch in Apartment 23"],
|
||||
"257571": ["Nazo no Kanojo X"],
|
||||
"257875": ["Lupin III - Mine Fujiko to Iu Onna", "Lupin III Fujiko to Iu Onna", "Lupin the Third - Mine Fujiko to Iu Onna"]
|
||||
},
|
||||
"message": ""
|
||||
}
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
addEvent('show.info', self.getShowInfo, priority = 5)
|
||||
addEvent('episode.info', self.getEpisodeInfo, priority = 5)
|
||||
|
||||
self.config = {}
|
||||
self.config['base_url'] = "http://thexem.de"
|
||||
self.config['url_single'] = u"%(base_url)s/map/single?" % self.config
|
||||
self.config['url_all'] = u"%(base_url)s/map/all?" % self.config
|
||||
self.config['url_names'] = u"%(base_url)s/map/names?" % self.config
|
||||
self.config['url_all_names'] = u"%(base_url)s/map/allNames?" % self.config
|
||||
|
||||
def getShowInfo(self, identifiers = None):
|
||||
if self.isDisabled():
|
||||
return {}
|
||||
|
||||
identifier = identifiers.get('thetvdb')
|
||||
|
||||
if not identifier:
|
||||
return {}
|
||||
|
||||
cache_key = 'xem.cache.%s' % identifier
|
||||
log.debug('Getting showInfo: %s', cache_key)
|
||||
result = self.getCache(cache_key) or {}
|
||||
if result:
|
||||
return result
|
||||
|
||||
result['seasons'] = {}
|
||||
|
||||
# Create season/episode and absolute mappings
|
||||
url = self.config['url_all'] + "id=%s&origin=tvdb" % tryUrlencode(identifier)
|
||||
response = self.getJsonData(url)
|
||||
|
||||
if response and response.get('result') == 'success':
|
||||
data = response.get('data', None)
|
||||
self.parseMaps(result, data)
|
||||
|
||||
# Create name alias mappings
|
||||
url = self.config['url_names'] + "id=%s&origin=tvdb" % tryUrlencode(identifier)
|
||||
response = self.getJsonData(url)
|
||||
|
||||
if response and response.get('result') == 'success':
|
||||
data = response.get('data', None)
|
||||
self.parseNames(result, data)
|
||||
|
||||
self.setCache(cache_key, result)
|
||||
return result
|
||||
|
||||
def getEpisodeInfo(self, identifiers = None, params = {}):
|
||||
episode_num = params.get('episode_number', None)
|
||||
if episode_num is None:
|
||||
return False
|
||||
|
||||
season_num = params.get('season_number', None)
|
||||
if season_num is None:
|
||||
return False
|
||||
|
||||
result = self.getShowInfo(identifiers)
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
# Find season
|
||||
if season_num not in result['seasons']:
|
||||
return False
|
||||
|
||||
season = result['seasons'][season_num]
|
||||
|
||||
# Find episode
|
||||
if episode_num not in season['episodes']:
|
||||
return False
|
||||
|
||||
return season['episodes'][episode_num]
|
||||
|
||||
def parseMaps(self, result, data, master = 'tvdb'):
|
||||
'''parses xem map and returns a custom formatted dict map
|
||||
|
||||
To retreive map for scene:
|
||||
if 'scene' in map['map_episode'][1][1]:
|
||||
print map['map_episode'][1][1]['scene']['season']
|
||||
'''
|
||||
if not isinstance(data, list):
|
||||
return
|
||||
|
||||
for episode_map in data:
|
||||
origin = episode_map.pop(master, None)
|
||||
if origin is None:
|
||||
continue # No master origin to map to
|
||||
|
||||
o_season = origin['season']
|
||||
o_episode = origin['episode']
|
||||
|
||||
# Create season info
|
||||
if o_season not in result['seasons']:
|
||||
result['seasons'][o_season] = {}
|
||||
|
||||
season = result['seasons'][o_season]
|
||||
|
||||
if 'episodes' not in season:
|
||||
season['episodes'] = {}
|
||||
|
||||
# Create episode info
|
||||
if o_episode not in season['episodes']:
|
||||
season['episodes'][o_episode] = {}
|
||||
|
||||
episode = season['episodes'][o_episode]
|
||||
episode['episode_map'] = episode_map
|
||||
|
||||
def parseNames(self, result, data):
|
||||
result['title_map'] = data.pop('all', None)
|
||||
|
||||
for season, title_map in data.items():
|
||||
season = int(season)
|
||||
|
||||
# Create season info
|
||||
if season not in result['seasons']:
|
||||
result['seasons'][season] = {}
|
||||
|
||||
season = result['seasons'][season]
|
||||
season['title_map'] = title_map
|
||||
|
||||
def isDisabled(self):
|
||||
if __name__ == '__main__':
|
||||
return False
|
||||
if self.conf('enabled'):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'xem',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'name': 'xem',
|
||||
'label': 'TheXem',
|
||||
'hidden': True,
|
||||
'description': 'Used for all calls to TheXem.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': True,
|
||||
'label': 'Enabled',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user