Browse Source

Merge branch 'master' into desktop

tags/build/3.0.0
Ruud 10 years ago
parent
commit
fc12a3e325
  1. 16
      .editorconfig
  2. 3
      .gitignore
  3. 198
      Gruntfile.js
  4. 51
      README.md
  5. 44
      config.rb
  6. 52
      couchpotato/core/_base/_core.py
  7. 189
      couchpotato/core/_base/clientscript.py
  8. 14
      couchpotato/core/_base/downloader/static/downloaders.js
  9. 7
      couchpotato/core/_base/updater/main.py
  10. 10
      couchpotato/core/_base/updater/static/updater.js
  11. 2
      couchpotato/core/database.py
  12. 447
      couchpotato/core/downloaders/hadouken.py
  13. 4
      couchpotato/core/downloaders/nzbget.py
  14. 4
      couchpotato/core/downloaders/putio/static/putio.js
  15. 5
      couchpotato/core/downloaders/sabnzbd.py
  16. 4
      couchpotato/core/downloaders/transmission.py
  17. 3
      couchpotato/core/helpers/variable.py
  18. 7
      couchpotato/core/media/__init__.py
  19. 7
      couchpotato/core/media/_base/providers/base.py
  20. 2
      couchpotato/core/media/_base/providers/nzb/newznab.py
  21. 2
      couchpotato/core/media/_base/providers/nzb/nzbclub.py
  22. 133
      couchpotato/core/media/_base/providers/torrent/alpharatio.py
  23. 131
      couchpotato/core/media/_base/providers/torrent/hd4free.py
  24. 5
      couchpotato/core/media/_base/providers/torrent/ilovetorrents.py
  25. 4
      couchpotato/core/media/_base/providers/torrent/iptorrents.py
  26. 12
      couchpotato/core/media/_base/providers/torrent/kickasstorrents.py
  27. 230
      couchpotato/core/media/_base/providers/torrent/rarbg.py
  28. 138
      couchpotato/core/media/_base/providers/torrent/scenetime.py
  29. 19
      couchpotato/core/media/_base/providers/torrent/thepiratebay.py
  30. 14
      couchpotato/core/media/_base/providers/torrent/torrentshack.py
  31. 11
      couchpotato/core/media/_base/providers/torrent/yify.py
  32. 277
      couchpotato/core/media/_base/search/static/search.css
  33. 128
      couchpotato/core/media/_base/search/static/search.js
  34. 503
      couchpotato/core/media/_base/search/static/search.scss
  35. 2
      couchpotato/core/media/_base/searcher/__init__.py
  36. 11
      couchpotato/core/media/movie/_base/main.py
  37. 154
      couchpotato/core/media/movie/_base/static/details.js
  38. 207
      couchpotato/core/media/movie/_base/static/list.js
  39. 26
      couchpotato/core/media/movie/_base/static/manage.js
  40. 902
      couchpotato/core/media/movie/_base/static/movie.actions.js
  41. 1074
      couchpotato/core/media/movie/_base/static/movie.css
  42. 316
      couchpotato/core/media/movie/_base/static/movie.js
  43. 1248
      couchpotato/core/media/movie/_base/static/movie.scss
  44. 50
      couchpotato/core/media/movie/_base/static/page.js
  45. 125
      couchpotato/core/media/movie/_base/static/search.js
  46. 107
      couchpotato/core/media/movie/_base/static/wanted.js
  47. 14
      couchpotato/core/media/movie/charts/__init__.py
  48. 101
      couchpotato/core/media/movie/charts/main.py
  49. 124
      couchpotato/core/media/movie/charts/static/charts.js
  50. 0
      couchpotato/core/media/movie/charts/static/charts.scss
  51. 61
      couchpotato/core/media/movie/providers/automation/bluray.py
  52. 104
      couchpotato/core/media/movie/providers/automation/hummingbird.py
  53. 68
      couchpotato/core/media/movie/providers/automation/imdb.py
  54. 13
      couchpotato/core/media/movie/providers/automation/moviemeter.py
  55. 11
      couchpotato/core/media/movie/providers/automation/popularmovies.py
  56. 95
      couchpotato/core/media/movie/providers/automation/rottentomatoes.py
  57. 83
      couchpotato/core/media/movie/providers/automation/trakt.py
  58. 31
      couchpotato/core/media/movie/providers/automation/trakt/__init__.py
  59. 76
      couchpotato/core/media/movie/providers/automation/trakt/main.py
  60. 67
      couchpotato/core/media/movie/providers/automation/trakt/static/trakt.js
  61. 5
      couchpotato/core/media/movie/providers/info/themoviedb.py
  62. 221
      couchpotato/core/media/movie/providers/metadata/wdtv.py
  63. 32
      couchpotato/core/media/movie/providers/torrent/alpharatio.py
  64. 11
      couchpotato/core/media/movie/providers/torrent/hd4free.py
  65. 11
      couchpotato/core/media/movie/providers/torrent/rarbg.py
  66. 11
      couchpotato/core/media/movie/providers/torrent/scenetime.py
  67. 20
      couchpotato/core/media/movie/providers/userscript/filmweb.py
  68. 6
      couchpotato/core/media/movie/providers/userscript/flickchart.py
  69. 14
      couchpotato/core/media/movie/providers/userscript/moviemeter.py
  70. 19
      couchpotato/core/media/movie/providers/userscript/rottentomatoes.py
  71. 8
      couchpotato/core/media/movie/providers/userscript/sharethe.py
  72. 4
      couchpotato/core/media/movie/providers/userscript/tmdb.py
  73. 8
      couchpotato/core/media/movie/providers/userscript/whiwa.py
  74. 55
      couchpotato/core/media/movie/suggestion.py
  75. 0
      couchpotato/core/media/movie/suggestion/__init__.py
  76. 162
      couchpotato/core/media/movie/suggestion/static/suggest.css
  77. 173
      couchpotato/core/media/movie/suggestion/static/suggest.js
  78. 78
      couchpotato/core/notifications/core/static/notification.js
  79. 14
      couchpotato/core/notifications/email_.py
  80. 89
      couchpotato/core/notifications/emby.py
  81. 29
      couchpotato/core/notifications/pushbullet.py
  82. 31
      couchpotato/core/notifications/pushover.py
  83. 126
      couchpotato/core/notifications/slack.py
  84. 49
      couchpotato/core/notifications/trakt.py
  85. 4
      couchpotato/core/notifications/twitter/static/twitter.js
  86. 28
      couchpotato/core/notifications/xbmc.py
  87. 92
      couchpotato/core/plugins/base.py
  88. 36
      couchpotato/core/plugins/category/static/category.js
  89. 19
      couchpotato/core/plugins/category/static/category.scss
  90. 8
      couchpotato/core/plugins/dashboard.py
  91. 7
      couchpotato/core/plugins/log/main.py
  92. 199
      couchpotato/core/plugins/log/static/log.css
  93. 157
      couchpotato/core/plugins/log/static/log.js
  94. 159
      couchpotato/core/plugins/log/static/log.scss
  95. 197
      couchpotato/core/plugins/profile/static/profile.css
  96. 76
      couchpotato/core/plugins/profile/static/profile.js
  97. 150
      couchpotato/core/plugins/profile/static/profile.scss
  98. 11
      couchpotato/core/plugins/quality/main.py
  99. 26
      couchpotato/core/plugins/quality/static/quality.css
  100. 42
      couchpotato/core/plugins/quality/static/quality.js

16
.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

3
.gitignore

@ -1,5 +1,8 @@
*.pyc *.pyc
/data/ /data/
/_env/
/_source/ /_source/
.project .project
.pydevproject .pydevproject
node_modules
.tmp

198
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'
]);
};

51
README.md

