diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7c1af9a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = tab +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.py] +indent_style = space + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.gitignore b/.gitignore index e156f87..666da1a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ *.pyc /data/ +/_env/ /_source/ .project .pydevproject +node_modules +.tmp diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..25ea797 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,198 @@ +'use strict'; + +module.exports = function(grunt){ + require('jit-grunt')(grunt); + require('time-grunt')(grunt); + + grunt.loadNpmTasks('grunt-shell-spawn'); + + // Configurable paths + var config = { + python: grunt.file.exists('./_env/bin/python') ? './_env/bin/python' : 'python', + tmp: '.tmp', + base: 'couchpotato', + css_dest: 'couchpotato/static/style/combined.min.css', + scripts_vendor_dest: 'couchpotato/static/scripts/combined.vendor.min.js', + scripts_base_dest: 'couchpotato/static/scripts/combined.base.min.js', + scripts_plugins_dest: 'couchpotato/static/scripts/combined.plugins.min.js' + }; + + var vendor_scripts_files = [ + 'couchpotato/static/scripts/vendor/mootools.js', + 'couchpotato/static/scripts/vendor/mootools_more.js', + 'couchpotato/static/scripts/vendor/Array.stableSort.js', + 'couchpotato/static/scripts/vendor/history.js', + 'couchpotato/static/scripts/vendor/dynamics.js', + 'couchpotato/static/scripts/vendor/fastclick.js' + ]; + + var scripts_files = [ + 'couchpotato/static/scripts/library/uniform.js', + 'couchpotato/static/scripts/library/question.js', + 'couchpotato/static/scripts/library/scrollspy.js', + 'couchpotato/static/scripts/couchpotato.js', + 'couchpotato/static/scripts/api.js', + 'couchpotato/static/scripts/page.js', + 'couchpotato/static/scripts/block.js', + 'couchpotato/static/scripts/block/navigation.js', + 'couchpotato/static/scripts/block/header.js', + 'couchpotato/static/scripts/block/footer.js', + 'couchpotato/static/scripts/block/menu.js', + 'couchpotato/static/scripts/page/home.js', + 'couchpotato/static/scripts/page/settings.js', + 'couchpotato/static/scripts/page/about.js', + 'couchpotato/static/scripts/page/login.js' + ]; + + 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', + '!<%= config.base %>/static/scripts/combined.*.js' + ] + }, + + // Compiles Sass to CSS and generates necessary files if requested + sass: { + options: { + compass: true, + update: true, + sourcemap: 'none' + }, + server: { + files: [{ + expand: true, + cwd: '<%= config.base %>/', + src: ['**/*.scss'], + dest: '<%= config.tmp %>/styles/', + ext: '.css' + }] + } + }, + + // Empties folders to start fresh + clean: { + server: '.tmp' + }, + + // Add vendor prefixed styles + autoprefixer: { + options: { + browsers: ['last 2 versions'], + remove: false, + cascade: false + }, + dist: { + files: [{ + expand: true, + cwd: '<%= config.tmp %>/styles/', + src: '{,**/}*.css', + dest: '<%= config.tmp %>/styles/' + }] + } + }, + + cssmin: { + dist: { + options: { + keepBreaks: true + }, + files: { + '<%= config.css_dest %>': ['<%= config.tmp %>/styles/**/*.css'] + } + } + }, + + uglify: { + options: { + mangle: false, + compress: false, + beautify: true, + screwIE8: true + }, + vendor: { + files: { + '<%= config.scripts_vendor_dest %>': vendor_scripts_files + } + }, + base: { + files: { + '<%= config.scripts_base_dest %>': scripts_files + } + }, + plugins: { + files: { + '<%= config.scripts_plugins_dest %>': ['<%= config.base %>/core/**/*.js'] + } + } + }, + + shell: { + runCouchPotato: { + command: '<%= config.python %> CouchPotato.py', + options: { + stdout: true, + stderr: true + } + } + }, + + // COOL TASKS ============================================================== + watch: { + scss: { + files: ['<%= config.base %>/**/*.{scss,sass}'], + tasks: ['sass:server', 'autoprefixer', 'cssmin'] + }, + js: { + files: [ + '<%= config.base %>/**/*.js', + '!<%= config.base %>/static/scripts/combined.*.js' + ], + tasks: ['uglify:base', 'uglify:plugins', 'jshint'] + }, + livereload: { + options: { + livereload: 35729 + }, + files: [ + '<%= config.css_dest %>', + '<%= config.scripts_vendor_dest %>', + '<%= config.scripts_base_dest %>', + '<%= config.scripts_plugins_dest %>' + ] + } + }, + + concurrent: { + options: { + logConcurrentOutput: true + }, + tasks: ['shell:runCouchPotato', 'watch'] + } + + }); + + grunt.registerTask('default', [ + 'clean:server', + 'sass:server', + 'autoprefixer', + 'cssmin', + 'uglify:vendor', + 'uglify:base', + 'uglify:plugins', + 'concurrent' + ]); + +}; diff --git a/README.md b/README.md index c1e6178..6e5f612 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ OS X: * If you're on Leopard (10.5) install Python 2.6+: [Python 2.6.5](http://www.python.org/download/releases/2.6.5/) * Install [GIT](http://git-scm.com/) +* Install [LXML](http://lxml.de/installation.html) for better/faster website scraping * Open up `Terminal` * Go to your App folder `cd /Applications` * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` @@ -35,6 +36,7 @@ Linux: * (Ubuntu / Debian) Install [GIT](http://git-scm.com/) with `apt-get install git-core` * (Fedora / CentOS) Install [GIT](http://git-scm.com/) with `yum install git` +* Install [LXML](http://lxml.de/installation.html) for better/faster website scraping * 'cd' to the folder of your choosing. * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * Then do `python CouchPotatoServer/CouchPotato.py` to start @@ -49,7 +51,7 @@ Linux: * Open your browser and go to `http://localhost:5050/` Docker: -* You can use [razorgirl's Dockerfile](https://github.com/razorgirl/docker-couchpotato) to quickly build your own isolated app container. It's based on the Linux instructions above. For more info about Docker check out the [official website](https://www.docker.com). +* You can use [linuxserver.io](https://github.com/linuxserver/docker-couchpotato) or [razorgirl's](https://github.com/razorgirl/docker-couchpotato) to quickly build your own isolated app container. It's based on the Linux instructions above. For more info about Docker check out the [official website](https://www.docker.com). FreeBSD: @@ -66,3 +68,17 @@ FreeBSD: * If not default install, specify options with startup flags in `ee /etc/rc.conf` * Finally, `service couchpotato start` * Open your browser and go to: `http://server:5050/` + + +## Development + +Be sure you're running the latest version of [Python 2.7](http://python.org/). + +If you're going to add styling or doing some javascript work you'll need a few tools that build and compress scss -> css and combine the javascript files. [Node/NPM](https://nodejs.org/), [Grunt](http://gruntjs.com/installing-grunt), [Compass](http://compass-style.org/install/) + +After you've got these tools you can install the packages using `npm install`. Once this process has finished you can start CP using the command `grunt`. This will start all the needed tools and watches any files for changes. +You can now change css and javascript and it wil reload the page when needed. + +By default it will combine files used in the core folder. If you're adding a new .scss or .js file, you might need to add it and then restart the grunt process for it to combine it properly. + +Don't forget to enable development inside the CP settings. This disables some functions and also makes sure javascript rrors are pushed to console instead of the log. diff --git a/config.rb b/config.rb new file mode 100644 index 0000000..a26a2ee --- /dev/null +++ b/config.rb @@ -0,0 +1,44 @@ +# 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 = "./couchpotato/static/style" +css_dir = "./couchpotato/static/style" + +# 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} : {} diff --git a/couchpotato/core/_base/_core.py b/couchpotato/core/_base/_core.py index 81f5fa5..eeaf96d 100644 --- a/couchpotato/core/_base/_core.py +++ b/couchpotato/core/_base/_core.py @@ -53,6 +53,7 @@ class Core(Plugin): addEvent('app.version', self.version) addEvent('app.load', self.checkDataDir) addEvent('app.load', self.cleanUpFolders) + addEvent('app.load.after', self.dependencies) addEvent('setting.save.core.password', self.md5Password) addEvent('setting.save.core.api_key', self.checkApikey) @@ -73,6 +74,15 @@ class Core(Plugin): except: log.debug('Failed setting default ssl context: %s', traceback.format_exc()) + def dependencies(self): + + # Check if lxml is available + try: from lxml import etree + except: log.error('LXML not available, please install for better/faster scraping support: `http://lxml.de/installation.html`') + + try: import OpenSSL + except: log.error('OpenSSL not available, please install for better requests validation: `https://pyopenssl.readthedocs.org/en/latest/install.html`') + def md5Password(self, value): return md5(value) if value else '' @@ -183,8 +193,9 @@ class Core(Plugin): if host == '0.0.0.0' or host == '': host = 'localhost' port = Env.setting('port') + ssl = Env.setting('ssl_cert') and Env.setting('ssl_key') - return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), Env.get('web_base')) + return '%s:%d%s' % (cleanHost(host, ssl = ssl).rstrip('/'), int(port), Env.get('web_base')) def createApiUrl(self): return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key')) @@ -276,6 +287,11 @@ config = [{ 'description': 'Let 3rd party app do stuff. Docs', }, { + 'name': 'dereferer', + 'default': 'http://www.dereferer.org/?', + 'description': 'Derefer links to external sites, keep empty for no dereferer. Example: http://www.dereferer.org/? or http://www.nullrefer.com/?.', + }, + { 'name': 'use_proxy', 'default': 0, 'type': 'bool', diff --git a/couchpotato/core/_base/clientscript.py b/couchpotato/core/_base/clientscript.py index e5dbd8f..ab52003 100644 --- a/couchpotato/core/_base/clientscript.py +++ b/couchpotato/core/_base/clientscript.py @@ -1,16 +1,10 @@ import os -import re -import traceback from couchpotato.core.event import addEvent -from couchpotato.core.helpers.encoding import ss 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 log = CPLog(__name__) @@ -20,129 +14,35 @@ autoload = 'ClientScript' class ClientScript(Plugin): - core_static = { + paths = { '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/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/footer.js', - 'scripts/block/menu.js', - 'scripts/page/home.js', - 'scripts/page/settings.js', - 'scripts/page/about.js', + 'scripts/combined.vendor.min.js', + 'scripts/combined.base.min.js', + 'scripts/combined.plugins.min.js', ], } - urls = {'style': {}, 'script': {}} - minified = {'style': {}, 'script': {}} - paths = {'style': {}, 'script': {}} - comment = { - 'style': '/*** %s:%d ***/\n', - 'script': '// %s:%d\n' - } - - html = { - 'style': '', - 'script': '', - } - def __init__(self): - addEvent('register_style', self.registerStyle) - addEvent('register_script', self.registerScript) - addEvent('clientscript.get_styles', self.getStyles) addEvent('clientscript.get_scripts', self.getScripts) - if not Env.get('dev'): - addEvent('app.load', self.minify) + self.makeRelative() - self.addCore() + def makeRelative(self): - def addCore(self): + for static_type in self.paths: - for static_type in self.core_static: - for rel_path in self.core_static.get(static_type): + updates_paths = [] + for rel_path in self.paths.get(static_type): file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path) - core_url = 'static/%s' % rel_path - - if static_type == 'script': - self.registerScript(core_url, file_path, position = 'front') - else: - self.registerStyle(core_url, file_path, position = 'front') - - def minify(self): - - # Create cache dir - cache = Env.get('cache_dir') - parent_dir = os.path.join(cache, 'minified') - self.makeDir(parent_dir) - - Env.get('app').add_handlers(".*$", [(Env.get('web_base') + 'minified/(.*)', StaticFileHandler, {'path': parent_dir})]) - - for file_type in ['style', 'script']: - ext = 'js' if file_type is 'script' else 'css' - positions = self.paths.get(file_type, {}) - for position in positions: - files = positions.get(position) - self._minify(file_type, files, position, position + '.' + ext) - - def _minify(self, file_type, files, position, out): - - cache = Env.get('cache_dir') - out_name = out - out = os.path.join(cache, 'minified', out_name) - - 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 - - raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data}) - - # 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' - - self.createFile(out, data.strip()) + core_url = 'static/%s?%d' % (rel_path, tryInt(os.path.getmtime(file_path))) - if not self.minified.get(file_type): - self.minified[file_type] = {} - if not self.minified[file_type].get(position): - self.minified[file_type][position] = [] + updates_paths.append(core_url) - minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out))) - self.minified[file_type][position].append(minified_url) + self.paths[static_type] = updates_paths def getStyles(self, *args, **kwargs): return self.get('style', *args, **kwargs) @@ -150,63 +50,8 @@ class ClientScript(Plugin): def getScripts(self, *args, **kwargs): return self.get('script', *args, **kwargs) - def get(self, type, as_html = False, location = 'head'): - - data = '' if as_html else [] - - try: - try: - if not Env.get('dev'): - return self.minified[type][location] - except: - pass - - return self.urls[type][location] - except: - log.error('Error getting minified %s, %s: %s', (type, location, traceback.format_exc())) - - return data - - def registerStyle(self, api_path, file_path, position = 'head'): - self.register(api_path, file_path, 'style', position) - - def registerScript(self, api_path, file_path, position = 'head'): - self.register(api_path, file_path, 'script', position) - - def register(self, api_path, file_path, type, location): - - 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.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('} ;', '} ') + def get(self, type): + if type in self.paths: + return self.paths[type] - return new_data + return [] diff --git a/couchpotato/core/_base/downloader/static/downloaders.js b/couchpotato/core/_base/downloader/static/downloaders.js index 4521515..f963ca8 100644 --- a/couchpotato/core/_base/downloader/static/downloaders.js +++ b/couchpotato/core/_base/downloader/static/downloaders.js @@ -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)); + }); }, @@ -27,7 +27,7 @@ var DownloadersBase = new Class({ if(button_name.contains('Downloaders')) return; - new Element('.ctrlHolder.test_button').adopt( + new Element('.ctrlHolder.test_button').grab( new Element('a.button', { 'text': button_name, 'events': { @@ -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); } }); } @@ -67,7 +67,7 @@ var DownloadersBase = new Class({ }, testButtonName: function(fieldset){ - var name = String(fieldset.getElement('h2').innerHTML).substring(0,String(fieldset.getElement('h2').innerHTML).indexOf(" 26214400 and last_check < time.time()-604800: # 25MB / 7 days + if last_check < time.time()-604800: # 7 days self.compact() Env.prop(prop_name, value = int(time.time())) diff --git a/couchpotato/core/downloaders/putio/static/putio.js b/couchpotato/core/downloaders/putio/static/putio.js index f58292a..438348f 100644 --- a/couchpotato/core/downloaders/putio/static/putio.js +++ b/couchpotato/core/downloaders/putio/static/putio.js @@ -17,7 +17,7 @@ var PutIODownloader = new Class({ var putio_set = 0; fieldset.getElements('input[type=text]').each(function(el){ - putio_set += +(el.get('value') != ''); + putio_set += +(el.get('value') !== ''); }); new Element('.ctrlHolder').adopt( @@ -57,7 +57,7 @@ var PutIODownloader = new Class({ } }) ).inject(fieldset.getElement('.test_button'), 'before'); - }) + }); } diff --git a/couchpotato/core/downloaders/sabnzbd.py b/couchpotato/core/downloaders/sabnzbd.py index 4859209..c9bb7cc 100644 --- a/couchpotato/core/downloaders/sabnzbd.py +++ b/couchpotato/core/downloaders/sabnzbd.py @@ -73,10 +73,11 @@ class Sabnzbd(DownloaderBase): return False log.debug('Result from SAB: %s', sab_data) - if sab_data.get('status') and not sab_data.get('error'): + nzo_ids = sab_data.get('nzo_ids', []) + if sab_data.get('status') and not sab_data.get('error') and isinstance(nzo_ids, list) and len(nzo_ids) > 0: log.info('NZB sent to SAB successfully.') if filedata: - return self.downloadReturnId(sab_data.get('nzo_ids')[0]) + return self.downloadReturnId(nzo_ids[0]) else: return True else: diff --git a/couchpotato/core/downloaders/transmission.py b/couchpotato/core/downloaders/transmission.py index 697f22a..78f8fd7 100644 --- a/couchpotato/core/downloaders/transmission.py +++ b/couchpotato/core/downloaders/transmission.py @@ -68,7 +68,7 @@ class Transmission(DownloaderBase): if self.conf('directory'): if os.path.isdir(self.conf('directory')): - params['download-dir'] = self.conf('directory') + params['download-dir'] = self.conf('directory').rstrip(os.path.sep) else: log.error('Download directory from Transmission settings: %s doesn\'t exist', self.conf('directory')) @@ -147,7 +147,7 @@ class Transmission(DownloaderBase): status = 'failed' elif torrent['status'] == 0 and torrent['percentDone'] == 1: status = 'completed' - elif torrent['status'] == 16 and torrent['percentDone'] == 1: + elif torrent['status'] == 16 and torrent['percentDone'] == 1: status = 'completed' elif torrent['status'] in [5, 6]: status = 'seeding' diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index 0d98600..3642c10 100755 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -88,8 +88,13 @@ class MediaBase(Plugin): if len(existing_files) == 0: del existing_files[file_type] + images = image_urls.get(image_type, []) + for y in ['SX300', 'tmdb']: + initially_try = [x for x in images if y in x] + images[:-1] = initially_try + # Loop over type - for image in image_urls.get(image_type, []): + for image in images: if not isinstance(image, (str, unicode)): continue diff --git a/couchpotato/core/media/_base/providers/nzb/nzbclub.py b/couchpotato/core/media/_base/providers/nzb/nzbclub.py index a266089..e585114 100644 --- a/couchpotato/core/media/_base/providers/nzb/nzbclub.py +++ b/couchpotato/core/media/_base/providers/nzb/nzbclub.py @@ -15,7 +15,7 @@ log = CPLog(__name__) class Base(NZBProvider, RSS): urls = { - 'search': 'https://www.nzbclub.com/nzbfeeds.aspx?%s', + 'search': 'https://www.nzbclub.com/nzbrss.aspx?%s', } http_time_between_calls = 4 # seconds diff --git a/couchpotato/core/media/_base/providers/torrent/hd4free.py b/couchpotato/core/media/_base/providers/torrent/hd4free.py new file mode 100644 index 0000000..ec80b76 --- /dev/null +++ b/couchpotato/core/media/_base/providers/torrent/hd4free.py @@ -0,0 +1,131 @@ +import re +import json +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://hd4free.xyz/', + 'detail': 'https://hd4free.xyz/details.php?id=%s', + 'search': 'https://hd4free.xyz/searchapi.php?apikey=%s&username=%s&imdbid=%s&internal=%s', + 'download': 'https://hd4free.xyz/download.php?torrent=%s&torrent_pass=%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'], result['torrentpass']), + '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': 'hd4free', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'HD4Free', + 'wizard': True, + 'description': 'HD4Free', + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABX1BMVEUF6nsH33cJ03EJ1XIJ1nMKzXIKz28Lym4MxGsMxWsMx2wNvmgNv2kNwGkNwWwOuGgOuWYOuWcOumcOu2cOvmgPtWQPtmUPt2UPt2YQr2IQsGIQsGMQsmMQs2QRqmARq2ARrmERrmISpV4SpmASp14SqF8ToFsToFwToVwTo10TpV0UnFoUn1sVllcVmFgWkFUWklYXjVQXjlMXkFUYh1EYilIYi1MZhlEafk0af04agE4agU4beEobeUsbe0wcdUkeaUQebUYfZEMfZ0QgX0AgYEAgYUEhWj4iVz0iWD0jTzkkSzcmQTMmQzQnPTInPjInPzIoNy8oOC8oODAoOTAoOjApMi0pNC4pNS4qLCoqLSsqLisqMCwrJygrKCgrKCkrKSkrKikrKiorKyosIyYsIycsJCcsJScsJigtHyUuGCIuGiMuGyMuHCMuHCQvEyAvFSEvFiEvFyE0ABU0ABY5lYz4AAAA3ElEQVR4AWNIQAMMiYmJCYkIkMCQnpKWkZ4KBGlARlpaLEOor194kI+Pj6+PT0CET0AYg46Alr22NDeHkBinnq6SkitDrolDgYtaapajdpGppoFfGkMhv2GxE0uuPwNfsk6mhHMOQ54isxmbUJKCtWx+tIZQcDpDtqSol7qIMqsRu3dIhJxxFkOBoF2JG5O7lSqjh5S/tkkWQ5SBTbqnfkymv2WGLa95YCSDhZiMvKIwj4GJCpesuDivK0N6VFRUYlRyfHJUchQQJDMkxsfHJcTHAxEIxMVj+BZDAACjwkqhYgsTAAAAAABJRU5ErkJggg==', + '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 Profile Security', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 0, + 'description': 'Will not be (re)moved until this seed ratio is met. HD4Free 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. HD4Free minimum is 72 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 HD4Free internal', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 0, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index f3cecdf..c76bd32 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -31,7 +31,8 @@ class Base(TorrentMagnetProvider): proxy_list = [ 'https://kat.cr', - 'http://katproxy.com', + 'https://kickass.unblocked.pw/', + 'https://katproxy.com', ] def _search(self, media, quality, results): @@ -66,7 +67,7 @@ class Base(TorrentMagnetProvider): link = td.find('div', {'class': 'torrentname'}).find_all('a')[2] new['id'] = temp.get('id')[-7:] new['name'] = link.text - new['url'] = td.find('a', 'imagnet')['href'] + new['url'] = td.find('a', {'href': re.compile('magnet:*')})['href'] new['detail_url'] = self.urls['detail'] % (self.getDomain(), link['href'][1:]) new['verified'] = True if td.find('a', 'iverify') else False new['score'] = 100 if new['verified'] else 0 diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index 53afb47..d5350a9 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -18,8 +18,6 @@ class Base(TorrentProvider): http_time_between_calls = 1 # seconds proxy_list = [ - 'https://yts.re', - 'https://yts.wf', 'https://yts.im', 'https://yts.to', 'https://yify.ml', diff --git a/couchpotato/core/media/_base/search/static/search.css b/couchpotato/core/media/_base/search/static/search.css deleted file mode 100644 index 4f7b77b..0000000 --- a/couchpotato/core/media/_base/search/static/search.css +++ /dev/null @@ -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; -} diff --git a/couchpotato/core/media/_base/search/static/search.js b/couchpotato/core/media/_base/search/static/search.js index a7cd364..7b9a536 100644 --- a/couchpotato/core/media/_base/search/static/search.js +++ b/couchpotato/core/media/_base/search/static/search.js @@ -1,7 +1,11 @@ -Block.Search = new Class({ +var BlockSearch = new Class({ Extends: BlockBase, + options: { + 'animate': true + }, + cache: {}, create: function(){ @@ -9,49 +13,47 @@ 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) + } + }), + self.wrapper = new Element('div.wrapper').adopt( + self.result_container = new Element('div.results_container', { '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'); + self.last_q = null; + }).delay(100); + } + } + }) + ) ) ); - self.mask = new Element('div.mask').inject(self.result_container).fade('hide'); + self.mask = new Element('div.mask').inject(self.result_container); }, @@ -67,11 +69,32 @@ 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'); + + // Animate in + if(self.options.animate){ + + dynamics.css(self.wrapper, { + opacity: 0, + scale: 0.1 + }); + + dynamics.animate(self.wrapper, { + opacity: 1, + scale: 1 + }, { + type: dynamics.spring, + frequency: 200, + friction: 270, + duration: 800 + }); + + } } }, @@ -105,7 +128,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 +138,10 @@ Block.Search = new Class({ if(!self.q()){ self.hideResults(true); - return + return; } - self.list() + self.list(); }, list: function(){ @@ -129,7 +152,9 @@ Block.Search = new Class({ self.hideResults(false); if(!cache){ - self.mask.fade('in'); + setTimeout(function(){ + self.mask.addClass('show'); + }, 10); if(!self.spinner) self.spinner = createSpinner(self.mask); @@ -139,7 +164,7 @@ Block.Search = new Class({ 'q': q }, 'onComplete': self.fill.bind(self, q) - }) + }); } else self.fill(q, cache); @@ -158,30 +183,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.removeClass('show'); }, loading: function(bool){ - this.el[bool ? 'addClass' : 'removeClass']('loading') + this.el[bool ? 'addClass' : 'removeClass']('loading'); }, q: function(){ diff --git a/couchpotato/core/media/_base/search/static/search.scss b/couchpotato/core/media/_base/search/static/search.scss new file mode 100644 index 0000000..352fdf1 --- /dev/null +++ b/couchpotato/core/media/_base/search/static/search.scss @@ -0,0 +1,503 @@ +@import "_mixins"; + +.search_form { + display: inline-block; + z-index: 11; + width: 44px; + position: relative; + + * { + transform: translateZ(0); + } + + .icon-search { + position: absolute; + z-index: 2; + top: 50%; + left: 0; + height: 100%; + text-align: center; + color: #FFF; + font-size: 20px; + transform: 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 { + 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; + } + + &:focus { + background: rgba($theme_off, .2); + + &::-webkit-input-placeholder { + color: $text_color; + opacity: .7; + } + &::-moz-placeholder { + color: $text_color; + opacity: .7; + } + &:-ms-input-placeholder { + color: $text_color; + opacity: .7; + } + } + } + } + + &.filled { + &.focused .icon-search:before, + .page.home & .icon-search:before { + content: '\e80e'; + } + + .input input { + background: rgba($theme_off, .4); + } + } + + &.focused, + &.shown, + .page.home & { + border-color: #04bce6; + + .wrapper { + display: block; + width: 380px; + transform-origin: 0 90%; + + @include media-phablet { + width: 260px; + } + } + + .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; + + @include media-phablet { + font-size: 12px; + } + + .options { + position: absolute; + height: 100%; + top: 0; + left: 30px; + right: 0; + background: rgba(0,0,0,.3); + display: flex; + align-items: center; + + @include media-phablet { + left: 0; + } + + > .in_library_wanted { + margin-top: -7px; + } + + > div { + border: 0; + display: flex; + padding: 10px; + align-items: stretch; + justify-content: space-between; + + @include media-phablet { + padding: 3px; + } + } + + select { + display: block; + height: 100%; + width: 100%; + + @include media-phablet { + min-width: 0; + margin-right: 2px; + } + } + + .title { + margin-right: 5px; + width: 210px; + + @include media-phablet { + width: 140px; + margin-right: 2px; + } + } + + .profile, .category { + margin: 0 5px 0 0; + + @include media-phablet { + margin-right: 2px; + } + } + + .add { + width: 42px; + flex: 1 auto; + + a { + color: #FFF; + } + } + + .button { + display: block; + background: $primary_color; + text-align: center; + margin: 0; + } + + .message { + font-size: 20px; + color: #fff; + } + + } + + .thumbnail { + width: 30px; + min-height: 100%; + display: block; + margin: 0; + vertical-align: top; + + @include media-phablet { + display: none; + } + } + + .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); + transform: translateX(0); + background: $background_color; + + @include media-phablet { + left: 0; + } + + &:hover { + transform: translateX(2%); + } + + &.open { + transform: translateX(100%); + } + + .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%; + display: flex; + + .title { + display: inline-block; + margin: 0; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex: 1 auto; + } + + .year { + opacity: .4; + padding: 0 5px; + width: auto; + display: none; + } + + .in_wanted, + .in_library { + position: absolute; + top: 15px; + left: 0; + font-size: 11px; + color: $primary_color; + } + + &.in_library_wanted { + .title { + margin-top: -7px; + } + } + } + } + } + + &: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; + } + } + + .page.home & { + $input_height: 66px; + $input_height_mobile: 44px; + + display: block; + padding: $padding; + width: 100%; + max-width: 500px; + margin: 0 auto; + height: $input_height + 2*$padding; + position: relative; + margin-top: $padding; + + @include media-phablet { + margin-top: $padding/2; + height: $input_height_mobile + $padding; + } + + .icon-search { + display: block; + color: #000; + right: $padding; + top: $padding; + width: $input_height; + height: $input_height; + line-height: $input_height; + left: auto; + transform: none; + font-size: 2em; + opacity: .5; + + @include media-phablet { + right: $padding/2; + width: $input_height_mobile; + height: $input_height_mobile; + line-height: $input_height_mobile; + right: $padding/2; + top: $padding/2; + font-size: 1.5em; + } + } + + .wrapper { + border-radius: 0; + box-shadow: none; + bottom: auto; + top: $padding; + left: $padding; + right: $padding; + position: absolute; + width: auto; + + @include media-phablet { + right: $padding/2; + top: $padding/2; + left: $padding/2; + } + + &:before { + display: none; + } + + .input { + border-radius: 0; + left: 0; + position: absolute; + top: 0; + height: $input_height; + + @include media-phablet { + height: $input_height_mobile; + } + + input { + box-shadow: 0; + font-size: 2em; + font-weight: 400; + + @include media-phablet { + font-size: 1em; + } + } + } + + .results_container { + min-height: $input_height; + position: absolute; + top: $input_height; + left: 0; + right: 0; + border: 1px solid #b7b7b7; + border-top: 0; + + @include media-phablet { + top: $input_height_mobile; + min-height: $input_height_mobile; + } + + + @include media-phablet-and-up { + .results { + max-height: 400px; + + .media_result { + height: $input_height; + + + @include media-phablet { + height: $input_height_mobile; + } + + .thumbnail { + width: 40px; + } + + .options { + left: 40px; + + .title { + margin-right: 5px; + width: 320px; + + @include media-phablet { + width: 140px; + margin-right: 2px; + } + } + } + + .data { + left: 40px; + } + } + } + } + + + @include media-phablet { + .results { + .media_result { + height: $input_height_mobile; + + .options { + + .title { + + width: 140px; + margin-right: 2px; + } + + } + + } + } + } + } + + } + + + } + +} + +.big_search { + background: $theme_off; +} diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 48a66b0..3ded1f8 100755 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -34,6 +34,7 @@ class MovieBase(MovieTypeBase): 'params': { 'identifier': {'desc': 'IMDB id of the movie your want to add.'}, 'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'}, + 'force_readd': {'desc': 'Force re-add even if movie already in wanted or manage. Default: True'}, 'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'}, 'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, } @@ -78,6 +79,11 @@ class MovieBase(MovieTypeBase): if not info or (info and len(info.get('titles', [])) == 0): info = fireEvent('movie.info', merge = True, extended = False, identifier = params.get('identifier')) + # Allow force re-add overwrite from param + if 'force_readd' in params: + fra = params.get('force_readd') + force_readd = fra.lower() not in ['0', '-1'] if not isinstance(fra, bool) else fra + # Set default title default_title = toUnicode(info.get('title')) titles = info.get('titles', []) @@ -224,11 +230,11 @@ class MovieBase(MovieTypeBase): try: m = db.get('id', media_id) - m['profile_id'] = kwargs.get('profile_id') + m['profile_id'] = kwargs.get('profile_id') or m['profile_id'] cat_id = kwargs.get('category_id') if cat_id is not None: - m['category_id'] = cat_id if len(cat_id) > 0 else None + m['category_id'] = cat_id if len(cat_id) > 0 else m['category_id'] # Remove releases for rel in fireEvent('release.for_media', m['_id'], single = True): @@ -249,6 +255,7 @@ class MovieBase(MovieTypeBase): fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id)) except: + print traceback.format_exc() log.error('Can\'t edit non-existing media') return { diff --git a/couchpotato/core/media/movie/_base/static/details.js b/couchpotato/core/media/movie/_base/static/details.js new file mode 100644 index 0000000..c6230bd --- /dev/null +++ b/couchpotato/core/media/movie/_base/static/details.js @@ -0,0 +1,154 @@ +var MovieDetails = new Class({ + + Extends: BlockBase, + + sections: null, + buttons: null, + + initialize: function(parent, options){ + var self = this; + + self.sections = {}; + + var category = parent.get('category'), + profile = parent.profile; + + 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.scroll_content').grab( + new Element('div.head').adopt( + new Element('h1').grab( + self.title_dropdown = new BlockMenu(self, { + 'class': 'title', + 'button_text': parent.getTitle() + (parent.get('year') ? ' (' + parent.get('year') + ')' : ''), + 'button_class': 'icon-dropdown' + }) + ), + self.buttons = new Element('div.buttons') + ) + ) + ); + + self.addSection('description', new Element('div', { + 'text': parent.get('plot') + })); + + + // Title dropdown + var titles = parent.get('info').titles; + $(self.title_dropdown).addEvents({ + 'click:relay(li a)': function(e, el){ + (e).stopPropagation(); + + // Update category + Api.request('movie.edit', { + 'data': { + 'id': parent.get('_id'), + 'default_title': el.get('text') + } + }); + + $(self.title_dropdown).getElements('.icon-ok').removeClass('icon-ok'); + el.addClass('icon-ok'); + + self.title_dropdown.button.set('text', el.get('text') + (parent.get('year') ? ' (' + parent.get('year') + ')' : '')); + + } + }); + + titles.each(function(t){ + self.title_dropdown.addLink(new Element('a', { + 'text': t, + 'class': parent.get('title') == t ? 'icon-ok' : '' + })); + }); + + }, + + 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) + ); + }, + + addButton: function(button){ + var self = this; + + self.buttons.grab(button); + }, + + open: function(){ + var self = this; + + self.el.addClass('show'); + + if(!App.mobile_screen){ + $(self.content).getElements('> .head, > .section').each(function(section, nr){ + dynamics.css(section, { + opacity: 0, + translateY: 100 + }); + + dynamics.animate(section, { + opacity: 1, + translateY: 0 + }, { + type: dynamics.spring, + frequency: 200, + friction: 300, + duration: 1200, + delay: 500 + (nr * 100) + }); + }); + } + + }, + + close: function(){ + var self = this; + + var ended = function() { + self.el.dispose(); + self.overlay.removeEventListener('transitionend', ended); + }; + self.overlay.addEventListener('transitionend', ended, false); + + // animate out + + if(!App.mobile_screen){ + $(self.content).getElements('> .head, > .section').reverse().each(function(section, nr){ + dynamics.animate(section, { + opacity: 0, + translateY: 100 + }, { + type: dynamics.spring, + frequency: 200, + friction: 300, + duration: 1200, + delay: (nr * 50) + }); + }); + + dynamics.setTimeout(function(){ + self.el.removeClass('show'); + }, 200); + } + else { + self.el.removeClass('show'); + } + } + +}); diff --git a/couchpotato/core/media/movie/_base/static/list.js b/couchpotato/core/media/movie/_base/static/list.js index 83ac4ed..6177da4 100644 --- a/couchpotato/core/media/movie/_base/static/list.js +++ b/couchpotato/core/media/movie/_base/static/list.js @@ -3,6 +3,7 @@ var MovieList = new Class({ Implements: [Events, Options], options: { + api_call: 'media.list', navigation: true, limit: 50, load_more: true, @@ -37,7 +38,28 @@ var MovieList = new Class({ 'html': self.options.description, 'styles': {'display': 'none'} }) : null, - self.movie_list = new Element('div'), + self.movie_list = new Element('div', { + 'events': { + 'click:relay(.movie)': function(e, el){ + el.retrieve('klass').onClick(e); + }, + 'mouseenter:relay(.movie)': function(e, el){ + (e).stopPropagation(); + el.retrieve('klass').onMouseenter(e); + }, + 'mouseleave:relay(.movie)': function(e, el){ + (e).stopPropagation(); + el.retrieve('klass').onMouseleave(e); + }, + 'change:relay(.movie input)': function(e, el){ + (e).stopPropagation(); + el = el.getParent(); + var klass = el.retrieve('klass'); + klass.fireEvent('select'); + klass.select(klass.select_checkbox.get('checked')); + } + } + }), self.load_more = self.options.load_more ? new Element('a.load_more', { 'events': { 'click': self.loadMore.bind(self) @@ -45,15 +67,17 @@ 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'); - self.getMovies(); + // Create the alphabet nav + if(self.options.navigation) + self.createNavigation(); + + if(self.options.api_call) + 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 +91,7 @@ var MovieList = new Class({ self.setCounter(self.counter_count-1); self.total_movies--; } - }) + }); } self.checkIfEmpty(); @@ -89,18 +113,15 @@ var MovieList = new Class({ create: function(){ var self = this; - // Create the alphabet nav - if(self.options.navigation) - self.createNavigation(); - - if(self.options.load_more) + if(self.options.load_more){ self.scrollspy = new ScrollSpy({ + container: self.el.getParent(), min: function(){ - var c = self.load_more.getCoordinates(); - return c.top - window.document.getSize().y - 300 + return self.load_more.getCoordinates().top; }, onEnter: self.loadMore.bind(self) }); + } self.created = true; }, @@ -108,6 +129,7 @@ var MovieList = new Class({ addMovies: function(movies, total){ var self = this; + if(!self.created) self.create(); // do scrollspy @@ -116,13 +138,12 @@ var MovieList = new Class({ self.scrollspy.stop(); } - Object.each(movies, function(movie){ - self.createMovie(movie); - }); + self.createMovie(movies, 'bottom'); self.total_movies += total; self.setCounter(total); + self.calculateSelected(); }, setCounter: function(count){ @@ -138,7 +159,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 '+self.filter.starts_with+'' : ''); @@ -167,20 +188,37 @@ var MovieList = new Class({ }, - createMovie: function(movie, inject_at){ - var self = this; - var m = new Movie(self, { - 'actions': self.options.actions, - 'view': self.current_view, - 'onSelect': self.calculateSelected.bind(self) - }, movie); + createMovie: function(movie, inject_at, nr){ + var self = this, + movies = Array.isArray(movie) ? movie : [movie], + movie_els = []; + inject_at = inject_at || 'bottom'; + + movies.each(function(movie, nr){ + + var m = new Movie(self, { + 'actions': self.options.actions, + 'view': self.current_view, + 'onSelect': self.calculateSelected.bind(self) + }, movie); - $(m).inject(self.movie_list, inject_at || 'bottom'); + var el = $(m); - m.fireEvent('injected'); + if(inject_at === 'bottom'){ + movie_els.push(el); + } + else { + el.inject(self.movie_list, inject_at); + } + + self.movies.include(m); + self.movies_added[movie._id] = true; + }); + + if(movie_els.length > 0){ + $(self.movie_list).adopt(movie_els); + } - self.movies.include(m); - self.movies_added[movie._id] = true; }, createNavigation: function(){ @@ -192,7 +230,7 @@ var MovieList = new Class({ 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', { + self.mass_edit_select = new Element('input[type=checkbox]', { 'events': { 'change': self.massEditToggleAll.bind(self) } @@ -230,38 +268,40 @@ 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).preventDefault(); + + 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); - - el.inject(el.getParent(), 'top'); - el.getSiblings().hide(); - setTimeout(function(){ - el.getSiblings().setStyle('display', null); - }, 100) + self.changeView(new_view); + + self.navigation_actions.getElement('[data-view='+new_view+']') + .addClass(a); + } } }), - 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); Quality.getActiveProfiles().each(function(profile){ 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 +313,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 +329,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 +341,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,24 +386,26 @@ 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, checked = selected == movies && selected > 0; - self.mass_edit_select.set('indeterminate', indeterminate); + document.body[selected > 0 ? 'addClass' : 'removeClass']('mass_editing'); - self.mass_edit_select_class[checked ? 'check' : 'uncheck'](); - self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate'); + if(self.mass_edit_select){ + self.mass_edit_select.set('checked', checked); + self.mass_edit_select.indeterminate = indeterminate; - self.mass_edit_selected.set('text', selected); + 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'; + 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 references in CouchPotato'; 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'), @@ -441,10 +483,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 +495,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 +535,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+'_view', new_view, {duration: 1000}); }, getSavedView: function(){ var self = this; - return Cookie.read(self.options.identifier+'_view2'); + return self.options.force_view ? self.options.view : Cookie.read(self.options.identifier+'_view'); }, search: function(){ @@ -537,23 +579,24 @@ var MovieList = new Class({ self.load_more.set('text', 'loading...'); } - if(self.movies.length == 0 && self.options.loader){ + var loader_timeout; + if(self.movies.length === 0 && self.options.loader){ - self.loader_first = new Element('div.loading').adopt( + self.loader_first = new Element('div.mask.loading.with_message').grab( new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'}) ).inject(self.el, 'top'); + createSpinner(self.loader_first); - createSpinner(self.loader_first, { - radius: 4, - length: 4, - width: 1 - }); + var lfc = self.loader_first; + loader_timeout = setTimeout(function(){ + lfc.addClass('show'); + }, 10); - self.el.setStyle('min-height', 93); + self.el.setStyle('min-height', 220); } - Api.request(self.options.api_call || 'media.list', { + Api.request(self.options.api_call, { 'data': Object.merge({ 'type': self.options.type || 'movie', 'status': self.options.status, @@ -564,13 +607,15 @@ var MovieList = new Class({ if(reset) self.movie_list.empty(); + if(loader_timeout) clearTimeout(loader_timeout); if(self.loader_first){ var lf = self.loader_first; - self.loader_first.addClass('hide'); self.loader_first = null; + lf.removeClass('show'); + setTimeout(function(){ lf.destroy(); - }, 20000); + }, 1000); self.el.setStyle('min-height', null); } @@ -590,7 +635,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 +648,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'](); diff --git a/couchpotato/core/media/movie/_base/static/manage.js b/couchpotato/core/media/movie/_base/static/manage.js index e861899..16d0a54 100644 --- a/couchpotato/core/media/movie/_base/static/manage.js +++ b/couchpotato/core/media/movie/_base/static/manage.js @@ -1,4 +1,4 @@ -Page.Manage = new Class({ +var MoviesManage = new Class({ Extends: PageBase, @@ -33,15 +33,12 @@ Page.Manage = new Class({ 'release_status': 'done', 'status_or': 1 }, - 'actions': [MA.IMDB, MA.Trailer, MA.Files, MA.Readd, MA.Edit, MA.Delete], + 'actions': [MA.IMDB, MA.Files, MA.Trailer, MA.Readd, MA.Delete], 'menu': [self.refresh_button, self.refresh_quick], 'on_empty_element': new Element('div.empty_manage').adopt( new Element('div', { - 'text': 'Seems like you don\'t have anything in your library yet.' - }), - new Element('div', { - 'text': 'Add your existing movie folders in ' - }).adopt( + 'text': 'Seems like you don\'t have anything in your library yet. Add your existing movie folders in ' + }).grab( new Element('a', { 'text': 'Settings > Manage', 'href': App.createUrl('settings/manage') @@ -49,7 +46,7 @@ Page.Manage = new Class({ ), new Element('div.after_manage', { 'text': 'When you\'ve done that, hit this button → ' - }).adopt( + }).grab( new Element('a.button.green', { 'text': 'Hit me, but not too hard', 'events':{ @@ -59,7 +56,7 @@ Page.Manage = new Class({ ) ) }); - $(self.list).inject(self.el); + $(self.list).inject(self.content); // Check if search is in progress self.startProgressInterval(); @@ -113,7 +110,8 @@ Page.Manage = new Class({ return; if(!self.progress_container) - self.progress_container = new Element('div.progress').inject(self.list.navigation, 'after'); + self.progress_container = new Element('div.progress') + .inject(self.list, 'top'); self.progress_container.empty(); @@ -126,12 +124,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 +139,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(); } }); diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js index e5dc08d..f729bb1 100644 --- a/couchpotato/core/media/movie/_base/static/movie.actions.js +++ b/couchpotato/core/media/movie/_base/static/movie.actions.js @@ -2,7 +2,12 @@ var MovieAction = new Class({ Implements: [Options], - class_name: 'action icon2', + class_name: 'action', + label: 'UNKNOWN', + icon: null, + button: null, + details: null, + detail_button: null, initialize: function(movie, options){ var self = this; @@ -11,33 +16,56 @@ var MovieAction = new Class({ self.movie = movie; self.create(); - if(self.el) - self.el.addClass(self.class_name) + + if(self.button){ + var wrapper = new Element('div', { + 'class': self.class_name + }); + self.button.inject(wrapper); + + self.button = wrapper; + } }, create: function(){}, + getButton: function(){ + return this.button || null; + }, + + getDetails: function(){ + return this.details || null; + }, + + getDetailButton: function(){ + return this.detail_button || 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(){ var self = this; try { - return self.movie.getTitle(); + return self.movie.getTitle(true); } catch(e){ try { return self.movie.original_title ? self.movie.original_title : self.movie.titles[0]; } - catch(e){ + catch(e2){ return 'Unknown'; } } @@ -46,10 +74,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 +91,7 @@ var MovieAction = new Class({ }, toElement: function(){ - return this.el || null + return this.el || null; } }); @@ -80,36 +108,33 @@ MA.IMDB = new Class({ self.id = self.movie.getIdentifier ? self.movie.getIdentifier() : self.get('imdb'); - self.el = new Element('a.imdb', { + self.button = self.createButton(); + self.detail_button = self.createButton(); + + if(!self.id) self.disable(); + }, + + createButton: function(){ + var self = this; + + return 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' }); - - if(!self.id) self.disable(); - } + }, }); 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 +143,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,17 +154,9 @@ 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.movie.data.releases || self.movie.data.releases.length === 0) return; if(!self.options_container || refresh){ self.options_container = new Element('div.options').grab( @@ -154,7 +171,8 @@ MA.Release = new Class({ new Element('span.size', {'text': 'Size'}), new Element('span.age', {'text': 'Age'}), new Element('span.score', {'text': 'Score'}), - new Element('span.provider', {'text': 'Provider'}) + new Element('span.provider', {'text': 'Provider'}), + new Element('span.actions') ).inject(self.release_container); if(self.movie.data.releases) @@ -162,53 +180,62 @@ 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(); } catch(e){} } + var size = info.size ? Math.floor(self.get(release, 'size')) : 0; + size = size ? ((size < 1000) ? size + 'MB' : Math.round(size*10/1024)/10 + 'GB') : 'n/a'; + // 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': size}), 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('span.actions').adopt( + info.detail_url ? new Element('a.icon-info', { + 'href': info.detail_url, + 'target': '_blank' + }) : new Element('a'), + new Element('a.icon-download', { + 'events': { + 'click': function(e){ + (e).stopPropagation(); + if(!this.hasClass('completed')) + self.download(release); + } } - } - }), - new Element('a.delete.icon2', { - 'events': { - 'click': function(e){ - (e).preventDefault(); - self.ignore(release); + }), + new Element('a', { + 'class': release.status == 'ignored' ? 'icon-redo' : 'icon-cancel', + 'events': { + 'click': function(e){ + (e).stopPropagation(); + self.ignore(release); + + this.toggleClass('icon-redo'); + this.toggleClass('icon-cancel'); + } } - } - }) + }) + ) ).inject(self.release_container); if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){ @@ -227,9 +254,9 @@ MA.Release = new Class({ 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); + var status_el = release.el.getElement('.status'); + status_el.set('class', '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); @@ -259,9 +286,9 @@ MA.Release = new Class({ self.trynext_container.adopt( new Element('span.or', { - 'text': 'If anything went wrong, download' + 'text': 'If anything went wrong, download ' }), - lr ? new Element('a.button.orange', { + lr ? new Element('a.orange', { 'text': 'the same release again', 'events': { 'click': function(){ @@ -270,9 +297,9 @@ MA.Release = new Class({ } }) : null, nr && lr ? new Element('span.or', { - 'text': ',' + 'text': ', ' }) : null, - nr ? [new Element('a.button.green', { + nr ? [new Element('a.green', { 'text': lr ? 'another release' : 'the best release', 'events': { 'click': function(){ @@ -281,9 +308,9 @@ MA.Release = new Class({ } }), new Element('span.or', { - 'text': 'or pick one below' + 'text': ' or pick one below' })] : null - ) + ); } self.last_release = null; @@ -291,71 +318,19 @@ MA.Release = new Class({ } - // Show it - self.options_container.inject(self.movie, 'top'); - self.movie.slide('in', self.options_container); - - }, - - showHelper: function(e){ - var self = this; - if(e) - (e).preventDefault(); - - var has_available = false, - has_snatched = false; - - if(self.movie.data.releases) - self.movie.data.releases.each(function(release){ - if(has_available && has_snatched) return; - - if(['snatched', 'downloaded', 'seeding', 'done'].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.movie.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.movie.quality.fireEvent('click'); - } - } - })] : null, - new Element('a.icon2.completed', { - 'text': 'mark this movie done', - 'events': { - 'click': self.markMovieDone.bind(self) - } - }) - ) - } + return self.options_container; }, 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){ var self = this; var release_el = self.release_container.getElement('#release_'+release._id), - icon = release_el.getElement('.download.icon2'); + icon = release_el.getElement('.icon-download'); if(icon) icon.addClass('icon spinner').removeClass('download'); @@ -386,293 +361,415 @@ MA.Release = new Class({ 'data': { 'id': release._id } - }) + }); + + } + +}); + +MA.Trailer = new Class({ + + Extends: MovieAction, + id: null, + label: 'Trailer', + + getDetails: function(){ + var self = this, + data_url = 'https://www.googleapis.com/youtube/v3/search?q="{title}" {year} trailer&maxResults=1&type=video&videoDefinition=high&videoEmbeddable=true&part=snippet&key=AIzaSyAT3li1KjfLidaL6Vt8T92MRU7n4VOrjYk'; + + + if(!self.player_container){ + self.id = 'trailer-'+randomString(); + self.player_container = new Element('div.icon-play[id='+self.id+']', { + 'events': { + 'click': self.watch.bind(self) + } + }).adopt( + new Element('span[text="watch"]'), + new Element('span[text="trailer"]') + ); + + self.container = new Element('div.trailer_container') + .grab(self.player_container); + + var url = data_url.substitute({ + 'title': encodeURI(self.getTitle()), + 'year': self.get('year') + }); + + new Request.JSONP({ + 'url': url, + 'onComplete': function(json){ + self.video_id = json.items[0].id.videoId; + self.container.grab(new Element('div.background', { + 'styles': { + 'background-image': 'url('+json.items[0].snippet.thumbnails.high.url+')' + } + })); + } + }).send(); + } + + return self.container; }, - markMovieDone: function(){ + watch: function(){ var self = this; - Api.request('media.delete', { - 'data': { - 'id': self.movie.get('_id'), - 'delete_from': 'wanted' - }, - 'onComplete': function(){ - var movie = $(self.movie); - movie.set('tween', { - 'duration': 300, - 'onComplete': function(){ - self.movie.destroy() - } - }); - movie.tween('height', 0); - } - }); + self.container.set('html', '' + })); + }, + fillVersion: function(json) { + if (!json) return; + var self = this; + var date = new Date(json.version.date * 1e3); + self.version_text.set("text", json.version.hash + (json.version.date ? " (" + date.toLocaleString() + ")" : "")); + self.updater_type.set("text", json.version.type != json.branch ? json.version.type + ", " + json.branch : json.branch); + } +}); + +window.addEvent("domready", function() { + new AboutSettingTab(); +}); + +window.addEvent("domready", function() { + var b = $(document.body), login_page = b.hasClass("login"); + if (login_page) { + var form = b.getElement("form"), els = b.getElements("h1, .username, .password, .remember_me, .button"); + els.each(function(el, nr) { + dynamics.css(el, { + opacity: 0, + translateY: 50 + }); + dynamics.animate(el, { + opacity: 1, + translateY: 0 + }, { + type: dynamics.spring, + frequency: 200, + friction: 300, + duration: 800, + anticipationSize: 175, + anticipationStrength: 400, + delay: nr * 100 + }); + }); + } +}); \ No newline at end of file diff --git a/couchpotato/static/scripts/combined.plugins.min.js b/couchpotato/static/scripts/combined.plugins.min.js new file mode 100644 index 0000000..a6b0571 --- /dev/null +++ b/couchpotato/static/scripts/combined.plugins.min.js @@ -0,0 +1,3844 @@ +var DownloadersBase = new Class({ + Implements: [ Events ], + initialize: function() { + var self = this; + App.addEvent("loadSettings", self.addTestButtons.bind(self)); + }, + addTestButtons: function() { + var self = this; + var setting_page = App.getPage("Settings"); + setting_page.addEvent("create", function() { + Object.each(setting_page.tabs.downloaders.groups, self.addTestButton.bind(self)); + }); + }, + addTestButton: function(fieldset, plugin_name) { + var self = this, button_name = self.testButtonName(fieldset); + if (button_name.contains("Downloaders")) return; + new Element(".ctrlHolder.test_button").grab(new Element("a.button", { + text: button_name, + events: { + click: function() { + var button = fieldset.getElement(".test_button .button"); + button.set("text", "Connecting..."); + Api.request("download." + plugin_name + ".test", { + onComplete: function(json) { + button.set("text", button_name); + var message; + if (json.success) { + message = new Element("span.success", { + text: "Connection successful" + }).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"); + } + (function() { + message.destroy(); + }).delay(3e3); + } + }); + } + } + })).inject(fieldset); + }, + testButtonName: function(fieldset) { + var name = fieldset.getElement("h2 .group_label").get("text"); + return "Test " + name; + } +}); + +var Downloaders = new DownloadersBase(); + +var UpdaterBase = new Class({ + Implements: [ Events ], + initialize: function() { + var self = this; + App.addEvent("load", self.info.bind(self, 2e3)); + App.addEvent("unload", function() { + if (self.timer) clearTimeout(self.timer); + }); + }, + check: function(onComplete) { + var self = this; + Api.request("updater.check", { + onComplete: function(json) { + if (onComplete) onComplete(json); + if (json.update_available) self.doUpdate(); else { + App.unBlockPage(); + App.trigger("message", [ "No updates available" ]); + } + } + }); + }, + info: function(timeout) { + var self = this; + if (self.timer) clearTimeout(self.timer); + self.timer = setTimeout(function() { + Api.request("updater.info", { + onComplete: function(json) { + self.json = json; + self.fireEvent("loaded", [ json ]); + if (json.update_version) { + self.createMessage(json); + } else { + if (self.message) self.message.destroy(); + } + } + }); + }, timeout || 0); + }, + getInfo: function() { + return this.json; + }, + createMessage: function(data) { + var self = this; + if (self.message) return; + var changelog = "https://github.com/" + data.repo_name + "/compare/" + data.version.hash + "..." + data.branch; + if (data.update_version.changelog) changelog = data.update_version.changelog + "#" + data.version.hash + "..." + data.update_version.hash; + self.message = new Element("div.message.update").adopt(new Element("span", { + text: "A new version is available" + }), new Element("a", { + href: changelog, + text: "see what has changed", + target: "_blank" + }), new Element("span[text=or]"), new Element("a", { + text: "just update, gogogo!", + events: { + click: self.doUpdate.bind(self) + } + })).inject(document.body); + }, + doUpdate: function() { + var self = this; + App.blockPage("Please wait while CouchPotato is being updated with more awesome stuff.", "Updating"); + Api.request("updater.update", { + onComplete: function(json) { + if (json.success) self.updating(); else App.unBlockPage(); + } + }); + }, + updating: function() { + App.checkAvailable.delay(500, App, [ 1e3, function() { + window.location.reload(); + } ]); + if (self.message) self.message.destroy(); + } +}); + +var Updater = new UpdaterBase(); + +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(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, 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(); +}); + +var BlockSearch = new Class({ + Extends: BlockBase, + options: { + animate: true + }, + cache: {}, + create: function() { + var self = this; + var focus_timer = 0; + self.el = new Element("div.search_form").adopt(new Element("a.icon-search", { + events: { + click: self.clear.bind(self) + } + }), self.wrapper = new Element("div.wrapper").adopt(self.result_container = new Element("div.results_container", { + events: { + mousewheel: function(e) { + e.stopPropagation(); + } + } + }).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"); + self.last_q = null; + }.delay(100); + } + } + })))); + self.mask = new Element("div.mask").inject(self.result_container); + }, + clear: function(e) { + var self = this; + e.preventDefault(); + if (self.last_q === "") { + self.input.blur(); + self.last_q = null; + } else { + self.last_q = ""; + self.input.set("value", ""); + self.el.addClass("focused"); + self.input.focus(); + self.media = {}; + self.results.empty(); + self.el.removeClass("filled"); + if (self.options.animate) { + dynamics.css(self.wrapper, { + opacity: 0, + scale: .1 + }); + dynamics.animate(self.wrapper, { + opacity: 1, + scale: 1 + }, { + type: dynamics.spring, + frequency: 200, + friction: 270, + duration: 800 + }); + } + } + }, + hideResults: function(bool) { + var self = this; + if (self.hidden == bool) return; + self.el[bool ? "removeClass" : "addClass"]("shown"); + if (bool) { + History.removeEvent("change", self.hideResults.bind(self, !bool)); + self.el.removeEvent("outerClick", self.hideResults.bind(self, !bool)); + } else { + History.addEvent("change", self.hideResults.bind(self, !bool)); + self.el.addEvent("outerClick", self.hideResults.bind(self, !bool)); + } + self.hidden = bool; + }, + keyup: function() { + var self = this; + self.el[self.q() ? "addClass" : "removeClass"]("filled"); + if (self.q() != self.last_q) { + if (self.api_request && self.api_request.isRunning()) self.api_request.cancel(); + if (self.autocomplete_timer) clearTimeout(self.autocomplete_timer); + self.autocomplete_timer = self.autocomplete.delay(300, self); + } + }, + autocomplete: function() { + var self = this; + if (!self.q()) { + self.hideResults(true); + return; + } + self.list(); + }, + list: function() { + var self = this, q = self.q(), cache = self.cache[q]; + self.hideResults(false); + if (!cache) { + setTimeout(function() { + self.mask.addClass("show"); + }, 10); + if (!self.spinner) self.spinner = createSpinner(self.mask); + self.api_request = Api.request("search", { + data: { + q: q + }, + onComplete: self.fill.bind(self, q) + }); + } else self.fill(q, cache); + self.last_q = q; + }, + fill: function(q, json) { + var self = this; + self.cache[q] = json; + self.media = {}; + self.results.empty(); + Object.each(json, function(media) { + if (typeOf(media) == "array") { + Object.each(media, function(me) { + var m = new (window["BlockSearch" + me.type.capitalize() + "Item"])(me); + $(m).inject(self.results); + self.media[m.imdb || "r-" + Math.floor(Math.random() * 1e4)] = m; + if (q == m.imdb) m.showOptions(); + }); + } + }); + self.mask.removeClass("show"); + }, + loading: function(bool) { + this.el[bool ? "addClass" : "removeClass"]("loading"); + }, + q: function() { + return this.input.get("value").trim(); + } +}); + +var MovieDetails = new Class({ + Extends: BlockBase, + sections: null, + buttons: null, + initialize: function(parent, options) { + var self = this; + self.sections = {}; + var category = parent.get("category"), profile = parent.profile; + 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.scroll_content").grab(new Element("div.head").adopt(new Element("h1").grab(self.title_dropdown = new BlockMenu(self, { + class: "title", + button_text: parent.getTitle() + (parent.get("year") ? " (" + parent.get("year") + ")" : ""), + button_class: "icon-dropdown" + })), self.buttons = new Element("div.buttons")))); + self.addSection("description", new Element("div", { + text: parent.get("plot") + })); + var titles = parent.get("info").titles; + $(self.title_dropdown).addEvents({ + "click:relay(li a)": function(e, el) { + e.stopPropagation(); + Api.request("movie.edit", { + data: { + id: parent.get("_id"), + default_title: el.get("text") + } + }); + $(self.title_dropdown).getElements(".icon-ok").removeClass("icon-ok"); + el.addClass("icon-ok"); + self.title_dropdown.button.set("text", el.get("text") + (parent.get("year") ? " (" + parent.get("year") + ")" : "")); + } + }); + titles.each(function(t) { + self.title_dropdown.addLink(new Element("a", { + text: t, + class: parent.get("title") == t ? "icon-ok" : "" + })); + }); + }, + 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)); + }, + addButton: function(button) { + var self = this; + self.buttons.grab(button); + }, + open: function() { + var self = this; + self.el.addClass("show"); + if (!App.mobile_screen) { + $(self.content).getElements("> .head, > .section").each(function(section, nr) { + dynamics.css(section, { + opacity: 0, + translateY: 100 + }); + dynamics.animate(section, { + opacity: 1, + translateY: 0 + }, { + type: dynamics.spring, + frequency: 200, + friction: 300, + duration: 1200, + delay: 500 + nr * 100 + }); + }); + } + }, + close: function() { + var self = this; + var ended = function() { + self.el.dispose(); + self.overlay.removeEventListener("transitionend", ended); + }; + self.overlay.addEventListener("transitionend", ended, false); + if (!App.mobile_screen) { + $(self.content).getElements("> .head, > .section").reverse().each(function(section, nr) { + dynamics.animate(section, { + opacity: 0, + translateY: 100 + }, { + type: dynamics.spring, + frequency: 200, + friction: 300, + duration: 1200, + delay: nr * 50 + }); + }); + dynamics.setTimeout(function() { + self.el.removeClass("show"); + }, 200); + } else { + self.el.removeClass("show"); + } + } +}); + +var MovieList = new Class({ + Implements: [ Events, Options ], + options: { + api_call: "media.list", + 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.movies").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", { + events: { + "click:relay(.movie)": function(e, el) { + el.retrieve("klass").onClick(e); + }, + "mouseenter:relay(.movie)": function(e, el) { + e.stopPropagation(); + el.retrieve("klass").onMouseenter(e); + }, + "mouseleave:relay(.movie)": function(e, el) { + e.stopPropagation(); + el.retrieve("klass").onMouseleave(e); + }, + "change:relay(.movie input)": function(e, el) { + e.stopPropagation(); + el = el.getParent(); + var klass = el.retrieve("klass"); + klass.fireEvent("select"); + klass.select(klass.select_checkbox.get("checked")); + } + } + }), self.load_more = self.options.load_more ? new Element("a.load_more", { + events: { + click: self.loadMore.bind(self) + } + }) : null); + self.changeView(self.getSavedView() || self.options.view || "thumb"); + if (self.options.navigation) self.createNavigation(); + if (self.options.api_call) 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.createMovie(notification.data, "top"); + self.setCounter(self.counter_count + 1); + self.checkIfEmpty(); + } + }, + create: function() { + var self = this; + if (self.options.load_more) { + self.scrollspy = new ScrollSpy({ + container: self.el.getParent(), + min: function() { + return self.load_more.getCoordinates().top; + }, + onEnter: self.loadMore.bind(self) + }); + } + self.created = true; + }, + addMovies: function(movies, total) { + var self = this; + if (!self.created) self.create(); + if (movies.length < self.options.limit && self.scrollspy) { + self.load_more.hide(); + self.scrollspy.stop(); + } + self.createMovie(movies, "bottom"); + self.total_movies += total; + self.setCounter(total); + self.calculateSelected(); + }, + setCounter: function(count) { + var self = this; + if (!self.navigation_counter) return; + self.counter_count = count; + self.navigation_counter.set("text", (count || 0) + " movies"); + 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 " + self.filter.starts_with + "" : ""); + self.empty_message = new Element(".message", { + html: "No movies found " + message + ".
" + }).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); + } + }, + createMovie: function(movie, inject_at, nr) { + var self = this, movies = Array.isArray(movie) ? movie : [ movie ], movie_els = []; + inject_at = inject_at || "bottom"; + movies.each(function(movie, nr) { + var m = new Movie(self, { + actions: self.options.actions, + view: self.current_view, + onSelect: self.calculateSelected.bind(self) + }, movie); + var el = $(m); + if (inject_at === "bottom") { + movie_els.push(el); + } else { + el.inject(self.movie_list, inject_at); + } + self.movies.include(m); + self.movies_added[movie._id] = true; + }); + if (movie_els.length > 0) { + $(self.movie_list).adopt(movie_els); + } + }, + 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]", { + 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 BlockMenu(self, { + class: "filter", + button_class: "icon-filter" + }), self.navigation_actions = new Element("div.actions", { + events: { + click: function(e, el) { + e.preventDefault(); + var new_view = self.current_view == "list" ? "thumb" : "list"; + var a = "active"; + self.navigation_actions.getElements("." + a).removeClass(a); + self.changeView(new_view); + self.navigation_actions.getElement("[data-view=" + new_view + "]").addClass(a); + } + } + }), self.navigation_menu = new BlockMenu(self, { + class: "extra", + button_class: "icon-dots" + }))); + 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 icon-search"); + var available_chars; + self.filter_menu.addEvent("open", function() { + self.navigation_search_input.focus(); + if (!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible())) Api.request("media.available_chars", { + data: Object.merge({ + 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); + } + } + })); + [ "thumb", "list" ].each(function(view) { + var current = self.current_view == view; + new Element("a", { + class: "icon-" + view + (current ? " active " : ""), + "data-view": view + }).inject(self.navigation_actions, current ? "top" : "bottom"); + }); + self.letters.all = new Element("li.letter_all.available.active", { + text: "ALL" + }).inject(self.navigation_alpha); + chars.split("").each(function(c) { + self.letters[c] = new Element("li", { + text: c, + class: "letter_" + c, + "data-letter": c + }).inject(self.navigation_alpha); + }); + 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; + document.body[selected > 0 ? "addClass" : "removeClass"]("mass_editing"); + if (self.mass_edit_select) { + self.mass_edit_select.set("checked", checked); + self.mass_edit_select.indeterminate = 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 references in CouchPotato"; + 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.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 + "_view", new_view, { + duration: 1e3 + }); + }, + getSavedView: function() { + var self = this; + return self.options.force_view ? self.options.view : Cookie.read(self.options.identifier + "_view"); + }, + 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..."); + } + var loader_timeout; + if (self.movies.length === 0 && self.options.loader) { + self.loader_first = new Element("div.mask.loading.with_message").grab(new Element("div.message", { + text: self.options.title ? "Loading '" + self.options.title + "'" : "Loading..." + })).inject(self.el, "top"); + createSpinner(self.loader_first); + var lfc = self.loader_first; + loader_timeout = setTimeout(function() { + lfc.addClass("show"); + }, 10); + self.el.setStyle("min-height", 220); + } + Api.request(self.options.api_call, { + 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 (loader_timeout) clearTimeout(loader_timeout); + if (self.loader_first) { + var lf = self.loader_first; + self.loader_first = null; + lf.removeClass("show"); + setTimeout(function() { + lf.destroy(); + }, 1e3); + self.el.setStyle("min-height", null); + } + self.store(json.movies); + self.addMovies(json.movies, json.total || json.movies.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; + } +}); + +var MoviesManage = new Class({ + Extends: PageBase, + order: 20, + name: "manage", + title: "Do stuff to your existing movies!", + indexAction: function() { + var self = this; + if (!self.list) { + self.refresh_button = new Element("a", { + title: "Rescan your library for new movies", + text: "Full library refresh", + events: { + click: self.refresh.bind(self, true) + } + }); + self.refresh_quick = new Element("a", { + title: "Just scan for recently changed", + text: "Quick library scan", + events: { + click: self.refresh.bind(self, false) + } + }); + self.list = new MovieList({ + identifier: "manage", + filter: { + status: "done", + release_status: "done", + status_or: 1 + }, + actions: [ MA.IMDB, MA.Files, MA.Trailer, MA.Readd, MA.Delete ], + menu: [ self.refresh_button, self.refresh_quick ], + on_empty_element: new Element("div.empty_manage").adopt(new Element("div", { + text: "Seems like you don't have anything in your library yet. Add your existing movie folders in " + }).grab(new Element("a", { + text: "Settings > Manage", + href: App.createUrl("settings/manage") + })), new Element("div.after_manage", { + text: "When you've done that, hit this button → " + }).grab(new Element("a.button.green", { + text: "Hit me, but not too hard", + events: { + click: self.refresh.bind(self, true) + } + }))) + }); + $(self.list).inject(self.content); + self.startProgressInterval(); + } + }, + refresh: function(full) { + var self = this; + if (!self.update_in_progress) { + Api.request("manage.update", { + data: { + full: +full + } + }); + self.startProgressInterval(); + } + }, + startProgressInterval: function() { + var self = this; + self.progress_interval = setInterval(function() { + if (self.progress_request && self.progress_request.running) return; + self.update_in_progress = true; + self.progress_request = Api.request("manage.progress", { + onComplete: function(json) { + if (!json || !json.progress) { + clearInterval(self.progress_interval); + self.update_in_progress = false; + if (self.progress_container) { + self.progress_container.destroy(); + self.list.update(); + } + } else { + var progress = json.progress; + if (!self.list.navigation) return; + if (!self.progress_container) self.progress_container = new Element("div.progress").inject(self.list, "top"); + self.progress_container.empty(); + var sorted_table = self.parseProgress(json.progress); + sorted_table.each(function(folder) { + var folder_progress = progress[folder]; + new Element("div").adopt(new Element("span.folder", { + text: folder + (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); + }); + } + } + }); + }, 1e3); + }, + parseProgress: function(progress_object) { + var folder, temp_array = []; + for (folder in progress_object) { + if (progress_object.hasOwnProperty(folder)) { + temp_array.push(folder); + } + } + return temp_array.stableSort(); + } +}); + +var MovieAction = new Class({ + Implements: [ Options ], + class_name: "action", + label: "UNKNOWN", + icon: null, + button: null, + details: null, + detail_button: null, + initialize: function(movie, options) { + var self = this; + self.setOptions(options); + self.movie = movie; + self.create(); + if (self.button) { + var wrapper = new Element("div", { + class: self.class_name + }); + self.button.inject(wrapper); + self.button = wrapper; + } + }, + create: function() {}, + getButton: function() { + return this.button || null; + }, + getDetails: function() { + return this.details || null; + }, + getDetailButton: function() { + return this.detail_button || null; + }, + getLabel: function() { + return this.label; + }, + 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.movie.getTitle(true); + } catch (e) { + try { + return self.movie.original_title ? self.movie.original_title : self.movie.titles[0]; + } catch (e2) { + return "Unknown"; + } + } + }, + get: function(key) { + var self = this; + try { + return self.movie.get(key); + } catch (e) { + return self.movie[key]; + } + }, + createMask: function() { + var self = this; + self.mask = new Element("div.mask", { + styles: { + "z-index": "1" + } + }).inject(self.movie, "top").fade("hide"); + }, + toElement: function() { + return this.el || null; + } +}); + +var MA = {}; + +MA.IMDB = new Class({ + Extends: MovieAction, + id: null, + create: function() { + var self = this; + self.id = self.movie.getIdentifier ? self.movie.getIdentifier() : self.get("imdb"); + self.button = self.createButton(); + self.detail_button = self.createButton(); + if (!self.id) self.disable(); + }, + createButton: function() { + var self = this; + return 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" + }); + } +}); + +MA.Release = new Class({ + Extends: MovieAction, + label: "Releases", + create: function() { + var self = this; + App.on("movie.searcher.ended", function(notification) { + if (self.movie.data._id != notification.data._id) return; + self.releases = null; + if (self.options_container) { + if (self.options_container.isDisplayed()) { + self.options_container.destroy(); + self.getDetails(); + } else { + self.options_container.destroy(); + self.options_container = null; + } + } + }); + }, + getDetails: function(refresh) { + var self = this; + if (!self.movie.data.releases || self.movie.data.releases.length === 0) return; + if (!self.options_container || refresh) { + self.options_container = new Element("div.options").grab(self.release_container = new Element("div.releases.table")); + 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" + }), new Element("span.actions")).inject(self.release_container); + if (self.movie.data.releases) self.movie.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) {} + } + var size = info.size ? Math.floor(self.get(release, "size")) : 0; + size = size ? size < 1e3 ? size + "MB" : Math.round(size * 10 / 1024) / 10 + "GB" : "n/a"; + 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: size + }), 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 + }), new Element("span.actions").adopt(info.detail_url ? new Element("a.icon-info", { + href: info.detail_url, + target: "_blank" + }) : new Element("a"), new Element("a.icon-download", { + events: { + click: function(e) { + e.stopPropagation(); + if (!this.hasClass("completed")) self.download(release); + } + } + }), new Element("a", { + class: release.status == "ignored" ? "icon-redo" : "icon-cancel", + events: { + click: function(e) { + e.stopPropagation(); + self.ignore(release); + this.toggleClass("icon-redo"); + this.toggleClass("icon-cancel"); + } + } + }))).inject(self.release_container); + 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.movie.quality.getElement(".q_" + release.quality), new_status = notification.data.status; + release.el.set("class", "item " + new_status); + var status_el = release.el.getElement(".status"); + status_el.set("class", "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.release_container.getElements("#release_" + self.last_release._id).addClass("last_release"); + if (self.next_release) self.release_container.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.release_container, "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.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.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; + } + return self.options_container; + }, + 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.release_container.getElement("#release_" + release._id), icon = release_el.getElement(".icon-download"); + 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 + } + }); + } +}); + +MA.Trailer = new Class({ + Extends: MovieAction, + id: null, + label: "Trailer", + getDetails: function() { + var self = this, data_url = 'https://www.googleapis.com/youtube/v3/search?q="{title}" {year} trailer&maxResults=1&type=video&videoDefinition=high&videoEmbeddable=true&part=snippet&key=AIzaSyAT3li1KjfLidaL6Vt8T92MRU7n4VOrjYk'; + if (!self.player_container) { + self.id = "trailer-" + randomString(); + self.player_container = new Element("div.icon-play[id=" + self.id + "]", { + events: { + click: self.watch.bind(self) + } + }).adopt(new Element('span[text="watch"]'), new Element('span[text="trailer"]')); + self.container = new Element("div.trailer_container").grab(self.player_container); + var url = data_url.substitute({ + title: encodeURI(self.getTitle()), + year: self.get("year") + }); + new Request.JSONP({ + url: url, + onComplete: function(json) { + self.video_id = json.items[0].id.videoId; + self.container.grab(new Element("div.background", { + styles: { + "background-image": "url(" + json.items[0].snippet.thumbnails.high.url + ")" + } + })); + } + }).send(); + } + return self.container; + }, + watch: function() { + var self = this; + self.container.set("html", '