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 1ccf689..6e5f612 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ CouchPotato ===== +[![Join the chat at https://gitter.im/RuudBurger/CouchPotatoServer](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/RuudBurger/CouchPotatoServer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + CouchPotato (CP) is an automatic NZB and torrent downloader. You can keep a "movies I want"-list and it will search for NZBs/torrents of these movies every X hours. Once a movie is found, it will send it to SABnzbd or download the torrent to a specified directory. @@ -23,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` @@ -33,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 @@ -47,23 +51,34 @@ 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). - -FreeBSD : - -* Update your ports tree `sudo portsnap fetch update` -* Install Python 2.6+ [lang/python](http://www.freshports.org/lang/python) with `cd /usr/ports/lang/python; sudo make install clean` -* Install port [databases/py-sqlite3](http://www.freshports.org/databases/py-sqlite3) with `cd /usr/ports/databases/py-sqlite3; sudo make install clean` -* Add a symlink to 'python2' `sudo ln -s /usr/local/bin/python /usr/local/bin/python2` -* Install port [ftp/libcurl](http://www.freshports.org/ftp/libcurl) with `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean` -* Install port [ftp/curl](http://www.freshports.org/ftp/bcurl), deselect 'Asynchronous DNS resolution via c-ares' when prompted as part of config `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean` -* Install port [textproc/docbook-xml-450](http://www.freshports.org/textproc/docbook-xml-450) with `cd /usr/ports/textproc/docbook-xml-450; sudo make install clean` -* Install port [GIT](http://git-scm.com/) with `cd /usr/ports/devel/git; sudo make install clean` -* 'cd' to the folder of your choosing. +* 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: + +* Become root with `su` +* Update your repo catalog `pkg update` +* Install required tools `pkg install python py27-sqlite3 fpc-libcurl docbook-xml git-lite` +* For default install location and running as root `cd /usr/local` +* If running as root, expects python here `ln -s /usr/local/bin/python /usr/bin/python` * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` -* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time -* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato` -* Change the paths inside the init script. `sudo vim /etc/rc.d/couchpotato` -* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato` -* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf` +* Copy the startup script `cp CouchPotatoServer/init/freebsd /usr/local/etc/rc.d/couchpotato` +* Make startup script executable `chmod 555 /usr/local/etc/rc.d/couchpotato` +* Add startup to boot `echo 'couchpotato_enable="YES"' >> /etc/rc.conf` +* Read the options at the top of `more /usr/local/etc/rc.d/couchpotato` +* 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 320131b..eeaf96d 100644 --- a/couchpotato/core/_base/_core.py +++ b/couchpotato/core/_base/_core.py @@ -5,6 +5,7 @@ import signal import time import traceback import webbrowser +import sys from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent @@ -52,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) @@ -64,6 +66,23 @@ class Core(Plugin): import socket socket.setdefaulttimeout(30) + # Don't check ssl by default + try: + if sys.version_info >= (2, 7, 9): + import ssl + ssl._create_default_https_context = ssl._create_unverified_context + 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 '' @@ -174,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')) @@ -230,6 +250,12 @@ config = [{ 'description': 'The port I should listen to.', }, { + 'name': 'ipv6', + 'default': 0, + 'type': 'bool', + 'description': 'Also bind the WebUI to ipv6 address', + }, + { 'name': 'ssl_cert', 'description': 'Path to SSL server.crt', 'advanced': True, @@ -261,6 +287,30 @@ 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', + 'description': 'Route outbound connections via proxy. Currently, only HTTP(S) proxies are supported. ', + }, + { + 'name': 'proxy_server', + 'description': 'Override system default proxy server. Currently, only HTTP(S) proxies are supported. Ex. \"127.0.0.1:8080\". Keep empty to use system default proxy server.', + }, + { + 'name': 'proxy_username', + 'description': 'Only HTTP Basic Auth is supported. Leave blank to disable authentication.', + }, + { + 'name': 'proxy_password', + 'type': 'password', + 'description': 'Leave blank for no password.', + }, + { 'name': 'debug', '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/hadouken.py b/couchpotato/core/downloaders/hadouken.py index c89ed0e..8bb0326 100644 --- a/couchpotato/core/downloaders/hadouken.py +++ b/couchpotato/core/downloaders/hadouken.py @@ -31,13 +31,33 @@ class Hadouken(DownloaderBase): log.error('Config properties are not filled in correctly, port is missing.') return False - if not self.conf('api_key'): - log.error('Config properties are not filled in correctly, API key is missing.') - return False + # This is where v4 and v5 begin to differ + if(self.conf('version') == 'v4'): + if not self.conf('api_key'): + log.error('Config properties are not filled in correctly, API key is missing.') + return False + + url = 'http://' + str(host[0]) + ':' + str(host[1]) + '/jsonrpc' + client = JsonRpcClient(url, 'Token ' + self.conf('api_key')) + self.hadouken_api = HadoukenAPIv4(client) - self.hadouken_api = HadoukenAPI(host[0], port = host[1], api_key = self.conf('api_key')) + return True + else: + auth_type = self.conf('auth_type') + header = None - return True + if auth_type == 'api_key': + header = 'Token ' + self.conf('api_key') + elif auth_type == 'user_pass': + header = 'Basic ' + b64encode(self.conf('auth_user') + ':' + self.conf('auth_pass')) + + url = 'http://' + str(host[0]) + ':' + str(host[1]) + '/api' + client = JsonRpcClient(url, header) + self.hadouken_api = HadoukenAPIv5(client) + + return True + + return False def download(self, data = None, media = None, filedata = None): """ Send a torrent/nzb file to the downloader @@ -66,6 +86,8 @@ class Hadouken(DownloaderBase): if self.conf('label'): torrent_params['label'] = self.conf('label') + # Set the tags array since that is what v5 expects. + torrent_params['tags'] = [self.conf('label')] torrent_filename = self.createFileName(data, filedata, media) @@ -132,71 +154,25 @@ class Hadouken(DownloaderBase): if torrent is None: continue - torrent_filelist = self.hadouken_api.get_files_by_hash(torrent['InfoHash']) + torrent_filelist = self.hadouken_api.get_files_by_hash(torrent.info_hash) torrent_files = [] - save_path = torrent['SavePath'] - - # The 'Path' key for each file_item contains - # the full path to the single file relative to the - # torrents save path. - - # For a single file torrent the result would be, - # - Save path: "C:\Downloads" - # - file_item['Path'] = "file1.iso" - # Resulting path: "C:\Downloads\file1.iso" - - # For a multi file torrent the result would be, - # - Save path: "C:\Downloads" - # - file_item['Path'] = "dirname/file1.iso" - # Resulting path: "C:\Downloads\dirname/file1.iso" - for file_item in torrent_filelist: - torrent_files.append(sp(os.path.join(save_path, file_item['Path']))) + torrent_files.append(sp(os.path.join(torrent.save_path, file_item))) release_downloads.append({ - 'id': torrent['InfoHash'].upper(), - 'name': torrent['Name'], - 'status': self.get_torrent_status(torrent), - 'seed_ratio': self.get_seed_ratio(torrent), - 'original_status': torrent['State'], + 'id': torrent.info_hash.upper(), + 'name': torrent.name, + 'status': torrent.get_status(), + 'seed_ratio': torrent.get_seed_ratio(), + 'original_status': torrent.state, 'timeleft': -1, - 'folder': sp(save_path if len(torrent_files == 1) else os.path.join(save_path, torrent['Name'])), + 'folder': sp(torrent.save_path if len(torrent_files == 1) else os.path.join(torrent.save_path, torrent.name)), 'files': torrent_files }) return release_downloads - def get_seed_ratio(self, torrent): - """ Returns the seed ratio for a given torrent. - - Keyword arguments: - torrent -- The torrent to calculate seed ratio for. - """ - - up = torrent['TotalUploadedBytes'] - down = torrent['TotalDownloadedBytes'] - - if up > 0 and down > 0: - return up / down - - return 0 - - def get_torrent_status(self, torrent): - """ Returns the CouchPotato status for a given torrent. - - Keyword arguments: - torrent -- The torrent to translate status for. - """ - - if torrent['IsSeeding'] and torrent['IsFinished'] and torrent['Paused']: - return 'completed' - - if torrent['IsSeeding']: - return 'seeding' - - return 'busy' - def pause(self, release_download, pause = True): """ Pauses or resumes the torrent specified by the ID field in release_download. @@ -243,45 +219,85 @@ class Hadouken(DownloaderBase): return self.hadouken_api.remove(release_download['id'], remove_data = delete_files) -class HadoukenAPI(object): - def __init__(self, host = 'localhost', port = 7890, api_key = None): - self.url = 'http://' + str(host) + ':' + str(port) - self.api_key = api_key - self.requestId = 0; +class JsonRpcClient(object): + def __init__(self, url, auth_header = None): + self.url = url + self.requestId = 0 self.opener = urllib2.build_opener() - self.opener.addheaders = [('User-agent', 'couchpotato-hadouken-client/1.0'), ('Accept', 'application/json')] + self.opener.addheaders = [ + ('User-Agent', 'couchpotato-hadouken-client/1.0'), + ('Accept', 'application/json'), + ('Content-Type', 'application/json') + ] + + if auth_header: + self.opener.addheaders.append(('Authorization', auth_header)) + + def invoke(self, method, params): + self.requestId += 1 - if not api_key: - log.error('API key missing.') + data = { + 'jsonrpc': '2.0', + 'id': self.requestId, + 'method': method, + 'params': params + } + + request = urllib2.Request(self.url, data = json.dumps(data)) + + try: + f = self.opener.open(request) + response = f.read() + f.close() + + obj = json.loads(response) + + if 'error' in obj.keys(): + log.error('JSONRPC error, %s: %s', obj['error']['code'], obj['error']['message']) + return False - def add_file(self, filedata, torrent_params): + if 'result' in obj.keys(): + return obj['result'] + + return True + except httplib.InvalidURL as err: + log.error('Invalid Hadouken host, check your config %s', err) + except urllib2.HTTPError as err: + if err.code == 401: + log.error('Could not authenticate, check your config') + else: + log.error('Hadouken HTTPError: %s', err) + except urllib2.URLError as err: + log.error('Unable to connect to Hadouken %s', err) + + return False + + +class HadoukenAPI(object): + def __init__(self, rpc_client): + self.rpc = rpc_client + + if not rpc_client: + log.error('No JSONRPC client specified.') + + def add_file(self, data, params): """ Add a file to Hadouken with the specified parameters. Keyword arguments: filedata -- The binary torrent data. torrent_params -- Additional parameters for the file. """ - data = { - 'method': 'torrents.addFile', - 'params': [b64encode(filedata), torrent_params] - } - - return self._request(data) + pass - def add_magnet_link(self, magnetLink, torrent_params): + def add_magnet_link(self, link, params): """ Add a magnet link to Hadouken with the specified parameters. Keyword arguments: magnetLink -- The magnet link to send. torrent_params -- Additional parameters for the magnet link. """ - data = { - 'method': 'torrents.addUrl', - 'params': [magnetLink, torrent_params] - } - - return self._request(data) + pass def get_by_hash_list(self, infoHashList): """ Gets a list of torrents filtered by the given info hash list. @@ -289,12 +305,7 @@ class HadoukenAPI(object): Keyword arguments: infoHashList -- A list of info hashes. """ - data = { - 'method': 'torrents.getByInfoHashList', - 'params': [infoHashList] - } - - return self._request(data) + pass def get_files_by_hash(self, infoHash): """ Gets a list of files for the torrent identified by the @@ -303,26 +314,11 @@ class HadoukenAPI(object): Keyword arguments: infoHash -- The info hash of the torrent to return files for. """ - data = { - 'method': 'torrents.getFiles', - 'params': [infoHash] - } - - return self._request(data) + pass def get_version(self): """ Gets the version, commitish and build date of Hadouken. """ - data = { - 'method': 'core.getVersion', - 'params': None - } - - result = self._request(data) - - if not result: - return False - - return result['Version'] + pass def pause(self, infoHash, pause): """ Pauses/unpauses the torrent identified by the given info hash. @@ -331,15 +327,7 @@ class HadoukenAPI(object): infoHash -- The info hash of the torrent to operate on. pause -- If true, pauses the torrent. Otherwise resumes. """ - data = { - 'method': 'torrents.pause', - 'params': [infoHash] - } - - if not pause: - data['method'] = 'torrents.resume' - - return self._request(data) + pass def remove(self, infoHash, remove_data = False): """ Removes the torrent identified by the given info hash and @@ -349,46 +337,190 @@ class HadoukenAPI(object): infoHash -- The info hash of the torrent to remove. remove_data -- If true, removes the data associated with the torrent. """ - data = { - 'method': 'torrents.remove', - 'params': [infoHash, remove_data] - } + pass - return self._request(data) +class TorrentItem(object): + @property + def info_hash(self): + pass - def _request(self, data): - self.requestId += 1 + @property + def save_path(self): + pass - data['jsonrpc'] = '2.0' - data['id'] = self.requestId + @property + def name(self): + pass - request = urllib2.Request(self.url + '/jsonrpc', data = json.dumps(data)) - request.add_header('Authorization', 'Token ' + self.api_key) - request.add_header('Content-Type', 'application/json') + @property + def state(self): + pass - try: - f = self.opener.open(request) - response = f.read() - f.close() + def get_status(self): + """ Returns the CouchPotato status for a given torrent.""" + pass - obj = json.loads(response) + def get_seed_ratio(self): + """ Returns the seed ratio for a given torrent.""" + pass - if not 'error' in obj.keys(): - return obj['result'] - log.error('JSONRPC error, %s: %s', obj['error']['code'], obj['error']['message']) - except httplib.InvalidURL as err: - log.error('Invalid Hadouken host, check your config %s', err) - except urllib2.HTTPError as err: - if err.code == 401: - log.error('Invalid Hadouken API key, check your config') - else: - log.error('Hadouken HTTPError: %s', err) - except urllib2.URLError as err: - log.error('Unable to connect to Hadouken %s', err) +class TorrentItemv5(TorrentItem): + def __init__(self, obj): + self.obj = obj + + def info_hash(self): + return self.obj['infoHash'] + + def save_path(self): + return self.obj['savePath'] + + def name(self): + return self.obj['name'] + + def state(self): + return self.obj['state'] + + def get_status(self): + if self.obj['isSeeding'] and self.obj['isFinished'] and self.obj['isPaused']: + return 'completed' + + if self.obj['isSeeding']: + return 'seeding' + + return 'busy' + + def get_seed_ratio(self): + up = self.obj['uploadedBytesTotal'] + down = self.obj['downloadedBytesTotal'] + + if up > 0 and down > 0: + return up / down + + return 0 + + +class HadoukenAPIv5(HadoukenAPI): + def add_file(self, data, params): + return self.rpc.invoke('session.addTorrentFile', [b64encode(data), params]) + + def add_magnet_link(self, link, params): + return self.rpc.invoke('session.addTorrentUri', [link, params]) + + def get_by_hash_list(self, infoHashList): + torrents = self.rpc.invoke('session.getTorrents') + result = [] + + for torrent in torrents.values(): + if torrent['infoHash'] in infoHashList: + result.append(TorrentItemv5(torrent)) + + return result + + def get_files_by_hash(self, infoHash): + files = self.rpc.invoke('torrent.getFiles', [infoHash]) + result = [] + + for file in files: + result.append(file['path']) + + return result + + def get_version(self): + result = self.rpc.invoke('core.getSystemInfo', None) + + if not result: + return False + + return result['versions']['hadouken'] + + def pause(self, infoHash, pause): + if pause: + return self.rpc.invoke('torrent.pause', [infoHash]) + + return self.rpc.invoke('torrent.resume', [infoHash]) + + def remove(self, infoHash, remove_data = False): + return self.rpc.invoke('session.removeTorrent', [infoHash, remove_data]) - return False + +class TorrentItemv4(TorrentItem): + def __init__(self, obj): + self.obj = obj + + def info_hash(self): + return self.obj['InfoHash'] + + def save_path(self): + return self.obj['SavePath'] + + def name(self): + return self.obj['Name'] + + def state(self): + return self.obj['State'] + + def get_status(self): + if self.obj['IsSeeding'] and self.obj['IsFinished'] and self.obj['Paused']: + return 'completed' + + if self.obj['IsSeeding']: + return 'seeding' + + return 'busy' + + def get_seed_ratio(self): + up = self.obj['TotalUploadedBytes'] + down = self.obj['TotalDownloadedBytes'] + + if up > 0 and down > 0: + return up / down + + return 0 + + +class HadoukenAPIv4(object): + def add_file(self, data, params): + return self.rpc.invoke('torrents.addFile', [b64encode(data), params]) + + def add_magnet_link(self, link, params): + return self.rpc.invoke('torrents.addUrl', [link, params]) + + def get_by_hash_list(self, infoHashList): + torrents = self.rpc.invoke('torrents.getByInfoHashList', [infoHashList]) + result = [] + + for torrent in torrents: + result.append(TorrentItemv4(torrent)) + + return result + + def get_files_by_hash(self, infoHash): + files = self.rpc.invoke('torrents.getFiles', [infoHash]) + result = [] + + for file in files: + result.append(file['Path']) + + return result + + def get_version(self): + result = self.rpc.invoke('core.getVersion', None) + + if not result: + return False + + return result['Version'] + + def pause(self, infoHash, pause): + if pause: + return self.rpc.invoke('torrents.pause', [infoHash]) + + return self.rpc.invoke('torrents.resume', [infoHash]) + + def remove(self, infoHash, remove_data = False): + return self.rpc.invoke('torrents.remove', [infoHash, remove_data]) config = [{ @@ -409,15 +541,42 @@ config = [{ 'radio_group': 'torrent' }, { + 'name': 'version', + 'label': 'Version', + 'type': 'dropdown', + 'default': 'v4', + 'values': [('v4.x', 'v4'), ('v5.x', 'v5')], + 'description': 'Hadouken version.', + }, + { 'name': 'host', 'default': 'localhost:7890' }, { + 'name': 'auth_type', + 'label': 'Auth. type', + 'type': 'dropdown', + 'default': 'api_key', + 'values': [('None', 'none'), ('API key/Token', 'api_key'), ('Username/Password', 'user_pass')], + 'description': 'Type of authentication', + }, + { 'name': 'api_key', - 'label': 'API key', + 'label': 'API key (v4)/Token (v5)', 'type': 'password' }, { + 'name': 'auth_user', + 'label': 'Username', + 'description': '(only for v5)' + }, + { + 'name': 'auth_pass', + 'label': 'Password', + 'type': 'password', + 'description': '(only for v5)' + }, + { 'name': 'label', 'description': 'Label to add torrent as.' } diff --git a/couchpotato/core/downloaders/nzbget.py b/couchpotato/core/downloaders/nzbget.py index 9fbed73..04527c4 100644 --- a/couchpotato/core/downloaders/nzbget.py +++ b/couchpotato/core/downloaders/nzbget.py @@ -295,8 +295,8 @@ config = [{ 'advanced': True, 'default': '0', 'type': 'dropdown', - 'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)], - 'description': 'Only change this if you are using NZBget 9.0 or higher', + 'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100), ('Forced', 900)], + 'description': 'Only change this if you are using NZBget 13.0 or higher', }, { 'name': 'manual', 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/helpers/variable.py b/couchpotato/core/helpers/variable.py index 519c369..50cf91f 100755 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -41,7 +41,8 @@ def symlink(src, dst): def getUserDir(): try: import pwd - os.environ['HOME'] = sp(pwd.getpwuid(os.geteuid()).pw_dir) + if not os.environ['HOME']: + os.environ['HOME'] = sp(pwd.getpwuid(os.geteuid()).pw_dir) except: pass 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/base.py b/couchpotato/core/media/_base/providers/base.py index 1062a1a..642d477 100644 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -5,6 +5,11 @@ import time import traceback import xml.etree.ElementTree as XMLTree +try: + from xml.etree.ElementTree import ParseError as XmlParseError +except ImportError: + from xml.parsers.expat import ExpatError as XmlParseError + from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \ @@ -94,7 +99,7 @@ class Provider(Plugin): try: data = XMLTree.fromstring(ss(data)) return self.getElements(data, item_path) - except XMLTree.ParseError: + except XmlParseError: log.error('Invalid XML returned, check "%s" manually for issues', url) except: log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) diff --git a/couchpotato/core/media/_base/providers/nzb/newznab.py b/couchpotato/core/media/_base/providers/nzb/newznab.py index b9aad55..6af5eae 100644 --- a/couchpotato/core/media/_base/providers/nzb/newznab.py +++ b/couchpotato/core/media/_base/providers/nzb/newznab.py @@ -27,7 +27,7 @@ class Base(NZBProvider, RSS): passwords_regex = 'password|wachtwoord' limits_reached = {} - http_time_between_calls = 1 # Seconds + http_time_between_calls = 2 # Seconds def search(self, media, quality): hosts = self.getHosts() 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/alpharatio.py b/couchpotato/core/media/_base/providers/torrent/alpharatio.py new file mode 100644 index 0000000..b081e62 --- /dev/null +++ b/couchpotato/core/media/_base/providers/torrent/alpharatio.py @@ -0,0 +1,133 @@ +import traceback + +from bs4 import BeautifulSoup +from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider +import six + + +log = CPLog(__name__) + + +class Base(TorrentProvider): + + urls = { + 'test': 'https://alpharatio.cc/', + 'login': 'https://alpharatio.cc/login.php', + 'login_check': 'https://alpharatio.cc/inbox.php', + 'detail': 'https://alpharatio.cc/torrents.php?torrentid=%s', + 'search': 'https://alpharatio.cc/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1', + 'download': 'https://alpharatio.cc/%s', + } + + http_time_between_calls = 1 # Seconds + + def _search(self, media, quality, results): + + url = self.urls['search'] % self.buildUrl(media, quality) + data = self.getHTMLData(url) + + if data: + html = BeautifulSoup(data) + + try: + result_table = html.find('table', attrs = {'id': 'torrent_table'}) + if not result_table: + return + + entries = result_table.find_all('tr', attrs = {'class': 'torrent'}) + for result in entries: + + link = result.find('a', attrs = {'dir': 'ltr'}) + url = result.find('a', attrs = {'title': 'Download'}) + tds = result.find_all('td') + size = tds[4].contents[0].strip('\n ') + + results.append({ + 'id': link['href'].replace('torrents.php?id=', '').split('&')[0], + 'name': link.contents[0], + 'url': self.urls['download'] % url['href'], + 'detail_url': self.urls['download'] % link['href'], + 'size': self.parseSize(size), + 'seeders': tryInt(tds[len(tds)-2].string), + 'leechers': tryInt(tds[len(tds)-1].string), + }) + except: + log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) + + + def getLoginParams(self): + return { + 'username': self.conf('username'), + 'password': self.conf('password'), + 'keeplogged': '1', + 'login': 'Login', + } + + def loginSuccess(self, output): + return 'logout.php' in output.lower() + + loginCheckSuccess = loginSuccess + + def getSceneOnly(self): + return '1' if self.conf('scene_only') else '' + + +config = [{ + 'name': 'alpharatio', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'AlphaRatio', + 'description': 'AlphaRatio', + 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACX0lEQVQ4jbWTX0hTURzHv+fu3umdV9GtOZ3pcllGBomJ9RCmkiWIEJUQET2EMqF86aFeegqLHgoio1ICScoieugPiBlFFmpROUjNIub+NKeba2rqvdvuPKeXDIcsgugHB378fj8+X37fcw5hjOFfgvtTc8o7mdveHWv0+YJ5iWb45SQWi2kc7olCnteoHCGUMqbpejBkO99rPDlW5rjV3FjZkmXU+3SiKK8EkOUVxj2+9bZOe8ebhZxSRTCIQmAES1oLQADKp4EIc8gRFr3t+/SNe0oLelatYM0zO56dqS3fmh4eXkoxIrWvAwXegLta8bymYyak9lyGR7d57eHHtOt7aNaQ0AORU8OEqlg0HURTnXi96cCaK0AYEW0l+MAoQoIp48PHke0JAYwyBkYhameUQ3vz7lTt3NRdKH0ajxgqQMJzAMdBkRVdYgAAEA71G2Z6MnOyvSmSJB/bFblN5DHEsosghf3zZduK+1fdQhyEcKitr+r0B2dMAyPOcmd02oxiC2jUjJaSwbPZpoLJhAA1Ci3hGURRlO0Of8nN9/MNUUXSkrQsFQ4meNORG6/G2O/jGXdZ044OKzg3z3r77TUre81tL1pxirLMWnsoMB00LtfjPLh67/OJH3xRMgiHb96JOCVbxbobRONBQNqScffJ6JE4E2VZFvv6BirbXpkboGcA4eGaDOV73G4LAFBKSWRhNsmqfnHCosG159Lxt++GdgC/XuLD3sH60/fdFxjJBNMDAAVZ8CNfVJxPLzbs/uqa2Lj/0stHkWSDFlwS4FIhRKei3a3VNeS//sa/iZ/B6hMIr7Fq4QAAAABJRU5ErkJggg==', + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'scene_only', + 'type': 'bool', + 'default': False, + 'description': 'Only allow scene releases.' + }, + { + '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/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/ilovetorrents.py b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py index b6ccecd..908f486 100644 --- a/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/ilovetorrents.py @@ -23,7 +23,8 @@ class Base(TorrentProvider): } cat_ids = [ - (['41'], ['720p', '1080p', 'brrip']), + (['80'], ['720p', '1080p']), + (['41'], ['brrip']), (['19'], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), (['20'], ['dvdr']) ] @@ -88,7 +89,7 @@ class Base(TorrentProvider): id = re.search('id=(?P\d+)&', link).group('id') url = self.urls['download'] % download - fileSize = self.parseSize(result.select('td.rowhead')[5].text) + fileSize = self.parseSize(result.select('td.rowhead')[8].text) results.append({ 'id': id, 'name': toUnicode(prelink.find('b').text), diff --git a/couchpotato/core/media/_base/providers/torrent/iptorrents.py b/couchpotato/core/media/_base/providers/torrent/iptorrents.py index 61ced9c..d0750d8 100644 --- a/couchpotato/core/media/_base/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/iptorrents.py @@ -16,9 +16,9 @@ class Base(TorrentProvider): urls = { 'test': 'https://iptorrents.eu/', 'base_url': 'https://iptorrents.eu', - 'login': 'https://iptorrents.eu/torrents/', + 'login': 'https://iptorrents.eu/', 'login_check': 'https://iptorrents.eu/inbox.php', - 'search': 'https://iptorrents.eu/torrents/?%s%%s&q=%s&qf=ti&p=%%d', + 'search': 'https://iptorrents.eu/t?%s%%s&q=%s&qf=#torrents&p=%%d', } http_time_between_calls = 1 # Seconds diff --git a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py index d6e3ee7..c76bd32 100644 --- a/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py +++ b/couchpotato/core/media/_base/providers/torrent/kickasstorrents.py @@ -30,13 +30,9 @@ class Base(TorrentMagnetProvider): cat_backup_id = None proxy_list = [ - 'https://kickass.to', - 'http://kickass.pw', - 'http://kickassto.come.in', - 'http://katproxy.ws', - 'http://kickass.bitproxy.eu', - 'http://katph.eu', - 'http://kickassto.come.in', + 'https://kat.cr', + 'https://kickass.unblocked.pw/', + 'https://katproxy.com', ] def _search(self, media, quality, results): @@ -71,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/rarbg.py b/couchpotato/core/media/_base/providers/torrent/rarbg.py new file mode 100644 index 0000000..db7144f --- /dev/null +++ b/couchpotato/core/media/_base/providers/torrent/rarbg.py @@ -0,0 +1,230 @@ +import re +import traceback +import random +from datetime import datetime + +from couchpotato import fireEvent +from couchpotato.core.helpers.variable import tryInt, getIdentifier +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider + +log = CPLog(__name__) + +class Base(TorrentMagnetProvider): + + urls = { + 'test': 'https://torrentapi.org/pubapi_v2.php?app_id=couchpotato', + 'token': 'https://torrentapi.org/pubapi_v2.php?get_token=get_token&app_id=couchpotato', + 'search': 'https://torrentapi.org/pubapi_v2.php?token=%s&mode=search&search_imdb=%s&min_seeders=%s&min_leechers' + '=%s&ranked=%s&category=movies&format=json_extended&app_id=couchpotato', + } + + http_time_between_calls = 2 # Seconds + _token = 0 + + def _search(self, movie, quality, results): + hasresults = 0 + curryear = datetime.now().year + movieid = getIdentifier(movie) + + try: + movieyear = movie['info']['year'] + except: + log.error('RARBG: Couldn\'t get movie year') + movieyear = 0 + + self.getToken() + + if (self._token != 0) and (movieyear == 0 or movieyear <= curryear): + data = self.getJsonData(self.urls['search'] % (self._token, movieid, self.conf('min_seeders'), + self.conf('min_leechers'), self.conf('ranked_only')), headers = self.getRequestHeaders()) + + if data: + if 'error_code' in data: + if data['error'] == 'No results found': + log.debug('RARBG: No results returned from Rarbg') + else: + if data['error_code'] == 10: + log.error(data['error'], movieid) + else: + log.error('RARBG: There is an error in the returned JSON: %s', data['error']) + else: + hasresults = 1 + + try: + if hasresults: + for result in data['torrent_results']: + name = result['title'] + titlesplit = re.split('-', name) + releasegroup = titlesplit[len(titlesplit)-1] + + xtrainfo = self.find_info(name) + encoding = xtrainfo[0] + resolution = xtrainfo[1] + # source = xtrainfo[2] + pubdate = result['pubdate'] # .strip(' +0000') + try: + pubdate = datetime.strptime(pubdate, '%Y-%m-%d %H:%M:%S +0000') + now = datetime.utcnow() + age = (now - pubdate).days + except ValueError: + log.debug('RARBG: Bad pubdate') + age = 0 + + torrentscore = self.conf('extra_score') + seeders = tryInt(result['seeders']) + torrent_desc = '/ %s / %s / %s / %s seeders' % (releasegroup, resolution, encoding, seeders) + + if seeders == 0: + torrentscore = 0 + + sliceyear = result['pubdate'][0:4] + year = tryInt(sliceyear) + + results.append({ + 'id': random.randint(100, 9999), + 'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)), + 'url': result['download'], + 'detail_url': result['info_page'], + 'size': tryInt(result['size']/1048576), # rarbg sends in bytes + 'seeders': tryInt(result['seeders']), + 'leechers': tryInt(result['leechers']), + 'age': tryInt(age), + 'score': torrentscore + }) + + except RuntimeError: + log.error('RARBG: Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) + + def getToken(self): + tokendata = self.getJsonData(self.urls['token'], cache_timeout = 900, headers = self.getRequestHeaders()) + if tokendata: + try: + token = tokendata['token'] + if self._token != token: + log.debug('RARBG: GOT TOKEN: %s', token) + self._token = token + except: + log.error('RARBG: Failed getting token from Rarbg: %s', traceback.format_exc()) + self._token = 0 + + def getRequestHeaders(self): + return { + 'User-Agent': fireEvent('app.version', single = True) + } + + @staticmethod + def find_info(filename): + # CODEC # + codec = 'x264' + v = re.search('(?i)(x265|h265|h\.265)', filename) + if v: + codec = 'x265' + + v = re.search('(?i)(xvid)', filename) + if v: + codec = 'xvid' + + # RESOLUTION # + resolution = 'SD' + a = re.search('(?i)(720p)', filename) + if a: + resolution = '720p' + + a = re.search('(?i)(1080p)', filename) + if a: + resolution = '1080p' + + a = re.search('(?i)(2160p)', filename) + if a: + resolution = '2160p' + + # SOURCE # + source = 'HD-Rip' + s = re.search('(?i)(WEB-DL|WEB_DL|WEB\.DL)', filename) + if s: + source = 'WEB-DL' + + s = re.search('(?i)(WEBRIP)', filename) + if s: + source = 'WEBRIP' + + s = re.search('(?i)(DVDR|DVDRip|DVD-Rip)', filename) + if s: + source = 'DVD-R' + + s = re.search('(?i)(BRRIP|BDRIP|BluRay)', filename) + if s: + source = 'BR-Rip' + + s = re.search('(?i)BluRay(.*)REMUX', filename) + if s: + source = 'BluRay-Remux' + + s = re.search('(?i)BluRay(.*)\.(AVC|VC-1)\.', filename) + if s: + source = 'BluRay-Full' + + return_info = [codec, resolution, source] + return return_info + +config = [{ + 'name': 'rarbg', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'Rarbg', + 'wizard': True, + 'description': 'RARBG', + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB+UlEQVQ4jYXTP2hcRxDH8c8JJZjbYNy8V7gIr0qhg5AiFnETX' + '+PmVAtSmKDaUhUiFyGxjXFlp0hhHy5cqFd9lSGcU55cBU6EEMIj5dsmMewSjNGmOJ3852wysMyww37n94OdXimlh49xDR/hxGr' + '8hZ/xx0qnlHK5lPKk/H/8U0r5oZTyQSmltzzr+AKfT+ed8UFLeHNAH1UVbA2r88NBfQcX8O2yv74sUqKNWT+T01sy2+zpUbS/w' + '/awvo7H+O0NQEA/LPKlQWXrSgUmR9HxcZQwmbZGw/pc4MsVAIT+IjcNw80aTjaaem1vPCNlGakj1C6uWFiqeDtyTvoyqAKhBn+' + '+E7CkxC6Zzjop57XpUSenpIuMhpXAc/zyHkAicRSjw6fHZ1ewPdqwszWAB2hXACln8+NWSlld9zX9YN7GhajQXz5+joPXR66de' + 'U1J27Zi7FzaqE0OdmwNGzF2Ymzt3j+E8/gJH64AFlozKS4+Be7tjwyaIKVsOpnavX0II9x8ByDLKco5SwvjL0MI/z64tyOcwsf' + 'jQw8PJvAdvsb6GSBlxI7UyTnD37i7OWhe3NrflvOit3djbDKdwR181SulXMXdrkubbdvKaOpK09S/4jP8iG9m8zmJjCoEg0HzO' + '77vna7zp7ju1TqfYIyZxT7dwCd4eWr7BR7h2X8S6gShJlbKYQAAAABJRU5ErkJggg==', + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'ranked_only', + 'advanced': True, + 'label': 'Ranked Only', + 'type': 'int', + 'default': 1, + 'description': 'Only ranked torrents (internal), scene releases, rarbg releases. ' + 'Enter 1 (true) or 0 (false)', + }, + { + 'name': 'min_seeders', + 'advanced': True, + 'label': 'Minimum Seeders', + 'type': 'int', + 'default': 10, + 'description': 'Minium amount of seeders the release must have.', + }, + { + 'name': 'min_leechers', + 'advanced': True, + 'label': 'Minimum leechers', + 'type': 'int', + 'default': 0, + 'description': 'Minium amount of leechers the release must have.', + }, + { + '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/scenetime.py b/couchpotato/core/media/_base/providers/torrent/scenetime.py new file mode 100644 index 0000000..9c31018 --- /dev/null +++ b/couchpotato/core/media/_base/providers/torrent/scenetime.py @@ -0,0 +1,138 @@ +import traceback + +from bs4 import BeautifulSoup +from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode +from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.providers.torrent.base import TorrentProvider + + +log = CPLog(__name__) + + +class Base(TorrentProvider): + + urls = { + 'test': 'https://www.scenetime.com/', + 'login': 'https://www.scenetime.com/takelogin.php', + 'login_check': 'https://www.scenetime.com/inbox.php', + 'detail': 'https://www.scenetime.com/details.php?id=%s', + 'search': 'https://www.scenetime.com/browse.php?search=%s&cat=%d', + 'download': 'https://www.scenetime.com/download.php/%s/%s', + } + + cat_ids = [ + ([59], ['720p', '1080p']), + ([81], ['brrip']), + ([102], ['bd50']), + ([3], ['dvdrip']), + ] + + http_time_between_calls = 1 # Seconds + cat_backup_id = None + + def _searchOnTitle(self, title, movie, quality, results): + + url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), self.getCatId(quality)[0]) + data = self.getHTMLData(url) + + if data: + html = BeautifulSoup(data) + + try: + result_table = html.find(attrs = {'id': 'torrenttable'}) + + if not result_table: + log.error('failed to generate result_table') + return + + entries = result_table.find_all('tr') + + for result in entries[1:]: + cells = result.find_all('td') + link = result.find('a', attrs = {'class': 'index'}) + torrent_id = link['href'].replace('download.php/','').split('/')[0] + torrent_file = link['href'].replace('download.php/','').split('/')[1] + size = self.parseSize(cells[5].contents[0] + cells[5].contents[2]) + name_row = cells[1].contents[0] + name = name_row.getText() + seeders_row = cells[6].contents[0] + seeders = seeders_row.getText() + + + results.append({ + 'id': torrent_id, + 'name': name, + 'url': self.urls['download'] % (torrent_id,torrent_file), + 'detail_url': self.urls['detail'] % torrent_id, + 'size': size, + 'seeders': seeders, + }) + + except: + log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) + + def getLoginParams(self): + return { + 'login': 'submit', + 'username': self.conf('username'), + 'password': self.conf('password'), + } + + def loginSuccess(self, output): + return 'logout.php' in output.lower() or 'Welcome' in output.lower() + + loginCheckSuccess = loginSuccess + + +config = [{ + 'name': 'scenetime', + 'groups': [ + { + 'tab': 'searcher', + 'list': 'torrent_providers', + 'name': 'SceneTime', + 'description': 'SceneTime', + 'wizard': True, + 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNWWFMmUAAAIwSURBVDhPZZFbSBRRGMePs7Mzjma7+9AWWxpeYrXLkrcIfUwIpIeK3tO1hWhfltKwhyJMFIqgCz2EpdHWRun2oGG02O2hlYyypY21CygrlbhRIYHizO6/mdk5szPtB785hzm//zeXj7Q89q4I4QaQBx6ZHQY84Efq4Rrbg4rxVmx61AJ2pFY/twzvhP1hU4ZwIQ8K7mw1wdzdhrrxQ7g8E0Q09R6flubw+mcM7tHWPJcwt91ghuTQUDWYW8rejbrRA3i1OA0xLYGWJO8bxw6q50YIc70CRoQbNbj2MQgpkwsrpTYI7ze5CoS5UgYjpTd3YWphWg1l1CuwLC4jufQNtaG9JleBWM67YKR6oBlzf+bVoPIOUiaNwVgIzcF9sF3aknMvZFfCnnNCp9eJqqsNSKQ+qw2USssNzrzoh9Dnynmaq6yEPe2AkfX9lXjy5akWz9ZkcgqVFz0mj0KsJ0tgROh2oCfSJ3/3ihaHPA0Rh+/7UNhtN7kKhAsI+J+a3u2If49r8WxFZiawtsuR5xLumBUU3s/B2bkOm0+V4V3yrTwFOgcg8SMBe8CmuxTC+SygFB3l8TzxDLOpWYiSqEWzFf0ahc2/RncphPcSUIqPWPFhPqZFcrUqraLzXkA+Z3WXQvh2eaNR3MHmNVB+YPjNMMqPb9Q9I6YGRR0WTMQj6hOV+f/++wuDLwfg7iqH4GVMQQrh28w3Nvgd2H22Hk09jag6UYoSH4/C9gKTo9NG8A8MPUM4DJp74gAAAABJRU5ErkJggg==', + 'options': [ + { + 'name': 'enabled', + 'type': 'enabler', + 'default': False, + }, + { + 'name': 'username', + 'default': '', + }, + { + 'name': 'password', + 'default': '', + 'type': 'password', + }, + { + 'name': 'seed_ratio', + 'label': 'Seed ratio', + 'type': 'float', + 'default': 1, + 'description': 'Will not be (re)moved until this seed ratio is met.', + }, + { + 'name': 'seed_time', + 'label': 'Seed time', + 'type': 'int', + 'default': 40, + 'description': 'Will not be (re)moved until this seed time (in hours) is met.', + }, + { + 'name': 'extra_score', + 'advanced': True, + 'label': 'Extra Score', + 'type': 'int', + 'default': 20, + 'description': 'Starting score for each release found via this provider.', + } + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py index 796ade3..9f474f4 100644 --- a/couchpotato/core/media/_base/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/_base/providers/torrent/thepiratebay.py @@ -24,18 +24,19 @@ class Base(TorrentMagnetProvider): http_time_between_calls = 0 proxy_list = [ - 'https://dieroschtibay.org', - 'https://thebay.al', - 'https://thepiratebay.se', - 'http://thepiratebay.se.net', - 'http://thebootlegbay.com', - 'http://tpb.ninja.so', + 'https://thepiratebay.mn', + 'https://thepiratebay.gd', + 'https://thepiratebay.bg', + 'https://thepiratebay.la', + 'https://thepiratebay.am', + 'https://thepiratebay.gs', 'http://proxybay.fr', 'http://pirateproxy.in', - 'http://piratebay.skey.sk', - 'http://pirateproxy.be', - 'http://bayproxy.li', 'http://proxybay.pw', + 'https://pirateproxy.sx', + 'https://tpbproxy.co', + 'https://arrr.xyz', + 'http://tpb.dashitz.com', ] def _search(self, media, quality, results): diff --git a/couchpotato/core/media/_base/providers/torrent/torrentshack.py b/couchpotato/core/media/_base/providers/torrent/torrentshack.py index 71f4e62..6a73416 100644 --- a/couchpotato/core/media/_base/providers/torrent/torrentshack.py +++ b/couchpotato/core/media/_base/providers/torrent/torrentshack.py @@ -13,12 +13,12 @@ log = CPLog(__name__) class Base(TorrentProvider): urls = { - 'test': 'https://theshack.us.to/', - 'login': 'https://theshack.us.to/login.php', - 'login_check': 'https://theshack.us.to/inbox.php', - 'detail': 'https://theshack.us.to/torrent/%s', - 'search': 'https://theshack.us.to/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1', - 'download': 'https://theshack.us.to/%s', + 'test': 'https://torrentshack.me/', + 'login': 'https://torrentshack.me/login.php', + 'login_check': 'https://torrentshack.me/inbox.php', + 'detail': 'https://torrentshack.me/torrent/%s', + 'search': 'https://torrentshack.me/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1', + 'download': 'https://torrentshack.me/%s', } http_time_between_calls = 1 # Seconds @@ -82,7 +82,7 @@ config = [{ 'tab': 'searcher', 'list': 'torrent_providers', 'name': 'TorrentShack', - 'description': 'TorrentShack', + 'description': 'TorrentShack', 'wizard': True, 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC', 'options': [ diff --git a/couchpotato/core/media/_base/providers/torrent/yify.py b/couchpotato/core/media/_base/providers/torrent/yify.py index 4071a15..d5350a9 100644 --- a/couchpotato/core/media/_base/providers/torrent/yify.py +++ b/couchpotato/core/media/_base/providers/torrent/yify.py @@ -18,9 +18,14 @@ 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', + 'https://yify.link', + 'https://yifytorrent.link', + 'https://yts.ch', + 'https://yts.click', + 'https://yify.me', ] def search(self, movie, quality): @@ -38,7 +43,7 @@ class Base(TorrentProvider): search_url = self.urls['search'] % (domain, getIdentifier(movie)) - data = self.getJsonData(search_url) + data = self.getJsonData(search_url) or {} data = data.get('data') if isinstance(data, dict) and data.get('movies'): 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/_base/searcher/__init__.py b/couchpotato/core/media/_base/searcher/__init__.py index 0e3655e..7f15b43 100644 --- a/couchpotato/core/media/_base/searcher/__init__.py +++ b/couchpotato/core/media/_base/searcher/__init__.py @@ -48,7 +48,7 @@ config = [{ { 'name': 'ignored_words', 'label': 'Ignored', - 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs, vain', + 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs, vain, HC', 'description': 'Ignores releases that match any of these sets. (Works like explained above)' }, ], 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 273df5a..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,295 +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", '