@ -1,6 +1,8 @@
CouchPotato 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. 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. 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/) * 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 [GIT](http://git-scm.com/)
* Install [LXML](http://lxml.de/installation.html) for better/faster website scraping
* Open up `Terminal` * Open up `Terminal`
* Go to your App folder `cd /Applications` * Go to your App folder `cd /Applications`
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * 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` * (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` * (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. * 'cd' to the folder of your choosing.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py` to start * Then do `python CouchPotatoServer/CouchPotato.py` to start
@ -47,23 +51,34 @@ Linux:
* Open your browser and go to `http://localhost:5050/` * Open your browser and go to `http://localhost:5050/`
Docker: Docker:
* You can use [razorgirl's Dockerfile](https://github.com/razorgirl/docker-couchpotato) to quickly build your own isolated app container. It's based on the Linux instructions above. For more info about Docker check out the [official website](https://www.docker.com). * You can use [linuxserver.io](https://github.com/linuxserver/docker-couchpotato) or [razorgirl's](https://github.com/razorgirl/docker-couchpotato) to quickly build your own isolated app container. It's based on the Linux instructions above. For more info about Docker check out the [official website](https://www.docker.com).
FreeBSD : FreeBSD:
* Update your ports tree `sudo portsnap fetch update` * Become root with `su`
* Install Python 2.6+ [lang/python](http://www.freshports.org/lang/python) with `cd /usr/ports/lang/python; sudo make install clean` * Update your repo catalog `pkg update`
* Install port [databases/py-sqlite3](http://www.freshports.org/databases/py-sqlite3) with `cd /usr/ports/databases/py-sqlite3; sudo make install clean` * Install required tools `pkg install python py27-sqlite3 fpc-libcurl docbook-xml git-lite`
* Add a symlink to 'python2' `sudo ln -s /usr/local/bin/python /usr/local/bin/python2` * For default install location and running as root `cd /usr/local`
* Install port [ftp/libcurl](http://www.freshports.org/ftp/libcurl) with `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean` * If running as root, expects python here `ln -s /usr/local/bin/python /usr/bin/python`
* 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.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time * Copy the startup script `cp CouchPotatoServer/init/freebsd /usr/local/etc/rc.d/couchpotato`
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato` * Make startup script executable `chmod 555 /usr/local/etc/rc.d/couchpotato`
* Change the paths inside the init script. `sudo vim /etc/rc.d/couchpotato` * Add startup to boot `echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato` * Read the options at the top of `more /usr/local/etc/rc.d/couchpotato`
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf` * 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/` * 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.

44
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} : {}

52
couchpotato/core/_base/_core.py

@ -5,6 +5,7 @@ import signal
import time import time
import traceback import traceback
import webbrowser import webbrowser
import sys
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
@ -52,6 +53,7 @@ class Core(Plugin):
addEvent('app.version', self.version) addEvent('app.version', self.version)
addEvent('app.load', self.checkDataDir) addEvent('app.load', self.checkDataDir)
addEvent('app.load', self.cleanUpFolders) addEvent('app.load', self.cleanUpFolders)
addEvent('app.load.after', self.dependencies)
addEvent('setting.save.core.password', self.md5Password) addEvent('setting.save.core.password', self.md5Password)
addEvent('setting.save.core.api_key', self.checkApikey) addEvent('setting.save.core.api_key', self.checkApikey)
@ -64,6 +66,23 @@ class Core(Plugin):
import socket import socket
socket.setdefaulttimeout(30) 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): def md5Password(self, value):
return md5(value) if value else '' return md5(value) if value else ''
@ -174,8 +193,9 @@ class Core(Plugin):
if host == '0.0.0.0' or host == '': if host == '0.0.0.0' or host == '':
host = 'localhost' host = 'localhost'
port = Env.setting('port') 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): def createApiUrl(self):
return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key')) return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
@ -230,6 +250,12 @@ config = [{
'description': 'The port I should listen to.', 'description': 'The port I should listen to.',
}, },
{ {
'name': 'ipv6',
'default': 0,
'type': 'bool',
'description': 'Also bind the WebUI to ipv6 address',
},
{
'name': 'ssl_cert', 'name': 'ssl_cert',
'description': 'Path to SSL server.crt', 'description': 'Path to SSL server.crt',
'advanced': True, 'advanced': True,
@ -261,6 +287,30 @@ config = [{
'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>', 'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>',
}, },
{ {
'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 <a target=_"blank" href="https://en.wikipedia.org/wiki/Proxy_server#Web_proxy_servers">HTTP(S) proxies</a> are supported. ',
},
{
'name': 'proxy_server',
'description': 'Override system default proxy server. Currently, only <a target=_"blank" href="https://en.wikipedia.org/wiki/Proxy_server#Web_proxy_servers">HTTP(S) proxies</a> are supported. Ex. <i>\"127.0.0.1:8080\"</i>. 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', 'name': 'debug',
'default': 0, 'default': 0,
'type': 'bool', 'type': 'bool',

189
couchpotato/core/_base/clientscript.py

@ -1,16 +1,10 @@
import os import os
import re
import traceback
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from minify.cssmin import cssmin
from minify.jsmin import jsmin
from tornado.web import StaticFileHandler
log = CPLog(__name__) log = CPLog(__name__)
@ -20,129 +14,35 @@ autoload = 'ClientScript'
class ClientScript(Plugin): class ClientScript(Plugin):
core_static = { paths = {
'style': [ 'style': [
'style/main.css', 'style/combined.min.css',
'style/uniform.generic.css',
'style/uniform.css',
'style/settings.css',
], ],
'script': [ 'script': [
'scripts/library/mootools.js', 'scripts/combined.vendor.min.js',
'scripts/library/mootools_more.js', 'scripts/combined.base.min.js',
'scripts/library/uniform.js', 'scripts/combined.plugins.min.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',
], ],
} }
urls = {'style': {}, 'script': {}}
minified = {'style': {}, 'script': {}}
paths = {'style': {}, 'script': {}}
comment = {
'style': '/*** %s:%d ***/\n',
'script': '// %s:%d\n'
}
html = {
'style': '<link rel="stylesheet" href="%s" type="text/css">',
'script': '<script type="text/javascript" src="%s"></script>',
}
def __init__(self): def __init__(self):
addEvent('register_style', self.registerStyle)
addEvent('register_script', self.registerScript)
addEvent('clientscript.get_styles', self.getStyles) addEvent('clientscript.get_styles', self.getStyles)
addEvent('clientscript.get_scripts', self.getScripts) addEvent('clientscript.get_scripts', self.getScripts)
if not Env.get('dev'): self.makeRelative()
addEvent('app.load', self.minify)
self.addCore() def makeRelative(self):
def addCore(self): for static_type in self.paths:
for static_type in self.core_static: updates_paths = []
for rel_path in self.core_static.get(static_type): for rel_path in self.paths.get(static_type):
file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path) file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path)
core_url = 'static/%s' % rel_path core_url = 'static/%s?%d' % (rel_path, tryInt(os.path.getmtime(file_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())
if not self.minified.get(file_type): updates_paths.append(core_url)
self.minified[file_type] = {}
if not self.minified[file_type].get(position):
self.minified[file_type][position] = []
minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out))) self.paths[static_type] = updates_paths
self.minified[file_type][position].append(minified_url)
def getStyles(self, *args, **kwargs): def getStyles(self, *args, **kwargs):
return self.get('style', *args, **kwargs) return self.get('style', *args, **kwargs)
@ -150,63 +50,8 @@ class ClientScript(Plugin):
def getScripts(self, *args, **kwargs): def getScripts(self, *args, **kwargs):
return self.get('script', *args, **kwargs) return self.get('script', *args, **kwargs)
def get(self, type, as_html = False, location = 'head'): def get(self, type):
if type in self.paths:
data = '' if as_html else [] return self.paths[type]
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('} ;', '} ')
return new_data return []

14
couchpotato/core/_base/downloader/static/downloaders.js

@ -16,8 +16,8 @@ var DownloadersBase = new Class({
var setting_page = App.getPage('Settings'); var setting_page = App.getPage('Settings');
setting_page.addEvent('create', function(){ setting_page.addEvent('create', function(){
Object.each(setting_page.tabs.downloaders.groups, self.addTestButton.bind(self)) Object.each(setting_page.tabs.downloaders.groups, self.addTestButton.bind(self));
}) });
}, },
@ -27,7 +27,7 @@ var DownloadersBase = new Class({
if(button_name.contains('Downloaders')) return; if(button_name.contains('Downloaders')) return;
new Element('.ctrlHolder.test_button').adopt( new Element('.ctrlHolder.test_button').grab(
new Element('a.button', { new Element('a.button', {
'text': button_name, 'text': button_name,
'events': { 'events': {
@ -44,19 +44,19 @@ var DownloadersBase = new Class({
if(json.success){ if(json.success){
message = new Element('span.success', { message = new Element('span.success', {
'text': 'Connection successful' 'text': 'Connection successful'
}).inject(button, 'after') }).inject(button, 'after');
} }
else { else {
var msg_text = 'Connection failed. Check logs for details.'; var msg_text = 'Connection failed. Check logs for details.';
if(json.hasOwnProperty('msg')) msg_text = json.msg; if(json.hasOwnProperty('msg')) msg_text = json.msg;
message = new Element('span.failed', { message = new Element('span.failed', {
'text': msg_text 'text': msg_text
}).inject(button, 'after') }).inject(button, 'after');
} }
(function(){ (function(){
message.destroy(); message.destroy();
}).delay(3000) }).delay(3000);
} }
}); });
} }
@ -67,7 +67,7 @@ var DownloadersBase = new Class({
}, },
testButtonName: function(fieldset){ testButtonName: function(fieldset){
var name = String(fieldset.getElement('h2').innerHTML).substring(0,String(fieldset.getElement('h2').innerHTML).indexOf("<span")); var name = fieldset.getElement('h2 .group_label').get('text');
return 'Test '+name; return 'Test '+name;
} }

7
couchpotato/core/_base/updater/main.py

@ -7,6 +7,7 @@ import traceback
import zipfile import zipfile
from datetime import datetime from datetime import datetime
from threading import RLock from threading import RLock
import re
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
@ -34,7 +35,10 @@ class Updater(Plugin):
if Env.get('desktop'): if Env.get('desktop'):
self.updater = DesktopUpdater() self.updater = DesktopUpdater()
elif os.path.isdir(os.path.join(Env.get('app_dir'), '.git')): elif os.path.isdir(os.path.join(Env.get('app_dir'), '.git')):
self.updater = GitUpdater(self.conf('git_command', default = 'git')) git_default = 'git'
git_command = self.conf('git_command', default = git_default)
git_command = git_command if git_command != git_default and (os.path.isfile(git_command) or re.match('^[a-zA-Z0-9_/\.\-]+$', git_command)) else git_default
self.updater = GitUpdater(git_command)
else: else:
self.updater = SourceUpdater() self.updater = SourceUpdater()
@ -163,7 +167,6 @@ class BaseUpdater(Plugin):
update_failed = False update_failed = False
update_version = None update_version = None
last_check = 0 last_check = 0
auto_register_static = False
def doUpdate(self): def doUpdate(self):
pass pass

10
couchpotato/core/_base/updater/static/updater.js

@ -27,7 +27,7 @@ var UpdaterBase = new Class({
App.trigger('message', ['No updates available']); App.trigger('message', ['No updates available']);
} }
} }
}) });
}, },
@ -50,8 +50,8 @@ var UpdaterBase = new Class({
self.message.destroy(); self.message.destroy();
} }
} }
}) });
}, (timeout || 0)) }, (timeout || 0));
}, },
@ -84,7 +84,7 @@ var UpdaterBase = new Class({
'click': self.doUpdate.bind(self) 'click': self.doUpdate.bind(self)
} }
}) })
).inject(document.body) ).inject(document.body);
}, },
doUpdate: function(){ doUpdate: function(){
@ -96,7 +96,7 @@ var UpdaterBase = new Class({
if(json.success) if(json.success)
self.updating(); self.updating();
else else
App.unBlockPage() App.unBlockPage();
} }
}); });
}, },

2
couchpotato/core/database.py

@ -272,7 +272,7 @@ class Database(object):
prop_name = 'last_db_compact' prop_name = 'last_db_compact'
last_check = int(Env.prop(prop_name, default = 0)) last_check = int(Env.prop(prop_name, default = 0))
if size > 26214400 and last_check < time.time()-604800: # 25MB / 7 days if last_check < time.time()-604800: # 7 days
self.compact() self.compact()
Env.prop(prop_name, value = int(time.time())) Env.prop(prop_name, value = int(time.time()))

447
couchpotato/core/downloaders/hadouken.py

@ -31,13 +31,33 @@ class Hadouken(DownloaderBase):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
if not self.conf('api_key'): # This is where v4 and v5 begin to differ
log.error('Config properties are not filled in correctly, API key is missing.') if(self.conf('version') == 'v4'):
return False 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): def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader """ Send a torrent/nzb file to the downloader
@ -66,6 +86,8 @@ class Hadouken(DownloaderBase):
if self.conf('label'): if self.conf('label'):
torrent_params['label'] = 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) torrent_filename = self.createFileName(data, filedata, media)
@ -132,71 +154,25 @@ class Hadouken(DownloaderBase):
if torrent is None: if torrent is None:
continue 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 = [] 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: 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({ release_downloads.append({
'id': torrent['InfoHash'].upper(), 'id': torrent.info_hash.upper(),
'name': torrent['Name'], 'name': torrent.name,
'status': self.get_torrent_status(torrent), 'status': torrent.get_status(),
'seed_ratio': self.get_seed_ratio(torrent), 'seed_ratio': torrent.get_seed_ratio(),
'original_status': torrent['State'], 'original_status': torrent.state,
'timeleft': -1, '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 'files': torrent_files
}) })
return release_downloads 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): def pause(self, release_download, pause = True):
""" Pauses or resumes the torrent specified by the ID field """ Pauses or resumes the torrent specified by the ID field
in release_download. in release_download.
@ -243,45 +219,85 @@ class Hadouken(DownloaderBase):
return self.hadouken_api.remove(release_download['id'], remove_data = delete_files) return self.hadouken_api.remove(release_download['id'], remove_data = delete_files)
class HadoukenAPI(object): class JsonRpcClient(object):
def __init__(self, host = 'localhost', port = 7890, api_key = None): def __init__(self, url, auth_header = None):
self.url = 'http://' + str(host) + ':' + str(port) self.url = url
self.api_key = api_key self.requestId = 0
self.requestId = 0;
self.opener = urllib2.build_opener() 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: data = {
log.error('API key missing.') '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. """ Add a file to Hadouken with the specified parameters.
Keyword arguments: Keyword arguments:
filedata -- The binary torrent data. filedata -- The binary torrent data.
torrent_params -- Additional parameters for the file. torrent_params -- Additional parameters for the file.
""" """
data = { pass
'method': 'torrents.addFile',
'params': [b64encode(filedata), torrent_params]
}
return self._request(data)
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. """ Add a magnet link to Hadouken with the specified parameters.
Keyword arguments: Keyword arguments:
magnetLink -- The magnet link to send. magnetLink -- The magnet link to send.
torrent_params -- Additional parameters for the magnet link. torrent_params -- Additional parameters for the magnet link.
""" """
data = { pass
'method': 'torrents.addUrl',
'params': [magnetLink, torrent_params]
}
return self._request(data)
def get_by_hash_list(self, infoHashList): def get_by_hash_list(self, infoHashList):
""" Gets a list of torrents filtered by the given info hash list. """ Gets a list of torrents filtered by the given info hash list.
@ -289,12 +305,7 @@ class HadoukenAPI(object):
Keyword arguments: Keyword arguments:
infoHashList -- A list of info hashes. infoHashList -- A list of info hashes.
""" """
data = { pass
'method': 'torrents.getByInfoHashList',
'params': [infoHashList]
}
return self._request(data)
def get_files_by_hash(self, infoHash): def get_files_by_hash(self, infoHash):
""" Gets a list of files for the torrent identified by the """ Gets a list of files for the torrent identified by the
@ -303,26 +314,11 @@ class HadoukenAPI(object):
Keyword arguments: Keyword arguments:
infoHash -- The info hash of the torrent to return files for. infoHash -- The info hash of the torrent to return files for.
""" """
data = { pass
'method': 'torrents.getFiles',
'params': [infoHash]
}
return self._request(data)
def get_version(self): def get_version(self):
""" Gets the version, commitish and build date of Hadouken. """ """ Gets the version, commitish and build date of Hadouken. """
data = { pass
'method': 'core.getVersion',
'params': None
}
result = self._request(data)
if not result:
return False
return result['Version']
def pause(self, infoHash, pause): def pause(self, infoHash, pause):
""" Pauses/unpauses the torrent identified by the given info hash. """ 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. infoHash -- The info hash of the torrent to operate on.
pause -- If true, pauses the torrent. Otherwise resumes. pause -- If true, pauses the torrent. Otherwise resumes.
""" """
data = { pass
'method': 'torrents.pause',
'params': [infoHash]
}
if not pause:
data['method'] = 'torrents.resume'
return self._request(data)
def remove(self, infoHash, remove_data = False): def remove(self, infoHash, remove_data = False):
""" Removes the torrent identified by the given info hash and """ 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. infoHash -- The info hash of the torrent to remove.
remove_data -- If true, removes the data associated with the torrent. remove_data -- If true, removes the data associated with the torrent.
""" """
data = { pass
'method': 'torrents.remove',
'params': [infoHash, remove_data]
}
return self._request(data)
class TorrentItem(object):
@property
def info_hash(self):
pass
def _request(self, data): @property
self.requestId += 1 def save_path(self):
pass
data['jsonrpc'] = '2.0' @property
data['id'] = self.requestId def name(self):
pass
request = urllib2.Request(self.url + '/jsonrpc', data = json.dumps(data)) @property
request.add_header('Authorization', 'Token ' + self.api_key) def state(self):
request.add_header('Content-Type', 'application/json') pass
try: def get_status(self):
f = self.opener.open(request) """ Returns the CouchPotato status for a given torrent."""
response = f.read() pass
f.close()
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']) class TorrentItemv5(TorrentItem):
except httplib.InvalidURL as err: def __init__(self, obj):
log.error('Invalid Hadouken host, check your config %s', err) self.obj = obj
except urllib2.HTTPError as err:
if err.code == 401: def info_hash(self):
log.error('Invalid Hadouken API key, check your config') return self.obj['infoHash']
else:
log.error('Hadouken HTTPError: %s', err) def save_path(self):
except urllib2.URLError as err: return self.obj['savePath']
log.error('Unable to connect to Hadouken %s', err)
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 = [{ config = [{
@ -409,15 +541,42 @@ config = [{
'radio_group': 'torrent' 'radio_group': 'torrent'
}, },
{ {
'name': 'version',
'label': 'Version',
'type': 'dropdown',
'default': 'v4',
'values': [('v4.x', 'v4'), ('v5.x', 'v5')],
'description': 'Hadouken version.',
},
{
'name': 'host', 'name': 'host',
'default': 'localhost:7890' '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', 'name': 'api_key',
'label': 'API key', 'label': 'API key (v4)/Token (v5)',
'type': 'password' 'type': 'password'
}, },
{ {
'name': 'auth_user',
'label': 'Username',
'description': '(only for v5)'
},
{
'name': 'auth_pass',
'label': 'Password',
'type': 'password',
'description': '(only for v5)'
},
{
'name': 'label', 'name': 'label',
'description': 'Label to add torrent as.' 'description': 'Label to add torrent as.'
} }

4
couchpotato/core/downloaders/nzbget.py

@ -295,8 +295,8 @@ config = [{
'advanced': True, 'advanced': True,
'default': '0', 'default': '0',
'type': 'dropdown', 'type': 'dropdown',
'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)], 'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100), ('Forced', 900)],
'description': 'Only change this if you are using NZBget 9.0 or higher', 'description': 'Only change this if you are using NZBget 13.0 or higher',
}, },
{ {
'name': 'manual', 'name': 'manual',

4
couchpotato/core/downloaders/putio/static/putio.js

@ -17,7 +17,7 @@ var PutIODownloader = new Class({
var putio_set = 0; var putio_set = 0;
fieldset.getElements('input[type=text]').each(function(el){ fieldset.getElements('input[type=text]').each(function(el){
putio_set += +(el.get('value') != ''); putio_set += +(el.get('value') !== '');
}); });
new Element('.ctrlHolder').adopt( new Element('.ctrlHolder').adopt(
@ -57,7 +57,7 @@ var PutIODownloader = new Class({
} }
}) })
).inject(fieldset.getElement('.test_button'), 'before'); ).inject(fieldset.getElement('.test_button'), 'before');
}) });
} }

5
couchpotato/core/downloaders/sabnzbd.py

@ -73,10 +73,11 @@ class Sabnzbd(DownloaderBase):
return False return False
log.debug('Result from SAB: %s', sab_data) 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.') log.info('NZB sent to SAB successfully.')
if filedata: if filedata:
return self.downloadReturnId(sab_data.get('nzo_ids')[0]) return self.downloadReturnId(nzo_ids[0])
else: else:
return True return True
else: else:

4
couchpotato/core/downloaders/transmission.py

@ -68,7 +68,7 @@ class Transmission(DownloaderBase):
if self.conf('directory'): if self.conf('directory'):
if os.path.isdir(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: else:
log.error('Download directory from Transmission settings: %s doesn\'t exist', self.conf('directory')) log.error('Download directory from Transmission settings: %s doesn\'t exist', self.conf('directory'))
@ -147,7 +147,7 @@ class Transmission(DownloaderBase):
status = 'failed' status = 'failed'
elif torrent['status'] == 0 and torrent['percentDone'] == 1: elif torrent['status'] == 0 and torrent['percentDone'] == 1:
status = 'completed' status = 'completed'
elif torrent['status'] == 16 and torrent['percentDone'] == 1: elif torrent['status'] == 16 and torrent['percentDone'] == 1:
status = 'completed' status = 'completed'
elif torrent['status'] in [5, 6]: elif torrent['status'] in [5, 6]:
status = 'seeding' status = 'seeding'

3
couchpotato/core/helpers/variable.py

@ -41,7 +41,8 @@ def symlink(src, dst):
def getUserDir(): def getUserDir():
try: try:
import pwd 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: except:
pass pass

7
couchpotato/core/media/__init__.py

@ -88,8 +88,13 @@ class MediaBase(Plugin):
if len(existing_files) == 0: if len(existing_files) == 0:
del existing_files[file_type] 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 # Loop over type
for image in image_urls.get(image_type, []): for image in images:
if not isinstance(image, (str, unicode)): if not isinstance(image, (str, unicode)):
continue continue

7
couchpotato/core/media/_base/providers/base.py

@ -5,6 +5,11 @@ import time
import traceback import traceback
import xml.etree.ElementTree as XMLTree 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.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \ from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
@ -94,7 +99,7 @@ class Provider(Plugin):
try: try:
data = XMLTree.fromstring(ss(data)) data = XMLTree.fromstring(ss(data))
return self.getElements(data, item_path) return self.getElements(data, item_path)
except XMLTree.ParseError: except XmlParseError:
log.error('Invalid XML returned, check "%s" manually for issues', url) log.error('Invalid XML returned, check "%s" manually for issues', url)
except: except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))

2
couchpotato/core/media/_base/providers/nzb/newznab.py

@ -27,7 +27,7 @@ class Base(NZBProvider, RSS):
passwords_regex = 'password|wachtwoord' passwords_regex = 'password|wachtwoord'
limits_reached = {} limits_reached = {}
http_time_between_calls = 1 # Seconds http_time_between_calls = 2 # Seconds
def search(self, media, quality): def search(self, media, quality):
hosts = self.getHosts() hosts = self.getHosts()

2
couchpotato/core/media/_base/providers/nzb/nzbclub.py

@ -15,7 +15,7 @@ log = CPLog(__name__)
class Base(NZBProvider, RSS): class Base(NZBProvider, RSS):
urls = { urls = {
'search': 'https://www.nzbclub.com/nzbfeeds.aspx?%s', 'search': 'https://www.nzbclub.com/nzbrss.aspx?%s',
} }
http_time_between_calls = 4 # seconds http_time_between_calls = 4 # seconds

133
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': '<a href="http://alpharatio.cc/">AlphaRatio</a>',
'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.',
}
],
},
],
}]

131
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': '<a href="https://hd4free.xyz">HD4Free</a>',
'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 <a href="https://hd4free.xyz/usercp.php?action=security">Profile Security</a>',
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 0,
'description': 'Will not be (re)moved until this seed ratio is met. 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.',
}
],
},
],
}]

5
couchpotato/core/media/_base/providers/torrent/ilovetorrents.py

@ -23,7 +23,8 @@ class Base(TorrentProvider):
} }
cat_ids = [ cat_ids = [
(['41'], ['720p', '1080p', 'brrip']), (['80'], ['720p', '1080p']),
(['41'], ['brrip']),
(['19'], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), (['19'], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
(['20'], ['dvdr']) (['20'], ['dvdr'])
] ]
@ -88,7 +89,7 @@ class Base(TorrentProvider):
id = re.search('id=(?P<id>\d+)&', link).group('id') id = re.search('id=(?P<id>\d+)&', link).group('id')
url = self.urls['download'] % download 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({ results.append({
'id': id, 'id': id,
'name': toUnicode(prelink.find('b').text), 'name': toUnicode(prelink.find('b').text),

4
couchpotato/core/media/_base/providers/torrent/iptorrents.py

@ -16,9 +16,9 @@ class Base(TorrentProvider):
urls = { urls = {
'test': 'https://iptorrents.eu/', 'test': 'https://iptorrents.eu/',
'base_url': 'https://iptorrents.eu', 'base_url': 'https://iptorrents.eu',
'login': 'https://iptorrents.eu/torrents/', 'login': 'https://iptorrents.eu/',
'login_check': 'https://iptorrents.eu/inbox.php', '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 http_time_between_calls = 1 # Seconds

12
couchpotato/core/media/_base/providers/torrent/kickasstorrents.py

@ -30,13 +30,9 @@ class Base(TorrentMagnetProvider):
cat_backup_id = None cat_backup_id = None
proxy_list = [ proxy_list = [
'https://kickass.to', 'https://kat.cr',
'http://kickass.pw', 'https://kickass.unblocked.pw/',
'http://kickassto.come.in', 'https://katproxy.com',
'http://katproxy.ws',
'http://kickass.bitproxy.eu',
'http://katph.eu',
'http://kickassto.come.in',
] ]
def _search(self, media, quality, results): def _search(self, media, quality, results):
@ -71,7 +67,7 @@ class Base(TorrentMagnetProvider):
link = td.find('div', {'class': 'torrentname'}).find_all('a')[2] link = td.find('div', {'class': 'torrentname'}).find_all('a')[2]
new['id'] = temp.get('id')[-7:] new['id'] = temp.get('id')[-7:]
new['name'] = link.text 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['detail_url'] = self.urls['detail'] % (self.getDomain(), link['href'][1:])
new['verified'] = True if td.find('a', 'iverify') else False new['verified'] = True if td.find('a', 'iverify') else False
new['score'] = 100 if new['verified'] else 0 new['score'] = 100 if new['verified'] else 0

230
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': '<a href="https://rarbg.to/torrents.php">RARBG</a>',
'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.',
}
],
},
],
}]

138
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': '<a href="https://www.scenetime.com">SceneTime</a>',
'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.',
}
],
},
],
}]

19
couchpotato/core/media/_base/providers/torrent/thepiratebay.py

@ -24,18 +24,19 @@ class Base(TorrentMagnetProvider):
http_time_between_calls = 0 http_time_between_calls = 0
proxy_list = [ proxy_list = [
'https://dieroschtibay.org', 'https://thepiratebay.mn',
'https://thebay.al', 'https://thepiratebay.gd',
'https://thepiratebay.se', 'https://thepiratebay.bg',
'http://thepiratebay.se.net', 'https://thepiratebay.la',
'http://thebootlegbay.com', 'https://thepiratebay.am',
'http://tpb.ninja.so', 'https://thepiratebay.gs',
'http://proxybay.fr', 'http://proxybay.fr',
'http://pirateproxy.in', 'http://pirateproxy.in',
'http://piratebay.skey.sk',
'http://pirateproxy.be',
'http://bayproxy.li',
'http://proxybay.pw', 'http://proxybay.pw',
'https://pirateproxy.sx',
'https://tpbproxy.co',
'https://arrr.xyz',
'http://tpb.dashitz.com',
] ]
def _search(self, media, quality, results): def _search(self, media, quality, results):

14
couchpotato/core/media/_base/providers/torrent/torrentshack.py

@ -13,12 +13,12 @@ log = CPLog(__name__)
class Base(TorrentProvider): class Base(TorrentProvider):
urls = { urls = {
'test': 'https://theshack.us.to/', 'test': 'https://torrentshack.me/',
'login': 'https://theshack.us.to/login.php', 'login': 'https://torrentshack.me/login.php',
'login_check': 'https://theshack.us.to/inbox.php', 'login_check': 'https://torrentshack.me/inbox.php',
'detail': 'https://theshack.us.to/torrent/%s', 'detail': 'https://torrentshack.me/torrent/%s',
'search': 'https://theshack.us.to/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1', 'search': 'https://torrentshack.me/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1',
'download': 'https://theshack.us.to/%s', 'download': 'https://torrentshack.me/%s',
} }
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds
@ -82,7 +82,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'TorrentShack', 'name': 'TorrentShack',
'description': '<a href="http://torrentshack.eu/">TorrentShack</a>', 'description': '<a href="https://torrentshack.me/">TorrentShack</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC', 'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC',
'options': [ 'options': [

11
couchpotato/core/media/_base/providers/torrent/yify.py

@ -18,9 +18,14 @@ class Base(TorrentProvider):
http_time_between_calls = 1 # seconds http_time_between_calls = 1 # seconds
proxy_list = [ proxy_list = [
'https://yts.re',
'https://yts.wf',
'https://yts.im', '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): def search(self, movie, quality):
@ -38,7 +43,7 @@ class Base(TorrentProvider):
search_url = self.urls['search'] % (domain, getIdentifier(movie)) search_url = self.urls['search'] % (domain, getIdentifier(movie))
data = self.getJsonData(search_url) data = self.getJsonData(search_url) or {}
data = data.get('data') data = data.get('data')
if isinstance(data, dict) and data.get('movies'): if isinstance(data, dict) and data.get('movies'):

277
couchpotato/core/media/_base/search/static/search.css

@ -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;
}

128
couchpotato/core/media/_base/search/static/search.js

@ -1,7 +1,11 @@
Block.Search = new Class({ var BlockSearch = new Class({
Extends: BlockBase, Extends: BlockBase,
options: {
'animate': true
},
cache: {}, cache: {},
create: function(){ create: function(){
@ -9,49 +13,47 @@ Block.Search = new Class({
var focus_timer = 0; var focus_timer = 0;
self.el = new Element('div.search_form').adopt( self.el = new Element('div.search_form').adopt(
new Element('div.input').adopt( new Element('a.icon-search', {
self.input = new Element('input', { 'events': {
'placeholder': 'Search & add a new media', 'click': self.clear.bind(self)
}
}),
self.wrapper = new Element('div.wrapper').adopt(
self.result_container = new Element('div.results_container', {
'events': { 'events': {
'input': self.keyup.bind(self), 'mousewheel': function(e){
'paste': self.keyup.bind(self), (e).stopPropagation();
'change': self.keyup.bind(self),
'keyup': self.keyup.bind(self),
'focus': function(){
if(focus_timer) clearTimeout(focus_timer);
self.el.addClass('focused');
if(this.get('value'))
self.hideResults(false)
},
'blur': function(){
focus_timer = (function(){
self.el.removeClass('focused')
}).delay(100);
} }
} }
}), }).grab(
new Element('a.icon2', { self.results = new Element('div.results')
'events': { ),
'click': self.clear.bind(self), new Element('div.input').grab(
'touchend': self.clear.bind(self) self.input = new Element('input', {
} 'placeholder': 'Search & add a new media',
}) 'events': {
), 'input': self.keyup.bind(self),
self.result_container = new Element('div.results_container', { 'paste': self.keyup.bind(self),
'tween': { 'change': self.keyup.bind(self),
'duration': 200 'keyup': self.keyup.bind(self),
}, 'focus': function(){
'events': { if(focus_timer) clearTimeout(focus_timer);
'mousewheel': function(e){ if(this.get('value'))
(e).stopPropagation(); self.hideResults(false);
} },
} 'blur': function(){
}).adopt( focus_timer = (function(){
self.results = new Element('div.results') 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.last_q = '';
self.input.set('value', ''); self.input.set('value', '');
self.el.addClass('focused');
self.input.focus(); self.input.focus();
self.media = {}; self.media = {};
self.results.empty(); self.results.empty();
self.el.removeClass('filled') self.el.removeClass('filled');
// 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(); self.api_request.cancel();
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer); if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer);
self.autocomplete_timer = self.autocomplete.delay(300, self) self.autocomplete_timer = self.autocomplete.delay(300, self);
} }
}, },
@ -115,10 +138,10 @@ Block.Search = new Class({
if(!self.q()){ if(!self.q()){
self.hideResults(true); self.hideResults(true);
return return;
} }
self.list() self.list();
}, },
list: function(){ list: function(){
@ -129,7 +152,9 @@ Block.Search = new Class({
self.hideResults(false); self.hideResults(false);
if(!cache){ if(!cache){
self.mask.fade('in'); setTimeout(function(){
self.mask.addClass('show');
}, 10);
if(!self.spinner) if(!self.spinner)
self.spinner = createSpinner(self.mask); self.spinner = createSpinner(self.mask);
@ -139,7 +164,7 @@ Block.Search = new Class({
'q': q 'q': q
}, },
'onComplete': self.fill.bind(self, q) 'onComplete': self.fill.bind(self, q)
}) });
} }
else else
self.fill(q, cache); self.fill(q, cache);
@ -158,30 +183,25 @@ Block.Search = new Class({
Object.each(json, function(media){ Object.each(json, function(media){
if(typeOf(media) == 'array'){ 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); $(m).inject(self.results);
self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m; self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m;
if(q == m.imdb) if(q == m.imdb)
m.showOptions() m.showOptions();
}); });
} }
}); });
// Calculate result heights self.mask.removeClass('show');
var w = window.getSize(),
rc = self.result_container.getCoordinates();
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px');
self.mask.fade('out')
}, },
loading: function(bool){ loading: function(bool){
this.el[bool ? 'addClass' : 'removeClass']('loading') this.el[bool ? 'addClass' : 'removeClass']('loading');
}, },
q: function(){ q: function(){

503
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;
}

2
couchpotato/core/media/_base/searcher/__init__.py

@ -48,7 +48,7 @@ config = [{
{ {
'name': 'ignored_words', 'name': 'ignored_words',
'label': 'Ignored', '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)' 'description': 'Ignores releases that match any of these sets. (Works like explained above)'
}, },
], ],

11
couchpotato/core/media/movie/_base/main.py

@ -34,6 +34,7 @@ class MovieBase(MovieTypeBase):
'params': { 'params': {
'identifier': {'desc': 'IMDB id of the movie your want to add.'}, '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.'}, '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.'}, '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.'}, '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): if not info or (info and len(info.get('titles', [])) == 0):
info = fireEvent('movie.info', merge = True, extended = False, identifier = params.get('identifier')) 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 # Set default title
default_title = toUnicode(info.get('title')) default_title = toUnicode(info.get('title'))
titles = info.get('titles', []) titles = info.get('titles', [])
@ -224,11 +230,11 @@ class MovieBase(MovieTypeBase):
try: try:
m = db.get('id', media_id) 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') cat_id = kwargs.get('category_id')
if cat_id is not None: 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 # Remove releases
for rel in fireEvent('release.for_media', m['_id'], single = True): 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)) fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id))
except: except:
print traceback.format_exc()
log.error('Can\'t edit non-existing media') log.error('Can\'t edit non-existing media')
return { return {

154
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');
}
}
});

207
couchpotato/core/media/movie/_base/static/list.js

@ -3,6 +3,7 @@ var MovieList = new Class({
Implements: [Events, Options], Implements: [Events, Options],
options: { options: {
api_call: 'media.list',
navigation: true, navigation: true,
limit: 50, limit: 50,
load_more: true, load_more: true,
@ -37,7 +38,28 @@ var MovieList = new Class({
'html': self.options.description, 'html': self.options.description,
'styles': {'display': 'none'} 'styles': {'display': 'none'}
}) : null, }) : 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', { self.load_more = self.options.load_more ? new Element('a.load_more', {
'events': { 'events': {
'click': self.loadMore.bind(self) 'click': self.loadMore.bind(self)
@ -45,15 +67,17 @@ var MovieList = new Class({
}) : null }) : null
); );
if($(window).getSize().x <= 480 && !self.options.force_view) self.changeView(self.getSavedView() || self.options.view || 'thumb');
self.changeView('list');
else
self.changeView(self.getSavedView() || self.options.view || 'details');
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.added', self.movieAdded.bind(self));
App.on('movie.deleted', self.movieDeleted.bind(self)) App.on('movie.deleted', self.movieDeleted.bind(self));
}, },
movieDeleted: function(notification){ movieDeleted: function(notification){
@ -67,7 +91,7 @@ var MovieList = new Class({
self.setCounter(self.counter_count-1); self.setCounter(self.counter_count-1);
self.total_movies--; self.total_movies--;
} }
}) });
} }
self.checkIfEmpty(); self.checkIfEmpty();
@ -89,18 +113,15 @@ var MovieList = new Class({
create: function(){ create: function(){
var self = this; var self = this;
// Create the alphabet nav if(self.options.load_more){
if(self.options.navigation)
self.createNavigation();
if(self.options.load_more)
self.scrollspy = new ScrollSpy({ self.scrollspy = new ScrollSpy({
container: self.el.getParent(),
min: function(){ min: function(){
var c = self.load_more.getCoordinates(); return self.load_more.getCoordinates().top;
return c.top - window.document.getSize().y - 300
}, },
onEnter: self.loadMore.bind(self) onEnter: self.loadMore.bind(self)
}); });
}
self.created = true; self.created = true;
}, },
@ -108,6 +129,7 @@ var MovieList = new Class({
addMovies: function(movies, total){ addMovies: function(movies, total){
var self = this; var self = this;
if(!self.created) self.create(); if(!self.created) self.create();
// do scrollspy // do scrollspy
@ -116,13 +138,12 @@ var MovieList = new Class({
self.scrollspy.stop(); self.scrollspy.stop();
} }
Object.each(movies, function(movie){ self.createMovie(movies, 'bottom');
self.createMovie(movie);
});
self.total_movies += total; self.total_movies += total;
self.setCounter(total); self.setCounter(total);
self.calculateSelected();
}, },
setCounter: function(count){ setCounter: function(count){
@ -138,7 +159,7 @@ var MovieList = new Class({
self.empty_message = null; self.empty_message = null;
} }
if(self.total_movies && count == 0 && !self.empty_message){ if(self.total_movies && count === 0 && !self.empty_message){
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') + var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : ''); (self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
@ -167,20 +188,37 @@ var MovieList = new Class({
}, },
createMovie: function(movie, inject_at){ createMovie: function(movie, inject_at, nr){
var self = this; var self = this,
var m = new Movie(self, { movies = Array.isArray(movie) ? movie : [movie],
'actions': self.options.actions, movie_els = [];
'view': self.current_view, inject_at = inject_at || 'bottom';
'onSelect': self.calculateSelected.bind(self)
}, movie); 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(){ createNavigation: function(){
@ -192,7 +230,7 @@ var MovieList = new Class({
self.navigation = new Element('div.alph_nav').adopt( self.navigation = new Element('div.alph_nav').adopt(
self.mass_edit_form = new Element('div.mass_edit_form').adopt( self.mass_edit_form = new Element('div.mass_edit_form').adopt(
new Element('span.select').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': { 'events': {
'change': self.massEditToggleAll.bind(self) 'change': self.massEditToggleAll.bind(self)
} }
@ -230,38 +268,40 @@ var MovieList = new Class({
), ),
new Element('div.menus').adopt( new Element('div.menus').adopt(
self.navigation_counter = new Element('span.counter[title=Total]'), self.navigation_counter = new Element('span.counter[title=Total]'),
self.filter_menu = new Block.Menu(self, { self.filter_menu = new BlockMenu(self, {
'class': 'filter' 'class': 'filter',
'button_class': 'icon-filter'
}), }),
self.navigation_actions = new Element('ul.actions', { self.navigation_actions = new Element('div.actions', {
'events': { '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'; var a = 'active';
self.navigation_actions.getElements('.'+a).removeClass(a); self.navigation_actions.getElements('.'+a).removeClass(a);
self.changeView(el.get('data-view')); self.changeView(new_view);
this.addClass(a);
self.navigation_actions.getElement('[data-view='+new_view+']')
el.inject(el.getParent(), 'top'); .addClass(a);
el.getSiblings().hide();
setTimeout(function(){
el.getSiblings().setStyle('display', null);
}, 100)
} }
} }
}), }),
self.navigation_menu = new Block.Menu(self, { self.navigation_menu = new BlockMenu(self, {
'class': 'extra' 'class': 'extra',
'button_class': 'icon-dots'
}) })
) )
).inject(self.el, 'top'); );
// Mass edit // Mass edit
self.mass_edit_select_class = new Form.Check(self.mass_edit_select);
Quality.getActiveProfiles().each(function(profile){ Quality.getActiveProfiles().each(function(profile){
new Element('option', { new Element('option', {
'value': profile.get('_id'), 'value': profile.get('_id'),
'text': profile.get('label') 'text': profile.get('label')
}).inject(self.mass_edit_quality) }).inject(self.mass_edit_quality);
}); });
self.filter_menu.addLink( self.filter_menu.addLink(
@ -273,7 +313,7 @@ var MovieList = new Class({
'change': self.search.bind(self) 'change': self.search.bind(self)
} }
}) })
).addClass('search'); ).addClass('search icon-search');
var available_chars; var available_chars;
self.filter_menu.addEvent('open', function(){ self.filter_menu.addEvent('open', function(){
@ -289,8 +329,8 @@ var MovieList = new Class({
available_chars = json.chars; available_chars = json.chars;
available_chars.each(function(c){ available_chars.each(function(c){
self.letters[c.capitalize()].addClass('available') self.letters[c.capitalize()].addClass('available');
}) });
} }
}); });
@ -301,23 +341,23 @@ var MovieList = new Class({
'events': { 'events': {
'click:relay(li.available)': function(e, el){ 'click:relay(li.available)': function(e, el){
self.activateLetter(el.get('data-letter')); self.activateLetter(el.get('data-letter'));
self.getMovies(true) self.getMovies(true);
} }
} }
}) })
); );
// Actions // Actions
['mass_edit', 'details', 'list'].each(function(view){ ['thumb', 'list'].each(function(view){
var current = self.current_view == view; var current = self.current_view == view;
new Element('li', { new Element('a', {
'class': 'icon2 ' + view + (current ? ' active ' : ''), 'class': 'icon-' + view + (current ? ' active ' : ''),
'data-view': view 'data-view': view
}).inject(self.navigation_actions, current ? 'top' : 'bottom'); }).inject(self.navigation_actions, current ? 'top' : 'bottom');
}); });
// All // All
self.letters['all'] = new Element('li.letter_all.available.active', { self.letters.all = new Element('li.letter_all.available.active', {
'text': 'ALL' 'text': 'ALL'
}).inject(self.navigation_alpha); }).inject(self.navigation_alpha);
@ -346,24 +386,26 @@ var MovieList = new Class({
var selected = 0, var selected = 0,
movies = self.movies.length; movies = self.movies.length;
self.movies.each(function(movie){ self.movies.each(function(movie){
selected += movie.isSelected() ? 1 : 0 selected += movie.isSelected() ? 1 : 0;
}); });
var indeterminate = selected > 0 && selected < movies, var indeterminate = selected > 0 && selected < movies,
checked = selected == movies && selected > 0; 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'](); if(self.mass_edit_select){
self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate'); 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(){ deleteSelected: function(){
var self = this, var self = this,
ids = self.getSelectedMovies(), 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, [{ 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'), 'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'),
@ -441,10 +483,10 @@ var MovieList = new Class({
var ids = []; var ids = [];
self.movies.each(function(movie){ self.movies.each(function(movie){
if (movie.isSelected()) if (movie.isSelected())
ids.include(movie.get('_id')) ids.include(movie.get('_id'));
}); });
return ids return ids;
}, },
massEditToggleAll: function(){ massEditToggleAll: function(){
@ -453,10 +495,10 @@ var MovieList = new Class({
var select = self.mass_edit_select.get('checked'); var select = self.mass_edit_select.get('checked');
self.movies.each(function(movie){ self.movies.each(function(movie){
movie.select(select) movie.select(select);
}); });
self.calculateSelected() self.calculateSelected();
}, },
reset: function(){ reset: function(){
@ -493,12 +535,12 @@ var MovieList = new Class({
.addClass(new_view+'_list'); .addClass(new_view+'_list');
self.current_view = new_view; self.current_view = new_view;
Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000}); Cookie.write(self.options.identifier+'_view', new_view, {duration: 1000});
}, },
getSavedView: function(){ getSavedView: function(){
var self = this; 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(){ search: function(){
@ -537,23 +579,24 @@ var MovieList = new Class({
self.load_more.set('text', 'loading...'); 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...'}) new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
).inject(self.el, 'top'); ).inject(self.el, 'top');
createSpinner(self.loader_first);
createSpinner(self.loader_first, { var lfc = self.loader_first;
radius: 4, loader_timeout = setTimeout(function(){
length: 4, lfc.addClass('show');
width: 1 }, 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({ 'data': Object.merge({
'type': self.options.type || 'movie', 'type': self.options.type || 'movie',
'status': self.options.status, 'status': self.options.status,
@ -564,13 +607,15 @@ var MovieList = new Class({
if(reset) if(reset)
self.movie_list.empty(); self.movie_list.empty();
if(loader_timeout) clearTimeout(loader_timeout);
if(self.loader_first){ if(self.loader_first){
var lf = self.loader_first; var lf = self.loader_first;
self.loader_first.addClass('hide');
self.loader_first = null; self.loader_first = null;
lf.removeClass('show');
setTimeout(function(){ setTimeout(function(){
lf.destroy(); lf.destroy();
}, 20000); }, 1000);
self.el.setStyle('min-height', null); self.el.setStyle('min-height', null);
} }
@ -590,7 +635,7 @@ var MovieList = new Class({
loadMore: function(){ loadMore: function(){
var self = this; var self = this;
if(self.offset >= self.options.limit) if(self.offset >= self.options.limit)
self.getMovies() self.getMovies();
}, },
store: function(movies){ store: function(movies){
@ -603,7 +648,7 @@ var MovieList = new Class({
checkIfEmpty: function(){ checkIfEmpty: function(){
var self = this; var self = this;
var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined); var is_empty = self.movies.length === 0 && (self.total_movies === 0 || self.total_movies === undefined);
if(self.title) if(self.title)
self.title[is_empty ? 'hide' : 'show'](); self.title[is_empty ? 'hide' : 'show']();

26
couchpotato/core/media/movie/_base/static/manage.js

@ -1,4 +1,4 @@
Page.Manage = new Class({ var MoviesManage = new Class({
Extends: PageBase, Extends: PageBase,
@ -33,15 +33,12 @@ Page.Manage = new Class({
'release_status': 'done', 'release_status': 'done',
'status_or': 1 '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], 'menu': [self.refresh_button, self.refresh_quick],
'on_empty_element': new Element('div.empty_manage').adopt( 'on_empty_element': new Element('div.empty_manage').adopt(
new Element('div', { new Element('div', {
'text': 'Seems like you don\'t have anything in your library yet.' 'text': 'Seems like you don\'t have anything in your library yet. Add your existing movie folders in '
}), }).grab(
new Element('div', {
'text': 'Add your existing movie folders in '
}).adopt(
new Element('a', { new Element('a', {
'text': 'Settings > Manage', 'text': 'Settings > Manage',
'href': App.createUrl('settings/manage') 'href': App.createUrl('settings/manage')
@ -49,7 +46,7 @@ Page.Manage = new Class({
), ),
new Element('div.after_manage', { new Element('div.after_manage', {
'text': 'When you\'ve done that, hit this button → ' 'text': 'When you\'ve done that, hit this button → '
}).adopt( }).grab(
new Element('a.button.green', { new Element('a.button.green', {
'text': 'Hit me, but not too hard', 'text': 'Hit me, but not too hard',
'events':{ '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 // Check if search is in progress
self.startProgressInterval(); self.startProgressInterval();
@ -113,7 +110,8 @@ Page.Manage = new Class({
return; return;
if(!self.progress_container) 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(); 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') : '') (folder_progress.eta > 0 ? ', ' + new Date ().increment('second', folder_progress.eta).timeDiffInWords().replace('from now', 'to go') : '')
}), }),
new Element('span.percentage', {'text': folder_progress.total ? Math.round(((folder_progress.total-folder_progress.to_go)/folder_progress.total)*100) + '%' : '0%'}) new Element('span.percentage', {'text': folder_progress.total ? Math.round(((folder_progress.total-folder_progress.to_go)/folder_progress.total)*100) + '%' : '0%'})
).inject(self.progress_container) ).inject(self.progress_container);
}); });
} }
} }
}) });
}, 1000); }, 1000);
}, },
@ -141,10 +139,10 @@ Page.Manage = new Class({
for (folder in progress_object) { for (folder in progress_object) {
if (progress_object.hasOwnProperty(folder)) { if (progress_object.hasOwnProperty(folder)) {
temp_array.push(folder) temp_array.push(folder);
} }
} }
return temp_array.stableSort() return temp_array.stableSort();
} }
}); });

902
couchpotato/core/media/movie/_base/static/movie.actions.js

File diff suppressed because it is too large

1074
couchpotato/core/media/movie/_base/static/movie.css

File diff suppressed because it is too large

316
couchpotato/core/media/movie/_base/static/movie.js

@ -1,23 +1,61 @@
var Movie = new Class({ var Movie = new Class({
Extends: BlockBase, Extends: BlockBase,
Implements: [Options, Events],
action: {}, actions: null,
details: null,
initialize: function(list, options, data){ initialize: function(list, options, data){
var self = this; var self = this;
self.actions = [];
self.data = data; self.data = data;
self.view = options.view || 'details';
self.list = list; self.list = list;
self.el = new Element('div.movie'); self.buttons = [];
self.el = new Element('a.movie');
self.el.store('klass', self);
self.profile = Quality.getProfile(data.profile_id) || {}; self.profile = Quality.getProfile(data.profile_id) || {};
self.category = CategoryList.getCategory(data.category_id) || {}; self.category = CategoryList.getCategory(data.category_id) || {};
self.parent(self, options); self.parent(self, options);
self.addEvents(); self.addEvents();
//if(data.identifiers.imdb == 'tt3181822'){
// self.el.fireEvent('mouseenter');
// self.openDetails();
//}
},
openDetails: function(){
var self = this;
if(!self.details){
self.details = new MovieDetails(self, {
'level': 3
});
// Add action items
self.actions.each(function(action, nr){
var details = action.getDetails();
if(details){
self.details.addSection(action.getLabel(), details);
}
else {
var button = action.getDetailButton();
if(button){
self.details.addButton(button);
}
}
});
}
App.getPageContainer().grab(self.details);
self.details.open.delay(10, self.details);
}, },
addEvents: function(){ addEvents: function(){
@ -30,7 +68,6 @@ var Movie = new Class({
if(self.data._id != notification.data._id) return; if(self.data._id != notification.data._id) return;
self.busy(false); self.busy(false);
self.removeView();
self.update.delay(2000, self, notification); self.update.delay(2000, self, notification);
}; };
App.on('movie.update', self.global_events['movie.update']); App.on('movie.update', self.global_events['movie.update']);
@ -47,7 +84,7 @@ var Movie = new Class({
// Remove spinner // Remove spinner
self.global_events['movie.searcher.ended'] = function(notification){ self.global_events['movie.searcher.ended'] = function(notification){
if(notification.data && self.data._id == notification.data._id) if(notification.data && self.data._id == notification.data._id)
self.busy(false) self.busy(false);
}; };
App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']); App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
@ -62,7 +99,7 @@ var Movie = new Class({
var updated = false; var updated = false;
self.data.releases.each(function(release){ self.data.releases.each(function(release){
if(release._id == data._id){ if(release._id == data._id){
release['status'] = data.status; release.status = data.status;
updated = true; updated = true;
} }
}); });
@ -85,6 +122,9 @@ var Movie = new Class({
self.list.checkIfEmpty(); self.list.checkIfEmpty();
if(self.details)
self.details.close();
// Remove events // Remove events
Object.each(self.global_events, function(handle, listener){ Object.each(self.global_events, function(handle, listener){
App.off(listener, handle); App.off(listener, handle);
@ -102,12 +142,12 @@ var Movie = new Class({
if(self.mask) if(self.mask)
self.mask.destroy(); self.mask.destroy();
if(self.spinner) if(self.spinner)
self.spinner.el.destroy(); self.spinner.destroy();
self.spinner = null; self.spinner = null;
self.mask = null; self.mask = null;
}, timeout || 400); }, timeout || 400);
} }
}, timeout || 1000) }, timeout || 1000);
} }
else if(!self.spinner) { else if(!self.spinner) {
self.createMask(); self.createMask();
@ -128,94 +168,121 @@ var Movie = new Class({
update: function(notification){ update: function(notification){
var self = this; var self = this;
self.actions = [];
self.data = notification.data; self.data = notification.data;
self.el.empty(); self.el.empty();
self.removeView();
self.profile = Quality.getProfile(self.data.profile_id) || {}; self.profile = Quality.getProfile(self.data.profile_id) || {};
self.category = CategoryList.getCategory(self.data.category_id) || {}; self.category = CategoryList.getCategory(self.data.category_id) || {};
self.create(); self.create();
self.select(self.select_checkbox.get('checked'));
self.busy(false); self.busy(false);
}, },
create: function(){ create: function(){
var self = this; var self = this,
d = new Date();
self.el.addClass('status_'+self.get('status')); self.el.addClass('status_'+self.get('status'));
var eta = null, var eta = null,
eta_date = null, eta_date = null,
now = Math.round(+new Date()/1000); now = Math.round(+d/1000);
if(self.data.info.release_date) if(self.data.info.release_date)
[self.data.info.release_date.dvd, self.data.info.release_date.theater].each(function(timestamp){ [self.data.info.release_date.dvd, self.data.info.release_date.theater].each(function(timestamp){
if (timestamp > 0 && (eta == null || Math.abs(timestamp - now) < Math.abs(eta - now))) if (timestamp > 0 && (eta === null || Math.abs(timestamp - now) < Math.abs(eta - now)))
eta = timestamp; eta = timestamp;
}); });
if(eta){ if(eta){
eta_date = new Date(eta * 1000); eta_date = new Date(eta * 1000);
eta_date = eta_date.toLocaleString('en-us', { month: "long" }) + ' ' + eta_date.getFullYear(); if(+eta_date/1000 < now){
eta_date = null;
}
else {
eta_date = eta_date.format('%b') + (d.getFullYear() != eta_date.getFullYear() ? ' ' + eta_date.getFullYear() : '');
}
} }
self.el.adopt( var rating, stars;
self.select_checkbox = new Element('input[type=checkbox].inlay', { if(['suggested','chart'].indexOf(self.data.status) > -1 && self.data.info && self.data.info.rating && self.data.info.rating.imdb){
'events': { rating = self.data.info.rating.imdb;
'change': function(){
self.fireEvent('select') stars = [];
}
var half_rating = rating[0]/2;
for(var i = 1; i <= 5; i++){
if(half_rating >= 1)
stars.push(new Element('span.icon-star'));
else if(half_rating > 0)
stars.push(new Element('span.icon-star-half'));
else
stars.push(new Element('span.icon-star-empty'));
half_rating -= 1;
}
}
var thumbnail = new Element('div.poster');
if(self.data.files && self.data.files.image_poster && self.data.files.image_poster.length > 0){
thumbnail = new Element('div', {
'class': 'type_image poster',
'styles': {
'background-image': 'url(' + Api.createUrl('file.cache') + self.data.files.image_poster[0].split(Api.getOption('path_sep')).pop() +')'
} }
}), });
self.thumbnail = (self.data.files && self.data.files.image_poster && self.data.files.image_poster.length > 0) ? new Element('img', { }
else if(self.data.info && self.data.info.images && self.data.info.images.poster && self.data.info.images.poster.length > 0){
thumbnail = new Element('div', {
'class': 'type_image poster', 'class': 'type_image poster',
'src': Api.createUrl('file.cache') + self.data.files.image_poster[0].split(Api.getOption('path_sep')).pop() 'styles': {
}): null, 'background-image': 'url(' + self.data.info.images.poster[0] +')'
self.data_container = new Element('div.data.inlay.light').adopt( }
self.info_container = new Element('div.info').adopt( });
new Element('div.title').adopt( }
self.title = new Element('span', {
'text': self.getTitle() || 'n/a' self.el.adopt(
}), self.select_checkbox = new Element('input[type=checkbox]'),
self.year = new Element('div.year', { new Element('div.poster_container').adopt(
'text': self.data.info.year || 'n/a' thumbnail,
}) self.actions_el = new Element('div.actions')
), ),
self.description = new Element('div.description.tiny_scroll', { new Element('div.info').adopt(
'text': self.data.info.plot new Element('div.title').adopt(
new Element('span', {
'text': self.getTitle() || 'n/a'
}), }),
self.eta = eta_date && (now+8035200 > eta) ? new Element('div.eta', { new Element('div.year', {
'text': eta_date, 'text': self.data.info.year || 'n/a'
'title': 'ETA'
}) : null,
self.quality = new Element('div.quality', {
'events': {
'click': function(e){
var releases = self.el.getElement('.actions .releases');
if(releases.isVisible())
releases.fireEvent('click', [e])
}
}
}) })
), ),
self.actions = new Element('div.actions') eta_date && (now+8035200 > eta) ? new Element('div.eta', {
'text': eta_date,
'title': 'ETA'
}) : null,
self.quality = new Element('div.quality'),
rating ? new Element('div.rating[title='+rating[0]+']').adopt(
stars,
new Element('span.votes[text=('+rating.join(' / ')+')][title=Votes]')
) : null
) )
); );
if(!self.thumbnail) if(!thumbnail)
self.el.addClass('no_thumbnail'); self.el.addClass('no_thumbnail');
//self.changeView(self.view);
self.select_checkbox_class = new Form.Check(self.select_checkbox);
// Add profile // Add profile
if(self.profile.data) if(self.profile.data)
self.profile.getTypes().each(function(type){ self.profile.getTypes().each(function(type){
var q = self.addQuality(type.get('quality'), type.get('3d')); var q = self.addQuality(type.get('quality'), type.get('3d'));
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){ if((type.finish === true || type.get('finish')) && !q.hasClass('finish')){
q.addClass('finish'); q.addClass('finish');
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.') q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.');
} }
}); });
@ -223,17 +290,84 @@ var Movie = new Class({
// Add releases // Add releases
self.updateReleases(); self.updateReleases();
Object.each(self.options.actions, function(action, key){ },
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
if(action.el)
self.actions.adopt(action) onClick: function(e){
}); var self = this;
if(e.target.getParents('.actions').length == 0 && e.target != self.select_checkbox){
(e).stopPropagation();
self.openDetails();
}
},
onMouseenter: function(){
var self = this;
if(self.actions.length <= 0){
self.options.actions.each(function(a){
var action = new a(self),
button = action.getButton();
if(button){
self.actions_el.grab(button);
self.buttons.push(button);
}
self.actions.push(action);
});
}
if(App.mobile_screen) return;
if(self.list.current_view == 'thumb'){
dynamics.css(self.el, {
scale: 1,
opacity: 1
});
dynamics.animate(self.el, {
scale: 0.9
}, { type: dynamics.bounce });
self.buttons.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,
delay: 100 + (nr * 40)
});
});
}
},
onMouseleave: function(){
var self = this;
if(App.mobile_screen) return;
if(self.list.current_view == 'thumb'){
dynamics.animate(self.el, {
scale: 1
}, { type: dynamics.spring });
}
}, },
updateReleases: function(){ updateReleases: function(){
var self = this; var self = this;
if(!self.data.releases || self.data.releases.length == 0) return; if(!self.data.releases || self.data.releases.length === 0) return;
self.data.releases.each(function(release){ self.data.releases.each(function(release){
@ -245,7 +379,7 @@ var Movie = new Class({
if (q && !q.hasClass(status)){ if (q && !q.hasClass(status)){
q.addClass(status); q.addClass(status);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status) q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status);
} }
}); });
@ -263,15 +397,15 @@ var Movie = new Class({
}, },
getTitle: function(){ getTitle: function(prefixed){
var self = this; var self = this;
if(self.data.title) if(self.data.title)
return self.getUnprefixedTitle(self.data.title); return prefixed ? self.data.title : self.getUnprefixedTitle(self.data.title);
else if(self.data.info.titles.length > 0) else if(self.data.info && self.data.info.titles && self.data.info.titles.length > 0)
return self.getUnprefixedTitle(self.data.info.titles[0]); return prefixed ? self.data.info.titles[0] : self.getUnprefixedTitle(self.data.info.titles[0]);
return 'Unknown movie' return 'Unknown movie';
}, },
getUnprefixedTitle: function(t){ getUnprefixedTitle: function(t){
@ -284,49 +418,6 @@ var Movie = new Class({
return t; return t;
}, },
slide: function(direction, el){
var self = this;
if(direction == 'in'){
self.temp_view = self.view;
self.changeView('details');
self.el.addEvent('outerClick', function(){
self.removeView();
self.slide('out')
});
el.show();
self.data_container.addClass('hide_right');
}
else {
self.el.removeEvents('outerClick');
setTimeout(function(){
if(self.el)
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
}, 600);
self.data_container.removeClass('hide_right');
}
},
changeView: function(new_view){
var self = this;
if(self.el)
self.el
.removeClass(self.view+'_view')
.addClass(new_view+'_view');
self.view = new_view;
},
removeView: function(){
var self = this;
self.el.removeClass(self.view+'_view')
},
getIdentifier: function(){ getIdentifier: function(){
var self = this; var self = this;
@ -339,12 +430,13 @@ var Movie = new Class({
}, },
get: function(attr){ get: function(attr){
return this.data[attr] || this.data.info[attr] return this.data[attr] || this.data.info[attr];
}, },
select: function(bool){ select: function(select){
var self = this; var self = this;
self.select_checkbox_class[bool ? 'check' : 'uncheck']() self.select_checkbox.set('checked', select);
self.el[self.select_checkbox.get('checked') ? 'addClass' : 'removeClass']('checked');
}, },
isSelected: function(){ isSelected: function(){

1248
couchpotato/core/media/movie/_base/static/movie.scss

File diff suppressed because it is too large

50
couchpotato/core/media/movie/_base/static/page.js

@ -0,0 +1,50 @@
Page.Movies = new Class({
Extends: PageBase,
name: 'movies',
icon: 'movie',
sub_pages: ['Wanted', 'Manage'],
default_page: 'Wanted',
current_page: null,
initialize: function(parent, options){
var self = this;
self.parent(parent, options);
self.navigation = new BlockNavigation();
$(self.navigation).inject(self.content, 'top');
},
defaultAction: function(action, params){
var self = this;
if(self.current_page){
self.current_page.hide();
if(self.current_page.list && self.current_page.list.navigation)
self.current_page.list.navigation.dispose();
}
var route = new Route();
route.parse(action);
var page_name = route.getPage() != 'index' ? route.getPage().capitalize() : self.default_page;
var page = self.sub_pages.filter(function(page){
return page.name == page_name;
}).pick()['class'];
page.open(route.getAction() || 'index', params);
page.show();
if(page.list && page.list.navigation)
page.list.navigation.inject(self.navigation);
self.current_page = page;
self.navigation.activate(page_name.toLowerCase());
}
});

125
couchpotato/core/media/movie/_base/static/search.js

@ -1,4 +1,4 @@
Block.Search.MovieItem = new Class({ var BlockSearchMovieItem = new Class({
Implements: [Options, Events], Implements: [Options, Events],
@ -16,28 +16,45 @@ Block.Search.MovieItem = new Class({
var self = this, var self = this,
info = self.info; info = self.info;
var in_library;
if(info.in_library){
in_library = [];
(info.in_library.releases || []).each(function(release){
in_library.include(release.quality);
});
}
self.el = new Element('div.media_result', { self.el = new Element('div.media_result', {
'id': info.imdb 'id': info.imdb,
'events': {
'click': self.showOptions.bind(self)//,
//'mouseenter': self.showOptions.bind(self),
//'mouseleave': self.closeOptions.bind(self)
}
}).adopt( }).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', { self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0], 'src': info.images.poster[0],
'height': null, 'height': null,
'width': null 'width': null
}) : null, }) : null,
self.options_el = new Element('div.options.inlay'), self.options_el = new Element('div.options'),
self.data_container = new Element('div.data', { self.data_container = new Element('div.data').grab(
'events': { self.info_container = new Element('div.info').grab(
'click': self.showOptions.bind(self) new Element('h2', {
} 'class': info.in_wanted && info.in_wanted.profile_id || in_library ? 'in_library_wanted' : '',
}).adopt( 'title': self.getTitle()
self.info_container = new Element('div.info').adopt( }).adopt(
new Element('h2').adopt(
self.title = new Element('span.title', { self.title = new Element('span.title', {
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown' 'text': self.getTitle()
}), }),
self.year = info.year ? new Element('span.year', { self.year = info.year ? new Element('span.year', {
'text': info.year 'text': info.year
}) : null }) : null,
info.in_wanted && info.in_wanted.profile_id ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + Quality.getProfile(info.in_wanted.profile_id).get('label')
}) : (in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + in_library.join(', ')
}) : null)
) )
) )
) )
@ -48,7 +65,7 @@ Block.Search.MovieItem = new Class({
self.alternativeTitle({ self.alternativeTitle({
'title': title 'title': title
}); });
}) });
}, },
alternativeTitle: function(alternative){ alternativeTitle: function(alternative){
@ -68,7 +85,7 @@ Block.Search.MovieItem = new Class({
}, },
get: function(key){ get: function(key){
return this.info[key] return this.info[key];
}, },
showOptions: function(){ showOptions: function(){
@ -77,7 +94,7 @@ Block.Search.MovieItem = new Class({
self.createOptions(); self.createOptions();
self.data_container.addClass('open'); self.data_container.addClass('open');
self.el.addEvent('outerClick', self.closeOptions.bind(self)) self.el.addEvent('outerClick', self.closeOptions.bind(self));
}, },
@ -85,7 +102,7 @@ Block.Search.MovieItem = new Class({
var self = this; var self = this;
self.data_container.removeClass('open'); self.data_container.removeClass('open');
self.el.removeEvents('outerClick') self.el.removeEvents('outerClick');
}, },
add: function(e){ add: function(e){
@ -105,7 +122,7 @@ Block.Search.MovieItem = new Class({
}, },
'onComplete': function(json){ 'onComplete': function(json){
self.options_el.empty(); self.options_el.empty();
self.options_el.adopt( self.options_el.grab(
new Element('div.message', { new Element('div.message', {
'text': json.success ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs' 'text': json.success ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs'
}) })
@ -116,7 +133,7 @@ Block.Search.MovieItem = new Class({
}, },
'onFailure': function(){ 'onFailure': function(){
self.options_el.empty(); self.options_el.empty();
self.options_el.adopt( self.options_el.grab(
new Element('div.message', { new Element('div.message', {
'text': 'Something went wrong, check the logs for more info.' 'text': 'Something went wrong, check the logs for more info.'
}) })
@ -132,56 +149,50 @@ Block.Search.MovieItem = new Class({
if(!self.options_el.hasClass('set')){ if(!self.options_el.hasClass('set')){
if(info.in_library){
var in_library = [];
(info.in_library.releases || []).each(function(release){
in_library.include(release.quality)
});
}
self.options_el.grab( self.options_el.grab(
new Element('div', { new Element('div').adopt(
'class': info.in_wanted && info.in_wanted.profile_id || in_library ? 'in_library_wanted' : '' new Element('div.title').grab(
}).adopt( self.title_select = new Element('select', {
info.in_wanted && info.in_wanted.profile_id ? new Element('span.in_wanted', { 'name': 'title'
'text': 'Already in wanted list: ' + Quality.getProfile(info.in_wanted.profile_id).get('label') })
}) : (in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + in_library.join(', ')
}) : null),
self.title_select = new Element('select', {
'name': 'title'
}),
self.profile_select = new Element('select', {
'name': 'profile'
}),
self.category_select = new Element('select', {
'name': 'category'
}).grab(
new Element('option', {'value': -1, 'text': 'None'})
), ),
self.add_button = new Element('a.button', { new Element('div.profile').grab(
'text': 'Add', self.profile_select = new Element('select', {
'events': { 'name': 'profile'
'click': self.add.bind(self) })
} ),
}) self.category_select_container = new Element('div.category').grab(
self.category_select = new Element('select', {
'name': 'category'
}).grab(
new Element('option', {'value': -1, 'text': 'None'})
)
),
new Element('div.add').grab(
self.add_button = new Element('a.button', {
'text': 'Add',
'events': {
'click': self.add.bind(self)
}
})
)
) )
); );
Array.each(self.alternative_titles, function(alt){ Array.each(self.alternative_titles, function(alt){
new Element('option', { new Element('option', {
'text': alt.title 'text': alt.title
}).inject(self.title_select) }).inject(self.title_select);
}); });
// Fill categories // Fill categories
var categories = CategoryList.getAll(); var categories = CategoryList.getAll();
if(categories.length == 0) if(categories.length === 0)
self.category_select.hide(); self.category_select_container.hide();
else { else {
self.category_select.show(); self.category_select_container.show();
categories.each(function(category){ categories.each(function(category){
new Element('option', { new Element('option', {
'value': category.data._id, 'value': category.data._id,
@ -199,12 +210,12 @@ Block.Search.MovieItem = new Class({
new Element('option', { new Element('option', {
'value': profile.get('_id'), 'value': profile.get('_id'),
'text': profile.get('label') 'text': profile.get('label')
}).inject(self.profile_select) }).inject(self.profile_select);
}); });
self.options_el.addClass('set'); self.options_el.addClass('set');
if(categories.length == 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 && if(categories.length === 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 &&
!(self.info.in_wanted && self.info.in_wanted.profile_id || in_library)) !(self.info.in_wanted && self.info.in_wanted.profile_id || in_library))
self.add(); self.add();
@ -218,12 +229,12 @@ Block.Search.MovieItem = new Class({
self.mask = new Element('div.mask').inject(self.el).fade('hide'); self.mask = new Element('div.mask').inject(self.el).fade('hide');
createSpinner(self.mask); createSpinner(self.mask);
self.mask.fade('in') self.mask.fade('in');
}, },
toElement: function(){ toElement: function(){
return this.el return this.el;
} }
}); });

107
couchpotato/core/media/movie/_base/static/wanted.js

@ -1,4 +1,4 @@
Page.Wanted = new Class({ var MoviesWanted = new Class({
Extends: PageBase, Extends: PageBase,
@ -10,7 +10,7 @@ Page.Wanted = new Class({
indexAction: function(){ indexAction: function(){
var self = this; var self = this;
if(!self.wanted){ if(!self.list){
self.manual_search = new Element('a', { self.manual_search = new Element('a', {
'title': 'Force a search for the full wanted list', 'title': 'Force a search for the full wanted list',
@ -20,25 +20,24 @@ Page.Wanted = new Class({
} }
}); });
self.scan_folder = new Element('a', {
self.scan_folder = new Element('a', { 'title': 'Scan a folder and rename all movies in it',
'title': 'Scan a folder and rename all movies in it', 'text': 'Manual folder scan',
'text': 'Manual folder scan', 'events':{
'events':{ 'click': self.scanFolder.bind(self)
'click': self.scanFolder.bind(self) }
} });
});
// Wanted movies // Wanted movies
self.wanted = new MovieList({ self.list = new MovieList({
'identifier': 'wanted', 'identifier': 'wanted',
'status': 'active', 'status': 'active',
'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Edit, MA.Refresh, MA.Readd, MA.Delete], 'actions': [MA.IMDB, MA.Release, MA.Trailer, MA.Refresh, MA.Readd, MA.Delete, MA.Category, MA.Profile],
'add_new': true, 'add_new': true,
'menu': [self.manual_search, self.scan_folder], 'menu': [self.manual_search, self.scan_folder],
'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted') 'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted')
}); });
$(self.wanted).inject(self.el); $(self.list).inject(self.content);
// Check if search is in progress // Check if search is in progress
self.startProgressInterval.delay(4000, self); self.startProgressInterval.delay(4000, self);
@ -82,43 +81,55 @@ Page.Wanted = new Class({
}, },
scanFolder: function(e) { scanFolder: function(e) {
(e).stop(); (e).stop();
var self = this;
var options = {
'name': 'Scan_folder'
};
if(!self.folder_browser){ var self = this;
self.folder_browser = new Option['Directory']("Scan", "folder", "", options); var options = {
'name': 'Scan_folder'
self.folder_browser.save = function() { };
var folder = self.folder_browser.getValue();
Api.request('renamer.scan', { if(!self.folder_browser){
'data': { self.folder_browser = new Option.Directory("Scan", "folder", "", options);
'base_folder': folder
self.folder_browser.save = function() {
var folder = self.folder_browser.getValue();
Api.request('renamer.scan', {
'data': {
'base_folder': folder
} }
}); });
}; };
self.folder_browser.inject(self.el, 'top'); self.folder_browser.inject(self.content, 'top');
self.folder_browser.fireEvent('injected'); self.folder_browser.fireEvent('injected');
// Hide the settings box // Hide the settings box
self.folder_browser.directory_inlay.hide(); self.folder_browser.directory_inlay.hide();
self.folder_browser.el.removeChild(self.folder_browser.el.firstChild); self.folder_browser.el.removeChild(self.folder_browser.el.firstChild);
self.folder_browser.showBrowser(); self.folder_browser.showBrowser();
// Make adjustments to the browser // Make adjustments to the browser
self.folder_browser.browser.getElements('.clear.button').hide(); self.folder_browser.browser.getElements('.clear.button').hide();
self.folder_browser.save_button.text = "Select"; self.folder_browser.save_button.text = "Select";
self.folder_browser.browser.style.zIndex=1000; self.folder_browser.browser.setStyles({
} 'z-index': 1000,
else{ 'right': 20,
self.folder_browser.showBrowser(); 'top': 0,
} 'margin': 0
} });
self.folder_browser.pointer.setStyles({
'right': 20
});
}
else{
self.folder_browser.showBrowser();
}
self.list.navigation_menu.hide();
}
}); });

14
couchpotato/core/media/movie/charts/__init__.py

@ -21,20 +21,6 @@ config = [{
'type': 'int', 'type': 'int',
'description': 'Maximum number of items displayed from each chart.', 'description': 'Maximum number of items displayed from each chart.',
}, },
{
'name': 'hide_wanted',
'default': False,
'type': 'bool',
'advanced': True,
'description': 'Hide the chart movies that are already in your wanted list.',
},
{
'name': 'hide_library',
'default': False,
'type': 'bool',
'advanced': True,
'description': 'Hide the chart movies that are already in your library.',
},
], ],
}, },
], ],

101
couchpotato/core/media/movie/charts/main.py

@ -1,8 +1,10 @@
import time from CodernityDB.database import RecordNotFound
from couchpotato import Env, get_db
from couchpotato.core.helpers.variable import getTitle, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent,fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@ -11,51 +13,72 @@ log = CPLog(__name__)
class Charts(Plugin): class Charts(Plugin):
update_in_progress = False
update_interval = 72 # hours
def __init__(self): def __init__(self):
addApiView('charts.view', self.automationView) addApiView('charts.view', self.automationView)
addEvent('app.load', self.setCrons) addApiView('charts.ignore', self.ignoreView)
def setCrons(self):
fireEvent('schedule.interval', 'charts.update_cache', self.updateViewCache, hours = self.update_interval)
def automationView(self, force_update = False, **kwargs): def automationView(self, force_update = False, **kwargs):
if force_update: db = get_db()
charts = self.updateViewCache()
else: charts = fireEvent('automation.get_chart_list', merge = True)
charts = self.getCache('charts_cached') ignored = splitString(Env.prop('charts_ignore', default = ''))
if not charts:
charts = self.updateViewCache() # Create a list the movie/list.js can use
for chart in charts:
medias = []
for media in chart.get('list', []):
identifier = media.get('imdb')
if identifier in ignored:
continue
try:
try:
in_library = db.get('media', 'imdb-%s' % identifier)
if in_library:
continue
except RecordNotFound:
pass
except:
pass
# Cache poster
posters = media.get('images', {}).get('poster', [])
poster = [x for x in posters if 'tmdb' in x]
posters = poster if len(poster) > 0 else posters
cached_poster = fireEvent('file.download', url = posters[0], single = True) if len(posters) > 0 else False
files = {'image_poster': [cached_poster] } if cached_poster else {}
medias.append({
'status': 'chart',
'title': getTitle(media),
'type': 'movie',
'info': media,
'files': files,
'identifiers': {
'imdb': identifier
}
})
chart['list'] = medias
return { return {
'success': True, 'success': True,
'count': len(charts), 'count': len(charts),
'charts': charts 'charts': charts,
'ignored': ignored,
} }
def updateViewCache(self): def ignoreView(self, imdb = None, **kwargs):
if self.update_in_progress: ignored = splitString(Env.prop('charts_ignore', default = ''))
while self.update_in_progress:
time.sleep(1) if imdb:
catched_charts = self.getCache('charts_cached') ignored.append(imdb)
if catched_charts: Env.prop('charts_ignore', ','.join(set(ignored)))
return catched_charts
return {
charts = [] 'result': True
try: }
self.update_in_progress = True
charts = fireEvent('automation.get_chart_list', merge = True)
for chart in charts:
chart['hide_wanted'] = self.conf('hide_wanted')
chart['hide_library'] = self.conf('hide_library')
self.setCache('charts_cached', charts, timeout = self.update_interval * 3600)
except:
log.error('Failed refreshing charts')
self.update_in_progress = False
return charts

124
couchpotato/core/media/movie/charts/static/charts.js

@ -42,12 +42,7 @@ var Charts = new Class({
) )
); );
if( Cookie.read('suggestions_charts_menu_selected') === 'charts'){ self.show();
self.show();
}
else
self.el.hide();
self.fireEvent.delay(0, self, 'created'); self.fireEvent.delay(0, self, 'created');
}, },
@ -59,94 +54,39 @@ var Charts = new Class({
self.el_refreshing_text.hide(); self.el_refreshing_text.hide();
self.el_refresh_link.show(); self.el_refresh_link.show();
if(!json || json.count == 0){ if(!json || json.count === 0){
self.el_no_charts_enabled.show(); self.el_no_charts_enabled.show();
self.el_refresh_link.show(); self.el_refresh_link.show();
self.el_refreshing_text.hide(); self.el_refreshing_text.hide();
} }
else { else {
self.el_no_charts_enabled.hide(); self.el_no_charts_enabled.hide();
json.charts.sort(function(a, b) { json.charts.sort(function(a, b) {
return a.order - b.order; return a.order - b.order;
}); });
Object.each(json.charts, function(chart){ Object.each(json.charts, function(chart){
var c = new Element('div.chart.tiny_scroll').grab( var chart_list = new MovieList({
new Element('h3').grab( new Element('a', { 'navigation': false,
'text': chart.name, 'identifier': chart.name.toLowerCase().replace(/[^a-z0-9]+/g, '_'),
'href': chart.url 'title': chart.name,
})) 'description': '<a href="'+chart.url+'">See source</a>',
); 'actions': [MA.Add, MA.ChartIgnore, MA.IMDB, MA.Trailer],
'load_more': false,
var it = 1; 'view': 'thumb',
'force_view': true,
Object.each(chart.list, function(movie){ 'api_call': null
var m = new Block.Search.MovieItem(movie, {
'onAdded': function(){
self.afterAdded(m, movie)
}
});
var in_database_class = (chart.hide_wanted && movie.in_wanted) ? 'hidden' : (movie.in_wanted ? 'chart_in_wanted' : ((chart.hide_library && movie.in_library) ? 'hidden': (movie.in_library ? 'chart_in_library' : ''))),
in_database_title = movie.in_wanted ? 'Movie in wanted list' : (movie.in_library ? 'Movie in library' : '');
m.el
.addClass(in_database_class)
.grab(
new Element('div.chart_number', {
'text': it++,
'title': in_database_title
})
);
m.data_container.grab(
new Element('div.actions').adopt(
new Element('a.add.icon2', {
'title': 'Add movie with your default quality',
'data-add': movie.imdb,
'events': {
'click': m.showOptions.bind(m)
}
}),
$(new MA.IMDB(m)),
$(new MA.Trailer(m, {
'height': 150
}))
)
);
m.data_container.removeEvents('click');
var plot = false;
if(m.info.plot && m.info.plot.length > 0)
plot = m.info.plot;
// Add rating
m.info_container.adopt(
m.rating = m.info.rating && m.info.rating.imdb && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', {
'text': parseFloat(m.info.rating.imdb[0]),
'title': parseInt(m.info.rating.imdb[1]) + ' votes'
}) : null,
m.genre = m.info.genres && m.info.genres.length > 0 ? new Element('span.genres', {
'text': m.info.genres.slice(0, 3).join(', ')
}) : null,
m.plot = plot ? new Element('span.plot', {
'text': plot,
'events': {
'click': function(){
this.toggleClass('full')
}
}
}) : null
);
$(m).inject(c);
}); });
c.inject(self.el); // Load movies in manually
chart_list.store(chart.list);
chart_list.addMovies(chart.list, chart.list.length);
chart_list.checkIfEmpty();
chart_list.fireEvent('loaded');
$(chart_list).inject(self.el);
}); });
@ -162,26 +102,16 @@ var Charts = new Class({
self.el.show(); self.el.show();
if(!self.shown_once){ if(!self.shown_once){
self.api_request = Api.request('charts.view', { setTimeout(function(){
'onComplete': self.fill.bind(self) self.api_request = Api.request('charts.view', {
}); 'onComplete': self.fill.bind(self)
});
}, 100);
self.shown_once = true; self.shown_once = true;
} }
}, },
hide: function(){
this.el.hide();
},
afterAdded: function(m){
$(m).getElement('div.chart_number')
.addClass('chart_in_wanted')
.set('title', 'Movie in wanted list');
},
toElement: function(){ toElement: function(){
return this.el; return this.el;
} }

0
couchpotato/core/media/movie/charts/static/charts.css → couchpotato/core/media/movie/charts/static/charts.scss

61
couchpotato/core/media/movie/providers/automation/bluray.py

@ -27,12 +27,13 @@ class Bluray(Automation, RSS):
if self.conf('backlog'): if self.conf('backlog'):
cookie = {'Cookie': 'listlayout_7=full'}
page = 0 page = 0
while True: while True:
page += 1 page += 1
url = self.backlog_url % page url = self.backlog_url % page
data = self.getHTMLData(url) data = self.getHTMLData(url, headers = cookie)
soup = BeautifulSoup(data) soup = BeautifulSoup(data)
try: try:
@ -104,41 +105,49 @@ class Bluray(Automation, RSS):
return movies return movies
def getChartList(self): def getChartList(self):
# Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id) cache_key = 'bluray.charts'
movie_list = {'name': 'Blu-ray.com - New Releases', 'url': self.display_url, 'order': self.chart_order, 'list': []} movie_list = {
movie_ids = [] 'name': 'Blu-ray.com - New Releases',
max_items = int(self.conf('max_items', section='charts', default=5)) 'url': self.display_url,
rss_movies = self.getRSSData(self.rss_url) 'order': self.chart_order,
'list': self.getCache(cache_key) or []
}
for movie in rss_movies: if not movie_list['list']:
name = self.getTextElement(movie, 'title').lower().split('blu-ray')[0].strip('(').rstrip() movie_ids = []
year = self.getTextElement(movie, 'description').split('|')[1].strip('(').strip() max_items = int(self.conf('max_items', section='charts', default=5))
rss_movies = self.getRSSData(self.rss_url)
if not name.find('/') == -1: # make sure it is not a double movie release for movie in rss_movies:
continue name = self.getTextElement(movie, 'title').lower().split('blu-ray')[0].strip('(').rstrip()
year = self.getTextElement(movie, 'description').split('|')[1].strip('(').strip()
movie = self.search(name, year) if not name.find('/') == -1: # make sure it is not a double movie release
continue
if movie: movie = self.search(name, year)
if movie.get('imdb') in movie_ids: if movie:
continue
is_movie = fireEvent('movie.is_movie', identifier = movie.get('imdb'), single = True) if movie.get('imdb') in movie_ids:
if not is_movie: continue
continue
movie_ids.append(movie.get('imdb')) is_movie = fireEvent('movie.is_movie', identifier = movie.get('imdb'), single = True)
movie_list['list'].append( movie ) if not is_movie:
if len(movie_list['list']) >= max_items: continue
break
if not movie_list['list']: movie_ids.append(movie.get('imdb'))
return movie_list['list'].append( movie )
if len(movie_list['list']) >= max_items:
break
if not movie_list['list']:
return
self.setCache(cache_key, movie_list['list'], timeout = 259200)
return [ movie_list ] return [movie_list]
config = [{ config = [{

104
couchpotato/core/media/movie/providers/automation/hummingbird.py

@ -0,0 +1,104 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.automation.base import Automation
log = CPLog(__name__)
autoload = 'Hummingbird'
class Hummingbird(Automation):
def getIMDBids(self):
movies = []
for movie in self.getWatchlist():
imdb = self.search(movie[0], movie[1])
if imdb:
movies.append(imdb['imdb'])
return movies
def getWatchlist(self):
if not self.conf('automation_username'):
log.error('You need to fill in a username')
return []
url = "http://hummingbird.me/api/v1/users/%s/library" % self.conf('automation_username')
data = self.getJsonData(url)
chosen_filter = {
'automation_list_current': 'currently-watching',
'automation_list_plan': 'plan-to-watch',
'automation_list_completed': 'completed',
'automation_list_hold': 'on-hold',
'automation_list_dropped': 'dropped',
}
chosen_lists = []
for x in chosen_filter:
if self.conf(x):
chosen_lists.append(chosen_filter[x])
entries = []
for item in data:
if item['anime']['show_type'] != 'Movie' or item['status'] not in chosen_lists:
continue
title = item['anime']['title']
year = item['anime']['started_airing']
if year:
year = year[:4]
entries.append([title, year])
return entries
config = [{
'name': 'hummingbird',
'groups': [
{
'tab': 'automation',
'list': 'watchlist_providers',
'name': 'hummingbird_automation',
'label': 'Hummingbird',
'description': 'Import movies from your Hummingbird.me lists',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_username',
'label': 'Username',
},
{
'name': 'automation_list_current',
'type': 'bool',
'label': 'Currently Watching',
'default': False,
},
{
'name': 'automation_list_plan',
'type': 'bool',
'label': 'Plan to Watch',
'default': True,
},
{
'name': 'automation_list_completed',
'type': 'bool',
'label': 'Completed',
'default': False,
},
{
'name': 'automation_list_hold',
'type': 'bool',
'label': 'On Hold',
'default': False,
},
{
'name': 'automation_list_dropped',
'type': 'bool',
'label': 'Dropped',
'default': False,
},
],
},
],
}]

68
couchpotato/core/media/movie/providers/automation/imdb.py

@ -19,7 +19,7 @@ autoload = 'IMDB'
class IMDB(MultiProvider): class IMDB(MultiProvider):
def getTypes(self): def getTypes(self):
return [IMDBWatchlist, IMDBAutomation] return [IMDBWatchlist, IMDBAutomation, IMDBCharts]
class IMDBBase(Automation, RSS): class IMDBBase(Automation, RSS):
@ -126,30 +126,6 @@ class IMDBAutomation(IMDBBase):
enabled_option = 'automation_providers_enabled' enabled_option = 'automation_providers_enabled'
charts = {
'theater': {
'order': 1,
'name': 'IMDB - Movies in Theaters',
'url': 'http://www.imdb.com/movies-in-theaters/',
},
'boxoffice': {
'order': 2,
'name': 'IMDB - Box Office',
'url': 'http://www.imdb.com/boxoffice/',
},
'rentals': {
'order': 3,
'name': 'IMDB - Top DVD rentals',
'url': 'http://www.imdb.com/boxoffice/rentals',
'type': 'json',
},
'top250': {
'order': 4,
'name': 'IMDB - Top 250 Movies',
'url': 'http://www.imdb.com/chart/top',
},
}
def getIMDBids(self): def getIMDBids(self):
movies = [] movies = []
@ -175,20 +151,53 @@ class IMDBAutomation(IMDBBase):
return movies return movies
def getChartList(self):
class IMDBCharts(IMDBBase):
charts = {
'theater': {
'order': 1,
'name': 'IMDB - Movies in Theaters',
'url': 'http://www.imdb.com/movies-in-theaters/',
},
'boxoffice': {
'order': 2,
'name': 'IMDB - Box Office',
'url': 'http://www.imdb.com/boxoffice/',
},
'rentals': {
'order': 3,
'name': 'IMDB - Top DVD rentals',
'url': 'http://www.imdb.com/boxoffice/rentals',
'type': 'json',
},
'top250': {
'order': 4,
'name': 'IMDB - Top 250 Movies',
'url': 'http://www.imdb.com/chart/top',
},
}
def getChartList(self):
# Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id) # Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id)
movie_lists = [] movie_lists = []
max_items = int(self.conf('max_items', section = 'charts', default=5)) max_items = int(self.conf('max_items', section = 'charts', default=5))
for name in self.charts: for name in self.charts:
chart = self.charts[name].copy() chart = self.charts[name].copy()
url = chart.get('url') cache_key = 'imdb.chart_display_%s' % name
if self.conf('chart_display_%s' % name): if self.conf('chart_display_%s' % name):
chart['list'] = [] cached = self.getCache(cache_key)
if cached:
chart['list'] = cached
movie_lists.append(chart)
continue
url = chart.get('url')
chart['list'] = []
imdb_ids = self.getFromURL(url) imdb_ids = self.getFromURL(url)
try: try:
@ -206,10 +215,11 @@ class IMDBAutomation(IMDBBase):
except: except:
log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc())) log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc()))
self.setCache(cache_key, chart['list'], timeout = 259200)
if chart['list']: if chart['list']:
movie_lists.append(chart) movie_lists.append(chart)
return movie_lists return movie_lists

13
couchpotato/core/media/movie/providers/automation/moviemeter.py

@ -1,4 +1,3 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.automation.base import Automation from couchpotato.core.media.movie.providers.automation.base import Automation
@ -20,16 +19,10 @@ class Moviemeter(Automation, RSS):
rss_movies = self.getRSSData(self.rss_url) rss_movies = self.getRSSData(self.rss_url)
for movie in rss_movies: for movie in rss_movies:
imdb = self.search(self.getTextElement(movie, 'title'))
title = self.getTextElement(movie, 'title') if imdb and self.isMinimalMovie(imdb):
name_year = fireEvent('scanner.name_year', title, single = True) movies.append(imdb['imdb'])
if name_year.get('name') and name_year.get('year'):
imdb = self.search(name_year.get('name'), name_year.get('year'))
if imdb and self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
else:
log.error('Failed getting name and year from: %s', title)
return movies return movies

11
couchpotato/core/media/movie/providers/automation/popularmovies.py

@ -17,11 +17,12 @@ class PopularMovies(Automation):
movies = [] movies = []
retrieved_movies = self.getJsonData(self.url) retrieved_movies = self.getJsonData(self.url)
for movie in retrieved_movies.get('movies'): if retrieved_movies:
imdb_id = movie.get('imdb_id') for movie in retrieved_movies.get('movies'):
info = fireEvent('movie.info', identifier = imdb_id, extended = False, merge = True) imdb_id = movie.get('imdb_id')
if self.isMinimalMovie(info): info = fireEvent('movie.info', identifier = imdb_id, extended = False, merge = True)
movies.append(imdb_id) if self.isMinimalMovie(info):
movies.append(imdb_id)
return movies return movies

95
couchpotato/core/media/movie/providers/automation/rottentomatoes.py

@ -1,95 +0,0 @@
from xml.etree.ElementTree import QName
import datetime
import re
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.automation.base import Automation
log = CPLog(__name__)
autoload = 'Rottentomatoes'
class Rottentomatoes(Automation, RSS):
interval = 1800
def getIMDBids(self):
movies = []
rotten_tomatoes_namespace = 'http://www.rottentomatoes.com/xmlns/rtmovie/'
urls = dict(zip(splitString(self.conf('automation_urls')), [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]))
for url in urls:
if not urls[url]:
continue
rss_movies = self.getRSSData(url)
rating_tag = str(QName(rotten_tomatoes_namespace, 'tomatometer_percent'))
for movie in rss_movies:
value = self.getTextElement(movie, "title")
result = re.search('(?<=%\s).*', value)
if result:
rating = tryInt(self.getTextElement(movie, rating_tag))
name = result.group(0)
print rating, tryInt(self.conf('tomatometer_percent'))
if rating < tryInt(self.conf('tomatometer_percent')):
log.info2('%s seems to be rotten...', name)
else:
log.info2('Found %s with fresh rating %s', (name, rating))
year = datetime.datetime.now().strftime("%Y")
imdb = self.search(name, year)
if imdb and self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
return movies
config = [{
'name': 'rottentomatoes',
'groups': [
{
'tab': 'automation',
'list': 'automation_providers',
'name': 'rottentomatoes_automation',
'label': 'Rottentomatoes',
'description': 'Imports movies from rottentomatoes rss feeds specified below.',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_urls_use',
'label': 'Use',
'default': '1',
},
{
'name': 'automation_urls',
'label': 'url',
'type': 'combined',
'combine': ['automation_urls_use', 'automation_urls'],
'default': 'http://www.rottentomatoes.com/syndication/rss/in_theaters.xml',
},
{
'name': 'tomatometer_percent',
'default': '80',
'label': 'Tomatometer',
'description': 'Use as extra scoring requirement',
},
],
},
],
}]

83
couchpotato/core/media/movie/providers/automation/trakt.py

@ -1,83 +0,0 @@
import base64
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import sha1
from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.automation.base import Automation
log = CPLog(__name__)
autoload = 'Trakt'
class Trakt(Automation):
urls = {
'base': 'http://api.trakt.tv/',
'watchlist': 'user/watchlist/movies.json/%s/',
}
def __init__(self):
super(Trakt, self).__init__()
addEvent('setting.save.trakt.automation_password', self.sha1Password)
def sha1Password(self, value):
return sha1(value) if value else ''
def getIMDBids(self):
movies = []
for movie in self.getWatchlist():
movies.append(movie.get('imdb_id'))
return movies
def getWatchlist(self):
method = (self.urls['watchlist'] % self.conf('automation_api_key')) + self.conf('automation_username')
return self.call(method)
def call(self, method_url):
headers = {}
if self.conf('automation_password'):
headers['Authorization'] = 'Basic %s' % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1]
data = self.getJsonData(self.urls['base'] + method_url, headers = headers)
return data if data else []
config = [{
'name': 'trakt',
'groups': [
{
'tab': 'automation',
'list': 'watchlist_providers',
'name': 'trakt_automation',
'label': 'Trakt',
'description': 'import movies from your own watchlist',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_api_key',
'label': 'Apikey',
},
{
'name': 'automation_username',
'label': 'Username',
},
{
'name': 'automation_password',
'label': 'Password',
'type': 'password',
'description': 'When you have "Protect my data" checked <a href="http://trakt.tv/settings/account">on trakt</a>.',
},
],
},
],
}]

31
couchpotato/core/media/movie/providers/automation/trakt/__init__.py

@ -0,0 +1,31 @@
from .main import Trakt
def autoload():
return Trakt()
config = [{
'name': 'trakt',
'groups': [
{
'tab': 'automation',
'list': 'watchlist_providers',
'name': 'trakt_automation',
'label': 'Trakt',
'description': 'Import movies from your own watchlist',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_oauth_token',
'label': 'Auth Token',
'advanced': 1
},
],
},
],
}]

76
couchpotato/core/media/movie/providers/automation/trakt/main.py

@ -0,0 +1,76 @@
import json
from couchpotato import Env
from couchpotato.api import addApiView
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.base import Provider
from couchpotato.core.media.movie.providers.automation.base import Automation
log = CPLog(__name__)
class TraktBase(Provider):
client_id = '8a54ed7b5e1b56d874642770ad2e8b73e2d09d6e993c3a92b1e89690bb1c9014'
api_url = 'https://api-v2launch.trakt.tv/'
def call(self, method_url, post_data = None):
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer %s' % self.conf('automation_oauth_token'),
'trakt-api-version': 2,
'trakt-api-key': self.client_id,
}
if post_data:
post_data = json.dumps(post_data)
data = self.getJsonData(self.api_url + method_url, data = post_data or {}, headers = headers)
return data if data else []
class Trakt(Automation, TraktBase):
urls = {
'watchlist': 'sync/watchlist/movies/',
'oauth': 'https://api.couchpota.to/authorize/trakt/',
}
def __init__(self):
addApiView('automation.trakt.auth_url', self.getAuthorizationUrl)
addApiView('automation.trakt.credentials', self.getCredentials)
super(Trakt, self).__init__()
def getIMDBids(self):
movies = []
for movie in self.getWatchlist():
movies.append(movie.get('movie').get('ids').get('imdb'))
return movies
def getWatchlist(self):
return self.call(self.urls['watchlist'])
def getAuthorizationUrl(self, host = None, **kwargs):
callback_url = cleanHost(host) + '%sautomation.trakt.credentials/' % (Env.get('api_base').lstrip('/'))
log.debug('callback_url is %s', callback_url)
target_url = self.urls['oauth'] + "?target=" + callback_url
log.debug('target_url is %s', target_url)
return {
'success': True,
'url': target_url,
}
def getCredentials(self, **kwargs):
try:
oauth_token = kwargs.get('oauth')
except:
return 'redirect', Env.get('web_base') + 'settings/automation/'
log.debug('oauth_token is: %s', oauth_token)
self.conf('automation_oauth_token', value = oauth_token)
return 'redirect', Env.get('web_base') + 'settings/automation/'

67
couchpotato/core/media/movie/providers/automation/trakt/static/trakt.js

@ -0,0 +1,67 @@
var TraktAutomation = new Class({
initialize: function(){
var self = this;
App.addEvent('loadSettings', self.addRegisterButton.bind(self));
},
addRegisterButton: function(){
var self = this,
setting_page = App.getPage('Settings');
setting_page.addEvent('create', function(){
var fieldset = setting_page.tabs.automation.groups.trakt_automation,
l = window.location;
var trakt_set = 0;
fieldset.getElements('input[type=text]').each(function(el){
trakt_set += +(el.get('value') !== '');
});
new Element('.ctrlHolder').adopt(
// Unregister button
(trakt_set > 0) ?
[
self.unregister = new Element('a.button.red', {
'text': 'Unregister',
'events': {
'click': function(){
fieldset.getElements('input[name*=oauth_token]').set('value', '').fireEvent('change');
self.unregister.destroy();
self.unregister_or.destroy();
}
}
}),
self.unregister_or = new Element('span[text=or]')
]
: null,
// Register button
new Element('a.button', {
'text': trakt_set > 0 ? 'Register a different account' : 'Register your trakt.tv account',
'events': {
'click': function(){
Api.request('automation.trakt.auth_url', {
'data': {
'host': l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '')
},
'onComplete': function(json){
window.location = json.url;
}
});
}
}
})
).inject(fieldset);
});
}
});
new TraktAutomation();

5
couchpotato/core/media/movie/providers/info/themoviedb.py

@ -173,7 +173,8 @@ class TheMovieDb(MovieProvider):
image_url = '' image_url = ''
try: try:
path = movie.get('%s_path' % type) path = movie.get('%s_path' % type)
image_url = '%s%s%s' % (self.configuration['images']['secure_base_url'], size, path) if path:
image_url = '%s%s%s' % (self.configuration['images']['secure_base_url'], size, path)
except: except:
log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie)))) log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie))))
@ -196,7 +197,7 @@ class TheMovieDb(MovieProvider):
params = tryUrlencode(params) params = tryUrlencode(params)
try: try:
url = 'http://api.themoviedb.org/3/%s?api_key=%s%s' % (call, self.conf('api_key'), '&%s' % params if params else '') url = 'https://api.themoviedb.org/3/%s?api_key=%s%s' % (call, self.conf('api_key'), '&%s' % params if params else '')
data = self.getJsonData(url, show_error = False) data = self.getJsonData(url, show_error = False)
except: except:
log.debug('Movie not found: %s, %s', (call, params)) log.debug('Movie not found: %s, %s', (call, params))

221
couchpotato/core/media/movie/providers/metadata/wdtv.py

@ -0,0 +1,221 @@
from xml.etree.ElementTree import Element, SubElement, tostring
import os
import re
import traceback
import xml.dom.minidom
from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog
autoload = 'WdtvLive'
log = CPLog(__name__)
class WdtvLive(MovieMetaData):
def getThumbnailName(self, name, root, i):
return self.createMetaName('%s.jpg', name, root)
def createMetaName(self, basename, name, root):
return os.path.join(root, basename.replace('%s', name))
def getNfoName(self, name, root, i):
return self.createMetaName('%s.xml', name, root)
def getNfo(self, movie_info=None, data=None, i=0):
if not data: data = {}
if not movie_info: movie_info = {}
nfoxml = Element('details')
# Title
try:
el = SubElement(nfoxml, 'title')
el.text = toUnicode(getTitle(data))
except:
pass
# IMDB id
try:
el = SubElement(nfoxml, 'id')
el.text = toUnicode(data['identifier'])
except:
pass
# Runtime
try:
runtime = SubElement(nfoxml, 'runtime')
runtime.text = '%s min' % movie_info.get('runtime')
except:
pass
# Other values
types = ['year', 'mpaa', 'originaltitle:original_title', 'outline', 'plot', 'tagline', 'premiered:released']
for type in types:
if ':' in type:
name, type = type.split(':')
else:
name = type
try:
if movie_info.get(type):
el = SubElement(nfoxml, name)
el.text = toUnicode(movie_info.get(type, ''))
except:
pass
# Rating
for rating_type in ['imdb', 'rotten', 'tmdb']:
try:
r, v = movie_info['rating'][rating_type]
rating = SubElement(nfoxml, 'rating')
rating.text = str(r)
votes = SubElement(nfoxml, 'votes')
votes.text = str(v)
break
except:
log.debug('Failed adding rating info from %s: %s', (rating_type, traceback.format_exc()))
# Genre
for genre in movie_info.get('genres', []):
genres = SubElement(nfoxml, 'genre')
genres.text = toUnicode(genre)
# Actors
for actor_name in movie_info.get('actor_roles', {}):
role_name = movie_info['actor_roles'][actor_name]
actor = SubElement(nfoxml, 'actor')
name = SubElement(actor, 'name')
name.text = toUnicode(actor_name)
if role_name:
role = SubElement(actor, 'role')
role.text = toUnicode(role_name)
if movie_info['images']['actors'].get(actor_name):
thumb = SubElement(actor, 'thumb')
thumb.text = toUnicode(movie_info['images']['actors'].get(actor_name))
# Directors
for director_name in movie_info.get('directors', []):
director = SubElement(nfoxml, 'director')
director.text = toUnicode(director_name)
# Writers
for writer in movie_info.get('writers', []):
writers = SubElement(nfoxml, 'credits')
writers.text = toUnicode(writer)
# Sets or collections
collection_name = movie_info.get('collection')
if collection_name:
collection = SubElement(nfoxml, 'set')
collection.text = toUnicode(collection_name)
sorttitle = SubElement(nfoxml, 'sorttitle')
sorttitle.text = '%s %s' % (toUnicode(collection_name), movie_info.get('year'))
# Images
for image_url in movie_info['images']['poster_original']:
image = SubElement(nfoxml, 'thumb')
image.text = toUnicode(image_url)
image_types = [
('fanart', 'backdrop_original'),
('banner', 'banner'),
('discart', 'disc_art'),
('logo', 'logo'),
('clearart', 'clear_art'),
('landscape', 'landscape'),
('extrathumb', 'extra_thumbs'),
('extrafanart', 'extra_fanart'),
]
for image_type in image_types:
sub, type = image_type
sub_element = SubElement(nfoxml, sub)
for image_url in movie_info['images'][type]:
image = SubElement(sub_element, 'thumb')
image.text = toUnicode(image_url)
# Add trailer if found
trailer_found = False
if data.get('renamed_files'):
for filename in data.get('renamed_files'):
if 'trailer' in filename:
trailer = SubElement(nfoxml, 'trailer')
trailer.text = toUnicode(filename)
trailer_found = True
if not trailer_found and data['files'].get('trailer'):
trailer = SubElement(nfoxml, 'trailer')
trailer.text = toUnicode(data['files']['trailer'][0])
# Add file metadata
fileinfo = SubElement(nfoxml, 'fileinfo')
streamdetails = SubElement(fileinfo, 'streamdetails')
# Video data
if data['meta_data'].get('video'):
video = SubElement(streamdetails, 'video')
codec = SubElement(video, 'codec')
codec.text = toUnicode(data['meta_data']['video'])
aspect = SubElement(video, 'aspect')
aspect.text = str(data['meta_data']['aspect'])
width = SubElement(video, 'width')
width.text = str(data['meta_data']['resolution_width'])
height = SubElement(video, 'height')
height.text = str(data['meta_data']['resolution_height'])
# Audio data
if data['meta_data'].get('audio'):
audio = SubElement(streamdetails, 'audio')
codec = SubElement(audio, 'codec')
codec.text = toUnicode(data['meta_data'].get('audio'))
channels = SubElement(audio, 'channels')
channels.text = toUnicode(data['meta_data'].get('audio_channels'))
# Clean up the xml and return it
nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
xml_string = nfoxml.toprettyxml(indent = ' ')
text_re = re.compile('>\n\s+([^<>\s].*?)\n\s+</', re.DOTALL)
xml_string = text_re.sub('>\g<1></', xml_string)
return xml_string.encode('utf-8')
config = [{
'name': 'wdtvlive',
'groups': [
{
'tab': 'renamer',
'subtab': 'metadata',
'name': 'wdtvlive_metadata',
'label': 'WDTV Live',
'description': 'Metadata for WDTV',
'options': [
{
'name': 'meta_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'meta_nfo',
'label': 'NFO',
'default': True,
'type': 'bool',
'description': 'Generate metadata xml',
},
{
'name': 'meta_thumbnail',
'label': 'Thumbnail',
'default': True,
'type': 'bool',
'description': 'Generate thumbnail jpg',
}
],
},
],
}]

32
couchpotato/core/media/movie/providers/torrent/alpharatio.py

@ -0,0 +1,32 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.alpharatio import Base
from couchpotato.core.media.movie.providers.base import MovieProvider
log = CPLog(__name__)
autoload = 'AlphaRatio'
class AlphaRatio(MovieProvider, Base):
# AlphaRatio movie search categories
# 7: MoviesHD
# 9: MoviePackHD
# 6: MoviesSD
# 8: MovePackSD
cat_ids = [
([7, 9], ['bd50']),
([7, 9], ['720p', '1080p']),
([6, 8], ['dvdr']),
([6, 8], ['brrip', 'dvdrip']),
]
cat_backup_id = 6
def buildUrl(self, media, quality):
query = (tryUrlencode(fireEvent('library.query', media, single = True)),
self.getSceneOnly(),
self.getCatId(quality)[0])
return query

11
couchpotato/core/media/movie/providers/torrent/hd4free.py

@ -0,0 +1,11 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.hd4free import Base
from couchpotato.core.media.movie.providers.base import MovieProvider
log = CPLog(__name__)
autoload = 'HD4Free'
class HD4Free(MovieProvider, Base):
pass

11
couchpotato/core/media/movie/providers/torrent/rarbg.py

@ -0,0 +1,11 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.rarbg import Base
from couchpotato.core.media.movie.providers.base import MovieProvider
log = CPLog(__name__)
autoload = 'Rarbg'
class Rarbg(MovieProvider, Base):
pass

11
couchpotato/core/media/movie/providers/torrent/scenetime.py

@ -0,0 +1,11 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.scenetime import Base
from couchpotato.core.media.movie.providers.base import MovieProvider
log = CPLog(__name__)
autoload = 'SceneTime'
class SceneTime(MovieProvider, Base):
pass

20
couchpotato/core/media/movie/providers/userscript/filmweb.py

@ -1,14 +1,14 @@
import re from bs4 import BeautifulSoup
from couchpotato import fireEvent
from couchpotato.core.media._base.providers.userscript.base import UserscriptBase from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
autoload = 'Filmweb' autoload = 'Filmweb'
class Filmweb(UserscriptBase): class Filmweb(UserscriptBase):
version = 2 version = 3
includes = ['http://www.filmweb.pl/film/*'] includes = ['http://www.filmweb.pl/film/*']
@ -21,14 +21,10 @@ class Filmweb(UserscriptBase):
except: except:
return return
name = re.search("<h2.*?class=\"text-large caption\">(?P<name>[^<]+)</h2>", data) html = BeautifulSoup(data)
name = html.find('meta', {'name': 'title'})['content'][:-9].strip()
if name is None: name_year = fireEvent('scanner.name_year', name, single = True)
name = re.search("<a.*?property=\"v:name\".*?>(?P<name>[^<]+)</a>", data) name = name_year.get('name')
year = name_year.get('year')
name = name.group('name').decode('string_escape')
year = re.search("<span.*?id=filmYear.*?>\((?P<year>[^\)]+)\).*?</span>", data)
year = year.group('year')
return self.search(name, year) return self.search(name, year)

6
couchpotato/core/media/movie/providers/userscript/flickchart.py

@ -12,6 +12,8 @@ autoload = 'Flickchart'
class Flickchart(UserscriptBase): class Flickchart(UserscriptBase):
version = 2
includes = ['http://www.flickchart.com/movie/*'] includes = ['http://www.flickchart.com/movie/*']
def getMovie(self, url): def getMovie(self, url):
@ -24,11 +26,11 @@ class Flickchart(UserscriptBase):
try: try:
start = data.find('<title>') start = data.find('<title>')
end = data.find('</title>', start) end = data.find('</title>', start)
page_title = data[start + len('<title>'):end].strip().split('-') page_title = data[start + len('<title>'):end].strip().split('- Flick')
year_name = fireEvent('scanner.name_year', page_title[0], single = True) year_name = fireEvent('scanner.name_year', page_title[0], single = True)
return self.search(**year_name) return self.search(year_name.get('name'), year_name.get('year'))
except: except:
log.error('Failed parsing page for title and year: %s', traceback.format_exc()) log.error('Failed parsing page for title and year: %s', traceback.format_exc())

14
couchpotato/core/media/movie/providers/userscript/moviemeter.py

@ -1,3 +1,4 @@
from couchpotato.core.helpers.variable import getImdb
from couchpotato.core.media._base.providers.userscript.base import UserscriptBase from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
autoload = 'MovieMeter' autoload = 'MovieMeter'
@ -6,3 +7,16 @@ autoload = 'MovieMeter'
class MovieMeter(UserscriptBase): class MovieMeter(UserscriptBase):
includes = ['http://*.moviemeter.nl/film/*', 'http://moviemeter.nl/film/*'] includes = ['http://*.moviemeter.nl/film/*', 'http://moviemeter.nl/film/*']
version = 2
def getMovie(self, url):
cookie = {'Cookie': 'cok=1'}
try:
data = self.urlopen(url, headers = cookie)
except:
return
return self.getInfo(getImdb(data))

19
couchpotato/core/media/movie/providers/userscript/rottentomatoes.py

@ -1,6 +1,7 @@
import re import re
import traceback import traceback
from couchpotato import fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.userscript.base import UserscriptBase from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
@ -15,7 +16,7 @@ class RottenTomatoes(UserscriptBase):
includes = ['*://www.rottentomatoes.com/m/*'] includes = ['*://www.rottentomatoes.com/m/*']
excludes = ['*://www.rottentomatoes.com/m/*/*/'] excludes = ['*://www.rottentomatoes.com/m/*/*/']
version = 2 version = 4
def getMovie(self, url): def getMovie(self, url):
@ -25,16 +26,12 @@ class RottenTomatoes(UserscriptBase):
return return
try: try:
name = None title = re.findall("<title>(.*)</title>", data)
year = None title = title[0].split(' - Rotten')[0].replace('&nbsp;', ' ').decode('unicode_escape')
metas = re.findall("property=\"(video:release_date|og:title)\" content=\"([^\"]*)\"", data) name_year = fireEvent('scanner.name_year', title, single = True)
for meta in metas: name = name_year.get('name')
mname, mvalue = meta year = name_year.get('year')
if mname == 'og:title':
name = mvalue.decode('unicode_escape')
elif mname == 'video:release_date':
year = mvalue[:4]
if name and year: if name and year:
return self.search(name, year) return self.search(name, year)

8
couchpotato/core/media/movie/providers/userscript/sharethe.py

@ -1,8 +0,0 @@
from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
autoload = 'ShareThe'
class ShareThe(UserscriptBase):
includes = ['http://*.sharethe.tv/movies/*', 'http://sharethe.tv/movies/*']

4
couchpotato/core/media/movie/providers/userscript/tmdb.py

@ -9,7 +9,9 @@ autoload = 'TMDB'
class TMDB(UserscriptBase): class TMDB(UserscriptBase):
includes = ['http://www.themoviedb.org/movie/*'] version = 2
includes = ['*://www.themoviedb.org/movie/*']
def getMovie(self, url): def getMovie(self, url):
match = re.search('(?P<id>\d+)', url) match = re.search('(?P<id>\d+)', url)

8
couchpotato/core/media/movie/providers/userscript/whiwa.py

@ -1,8 +0,0 @@
from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
autoload = 'WHiWA'
class WHiWA(UserscriptBase):
includes = ['http://whiwa.net/stats/movie/*']

55
couchpotato/core/media/movie/suggestion/main.py → couchpotato/core/media/movie/suggestion.py

@ -1,6 +1,7 @@
import time
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.variable import splitString, removeDuplicate, getIdentifier from couchpotato.core.helpers.variable import splitString, removeDuplicate, getIdentifier, getTitle
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
@ -15,6 +16,12 @@ class Suggestion(Plugin):
addApiView('suggestion.view', self.suggestView) addApiView('suggestion.view', self.suggestView)
addApiView('suggestion.ignore', self.ignoreView) addApiView('suggestion.ignore', self.ignoreView)
def test():
time.sleep(1)
self.suggestView()
addEvent('app.load', test)
def suggestView(self, limit = 6, **kwargs): def suggestView(self, limit = 6, **kwargs):
movies = splitString(kwargs.get('movies', '')) movies = splitString(kwargs.get('movies', ''))
@ -38,10 +45,31 @@ class Suggestion(Plugin):
suggestions = fireEvent('movie.suggest', movies = movies, ignore = ignored, single = True) suggestions = fireEvent('movie.suggest', movies = movies, ignore = ignored, single = True)
self.setCache('suggestion_cached', suggestions, timeout = 6048000) # Cache for 10 weeks self.setCache('suggestion_cached', suggestions, timeout = 6048000) # Cache for 10 weeks
medias = []
for suggestion in suggestions[:int(limit)]:
# Cache poster
posters = suggestion.get('images', {}).get('poster', [])
poster = [x for x in posters if 'tmdb' in x]
posters = poster if len(poster) > 0 else posters
cached_poster = fireEvent('file.download', url = posters[0], single = True) if len(posters) > 0 else False
files = {'image_poster': [cached_poster] } if cached_poster else {}
medias.append({
'status': 'suggested',
'title': getTitle(suggestion),
'type': 'movie',
'info': suggestion,
'files': files,
'identifiers': {
'imdb': suggestion.get('imdb')
}
})
return { return {
'success': True, 'success': True,
'count': len(suggestions), 'movies': medias
'suggestions': suggestions[:int(limit)]
} }
def ignoreView(self, imdb = None, limit = 6, remove_only = False, mark_seen = False, **kwargs): def ignoreView(self, imdb = None, limit = 6, remove_only = False, mark_seen = False, **kwargs):
@ -60,10 +88,25 @@ class Suggestion(Plugin):
new_suggestions = self.updateSuggestionCache(ignore_imdb = imdb, limit = limit, ignored = ignored, seen = seen) new_suggestions = self.updateSuggestionCache(ignore_imdb = imdb, limit = limit, ignored = ignored, seen = seen)
if len(new_suggestions) <= limit:
return {
'result': False
}
# Only return new (last) item
media = {
'status': 'suggested',
'title': getTitle(new_suggestions[limit]),
'type': 'movie',
'info': new_suggestions[limit],
'identifiers': {
'imdb': new_suggestions[limit].get('imdb')
}
}
return { return {
'result': True, 'result': True,
'ignore_count': len(ignored), 'movie': media
'suggestions': new_suggestions[limit - 1:limit]
} }
def updateSuggestionCache(self, ignore_imdb = None, limit = 6, ignored = None, seen = None): def updateSuggestionCache(self, ignore_imdb = None, limit = 6, ignored = None, seen = None):

0
couchpotato/core/media/movie/suggestion/__init__.py

162
couchpotato/core/media/movie/suggestion/static/suggest.css

@ -1,162 +0,0 @@
.suggestions {
clear: both;
padding-top: 10px;
margin-bottom: 30px;
}
.suggestions > h2 {
height: 40px;
}
.suggestions .media_result {
display: inline-block;
width: 33.333%;
height: 150px;
}
@media all and (max-width: 960px) {
.suggestions .media_result {
width: 50%;
}
}
@media all and (max-width: 600px) {
.suggestions .media_result {
width: 100%;
}
}
.suggestions .media_result .data {
left: 100px;
background: #4e5969;
border: none;
}
.suggestions .media_result .data .info {
top: 10px;
left: 15px;
right: 15px;
bottom: 10px;
overflow: hidden;
}
.suggestions .media_result .data .info h2 {
white-space: normal;
max-height: 120px;
font-size: 18px;
line-height: 18px;
}
.suggestions .media_result .data .info .rating,
.suggestions .media_result .data .info .genres,
.suggestions .media_result .data .info .year {
position: static;
display: block;
padding: 0;
opacity: .6;
}
.suggestions .media_result .data .info .year {
margin: 10px 0 0;
}
.suggestions .media_result .data .info .rating {
font-size: 20px;
float: right;
margin-top: -20px;
}
.suggestions .media_result .data .info .rating:before {
content: "\e031";
font-family: 'Elusive-Icons';
font-size: 14px;
margin: 0 5px 0 0;
vertical-align: bottom;
}
.suggestions .media_result .data .info .genres {
font-size: 11px;
font-style: italic;
text-align: right;
}
.suggestions .media_result .data .info .plot {
display: block;
font-size: 11px;
overflow: hidden;
text-align: justify;
height: 100%;
z-index: 2;
top: 64px;
position: absolute;
background: #4e5969;
cursor: pointer;
transition: all .4s ease-in-out;
padding: 0 3px 10px 0;
}
.suggestions .media_result .data:before {
content: '';
display: block;
height: 10px;
right: 0;
left: 0;
bottom: 10px;
position: absolute;
background: linear-gradient(
0deg,
rgba(78, 89, 105, 1) 0%,
rgba(78, 89, 105, 0) 100%
);
z-index: 3;
pointer-events: none;
}
.suggestions .media_result .data .info .plot.full {
top: 0;
overflow: auto;
}
.suggestions .media_result .data {
cursor: default;
}
.suggestions .media_result .options {
left: 100px;
}
.suggestions .media_result .options select[name=title] { width: 100%; }
.suggestions .media_result .options select[name=profile] { width: 100%; }
.suggestions .media_result .options select[name=category] { width: 100%; }
.suggestions .media_result .button {
position: absolute;
margin: 2px 0 0 0;
right: 15px;
bottom: 15px;
}
.suggestions .media_result .thumbnail {
width: 100px;
}
.suggestions .media_result .actions {
position: absolute;
top: 10px;
right: 10px;
display: none;
width: 140px;
}
.suggestions .media_result:hover .actions {
display: block;
}
.suggestions .media_result:hover h2 .title {
opacity: 0;
}
.suggestions .media_result .data.open .actions {
display: none;
}
.suggestions .media_result .actions a {
margin-left: 10px;
vertical-align: middle;
}

173
couchpotato/core/media/movie/suggestion/static/suggest.js

@ -1,173 +0,0 @@
var SuggestList = new Class({
Implements: [Options, Events],
shown_once: false,
initialize: function(options){
var self = this;
self.setOptions(options);
self.create();
},
create: function(){
var self = this;
self.el = new Element('div.suggestions', {
'events': {
'click:relay(a.delete)': function(e, el){
(e).stop();
$(el).getParent('.media_result').destroy();
Api.request('suggestion.ignore', {
'data': {
'imdb': el.get('data-ignore')
},
'onComplete': self.fill.bind(self)
});
},
'click:relay(a.eye-open)': function(e, el){
(e).stop();
$(el).getParent('.media_result').destroy();
Api.request('suggestion.ignore', {
'data': {
'imdb': el.get('data-seen'),
'mark_seen': 1
},
'onComplete': self.fill.bind(self)
});
}
}
});
var cookie_menu_select = Cookie.read('suggestions_charts_menu_selected') || 'suggestions';
if( cookie_menu_select === 'suggestions')
self.show();
else
self.hide();
self.fireEvent.delay(0, self, 'created');
},
fill: function(json){
var self = this;
if(!json || json.count == 0){
self.el.hide();
}
else {
Object.each(json.suggestions, function(movie){
var m = new Block.Search.MovieItem(movie, {
'onAdded': function(){
self.afterAdded(m, movie)
}
});
m.data_container.grab(
new Element('div.actions').adopt(
new Element('a.add.icon2', {
'title': 'Add movie with your default quality',
'data-add': movie.imdb,
'events': {
'click': m.showOptions.bind(m)
}
}),
$(new MA.IMDB(m)),
$(new MA.Trailer(m, {
'height': 150
})),
new Element('a.delete.icon2', {
'title': 'Don\'t suggest this movie again',
'data-ignore': movie.imdb
}),
new Element('a.eye-open.icon2', {
'title': 'Seen it, like it, don\'t add',
'data-seen': movie.imdb
})
)
);
m.data_container.removeEvents('click');
var plot = false;
if(m.info.plot && m.info.plot.length > 0)
plot = m.info.plot;
// Add rating
m.info_container.adopt(
m.rating = m.info.rating && m.info.rating.imdb && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', {
'text': parseFloat(m.info.rating.imdb[0]),
'title': parseInt(m.info.rating.imdb[1]) + ' votes'
}) : null,
m.genre = m.info.genres && m.info.genres.length > 0 ? new Element('span.genres', {
'text': m.info.genres.slice(0, 3).join(', ')
}) : null,
m.plot = plot ? new Element('span.plot', {
'text': plot,
'events': {
'click': function(){
this.toggleClass('full')
}
}
}) : null
);
$(m).inject(self.el);
});
}
self.fireEvent('loaded');
},
afterAdded: function(m, movie){
var self = this;
setTimeout(function(){
$(m).destroy();
Api.request('suggestion.ignore', {
'data': {
'imdb': movie.imdb,
'remove_only': true
},
'onComplete': self.fill.bind(self)
});
}, 3000);
},
show: function(){
var self = this;
self.el.show();
if(!self.shown_once){
self.api_request = Api.request('suggestion.view', {
'onComplete': self.fill.bind(self)
});
self.shown_once = true;
}
},
hide: function(){
this.el.hide();
},
toElement: function(){
return this.el;
}
});

78
couchpotato/core/notifications/core/static/notification.js

@ -20,8 +20,8 @@ var NotificationBase = new Class({
self.notifications = []; self.notifications = [];
App.addEvent('load', function(){ App.addEvent('load', function(){
App.block.notification = new Block.Menu(self, { App.block.notification = new BlockMenu(self, {
'button_class': 'icon2.eye-open', 'button_class': 'icon-notifications',
'class': 'notification_menu', 'class': 'notification_menu',
'onOpen': self.markAsRead.bind(self) 'onOpen': self.markAsRead.bind(self)
}); });
@ -32,7 +32,7 @@ var NotificationBase = new Class({
window.addEvent('load', function(){ window.addEvent('load', function(){
self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 100, self); self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 100, self);
}) });
}, },
@ -46,16 +46,15 @@ var NotificationBase = new Class({
new Element('span.'+(result.read ? 'read' : '' )).adopt( new Element('span.'+(result.read ? 'read' : '' )).adopt(
new Element('span.message', {'html': result.message}), new Element('span.message', {'html': result.message}),
new Element('span.added', {'text': added.timeDiffInWords(), 'title': added}) new Element('span.added', {'text': added.timeDiffInWords(), 'title': added})
) ), 'top');
, 'top');
self.notifications.include(result); self.notifications.include(result);
if((result.important !== undefined || result.sticky !== undefined) && !result.read){ if((result.important !== undefined || result.sticky !== undefined) && !result.read){
var sticky = true; var sticky = true;
App.trigger('message', [result.message, sticky, result]) App.trigger('message', [result.message, sticky, result]);
} }
else if(!result.read){ else if(!result.read){
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length) self.setBadge(self.notifications.filter(function(n){ return !n.read; }).length);
} }
}, },
@ -63,7 +62,7 @@ var NotificationBase = new Class({
setBadge: function(value){ setBadge: function(value){
var self = this; var self = this;
self.badge.set('text', value); self.badge.set('text', value);
self.badge[value ? 'show' : 'hide']() self.badge[value ? 'show' : 'hide']();
}, },
markAsRead: function(force_ids){ markAsRead: function(force_ids){
@ -72,13 +71,13 @@ var NotificationBase = new Class({
if(!force_ids) { if(!force_ids) {
var rn = self.notifications.filter(function(n){ var rn = self.notifications.filter(function(n){
return !n.read && n.important === undefined return !n.read && n.important === undefined;
}); });
var ids = []; ids = [];
rn.each(function(n){ rn.each(function(n){
ids.include(n._id) ids.include(n._id);
}) });
} }
if(ids.length > 0) if(ids.length > 0)
@ -87,9 +86,9 @@ var NotificationBase = new Class({
'ids': ids.join(',') 'ids': ids.join(',')
}, },
'onSuccess': function(){ 'onSuccess': function(){
self.setBadge('') self.setBadge('');
} }
}) });
}, },
@ -102,9 +101,9 @@ var NotificationBase = new Class({
} }
self.request = Api.request('notification.listener', { self.request = Api.request('notification.listener', {
'data': {'init':true}, 'data': {'init':true},
'onSuccess': function(json){ 'onSuccess': function(json){
self.processData(json, true) self.processData(json, true);
} }
}).send(); }).send();
@ -112,7 +111,7 @@ var NotificationBase = new Class({
if(self.request && self.request.isRunning()){ if(self.request && self.request.isRunning()){
self.request.cancel(); self.request.cancel();
self.startPoll() self.startPoll();
} }
}, 120000); }, 120000);
@ -129,16 +128,16 @@ var NotificationBase = new Class({
self.request.cancel(); self.request.cancel();
self.request = Api.request('nonblock/notification.listener', { self.request = Api.request('nonblock/notification.listener', {
'onSuccess': function(json){ 'onSuccess': function(json){
self.processData(json, false) self.processData(json, false);
}, },
'data': { 'data': {
'last_id': self.last_id 'last_id': self.last_id
}, },
'onFailure': function(){ 'onFailure': function(){
self.startPoll.delay(2000, self) self.startPoll.delay(2000, self);
} }
}).send() }).send();
}, },
@ -160,7 +159,7 @@ var NotificationBase = new Class({
}); });
if(json.result.length > 0) if(json.result.length > 0)
self.last_id = json.result.getLast().message_id self.last_id = json.result.getLast().message_id;
} }
// Restart poll // Restart poll
@ -175,11 +174,11 @@ var NotificationBase = new Class({
var new_message = new Element('div', { var new_message = new Element('div', {
'class': 'message' + (sticky ? ' sticky' : ''), 'class': 'message' + (sticky ? ' sticky' : ''),
'html': message 'html': '<div class="inner">' + message + '</div>'
}).inject(self.message_container, 'top'); }).inject(self.message_container, 'top');
setTimeout(function(){ setTimeout(function(){
new_message.addClass('show') new_message.addClass('show');
}, 10); }, 10);
var hide_message = function(){ var hide_message = function(){
@ -211,8 +210,8 @@ var NotificationBase = new Class({
var setting_page = App.getPage('Settings'); var setting_page = App.getPage('Settings');
setting_page.addEvent('create', function(){ setting_page.addEvent('create', function(){
Object.each(setting_page.tabs.notifications.groups, self.addTestButton.bind(self)) Object.each(setting_page.tabs.notifications.groups, self.addTestButton.bind(self));
}) });
}, },
@ -222,7 +221,7 @@ var NotificationBase = new Class({
if(button_name.contains('Notifications')) return; if(button_name.contains('Notifications')) return;
new Element('.ctrlHolder.test_button').adopt( new Element('.ctrlHolder.test_button').grab(
new Element('a.button', { new Element('a.button', {
'text': button_name, 'text': button_name,
'events': { 'events': {
@ -235,20 +234,21 @@ var NotificationBase = new Class({
button.set('text', button_name); button.set('text', button_name);
var message;
if(json.success){ if(json.success){
var message = new Element('span.success', { message = new Element('span.success', {
'text': 'Notification successful' 'text': 'Notification successful'
}).inject(button, 'after') }).inject(button, 'after');
} }
else { else {
var message = new Element('span.failed', { message = new Element('span.failed', {
'text': 'Notification failed. Check logs for details.' 'text': 'Notification failed. Check logs for details.'
}).inject(button, 'after') }).inject(button, 'after');
} }
(function(){ (function(){
message.destroy(); message.destroy();
}).delay(3000) }).delay(3000);
} }
}); });
} }
@ -258,7 +258,7 @@ var NotificationBase = new Class({
}, },
testButtonName: function(fieldset){ testButtonName: function(fieldset){
var name = String(fieldset.getElement('h2').innerHTML).substring(0,String(fieldset.getElement('h2').innerHTML).indexOf("<span")); //.get('text'); var name = fieldset.getElement('h2 .group_label').get('text');
return 'Test '+name; return 'Test '+name;
} }

14
couchpotato/core/notifications/email_.py

@ -31,12 +31,12 @@ class Email(Notification):
starttls = self.conf('starttls') starttls = self.conf('starttls')
# Make the basic message # Make the basic message
message = MIMEText(toUnicode(message), _charset = Env.get('encoding')) email = MIMEText(toUnicode(message), _charset = Env.get('encoding'))
message['Subject'] = self.default_title email['Subject'] = '%s: %s' % (self.default_title, toUnicode(message))
message['From'] = from_address email['From'] = from_address
message['To'] = to_address email['To'] = to_address
message['Date'] = formatdate(localtime = 1) email['Date'] = formatdate(localtime = 1)
message['Message-ID'] = make_msgid() email['Message-ID'] = make_msgid()
try: try:
# Open the SMTP connection, via SSL if requested # Open the SMTP connection, via SSL if requested
@ -58,7 +58,7 @@ class Email(Notification):
# Send the e-mail # Send the e-mail
log.debug("Sending the email") log.debug("Sending the email")
mailserver.sendmail(from_address, splitString(to_address), message.as_string()) mailserver.sendmail(from_address, splitString(to_address), email.as_string())
# Close the SMTP connection # Close the SMTP connection
mailserver.quit() mailserver.quit()

89
couchpotato/core/notifications/emby.py

@ -0,0 +1,89 @@
import json
import urllib, urllib2
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
autoload = 'Emby'
class Emby(Notification):
def notify(self, message = '', data = None, listener = None):
host = self.conf('host')
apikey = self.conf('apikey')
host = cleanHost(host)
url = '%semby/Library/Series/Updated' % (host)
values = {}
data = urllib.urlencode(values)
try:
req = urllib2.Request(url, data)
req.add_header('X-MediaBrowser-Token', apikey)
response = urllib2.urlopen(req)
result = response.read()
response.close()
return True
except (urllib2.URLError, IOError), e:
return False
def test(self, **kwargs):
host = self.conf('host')
apikey = self.conf('apikey')
message = self.test_message
host = cleanHost(host)
url = '%semby/Notifications/Admin' % (host)
values = {'Name': 'CouchPotato', 'Description': message, 'ImageUrl': 'https://raw.githubusercontent.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/notify.couch.small.png'}
data = json.dumps(values)
try:
req = urllib2.Request(url, data)
req.add_header('X-MediaBrowser-Token', apikey)
req.add_header('Content-Type', 'application/json')
response = urllib2.urlopen(req)
result = response.read()
response.close()
return {
'success': True
}
except (urllib2.URLError, IOError), e:
return False
config = [{
'name': 'emby',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'emby',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'host',
'default': 'localhost:8096',
'description': 'IP:Port, default localhost:8096'
},
{
'name': 'apikey',
'label': 'API Key',
'default': '',
},
],
}
],
}]

29
couchpotato/core/notifications/pushbullet.py

@ -19,14 +19,8 @@ class Pushbullet(Notification):
def notify(self, message = '', data = None, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {} if not data: data = {}
devices = self.getDevices()
if devices is None:
return False
# Get all the device IDs linked to this user # Get all the device IDs linked to this user
if not len(devices): devices = self.getDevices() or []
devices = [None]
successful = 0 successful = 0
for device in devices: for device in devices:
response = self.request( response = self.request(
@ -43,17 +37,30 @@ class Pushbullet(Notification):
else: else:
log.error('Unable to push notification to Pushbullet device with ID %s' % device) log.error('Unable to push notification to Pushbullet device with ID %s' % device)
for channel in self.getChannels():
response = self.request(
'pushes',
cache = False,
channel_tag = channel,
type = 'note',
title = self.default_title,
body = toUnicode(message)
)
return successful == len(devices) return successful == len(devices)
def getDevices(self): def getDevices(self):
return splitString(self.conf('devices')) return splitString(self.conf('devices'))
def getChannels(self):
return splitString(self.conf('channels'))
def request(self, method, cache = True, **kwargs): def request(self, method, cache = True, **kwargs):
try: try:
base64string = base64.encodestring('%s:' % self.conf('api_key'))[:-1] base64string = base64.encodestring('%s:' % self.conf('api_key'))[:-1]
headers = { headers = {
"Authorization": "Basic %s" % base64string 'Authorization': 'Basic %s' % base64string
} }
if cache: if cache:
@ -94,6 +101,12 @@ config = [{
'description': 'IDs of devices to send notifications to, empty = all devices' 'description': 'IDs of devices to send notifications to, empty = all devices'
}, },
{ {
'name': 'channels',
'default': '',
'advanced': True,
'description': 'IDs of channels to send notifications to, empty = no channels'
},
{
'name': 'on_snatch', 'name': 'on_snatch',
'default': 0, 'default': 0,
'type': 'bool', 'type': 'bool',

31
couchpotato/core/notifications/pushover.py

@ -1,6 +1,4 @@
from httplib import HTTPSConnection from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.variable import getTitle, getIdentifier from couchpotato.core.helpers.variable import getTitle, getIdentifier
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
@ -13,12 +11,11 @@ autoload = 'Pushover'
class Pushover(Notification): class Pushover(Notification):
api_url = 'https://api.pushover.net'
def notify(self, message = '', data = None, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {} if not data: data = {}
http_handler = HTTPSConnection("api.pushover.net:443")
api_data = { api_data = {
'user': self.conf('user_key'), 'user': self.conf('user_key'),
'token': self.conf('api_token'), 'token': self.conf('api_token'),
@ -33,25 +30,17 @@ class Pushover(Notification):
'url_title': toUnicode('%s on IMDb' % getTitle(data)), 'url_title': toUnicode('%s on IMDb' % getTitle(data)),
}) })
http_handler.request('POST', '/1/messages.json', try:
headers = {'Content-type': 'application/x-www-form-urlencoded'}, data = self.urlopen('%s/%s' % (self.api_url, '1/messages.json'),
body = tryUrlencode(api_data) headers = {'Content-type': 'application/x-www-form-urlencoded'},
) data = api_data)
log.info2('Pushover responded with: %s', data)
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
log.info('Pushover notifications sent.')
return True return True
elif request_status == 401: except:
log.error('Pushover auth failed: %s', response.reason)
return False
else:
log.error('Pushover notification failed: %s', request_status)
return False return False
config = [{ config = [{
'name': 'pushover', 'name': 'pushover',
'groups': [ 'groups': [
@ -79,7 +68,7 @@ config = [{
'name': 'priority', 'name': 'priority',
'default': 0, 'default': 0,
'type': 'dropdown', 'type': 'dropdown',
'values': [('Normal', 0), ('High', 1)], 'values': [('Lowest', -2), ('Low', -1), ('Normal', 0), ('High', 1)],
}, },
{ {
'name': 'on_snatch', 'name': 'on_snatch',

126
couchpotato/core/notifications/slack.py

@ -0,0 +1,126 @@
import json
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
autoload = 'Slack'
class Slack(Notification):
url = 'https://slack.com/api/chat.postMessage'
required_confs = ('token', 'channels',)
def notify(self, message='', data=None, listener=None):
for key in self.required_confs:
if not self.conf(key):
log.warning('Slack notifications are enabled, but '
'"{0}" is not specified.'.format(key))
return False
data = data or {}
message = message.strip()
if self.conf('include_imdb') and 'identifier' in data:
template = ' http://www.imdb.com/title/{0[identifier]}/'
message += template.format(data)
payload = {
'token': self.conf('token'),
'text': message,
'username': self.conf('bot_name'),
'unfurl_links': self.conf('include_imdb'),
'as_user': self.conf('as_user'),
'icon_url': self.conf('icon_url'),
'icon_emoji': self.conf('icon_emoji')
}
channels = self.conf('channels').split(',')
for channel in channels:
payload['channel'] = channel.strip()
response = self.urlopen(self.url, data=payload)
response = json.loads(response)
if not response['ok']:
log.warning('Notification sending to Slack has failed. Error '
'code: %s.', response['error'])
return False
return True
config = [{
'name': 'slack',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'slack',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'token',
'description': (
'Your Slack authentication token.',
'Can be created at https://api.slack.com/web'
)
},
{
'name': 'channels',
'description': (
'Channel to send notifications to.',
'Can be a public channel, private group or IM '
'channel. Can be an encoded ID or a name '
'(staring with a hashtag, e.g. #general). '
'Separate with commas in order to notify multiple '
'channels. It is however recommended to send '
'notifications to only one channel due to '
'the Slack API rate limits.'
)
},
{
'name': 'include_imdb',
'default': True,
'type': 'bool',
'descrpition': 'Include a link to the movie page on IMDB.'
},
{
'name': 'bot_name',
'description': 'Name of bot.',
'default': 'CouchPotato',
'advanced': True,
},
{
'name': 'as_user',
'description': 'Send message as the authentication token '
' user.',
'default': False,
'type': 'bool',
'advanced': True
},
{
'name': 'icon_url',
'description': 'URL to an image to use as the icon for '
'notifications.',
'advanced': True,
},
{
'name': 'icon_emoji',
'description': (
'Emoji to use as the icon for notifications.',
'Overrides icon_url'
),
'advanced': True,
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]

49
couchpotato/core/notifications/trakt.py

@ -1,5 +1,6 @@
from couchpotato.core.helpers.variable import getTitle, getIdentifier from couchpotato.core.helpers.variable import getTitle, getIdentifier
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie.providers.automation.trakt.main import TraktBase
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
log = CPLog(__name__) log = CPLog(__name__)
@ -7,65 +8,37 @@ log = CPLog(__name__)
autoload = 'Trakt' autoload = 'Trakt'
class Trakt(Notification): class Trakt(Notification, TraktBase):
urls = { urls = {
'base': 'http://api.trakt.tv/%s', 'library': 'sync/collection',
'library': 'movie/library/%s', 'unwatchlist': 'sync/watchlist/remove',
'unwatchlist': 'movie/unwatchlist/%s', 'test': 'sync/last_activities',
'test': 'account/test/%s',
} }
listen_to = ['movie.snatched'] listen_to = ['renamer.after']
enabled_option = 'notification_enabled' enabled_option = 'notification_enabled'
def notify(self, message = '', data = None, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {} if not data: data = {}
if listener == 'test': if listener == 'test':
result = self.call((self.urls['test']))
post_data = {
'username': self.conf('automation_username'),
'password': self.conf('automation_password'),
}
result = self.call((self.urls['test'] % self.conf('automation_api_key')), post_data)
return result return result
else: else:
post_data = { post_data = {
'username': self.conf('automation_username'), 'movies': [{'ids': {'imdb': getIdentifier(data)}}] if data else []
'password': self.conf('automation_password'),
'movies': [{
'imdb_id': getIdentifier(data),
'title': getTitle(data),
'year': data['info']['year']
}] if data else []
} }
result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data) result = self.call((self.urls['library']), post_data)
if self.conf('remove_watchlist_enabled'): if self.conf('remove_watchlist_enabled'):
result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data) result = result and self.call((self.urls['unwatchlist']), post_data)
return result return result
def call(self, method_url, post_data):
try:
response = self.getJsonData(self.urls['base'] % method_url, data = post_data, cache_timeout = 1)
if response:
if response.get('status') == "success":
log.info('Successfully called Trakt')
return True
except:
pass
log.error('Failed to call trakt, check your login.')
return False
config = [{ config = [{
'name': 'trakt', 'name': 'trakt',
@ -75,7 +48,7 @@ config = [{
'list': 'notification_providers', 'list': 'notification_providers',
'name': 'trakt', 'name': 'trakt',
'label': 'Trakt', 'label': 'Trakt',
'description': 'add movies to your collection once downloaded. Fill in your username and password in the <a href="../automation/">Automation Trakt settings</a>', 'description': 'add movies to your collection once downloaded. Connect your account in <a href="../automation/">Automation Trakt settings</a>',
'options': [ 'options': [
{ {
'name': 'notification_enabled', 'name': 'notification_enabled',

4
couchpotato/core/notifications/twitter/static/twitter.js

@ -16,7 +16,7 @@ var TwitterNotification = new Class({
var twitter_set = 0; var twitter_set = 0;
fieldset.getElements('input[type=text]').each(function(el){ fieldset.getElements('input[type=text]').each(function(el){
twitter_set += +(el.get('value') != ''); twitter_set += +(el.get('value') !== '');
}); });
@ -57,7 +57,7 @@ var TwitterNotification = new Class({
} }
}) })
).inject(fieldset.getElement('.test_button'), 'before'); ).inject(fieldset.getElement('.test_button'), 'before');
}) });
} }

28
couchpotato/core/notifications/xbmc.py

@ -83,7 +83,7 @@ class XBMC(Notification):
# v6 (as of XBMC v12(Frodo)) is required to send notifications # v6 (as of XBMC v12(Frodo)) is required to send notifications
xbmc_rpc_version = str(result['result']['version']) xbmc_rpc_version = str(result['result']['version'])
log.debug('XBMC JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]', xbmc_rpc_version) log.debug('Kodi JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]', xbmc_rpc_version)
# disable JSON use # disable JSON use
self.use_json_notifications[host] = False self.use_json_notifications[host] = False
@ -96,7 +96,7 @@ class XBMC(Notification):
success = True success = True
break break
elif r.get('error'): elif r.get('error'):
log.error('XBMC error; %s: %s (%s)', (r['id'], r['error']['message'], r['error']['code'])) log.error('Kodi error; %s: %s (%s)', (r['id'], r['error']['message'], r['error']['code']))
break break
elif result.get('result') and type(result['result']['version']).__name__ == 'dict': elif result.get('result') and type(result['result']['version']).__name__ == 'dict':
@ -106,7 +106,7 @@ class XBMC(Notification):
xbmc_rpc_version += '.' + str(result['result']['version']['minor']) xbmc_rpc_version += '.' + str(result['result']['version']['minor'])
xbmc_rpc_version += '.' + str(result['result']['version']['patch']) xbmc_rpc_version += '.' + str(result['result']['version']['patch'])
log.debug('XBMC JSON-RPC Version: %s', xbmc_rpc_version) log.debug('Kodie JSON-RPC Version: %s', xbmc_rpc_version)
# ok, XBMC version is supported # ok, XBMC version is supported
self.use_json_notifications[host] = True self.use_json_notifications[host] = True
@ -119,12 +119,12 @@ class XBMC(Notification):
success = True success = True
break break
elif r.get('error'): elif r.get('error'):
log.error('XBMC error; %s: %s (%s)', (r['id'], r['error']['message'], r['error']['code'])) log.error('Kodi error; %s: %s (%s)', (r['id'], r['error']['message'], r['error']['code']))
break break
# error getting version info (we do have contact with XBMC though) # error getting version info (we do have contact with XBMC though)
elif result.get('error'): elif result.get('error'):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code'])) log.error('Kodi error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
log.debug('Use JSON notifications: %s ', self.use_json_notifications) log.debug('Use JSON notifications: %s ', self.use_json_notifications)
@ -173,10 +173,10 @@ class XBMC(Notification):
return [{'result': 'Error'}] return [{'result': 'Error'}]
except (MaxRetryError, Timeout, ConnectionError): except (MaxRetryError, Timeout, ConnectionError):
log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off') log.info2('Couldn\'t send request to Kodi, assuming it\'s turned off')
return [{'result': 'Error'}] return [{'result': 'Error'}]
except: except:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc()) log.error('Failed sending non-JSON-type request to Kodi: %s', traceback.format_exc())
return [{'result': 'Error'}] return [{'result': 'Error'}]
def request(self, host, do_requests): def request(self, host, do_requests):
@ -209,10 +209,10 @@ class XBMC(Notification):
return response return response
except (MaxRetryError, Timeout, ConnectionError): except (MaxRetryError, Timeout, ConnectionError):
log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off') log.info2('Couldn\'t send request to Kodi, assuming it\'s turned off')
return [] return []
except: except:
log.error('Failed sending request to XBMC: %s', traceback.format_exc()) log.error('Failed sending request to Kodi: %s', traceback.format_exc())
return [] return []
@ -223,8 +223,8 @@ config = [{
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers', 'list': 'notification_providers',
'name': 'xbmc', 'name': 'xbmc',
'label': 'XBMC', 'label': 'Kodi',
'description': 'v11 (Eden), v12 (Frodo), v13 (Gotham)', 'description': 'v14 (Helix), v15 (Isengard)',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@ -249,7 +249,7 @@ config = [{
'default': 0, 'default': 0,
'type': 'bool', 'type': 'bool',
'advanced': True, 'advanced': True,
'description': 'Only update the first host when movie snatched, useful for synced XBMC', 'description': 'Only update the first host when movie snatched, useful for synced Kodi',
}, },
{ {
'name': 'remote_dir_scan', 'name': 'remote_dir_scan',
@ -257,7 +257,7 @@ config = [{
'default': 0, 'default': 0,
'type': 'bool', 'type': 'bool',
'advanced': True, 'advanced': True,
'description': ('Only scan new movie folder at remote XBMC servers.', 'Useful if the XBMC path is different from the path CPS uses.'), 'description': ('Only scan new movie folder at remote Kodi servers.', 'Useful if the Kodi path is different from the path CPS uses.'),
}, },
{ {
'name': 'force_full_scan', 'name': 'force_full_scan',
@ -265,7 +265,7 @@ config = [{
'default': 0, 'default': 0,
'type': 'bool', 'type': 'bool',
'advanced': True, 'advanced': True,
'description': ('Do a full scan instead of only the new movie.', 'Useful if the XBMC path is different from the path CPS uses.'), 'description': ('Do a full scan instead of only the new movie.', 'Useful if the Kodi path is different from the path CPS uses.'),
}, },
{ {
'name': 'on_snatch', 'name': 'on_snatch',

92
couchpotato/core/plugins/base.py

@ -1,17 +1,14 @@
import threading import threading
from urllib import quote from urllib import quote, getproxies
from urlparse import urlparse from urlparse import urlparse
import glob
import inspect
import os.path import os.path
import re
import time import time
import traceback import traceback
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import ss, toSafeString, \ from couchpotato.core.helpers.encoding import ss, toSafeString, \
toUnicode, sp toUnicode, sp
from couchpotato.core.helpers.variable import getExt, md5, isLocalIP, scanForPassword, tryInt, getIdentifier, \ from couchpotato.core.helpers.variable import md5, isLocalIP, scanForPassword, tryInt, getIdentifier, \
randomString randomString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
@ -19,8 +16,6 @@ import requests
from requests.packages.urllib3 import Timeout from requests.packages.urllib3 import Timeout
from requests.packages.urllib3.exceptions import MaxRetryError from requests.packages.urllib3.exceptions import MaxRetryError
from tornado import template from tornado import template
from tornado.web import StaticFileHandler
log = CPLog(__name__) log = CPLog(__name__)
@ -32,7 +27,6 @@ class Plugin(object):
plugin_path = None plugin_path = None
enabled_option = 'enabled' enabled_option = 'enabled'
auto_register_static = True
_needs_shutdown = False _needs_shutdown = False
_running = None _running = None
@ -41,6 +35,7 @@ class Plugin(object):
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:34.0) Gecko/20100101 Firefox/34.0' user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:34.0) Gecko/20100101 Firefox/34.0'
http_last_use = {} http_last_use = {}
http_last_use_queue = {}
http_time_between_calls = 0 http_time_between_calls = 0
http_failed_request = {} http_failed_request = {}
http_failed_disabled = {} http_failed_disabled = {}
@ -56,9 +51,6 @@ class Plugin(object):
addEvent('plugin.running', self.isRunning) addEvent('plugin.running', self.isRunning)
self._running = [] self._running = []
if self.auto_register_static:
self.registerStatic(inspect.getfile(self.__class__))
# Setup database # Setup database
if self._database: if self._database:
addEvent('database.setup', self.databaseSetup) addEvent('database.setup', self.databaseSetup)
@ -88,32 +80,6 @@ class Plugin(object):
t = template.Template(open(os.path.join(os.path.dirname(parent_file), templ), 'r').read()) t = template.Template(open(os.path.join(os.path.dirname(parent_file), templ), 'r').read())
return t.generate(**params) return t.generate(**params)
def registerStatic(self, plugin_file, add_to_head = True):
# Register plugin path
self.plugin_path = os.path.dirname(plugin_file)
static_folder = toUnicode(os.path.join(self.plugin_path, 'static'))
if not os.path.isdir(static_folder):
return
# Get plugin_name from PluginName
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__)
class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
# View path
path = 'static/plugin/%s/' % class_name
# Add handler to Tornado
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + path + '(.*)', StaticFileHandler, {'path': static_folder})])
# Register for HTML <HEAD>
if add_to_head:
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
ext = getExt(f)
if ext in ['js', 'css']:
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
def createFile(self, path, content, binary = False): def createFile(self, path, content, binary = False):
path = sp(path) path = sp(path)
@ -144,7 +110,7 @@ class Plugin(object):
f.close() f.close()
os.chmod(path, Env.getPermission('file')) os.chmod(path, Env.getPermission('file'))
except: except:
log.error('Unable writing to file "%s": %s', (path, traceback.format_exc())) log.error('Unable to write file "%s": %s', (path, traceback.format_exc()))
if os.path.isfile(path): if os.path.isfile(path):
os.remove(path) os.remove(path)
@ -199,6 +165,23 @@ class Plugin(object):
headers['Connection'] = headers.get('Connection', 'keep-alive') headers['Connection'] = headers.get('Connection', 'keep-alive')
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0') headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
use_proxy = Env.setting('use_proxy')
proxy_url = None
if use_proxy:
proxy_server = Env.setting('proxy_server')
proxy_username = Env.setting('proxy_username')
proxy_password = Env.setting('proxy_password')
if proxy_server:
loc = "{0}:{1}@{2}".format(proxy_username, proxy_password, proxy_server) if proxy_username else proxy_server
proxy_url = {
"http": "http://"+loc,
"https": "https://"+loc,
}
else:
proxy_url = getproxies()
r = Env.get('http_opener') r = Env.get('http_opener')
# Don't try for failed requests # Don't try for failed requests
@ -213,7 +196,7 @@ class Plugin(object):
del self.http_failed_request[host] del self.http_failed_request[host]
del self.http_failed_disabled[host] del self.http_failed_disabled[host]
self.wait(host) self.wait(host, url)
status_code = None status_code = None
try: try:
@ -224,6 +207,7 @@ class Plugin(object):
'files': files, 'files': files,
'verify': False, #verify_ssl, Disable for now as to many wrongly implemented certificates.. 'verify': False, #verify_ssl, Disable for now as to many wrongly implemented certificates..
'stream': stream, 'stream': stream,
'proxies': proxy_url,
} }
method = 'post' if len(data) > 0 or files else 'get' method = 'post' if len(data) > 0 or files else 'get'
@ -267,20 +251,34 @@ class Plugin(object):
return data return data
def wait(self, host = ''): def wait(self, host = '', url = ''):
if self.http_time_between_calls == 0: if self.http_time_between_calls == 0:
return return
now = time.time() try:
if host not in self.http_last_use_queue:
self.http_last_use_queue[host] = []
last_use = self.http_last_use.get(host, 0) self.http_last_use_queue[host].append(url)
if last_use > 0:
wait = (last_use - now) + self.http_time_between_calls while True and not self.shuttingDown():
wait = (self.http_last_use.get(host, 0) - time.time()) + self.http_time_between_calls
if self.http_last_use_queue[host][0] != url:
time.sleep(.1)
continue
if wait > 0:
log.debug('Waiting for %s, %d seconds', (self.getName(), max(1, wait)))
time.sleep(min(wait, 30))
else:
self.http_last_use_queue[host] = self.http_last_use_queue[host][1:]
self.http_last_use[host] = time.time()
break
except:
log.error('Failed handling waiting call: %s', traceback.format_exc())
time.sleep(self.http_time_between_calls)
if wait > 0:
log.debug('Waiting for %s, %d seconds', (self.getName(), max(1, wait)))
time.sleep(min(wait, 30))
def beforeCall(self, handler): def beforeCall(self, handler):
self.isRunning('%s.%s' % (self.getName(), handler.__name__)) self.isRunning('%s.%s' % (self.getName(), handler.__name__))

36
couchpotato/core/plugins/category/static/category.js

@ -52,7 +52,7 @@ var CategoryListBase = new Class({
}); });
}) });
}, },
@ -71,7 +71,7 @@ var CategoryListBase = new Class({
'events': { 'events': {
'click': function(){ 'click': function(){
var category = self.createCategory(); var category = self.createCategory();
$(category).inject(self.category_container) $(category).inject(self.category_container);
} }
} }
}) })
@ -79,15 +79,15 @@ var CategoryListBase = new Class({
// Add categories, that aren't part of the core (for editing) // Add categories, that aren't part of the core (for editing)
Array.each(self.categories, function(category){ Array.each(self.categories, function(category){
$(category).inject(self.category_container) $(category).inject(self.category_container);
}); });
}, },
getCategory: function(id){ getCategory: function(id){
return this.categories.filter(function(category){ return this.categories.filter(function(category){
return category.data._id == id return category.data._id == id;
}).pick() }).pick();
}, },
getAll: function(){ getAll: function(){
@ -97,7 +97,7 @@ var CategoryListBase = new Class({
createCategory: function(data){ createCategory: function(data){
var self = this; var self = this;
var data = data || {'id': randomString()}; data = data || {'id': randomString()};
var category = new Category(data); var category = new Category(data);
self.categories.include(category); self.categories.include(category);
@ -115,7 +115,7 @@ var CategoryListBase = new Class({
new Element('label[text=Order]'), new Element('label[text=Order]'),
category_list = new Element('ul'), category_list = new Element('ul'),
new Element('p.formHint', { new Element('p.formHint', {
'html': 'Change the order the categories are in the dropdown list.<br />First one will be default.' 'html': 'Change the order the categories are in the dropdown list.'
}) })
) )
).inject(self.content); ).inject(self.content);
@ -125,7 +125,7 @@ var CategoryListBase = new Class({
new Element('span.category_label', { new Element('span.category_label', {
'text': category.data.label 'text': category.data.label
}), }),
new Element('span.handle') new Element('span.handle.icon-handle')
).inject(category_list); ).inject(category_list);
}); });
@ -192,7 +192,7 @@ var Category = new Class({
}), }),
new Element('.category_label.ctrlHolder').adopt( new Element('.category_label.ctrlHolder').adopt(
new Element('label', {'text':'Name'}), new Element('label', {'text':'Name'}),
new Element('input.inlay', { new Element('input', {
'type':'text', 'type':'text',
'value': data.label, 'value': data.label,
'placeholder': 'Example: Kids, Horror or His' 'placeholder': 'Example: Kids, Horror or His'
@ -201,7 +201,7 @@ var Category = new Class({
), ),
new Element('.category_preferred.ctrlHolder').adopt( new Element('.category_preferred.ctrlHolder').adopt(
new Element('label', {'text':'Preferred'}), new Element('label', {'text':'Preferred'}),
new Element('input.inlay', { new Element('input', {
'type':'text', 'type':'text',
'value': data.preferred, 'value': data.preferred,
'placeholder': 'Blu-ray, DTS' 'placeholder': 'Blu-ray, DTS'
@ -209,7 +209,7 @@ var Category = new Class({
), ),
new Element('.category_required.ctrlHolder').adopt( new Element('.category_required.ctrlHolder').adopt(
new Element('label', {'text':'Required'}), new Element('label', {'text':'Required'}),
new Element('input.inlay', { new Element('input', {
'type':'text', 'type':'text',
'value': data.required, 'value': data.required,
'placeholder': 'Example: DTS, AC3 & English' 'placeholder': 'Example: DTS, AC3 & English'
@ -217,7 +217,7 @@ var Category = new Class({
), ),
new Element('.category_ignored.ctrlHolder').adopt( new Element('.category_ignored.ctrlHolder').adopt(
new Element('label', {'text':'Ignored'}), new Element('label', {'text':'Ignored'}),
new Element('input.inlay', { new Element('input', {
'type':'text', 'type':'text',
'value': data.ignored, 'value': data.ignored,
'placeholder': 'Example: dubbed, swesub, french' 'placeholder': 'Example: dubbed, swesub, french'
@ -225,7 +225,7 @@ var Category = new Class({
) )
); );
self.makeSortable() self.makeSortable();
}, },
@ -248,7 +248,7 @@ var Category = new Class({
} }
}); });
}).delay(delay || 0, self) }).delay(delay || 0, self);
}, },
@ -262,13 +262,13 @@ var Category = new Class({
'preferred' : self.el.getElement('.category_preferred input').get('value'), 'preferred' : self.el.getElement('.category_preferred input').get('value'),
'ignored' : self.el.getElement('.category_ignored input').get('value'), 'ignored' : self.el.getElement('.category_ignored input').get('value'),
'destination': self.data.destination 'destination': self.data.destination
} };
}, },
del: function(){ del: function(){
var self = this; var self = this;
if(self.data.label == undefined){ if(self.data.label === undefined){
self.el.destroy(); self.el.destroy();
return; return;
} }
@ -318,11 +318,11 @@ var Category = new Class({
}, },
get: function(attr){ get: function(attr){
return this.data[attr] return this.data[attr];
}, },
toElement: function(){ toElement: function(){
return this.el return this.el;
} }
}); });

19
couchpotato/core/plugins/category/static/category.css → couchpotato/core/plugins/category/static/category.scss

@ -1,13 +1,14 @@
@import "_mixins";
.add_new_category { .add_new_category {
padding: 20px; padding: 20px;
display: block; display: block;
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
border-bottom: 1px solid rgba(255,255,255,0.2);
} }
.category { .category {
border-bottom: 1px solid rgba(255,255,255,0.2); margin-bottom: 20px;
position: relative; position: relative;
} }
@ -28,8 +29,6 @@
} }
.category .formHint { .category .formHint {
width: 250px !important;
margin: 0 !important;
opacity: 0.1; opacity: 0.1;
} }
.category:hover .formHint { .category:hover .formHint {
@ -48,11 +47,10 @@
} }
#category_ordering li { #category_ordering li {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab; cursor: grab;
border-bottom: 1px solid rgba(255,255,255,0.2); border-bottom: 1px solid $theme_off;
padding: 0 5px; padding: 5px;
list-style: none;
} }
#category_ordering li:last-child { border: 0; } #category_ordering li:last-child { border: 0; }
@ -69,14 +67,9 @@
} }
#category_ordering li .handle { #category_ordering li .handle {
background: url('../../images/handle.png') center;
width: 20px; width: 20px;
float: right; float: right;
} }
#category_ordering .formHint { #category_ordering .formHint {
clear: none;
float: right;
width: 250px;
margin: 0;
} }

8
couchpotato/core/plugins/dashboard.py

@ -1,6 +1,6 @@
import random as rndm import random as rndm
import time import time
from CodernityDB.database import RecordDeleted from CodernityDB.database import RecordDeleted, RecordNotFound
from couchpotato import get_db from couchpotato import get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
@ -65,6 +65,10 @@ class Dashboard(Plugin):
log.debug('Record already deleted: %s', media_id) log.debug('Record already deleted: %s', media_id)
continue continue
except RecordNotFound:
log.debug('Record not found: %s', media_id)
continue
pp = profile_pre.get(media.get('profile_id')) pp = profile_pre.get(media.get('profile_id'))
if not pp: continue if not pp: continue
@ -92,7 +96,7 @@ class Dashboard(Plugin):
if late: if late:
media['releases'] = fireEvent('release.for_media', media['_id'], single = True) media['releases'] = fireEvent('release.for_media', media['_id'], single = True)
for release in media.get('releases'): for release in media.get('releases', []):
if release.get('status') in ['snatched', 'available', 'seeding', 'downloaded']: if release.get('status') in ['snatched', 'available', 'seeding', 'downloaded']:
add = False add = False
break break

7
couchpotato/core/plugins/log/main.py

@ -131,12 +131,13 @@ class Logging(Plugin):
def toList(self, log_content = ''): def toList(self, log_content = ''):
logs_raw = toUnicode(log_content).split('[0m\n') logs_raw = re.split(r'\[0m\n', toUnicode(log_content))
logs = [] logs = []
re_split = r'\x1b'
for log_line in logs_raw: for log_line in logs_raw:
split = splitString(log_line, '\x1b') split = re.split(re_split, log_line)
if split: if split and len(split) == 3:
try: try:
date, time, log_type = splitString(split[0], ' ') date, time, log_type = splitString(split[0], ' ')
timestamp = '%s %s' % (date, time) timestamp = '%s %s' % (date, time)

199
couchpotato/core/plugins/log/static/log.css

@ -1,199 +0,0 @@
.page.log .nav {
display: block;
text-align: center;
padding: 0 0 30px;
margin: 0;
font-size: 20px;
position: fixed;
width: 100%;
bottom: 0;
left: 0;
background: #4E5969;
z-index: 100;
}
.page.log .nav li {
display: inline-block;
padding: 5px 10px;
margin: 0;
}
.page.log .nav li.select,
.page.log .nav li.clear {
cursor: pointer;
}
.page.log .nav li:hover:not(.active):not(.filter) {
background: rgba(255, 255, 255, 0.1);
}
.page.log .nav li.active {
font-weight: bold;
cursor: default;
background: rgba(255,255,255,.1);
}
@media all and (max-width: 480px) {
.page.log .nav {
font-size: 14px;
}
.page.log .nav li {
padding: 5px;
}
}
.page.log .nav li.hint {
text-align: center;
width: 400px;
left: 50%;
margin-left: -200px;
font-style: italic;
font-size: 11px;
position: absolute;
right: 20px;
opacity: .5;
bottom: 5px;
}
.page.log .loading {
text-align: center;
font-size: 20px;
padding: 50px;
}
.page.log .container {
padding: 30px 0 60px;
overflow: hidden;
line-height: 150%;
font-size: 11px;
color: #FFF;
}
.page.log .container select {
vertical-align: top;
}
.page.log .container .time {
clear: both;
color: lightgrey;
font-size: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
overflow: hidden;
padding: 0 3px;
font-family: Lucida Console, Monaco, Nimbus Mono L, monospace, serif;
}
.page.log .container .time.highlight {
background: rgba(255, 255, 255, 0.1);
}
.page.log .container .time span {
padding: 5px 0 3px;
display: inline-block;
vertical-align: middle;
}
.page.log[data-filter=INFO] .error,
.page.log[data-filter=INFO] .debug,
.page.log[data-filter=ERROR] .debug,
.page.log[data-filter=ERROR] .info,
.page.log[data-filter=DEBUG] .info,
.page.log[data-filter=DEBUG] .error {
display: none;
}
.page.log .container .type {
margin-left: 10px;
}
.page.log .container .message {
float: right;
width: 86%;
white-space: pre-wrap;
}
.page.log .container .error { color: #FFA4A4; }
.page.log .container .debug span { opacity: .6; }
.do_report {
position: absolute;
padding: 10px;
}
.page.log .report {
position: fixed;
width: 100%;
height: 100%;
background: rgba(0,0,0,.7);
left: 0;
top: 0;
z-index: 99999;
font-size: 14px;
}
.page.log .report .button {
display: inline-block;
margin: 10px 0;
padding: 10px;
}
.page.log .report .bug {
width: 800px;
height: 80%;
position: absolute;
left: 50%;
top: 50%;
margin: 0 0 0 -400px;
transform: translate(0, -50%);
}
.page.log .report .bug textarea {
display: block;
width: 100%;
background: #FFF;
padding: 20px;
overflow: auto;
color: #666;
height: 70%;
font-size: 12px;
}
.page.log .container .time ::-webkit-selection {
background-color: #000;
color: #FFF;
}
.page.log .container .time ::-moz-selection {
background-color: #000;
color: #FFF;
}
.page.log .container .time ::-ms-selection {
background-color: #000;
color: #FFF;
}
.page.log .container .time.highlight ::selection {
background-color: transparent;
color: inherit;
}
.page.log .container .time.highlight ::-webkit-selection {
background-color: transparent;
color: inherit;
}
.page.log .container .time.highlight ::-moz-selection {
background-color: transparent;
color: inherit;
}
.page.log .container .time.highlight ::-ms-selection {
background-color: transparent;
color: inherit;
}
.page.log .container .time.highlight ::selection {
background-color: transparent;
color: inherit;
}

157
couchpotato/core/plugins/log/static/log.js

@ -7,21 +7,21 @@ Page.Log = new Class({
title: 'Show recent logs.', title: 'Show recent logs.',
has_tab: false, has_tab: false,
navigation: null,
log_items: [], log_items: [],
report_text: '\ report_text: '### Steps to reproduce:\n'+
### Steps to reproduce:\n\ '1. ..\n'+
1. ..\n\ '2. ..\n'+
2. ..\n\ '\n'+
\n\ '### Information:\n'+
### Information:\n\ 'Movie(s) I have this with: ...\n'+
Movie(s) I have this with: ...\n\ 'Quality of the movie being searched: ...\n'+
Quality of the movie being searched: ...\n\ 'Providers I use: ...\n'+
Providers I use: ...\n\ 'Version of CouchPotato: {version}\n'+
Version of CouchPotato: {version}\n\ 'Running on: ...\n'+
Running on: ...\n\ '\n'+
\n\ '### Logs:\n'+
### Logs:\n\ '```\n{issue}```',
```\n{issue}```',
indexAction: function () { indexAction: function () {
var self = this; var self = this;
@ -34,6 +34,7 @@ Running on: ...\n\
var self = this; var self = this;
if (self.log) self.log.destroy(); if (self.log) self.log.destroy();
self.log = new Element('div.container.loading', { self.log = new Element('div.container.loading', {
'text': 'loading...', 'text': 'loading...',
'events': { 'events': {
@ -41,9 +42,17 @@ Running on: ...\n\
self.showSelectionButton.delay(100, self, e); self.showSelectionButton.delay(100, self, e);
} }
} }
}).inject(self.el); }).inject(self.content);
if(self.navigation){
var nav = self.navigation.getElement('.nav');
nav.getElements('.active').removeClass('active');
self.navigation.getElements('li')[nr+1].addClass('active');
}
Api.request('logging.get', { if(self.request && self.request.running) self.request.cancel();
self.request = Api.request('logging.get', {
'data': { 'data': {
'nr': nr 'nr': nr
}, },
@ -52,65 +61,68 @@ Running on: ...\n\
self.log_items = self.createLogElements(json.log); self.log_items = self.createLogElements(json.log);
self.log.adopt(self.log_items); self.log.adopt(self.log_items);
self.log.removeClass('loading'); self.log.removeClass('loading');
self.scrollToBottom();
var nav = new Element('ul.nav', { if(!self.navigation){
'events': { self.navigation = new Element('div.navigation').adopt(
'click:relay(li.select)': function (e, el) { new Element('h2[text=Logs]'),
self.getLogs(parseInt(el.get('text')) - 1); new Element('div.hint', {
} 'text': 'Select multiple lines & report an issue'
} })
}); );
// Type selection var nav = new Element('ul.nav', {
new Element('li.filter').grab(
new Element('select', {
'events': { 'events': {
'change': function () { 'click:relay(li.select)': function (e, el) {
var type_filter = this.getSelected()[0].get('value'); self.getLogs(parseInt(el.get('text')) - 1);
self.el.set('data-filter', type_filter);
self.scrollToBottom();
} }
} }
}).adopt( }).inject(self.navigation);
new Element('option', {'value': 'ALL', 'text': 'Show all logs'}),
new Element('option', {'value': 'INFO', 'text': 'Show only INFO'}), // Type selection
new Element('option', {'value': 'DEBUG', 'text': 'Show only DEBUG'}), new Element('li.filter').grab(
new Element('option', {'value': 'ERROR', 'text': 'Show only ERROR'}) new Element('select', {
) 'events': {
).inject(nav); 'change': function () {
var type_filter = this.getSelected()[0].get('value');
// Selections self.content.set('data-filter', type_filter);
for (var i = 0; i <= json.total; i++) { self.scrollToBottom();
new Element('li', {
'text': i + 1,
'class': 'select ' + (nr == i ? 'active' : '')
}).inject(nav);
}
// Clear button
new Element('li.clear', {
'text': 'clear',
'events': {
'click': function () {
Api.request('logging.clear', {
'onComplete': function () {
self.getLogs(0);
} }
}); }
}).adopt(
} new Element('option', {'value': 'ALL', 'text': 'Show all logs'}),
new Element('option', {'value': 'INFO', 'text': 'Show only INFO'}),
new Element('option', {'value': 'DEBUG', 'text': 'Show only DEBUG'}),
new Element('option', {'value': 'ERROR', 'text': 'Show only ERROR'})
)
).inject(nav);
// Selections
for (var i = 0; i <= json.total; i++) {
new Element('li', {
'text': i + 1,
'class': 'select ' + (nr == i ? 'active' : '')
}).inject(nav);
} }
}).inject(nav);
// Hint // Clear button
new Element('li.hint', { new Element('li.clear', {
'text': 'Select multiple lines & report an issue' 'text': 'clear',
}).inject(nav); 'events': {
'click': function () {
Api.request('logging.clear', {
'onComplete': function () {
self.getLogs(0);
}
});
// Add to page }
nav.inject(self.log, 'top'); }
}).inject(nav);
self.scrollToBottom(); // Add to page
self.navigation.inject(self.content, 'top');
}
} }
}); });
@ -133,14 +145,14 @@ Running on: ...\n\
new Element('span.message', { new Element('span.message', {
'text': log.message 'text': log.message
}) })
)) ));
}); });
return elements; return elements;
}, },
scrollToBottom: function () { scrollToBottom: function () {
new Fx.Scroll(window, {'duration': 0}).toBottom(); new Fx.Scroll(this.content, {'duration': 0}).toBottom();
}, },
showSelectionButton: function(e){ showSelectionButton: function(e){
@ -213,7 +225,7 @@ Running on: ...\n\
.replace('{version}', version ? version.version.repr : '...'), .replace('{version}', version ? version.version.repr : '...'),
textarea; textarea;
var overlay = new Element('div.report', { var overlay = new Element('div.mask.report_popup', {
'method': 'post', 'method': 'post',
'events': { 'events': {
'click': function(e){ 'click': function(e){
@ -245,12 +257,7 @@ Running on: ...\n\
}) })
), ),
textarea = new Element('textarea', { textarea = new Element('textarea', {
'text': body, 'text': body
'events': {
'click': function(){
this.select();
}
}
}), }),
new Element('a.button', { new Element('a.button', {
'target': '_blank', 'target': '_blank',
@ -270,7 +277,7 @@ Running on: ...\n\
) )
); );
overlay.inject(self.log); overlay.inject(document.body);
}, },
getSelected: function(){ getSelected: function(){

159
couchpotato/core/plugins/log/static/log.scss

@ -0,0 +1,159 @@
@import "_mixins";
.page.log {
.nav {
text-align: right;
padding: 0;
margin: 0;
li {
display: inline-block;
padding: 5px 10px;
margin: 0;
&.select, &.clear {
cursor: pointer;
}
&:hover:not(.active):not(.filter) {
background: rgba(255, 255, 255, 0.1);
}
&.active {
font-weight: bold;
cursor: default;
background: rgba(255,255,255,.1);
}
}
}
.hint {
font-style: italic;
opacity: .5;
margin-top: 3px;
}
.container {
padding: $padding;
overflow: hidden;
line-height: 150%;
&.loading {
text-align: center;
font-size: 20px;
padding: 100px 50px;
}
select {
vertical-align: top;
}
.time {
clear: both;
font-size: .75em;
border-top: 1px solid rgba(255, 255, 255, 0.1);
overflow: hidden;
padding: 0 3px;
font-family: Lucida Console, Monaco, Nimbus Mono L, monospace, serif;
display: flex;
&.highlight {
background: $theme_off;
}
span {
padding: 5px 0 3px;
display: inline-block;
vertical-align: middle;
width: 90px;
}
::selection {
background-color: #000;
color: #FFF;
}
}
.type.type {
margin-left: 10px;
width: 40px;
}
.message {
white-space: pre-wrap;
flex: 1 auto;
}
.error { color: #FFA4A4; }
.debug span { opacity: .6; }
}
[data-filter=INFO] .error,
[data-filter=INFO] .debug,
[data-filter=ERROR] .debug,
[data-filter=ERROR] .info,
[data-filter=DEBUG] .info,
[data-filter=DEBUG] .error {
display: none;
}
}
.report_popup.report_popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 99999;
font-size: 14px;
display: flex;
justify-content: center;
align-items: center;
opacity: 1;
color: #FFF;
pointer-events: auto;
.button {
margin: 10px 0;
padding: 10px;
color: $background_color;
background: $primary_color;
}
.bug {
width: 80%;
height: 80%;
max-height: 800px;
max-width: 800px;
display: flex;
flex-flow: column nowrap;
> span {
margin: $padding/2 0 $padding 0;
}
textarea {
display: block;
width: 100%;
background: #FFF;
padding: 20px;
overflow: auto;
color: #666;
height: 70%;
font-size: 12px;
}
}
}
.do_report.do_report {
z-index: 10000;
position: absolute;
padding: 10px;
background: $primary_color;
color: #FFF;
}

197
couchpotato/core/plugins/profile/static/profile.css

@ -1,197 +0,0 @@
.add_new_profile {
padding: 20px;
display: block;
text-align: center;
font-size: 20px;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
.profile {
border-bottom: 1px solid rgba(255,255,255,0.2);
position: relative;
}
.profile > .delete {
position: absolute;
padding: 16px;
right: 0;
cursor: pointer;
opacity: 0.6;
color: #fd5353;
}
.profile > .delete:hover {
opacity: 1;
}
.profile .ctrlHolder:hover {
background: none;
}
.profile .qualities {
min-height: 80px;
}
.profile .formHint {
width: 210px !important;
vertical-align: top !important;
margin: 0 !important;
padding-left: 3px !important;
opacity: 0.1;
}
.profile:hover .formHint {
opacity: 1;
}
.profile .wait_for {
padding-top: 0;
padding-bottom: 20px;
}
.profile .wait_for input {
margin: 0 5px !important;
}
.profile .wait_for .minimum_score_input {
width: 40px !important;
text-align: left;
}
.profile .types {
padding: 0;
margin: 0 20px 0 -4px;
display: inline-block;
}
.profile .types li {
padding: 3px 5px;
border-bottom: 1px solid rgba(255,255,255,0.2);
list-style: none;
}
.profile .types li:last-child { border: 0; }
.profile .types li > * {
display: inline-block;
vertical-align: middle;
line-height: 0;
margin-right: 10px;
}
.profile .type .check {
margin-top: -1px;
}
.profile .quality_type select {
width: 120px;
margin-left: -1px;
}
.profile .types li.is_empty .check,
.profile .types li.is_empty .delete,
.profile .types li.is_empty .handle,
.profile .types li.is_empty .check_label {
visibility: hidden;
}
.profile .types .type label {
display: inline-block;
width: auto;
float: none;
text-transform: uppercase;
font-size: 11px;
font-weight: normal;
margin-right: 20px;
text-shadow: none;
vertical-align: bottom;
padding: 0;
height: 17px;
}
.profile .types .type label .check {
margin-right: 5px;
}
.profile .types .type label .check_label {
display: inline-block;
vertical-align: top;
height: 16px;
line-height: 13px;
}
.profile .types .type .threed {
display: none;
}
.profile .types .type.allow_3d .threed {
display: inline-block;
}
.profile .types .type .handle {
background: url('../../images/handle.png') center;
display: inline-block;
height: 20px;
width: 20px;
cursor: -moz-grab;
cursor: -webkit-grab;
cursor: grab;
margin: 0;
}
.profile .types .type .delete {
height: 20px;
width: 20px;
line-height: 20px;
visibility: hidden;
cursor: pointer;
font-size: 13px;
color: #fd5353;
}
.profile .types .type:not(.allow_3d) .delete {
margin-left: 55px;
}
.profile .types .type:hover:not(.is_empty) .delete {
visibility: visible;
}
#profile_ordering {
}
#profile_ordering ul {
float: left;
margin: 0;
width: 275px;
padding: 0;
}
#profile_ordering li {
border-bottom: 1px solid rgba(255,255,255,0.2);
padding: 0 5px;
}
#profile_ordering li:last-child { border: 0; }
#profile_ordering li .check {
margin: 2px 10px 0 0;
vertical-align: top;
}
#profile_ordering li > span {
display: inline-block;
height: 20px;
vertical-align: top;
line-height: 20px;
}
#profile_ordering li .handle {
background: url('../../images/handle.png') center;
width: 20px;
float: right;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
#profile_ordering .formHint {
clear: none;
float: right;
width: 250px;
margin: 0;
}

76
couchpotato/core/plugins/profile/static/profile.js

@ -31,7 +31,7 @@ var Profile = new Class({
}), }),
new Element('.quality_label.ctrlHolder').adopt( new Element('.quality_label.ctrlHolder').adopt(
new Element('label', {'text':'Name'}), new Element('label', {'text':'Name'}),
new Element('input.inlay', { new Element('input', {
'type':'text', 'type':'text',
'value': data.label, 'value': data.label,
'placeholder': 'Profile name' 'placeholder': 'Profile name'
@ -47,7 +47,7 @@ var Profile = new Class({
new Element('div.wait_for.ctrlHolder').adopt( new Element('div.wait_for.ctrlHolder').adopt(
// "Wait the entered number of days for a checked quality, before downloading a lower quality release." // "Wait the entered number of days for a checked quality, before downloading a lower quality release."
new Element('span', {'text':'Wait'}), new Element('span', {'text':'Wait'}),
new Element('input.inlay.wait_for_input.xsmall', { new Element('input.wait_for_input.xsmall', {
'type':'text', 'type':'text',
'value': data.wait_for && data.wait_for.length > 0 ? data.wait_for[0] : 0 'value': data.wait_for && data.wait_for.length > 0 ? data.wait_for[0] : 0
}), }),
@ -55,7 +55,7 @@ var Profile = new Class({
new Element('span.advanced', {'text':'and keep searching'}), new Element('span.advanced', {'text':'and keep searching'}),
// "After a checked quality is found and downloaded, continue searching for even better quality releases for the entered number of days." // "After a checked quality is found and downloaded, continue searching for even better quality releases for the entered number of days."
new Element('input.inlay.xsmall.stop_after_input.advanced', { new Element('input.xsmall.stop_after_input.advanced', {
'type':'text', 'type':'text',
'value': data.stop_after && data.stop_after.length > 0 ? data.stop_after[0] : 0 'value': data.stop_after && data.stop_after.length > 0 ? data.stop_after[0] : 0
}), }),
@ -63,7 +63,7 @@ var Profile = new Class({
// Minimum score of // Minimum score of
new Element('span.advanced', {'html':'<br/>Releases need a minimum score of'}), new Element('span.advanced', {'html':'<br/>Releases need a minimum score of'}),
new Element('input.advanced.inlay.xsmall.minimum_score_input', { new Element('input.advanced.xsmall.minimum_score_input', {
'size': 4, 'size': 4,
'type':'text', 'type':'text',
'value': data.minimum_score || 1 'value': data.minimum_score || 1
@ -81,7 +81,7 @@ var Profile = new Class({
'quality': quality, 'quality': quality,
'finish': data.finish[nr] || false, 'finish': data.finish[nr] || false,
'3d': data['3d'] ? data['3d'][nr] || false : false '3d': data['3d'] ? data['3d'][nr] || false : false
}) });
}); });
} }
@ -123,7 +123,7 @@ var Profile = new Class({
} }
}); });
}).delay(delay, self) }).delay(delay, self);
}, },
@ -148,7 +148,7 @@ var Profile = new Class({
}); });
}); });
return data return data;
}, },
addType: function(data){ addType: function(data){
@ -177,7 +177,7 @@ var Profile = new Class({
var self = this; var self = this;
return self.types.filter(function(type){ return self.types.filter(function(type){
return type.get('quality') return type.get('quality');
}); });
}, },
@ -231,15 +231,15 @@ var Profile = new Class({
}, },
get: function(attr){ get: function(attr){
return this.data[attr] return this.data[attr];
}, },
isCore: function(){ isCore: function(){
return this.data.core return this.data.core;
}, },
toElement: function(){ toElement: function(){
return this.el return this.el;
} }
}); });
@ -270,47 +270,42 @@ Profile.Type = new Class({
var data = self.data; var data = self.data;
self.el = new Element('li.type').adopt( self.el = new Element('li.type').adopt(
new Element('span.quality_type').grab( new Element('span.quality_type.select_wrapper.icon-dropdown').grab(
self.fillQualities() self.fillQualities()
), ),
self.finish_container = new Element('label.finish').adopt( self.finish_container = new Element('label.finish').adopt(
new Element('span.finish').grab( self.finish = new Element('input.finish[type=checkbox]', {
self.finish = new Element('input.inlay.finish[type=checkbox]', { 'checked': data.finish !== undefined ? data.finish : 1,
'checked': data.finish !== undefined ? data.finish : 1, 'events': {
'events': { 'change': function(){
'change': function(){ if(self.el == self.el.getParent().getElement(':first-child')){
if(self.el == self.el.getParent().getElement(':first-child')){ alert('Top quality always finishes the search');
self.finish_class.check(); return;
alert('Top quality always finishes the search');
return;
}
self.fireEvent('change');
} }
self.fireEvent('change');
} }
}) }
), }),
new Element('span.check_label[text=finish]') new Element('span.check_label[text=finish]')
), ),
self['3d_container'] = new Element('label.threed').adopt( self['3d_container'] = new Element('label.threed').adopt(
new Element('span.3d').grab( self['3d'] = new Element('input.3d[type=checkbox]', {
self['3d'] = new Element('input.inlay.3d[type=checkbox]', { 'checked': data['3d'] !== undefined ? data['3d'] : 0,
'checked': data['3d'] !== undefined ? data['3d'] : 0, 'events': {
'events': { 'change': function(){
'change': function(){ self.fireEvent('change');
self.fireEvent('change');
}
} }
}) }
), }),
new Element('span.check_label[text=3D]') new Element('span.check_label[text=3D]')
), ),
new Element('span.delete.icon2', { new Element('span.delete.icon-cancel', {
'events': { 'events': {
'click': self.del.bind(self) 'click': self.del.bind(self)
} }
}), }),
new Element('span.handle') new Element('span.handle.icon-handle')
); );
self.el[self.data.quality ? 'removeClass' : 'addClass']('is_empty'); self.el[self.data.quality ? 'removeClass' : 'addClass']('is_empty');
@ -318,9 +313,6 @@ Profile.Type = new Class({
if(self.data.quality && Quality.getQuality(self.data.quality).allow_3d) if(self.data.quality && Quality.getQuality(self.data.quality).allow_3d)
self.el.addClass('allow_3d'); self.el.addClass('allow_3d');
self.finish_class = new Form.Check(self.finish);
self['3d_class'] = new Form.Check(self['3d']);
}, },
fillQualities: function(){ fillQualities: function(){
@ -342,7 +334,7 @@ Profile.Type = new Class({
'text': q.label, 'text': q.label,
'value': q.identifier, 'value': q.identifier,
'data-allow_3d': q.allow_3d 'data-allow_3d': q.allow_3d
}).inject(self.qualities) }).inject(self.qualities);
}); });
self.qualities.set('value', self.data.quality); self.qualities.set('value', self.data.quality);
@ -358,7 +350,7 @@ Profile.Type = new Class({
'quality': self.qualities.get('value'), 'quality': self.qualities.get('value'),
'finish': +self.finish.checked, 'finish': +self.finish.checked,
'3d': +self['3d'].checked '3d': +self['3d'].checked
} };
}, },
get: function(key){ get: function(key){

150
couchpotato/core/plugins/profile/static/profile.scss

@ -0,0 +1,150 @@
@import "_mixins";
.add_new_profile {
padding: 20px;
display: block;
text-align: center;
font-size: 20px;
border-bottom: 1px solid $theme_off;
}
.profile {
margin-bottom: 20px;
.quality_label input {
font-weight: bold;
}
.ctrlHolder {
.types {
flex: 1 1 auto;
min-width: 360px;
.type {
display: flex;
flex-row: row nowrap;
align-items: center;
padding: 2px 0;
label {
min-width: 0;
margin-left: $padding/2;
span {
font-size: .9em;
}
}
input[type=checkbox] {
margin-right: 3px;
}
.delete, .handle {
margin-left: $padding/4;
width: 20px;
font-size: 20px;
opacity: .1;
text-align: center;
cursor: pointer;
&.handle {
cursor: move;
cursor: grab;
}
&:hover {
opacity: 1;
}
}
&.is_empty {
.delete, .handle {
display: none;
}
}
}
}
&.wait_for.wait_for {
display: block;
input {
min-width: 0;
width: 40px;
text-align: center;
margin: 0 2px;
}
.advanced {
display: none;
color: $primary_color;
.show_advanced & {
display: inline;
}
}
}
.formHint {
}
}
}
#profile_ordering {
ul {
list-style: none;
margin: 0;
width: 275px;
padding: 0;
}
li {
border-bottom: 1px solid $theme_off;
padding: 5px;
display: flex;
align-items: center;
&:hover {
background: $theme_off;
}
&:last-child { border: 0; }
input[type=checkbox] {
margin: 2px 10px 0 0;
vertical-align: top;
}
> span {
display: inline-block;
height: 20px;
vertical-align: top;
line-height: 20px;
&.profile_label {
flex: 1 1 auto;
}
}
.handle {
font-size: 20px;
width: 20px;
float: right;
cursor: move;
cursor: grab;
opacity: .5;
text-align: center;
&:hover {
opacity: 1;
}
}
}
.formHint {
}
}

11
couchpotato/core/plugins/quality/main.py

@ -24,8 +24,8 @@ class QualityPlugin(Plugin):
qualities = [ qualities = [
{'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'median_size': 40000, 'label': 'BR-Disk', 'alternative': ['bd25', ('br', 'disk')], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']}, {'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'median_size': 40000, 'label': 'BR-Disk', 'alternative': ['bd25', ('br', 'disk')], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']},
{'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'median_size': 10000, 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts'], 'tags': ['m2ts', 'x264', 'h264']}, {'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'median_size': 10000, 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts'], 'tags': ['m2ts', 'x264', 'h264', '1080']},
{'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'median_size': 5500, 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']}, {'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'median_size': 5500, 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264', '720']},
{'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'median_size': 2000, 'label': 'BR-Rip', 'alternative': ['bdrip', ('br', 'rip'), 'hdtv', 'hdrip'], 'allow': ['720p', '1080p'], 'ext':['mp4', 'avi'], 'tags': ['webdl', ('web', 'dl')]}, {'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'median_size': 2000, 'label': 'BR-Rip', 'alternative': ['bdrip', ('br', 'rip'), 'hdtv', 'hdrip'], 'allow': ['720p', '1080p'], 'ext':['mp4', 'avi'], 'tags': ['webdl', ('web', 'dl')]},
{'identifier': 'dvdr', 'size': (3000, 10000), 'median_size': 4500, 'label': 'DVD-R', 'alternative': ['br2dvd', ('dvd', 'r')], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'median_size': 4500, 'label': 'DVD-R', 'alternative': ['br2dvd', ('dvd', 'r')], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'median_size': 1500, 'label': 'DVD-Rip', 'width': 720, 'alternative': [('dvd', 'rip')], 'allow': [], 'ext':['avi'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, {'identifier': 'dvdrip', 'size': (600, 2400), 'median_size': 1500, 'label': 'DVD-Rip', 'width': 720, 'alternative': [('dvd', 'rip')], 'allow': [], 'ext':['avi'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
@ -271,8 +271,8 @@ class QualityPlugin(Plugin):
words = words[:-1] words = words[:-1]
points = { points = {
'identifier': 20, 'identifier': 25,
'label': 20, 'label': 25,
'alternative': 20, 'alternative': 20,
'tags': 11, 'tags': 11,
'ext': 5, 'ext': 5,
@ -487,11 +487,12 @@ class QualityPlugin(Plugin):
'Movie Name (2014).mkv': {'size': 4500, 'quality': '720p', 'extra': {'titles': ['Movie Name 2014 720p Bluray']}}, 'Movie Name (2014).mkv': {'size': 4500, 'quality': '720p', 'extra': {'titles': ['Movie Name 2014 720p Bluray']}},
'Movie Name (2015).mkv': {'size': 500, 'quality': '1080p', 'extra': {'resolution_width': 1920}}, 'Movie Name (2015).mkv': {'size': 500, 'quality': '1080p', 'extra': {'resolution_width': 1920}},
'Movie Name (2015).mp4': {'size': 6500, 'quality': 'brrip'}, 'Movie Name (2015).mp4': {'size': 6500, 'quality': 'brrip'},
'Movie Name (2015).mp4': {'size': 6500, 'quality': 'brrip'},
'Movie Name.2014.720p Web-Dl Aac2.0 h264-ReleaseGroup': {'size': 3800, 'quality': 'brrip'}, 'Movie Name.2014.720p Web-Dl Aac2.0 h264-ReleaseGroup': {'size': 3800, 'quality': 'brrip'},
'Movie Name.2014.720p.WEBRip.x264.AC3-ReleaseGroup': {'size': 3000, 'quality': 'scr'}, 'Movie Name.2014.720p.WEBRip.x264.AC3-ReleaseGroup': {'size': 3000, 'quality': 'scr'},
'Movie.Name.2014.1080p.HDCAM.-.ReleaseGroup': {'size': 5300, 'quality': 'cam'}, 'Movie.Name.2014.1080p.HDCAM.-.ReleaseGroup': {'size': 5300, 'quality': 'cam'},
'Movie.Name.2014.720p.HDSCR.4PARTS.MP4.AAC.ReleaseGroup': {'size': 2401, 'quality': 'scr'}, 'Movie.Name.2014.720p.HDSCR.4PARTS.MP4.AAC.ReleaseGroup': {'size': 2401, 'quality': 'scr'},
'Movie.Name.2014.720p.BluRay.x264-ReleaseGroup': {'size': 10300, 'quality': '720p'},
'Movie.Name.2014.720.Bluray.x264.DTS-ReleaseGroup': {'size': 9700, 'quality': '720p'},
} }
correct = 0 correct = 0

26
couchpotato/core/plugins/quality/static/quality.css

@ -1,26 +0,0 @@
.group_sizes {
}
.group_sizes .head {
font-weight: bold;
}
.group_sizes .ctrlHolder {
padding-top: 4px !important;
padding-bottom: 4px !important;
font-size: 12px;
}
.group_sizes .label {
max-width: 120px;
}
.group_sizes .min, .group_sizes .max {
text-align: center;
width: 50px;
max-width: 50px;
margin: 0 5px !important;
padding: 0 3px;
display: inline-block;
}

42
couchpotato/core/plugins/quality/static/quality.js

@ -12,20 +12,20 @@ var QualityBase = new Class({
self.profiles = []; self.profiles = [];
Array.each(data.profiles, self.createProfilesClass.bind(self)); Array.each(data.profiles, self.createProfilesClass.bind(self));
App.addEvent('loadSettings', self.addSettings.bind(self)) App.addEvent('loadSettings', self.addSettings.bind(self));
}, },
getProfile: function(id){ getProfile: function(id){
return this.profiles.filter(function(profile){ return this.profiles.filter(function(profile){
return profile.data._id == id return profile.data._id == id;
}).pick() }).pick();
}, },
// Hide items when getting profiles // Hide items when getting profiles
getActiveProfiles: function(){ getActiveProfiles: function(){
return Array.filter(this.profiles, function(profile){ return Array.filter(this.profiles, function(profile){
return !profile.data.hide return !profile.data.hide;
}); });
}, },
@ -37,7 +37,7 @@ var QualityBase = new Class({
} }
catch(e){} catch(e){}
return {} return {};
}, },
addSettings: function(){ addSettings: function(){
@ -58,7 +58,7 @@ var QualityBase = new Class({
self.createProfileOrdering(); self.createProfileOrdering();
self.createSizes(); self.createSizes();
}) });
}, },
@ -68,7 +68,7 @@ var QualityBase = new Class({
createProfiles: function(){ createProfiles: function(){
var self = this; var self = this;
var non_core_profiles = Array.filter(self.profiles, function(profile){ return !profile.isCore() }); var non_core_profiles = Array.filter(self.profiles, function(profile){ return !profile.isCore(); });
var count = non_core_profiles.length; var count = non_core_profiles.length;
self.settings.createGroup({ self.settings.createGroup({
@ -81,7 +81,7 @@ var QualityBase = new Class({
'events': { 'events': {
'click': function(){ 'click': function(){
var profile = self.createProfilesClass(); var profile = self.createProfilesClass();
$(profile).inject(self.profile_container) $(profile).inject(self.profile_container);
} }
} }
}) })
@ -89,7 +89,7 @@ var QualityBase = new Class({
// Add profiles, that aren't part of the core (for editing) // Add profiles, that aren't part of the core (for editing)
Array.each(non_core_profiles, function(profile){ Array.each(non_core_profiles, function(profile){
$(profile).inject(self.profile_container) $(profile).inject(self.profile_container);
}); });
}, },
@ -97,7 +97,7 @@ var QualityBase = new Class({
createProfilesClass: function(data){ createProfilesClass: function(data){
var self = this; var self = this;
var data = data || {'id': randomString()}; data = data || {'id': randomString()};
var profile = new Profile(data); var profile = new Profile(data);
self.profiles.include(profile); self.profiles.include(profile);
@ -110,7 +110,7 @@ var QualityBase = new Class({
self.settings.createGroup({ self.settings.createGroup({
'label': 'Profile Defaults', 'label': 'Profile Defaults',
'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)' 'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
}).adopt( }).grab(
new Element('.ctrlHolder#profile_ordering').adopt( new Element('.ctrlHolder#profile_ordering').adopt(
new Element('label[text=Order]'), new Element('label[text=Order]'),
self.profiles_list = new Element('ul'), self.profiles_list = new Element('ul'),
@ -123,7 +123,7 @@ var QualityBase = new Class({
Array.each(self.profiles, function(profile){ Array.each(self.profiles, function(profile){
var check; var check;
new Element('li', {'data-id': profile.data._id}).adopt( new Element('li', {'data-id': profile.data._id}).adopt(
check = new Element('input.inlay[type=checkbox]', { check = new Element('input[type=checkbox]', {
'checked': !profile.data.hide, 'checked': !profile.data.hide,
'events': { 'events': {
'change': self.saveProfileOrdering.bind(self) 'change': self.saveProfileOrdering.bind(self)
@ -132,11 +132,8 @@ var QualityBase = new Class({
new Element('span.profile_label', { new Element('span.profile_label', {
'text': profile.data.label 'text': profile.data.label
}), }),
new Element('span.handle') new Element('span.handle.icon-handle')
).inject(self.profiles_list); ).inject(self.profiles_list);
new Form.Check(check);
}); });
// Sortable // Sortable
@ -190,7 +187,6 @@ var QualityBase = new Class({
'name': 'sizes' 'name': 'sizes'
}).inject(self.content); }).inject(self.content);
new Element('div.item.head.ctrlHolder').adopt( new Element('div.item.head.ctrlHolder').adopt(
new Element('span.label', {'text': 'Quality'}), new Element('span.label', {'text': 'Quality'}),
new Element('span.min', {'text': 'Min'}), new Element('span.min', {'text': 'Min'}),
@ -200,23 +196,23 @@ var QualityBase = new Class({
Array.each(self.qualities, function(quality){ Array.each(self.qualities, function(quality){
new Element('div.ctrlHolder.item').adopt( new Element('div.ctrlHolder.item').adopt(
new Element('span.label', {'text': quality.label}), new Element('span.label', {'text': quality.label}),
new Element('input.min.inlay[type=text]', { new Element('input.min[type=text]', {
'value': quality.size_min, 'value': quality.size_min,
'events': { 'events': {
'keyup': function(e){ 'keyup': function(e){
self.changeSize(quality.identifier, 'size_min', e.target.get('value')) self.changeSize(quality.identifier, 'size_min', e.target.get('value'));
} }
} }
}), }),
new Element('input.max.inlay[type=text]', { new Element('input.max[type=text]', {
'value': quality.size_max, 'value': quality.size_max,
'events': { 'events': {
'keyup': function(e){ 'keyup': function(e){
self.changeSize(quality.identifier, 'size_max', e.target.get('value')) self.changeSize(quality.identifier, 'size_max', e.target.get('value'));
} }
} }
}) })
).inject(group) ).inject(group);
}); });
}, },
@ -235,7 +231,7 @@ var QualityBase = new Class({
'value': value 'value': value
} }
}); });
}).delay(300) }).delay(300);
} }

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

Loading…
Cancel
Save