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
/data/
/_env/
/_source/
.project
.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
=====
[![Join the chat at https://gitter.im/RuudBurger/CouchPotatoServer](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/RuudBurger/CouchPotatoServer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
CouchPotato (CP) is an automatic NZB and torrent downloader. You can keep a "movies I want"-list and it will search for NZBs/torrents of these movies every X hours.
Once a movie is found, it will send it to SABnzbd or download the torrent to a specified directory.
@ -23,6 +25,7 @@ OS X:
* If you're on Leopard (10.5) install Python 2.6+: [Python 2.6.5](http://www.python.org/download/releases/2.6.5/)
* Install [GIT](http://git-scm.com/)
* Install [LXML](http://lxml.de/installation.html) for better/faster website scraping
* Open up `Terminal`
* Go to your App folder `cd /Applications`
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
@ -33,6 +36,7 @@ Linux:
* (Ubuntu / Debian) Install [GIT](http://git-scm.com/) with `apt-get install git-core`
* (Fedora / CentOS) Install [GIT](http://git-scm.com/) with `yum install git`
* Install [LXML](http://lxml.de/installation.html) for better/faster website scraping
* 'cd' to the folder of your choosing.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py` to start
@ -47,23 +51,34 @@ Linux:
* Open your browser and go to `http://localhost:5050/`
Docker:
* You can use [razorgirl's Dockerfile](https://github.com/razorgirl/docker-couchpotato) to quickly build your own isolated app container. It's based on the Linux instructions above. For more info about Docker check out the [official website](https://www.docker.com).
FreeBSD :
* Update your ports tree `sudo portsnap fetch update`
* Install Python 2.6+ [lang/python](http://www.freshports.org/lang/python) with `cd /usr/ports/lang/python; sudo make install clean`
* Install port [databases/py-sqlite3](http://www.freshports.org/databases/py-sqlite3) with `cd /usr/ports/databases/py-sqlite3; sudo make install clean`
* Add a symlink to 'python2' `sudo ln -s /usr/local/bin/python /usr/local/bin/python2`
* Install port [ftp/libcurl](http://www.freshports.org/ftp/libcurl) with `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
* Install port [ftp/curl](http://www.freshports.org/ftp/bcurl), deselect 'Asynchronous DNS resolution via c-ares' when prompted as part of config `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
* Install port [textproc/docbook-xml-450](http://www.freshports.org/textproc/docbook-xml-450) with `cd /usr/ports/textproc/docbook-xml-450; sudo make install clean`
* Install port [GIT](http://git-scm.com/) with `cd /usr/ports/devel/git; sudo make install clean`
* 'cd' to the folder of your choosing.
* You can use [linuxserver.io](https://github.com/linuxserver/docker-couchpotato) or [razorgirl's](https://github.com/razorgirl/docker-couchpotato) to quickly build your own isolated app container. It's based on the Linux instructions above. For more info about Docker check out the [official website](https://www.docker.com).
FreeBSD:
* Become root with `su`
* Update your repo catalog `pkg update`
* Install required tools `pkg install python py27-sqlite3 fpc-libcurl docbook-xml git-lite`
* For default install location and running as root `cd /usr/local`
* If running as root, expects python here `ln -s /usr/local/bin/python /usr/bin/python`
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato`
* Change the paths inside the init script. `sudo vim /etc/rc.d/couchpotato`
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato`
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
* Copy the startup script `cp CouchPotatoServer/init/freebsd /usr/local/etc/rc.d/couchpotato`
* Make startup script executable `chmod 555 /usr/local/etc/rc.d/couchpotato`
* Add startup to boot `echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
* Read the options at the top of `more /usr/local/etc/rc.d/couchpotato`
* If not default install, specify options with startup flags in `ee /etc/rc.conf`
* Finally, `service couchpotato start`
* Open your browser and go to: `http://server:5050/`
## Development
Be sure you're running the latest version of [Python 2.7](http://python.org/).
If you're going to add styling or doing some javascript work you'll need a few tools that build and compress scss -> css and combine the javascript files. [Node/NPM](https://nodejs.org/), [Grunt](http://gruntjs.com/installing-grunt), [Compass](http://compass-style.org/install/)
After you've got these tools you can install the packages using `npm install`. Once this process has finished you can start CP using the command `grunt`. This will start all the needed tools and watches any files for changes.
You can now change css and javascript and it wil reload the page when needed.
By default it will combine files used in the core folder. If you're adding a new .scss or .js file, you might need to add it and then restart the grunt process for it to combine it properly.
Don't forget to enable development inside the CP settings. This disables some functions and also makes sure javascript rrors are pushed to console instead of the log.

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 traceback
import webbrowser
import sys
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent
@ -52,6 +53,7 @@ class Core(Plugin):
addEvent('app.version', self.version)
addEvent('app.load', self.checkDataDir)
addEvent('app.load', self.cleanUpFolders)
addEvent('app.load.after', self.dependencies)
addEvent('setting.save.core.password', self.md5Password)
addEvent('setting.save.core.api_key', self.checkApikey)
@ -64,6 +66,23 @@ class Core(Plugin):
import socket
socket.setdefaulttimeout(30)
# Don't check ssl by default
try:
if sys.version_info >= (2, 7, 9):
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
except:
log.debug('Failed setting default ssl context: %s', traceback.format_exc())
def dependencies(self):
# Check if lxml is available
try: from lxml import etree
except: log.error('LXML not available, please install for better/faster scraping support: `http://lxml.de/installation.html`')
try: import OpenSSL
except: log.error('OpenSSL not available, please install for better requests validation: `https://pyopenssl.readthedocs.org/en/latest/install.html`')
def md5Password(self, value):
return md5(value) if value else ''
@ -174,8 +193,9 @@ class Core(Plugin):
if host == '0.0.0.0' or host == '':
host = 'localhost'
port = Env.setting('port')
ssl = Env.setting('ssl_cert') and Env.setting('ssl_key')
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), Env.get('web_base'))
return '%s:%d%s' % (cleanHost(host, ssl = ssl).rstrip('/'), int(port), Env.get('web_base'))
def createApiUrl(self):
return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
@ -230,6 +250,12 @@ config = [{
'description': 'The port I should listen to.',
},
{
'name': 'ipv6',
'default': 0,
'type': 'bool',
'description': 'Also bind the WebUI to ipv6 address',
},
{
'name': 'ssl_cert',
'description': 'Path to SSL server.crt',
'advanced': True,
@ -261,6 +287,30 @@ config = [{
'description': 'Let 3rd party app do stuff. <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',
'default': 0,
'type': 'bool',

189
couchpotato/core/_base/clientscript.py

@ -1,16 +1,10 @@
import os
import re
import traceback
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from minify.cssmin import cssmin
from minify.jsmin import jsmin
from tornado.web import StaticFileHandler
log = CPLog(__name__)
@ -20,129 +14,35 @@ autoload = 'ClientScript'
class ClientScript(Plugin):
core_static = {
paths = {
'style': [
'style/main.css',
'style/uniform.generic.css',
'style/uniform.css',
'style/settings.css',
'style/combined.min.css',
],
'script': [
'scripts/library/mootools.js',
'scripts/library/mootools_more.js',
'scripts/library/uniform.js',
'scripts/library/form_replacement/form_check.js',
'scripts/library/form_replacement/form_radio.js',
'scripts/library/form_replacement/form_dropdown.js',
'scripts/library/form_replacement/form_selectoption.js',
'scripts/library/question.js',
'scripts/library/scrollspy.js',
'scripts/library/spin.js',
'scripts/library/Array.stableSort.js',
'scripts/library/async.js',
'scripts/couchpotato.js',
'scripts/api.js',
'scripts/library/history.js',
'scripts/page.js',
'scripts/block.js',
'scripts/block/navigation.js',
'scripts/block/footer.js',
'scripts/block/menu.js',
'scripts/page/home.js',
'scripts/page/settings.js',
'scripts/page/about.js',
'scripts/combined.vendor.min.js',
'scripts/combined.base.min.js',
'scripts/combined.plugins.min.js',
],
}
urls = {'style': {}, 'script': {}}
minified = {'style': {}, 'script': {}}
paths = {'style': {}, 'script': {}}
comment = {
'style': '/*** %s:%d ***/\n',
'script': '// %s:%d\n'
}
html = {
'style': '<link rel="stylesheet" href="%s" type="text/css">',
'script': '<script type="text/javascript" src="%s"></script>',
}
def __init__(self):
addEvent('register_style', self.registerStyle)
addEvent('register_script', self.registerScript)
addEvent('clientscript.get_styles', self.getStyles)
addEvent('clientscript.get_scripts', self.getScripts)
if not Env.get('dev'):
addEvent('app.load', self.minify)
self.makeRelative()
self.addCore()
def makeRelative(self):
def addCore(self):
for static_type in self.paths:
for static_type in self.core_static:
for rel_path in self.core_static.get(static_type):
updates_paths = []
for rel_path in self.paths.get(static_type):
file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path)
core_url = 'static/%s' % rel_path
if static_type == 'script':
self.registerScript(core_url, file_path, position = 'front')
else:
self.registerStyle(core_url, file_path, position = 'front')
def minify(self):
# Create cache dir
cache = Env.get('cache_dir')
parent_dir = os.path.join(cache, 'minified')
self.makeDir(parent_dir)
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + 'minified/(.*)', StaticFileHandler, {'path': parent_dir})])
for file_type in ['style', 'script']:
ext = 'js' if file_type is 'script' else 'css'
positions = self.paths.get(file_type, {})
for position in positions:
files = positions.get(position)
self._minify(file_type, files, position, position + '.' + ext)
def _minify(self, file_type, files, position, out):
cache = Env.get('cache_dir')
out_name = out
out = os.path.join(cache, 'minified', out_name)
raw = []
for file_path in files:
f = open(file_path, 'r').read()
if file_type == 'script':
data = jsmin(f)
else:
data = self.prefix(f)
data = cssmin(data)
data = data.replace('../images/', '../static/images/')
data = data.replace('../fonts/', '../static/fonts/')
data = data.replace('../../static/', '../static/') # Replace inside plugins
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
# Combine all files together with some comments
data = ''
for r in raw:
data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
data += r.get('data') + '\n\n'
self.createFile(out, data.strip())
core_url = 'static/%s?%d' % (rel_path, tryInt(os.path.getmtime(file_path)))
if not self.minified.get(file_type):
self.minified[file_type] = {}
if not self.minified[file_type].get(position):
self.minified[file_type][position] = []
updates_paths.append(core_url)
minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out)))
self.minified[file_type][position].append(minified_url)
self.paths[static_type] = updates_paths
def getStyles(self, *args, **kwargs):
return self.get('style', *args, **kwargs)
@ -150,63 +50,8 @@ class ClientScript(Plugin):
def getScripts(self, *args, **kwargs):
return self.get('script', *args, **kwargs)
def get(self, type, as_html = False, location = 'head'):
data = '' if as_html else []
try:
try:
if not Env.get('dev'):
return self.minified[type][location]
except:
pass
return self.urls[type][location]
except:
log.error('Error getting minified %s, %s: %s', (type, location, traceback.format_exc()))
return data
def registerStyle(self, api_path, file_path, position = 'head'):
self.register(api_path, file_path, 'style', position)
def registerScript(self, api_path, file_path, position = 'head'):
self.register(api_path, file_path, 'script', position)
def register(self, api_path, file_path, type, location):
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
if not self.urls[type].get(location):
self.urls[type][location] = []
self.urls[type][location].append(api_path)
if not self.paths[type].get(location):
self.paths[type][location] = []
self.paths[type][location].append(file_path)
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
prefix_tags = ['ms', 'moz', 'webkit']
def prefix(self, data):
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
new_data = ''
colon_split = trimmed_data.split(';')
for splt in colon_split:
curl_split = splt.strip().split('{')
for curly in curl_split:
curly = curly.strip()
for prop in self.prefix_properties:
if curly[:len(prop) + 1] == prop + ':':
for tag in self.prefix_tags:
new_data += ' -%s-%s; ' % (tag, curly)
new_data += curly + (' { ' if len(curl_split) > 1 else ' ')
new_data += '; '
new_data = new_data.replace('{ ;', '; ').replace('} ;', '} ')
def get(self, type):
if type in self.paths:
return self.paths[type]
return new_data
return []

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

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

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

@ -7,6 +7,7 @@ import traceback
import zipfile
from datetime import datetime
from threading import RLock
import re
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
@ -34,7 +35,10 @@ class Updater(Plugin):
if Env.get('desktop'):
self.updater = DesktopUpdater()
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:
self.updater = SourceUpdater()
@ -163,7 +167,6 @@ class BaseUpdater(Plugin):
update_failed = False
update_version = None
last_check = 0
auto_register_static = False
def doUpdate(self):
pass

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

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

2
couchpotato/core/database.py

@ -272,7 +272,7 @@ class Database(object):
prop_name = 'last_db_compact'
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()
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.')
return False
if not self.conf('api_key'):
log.error('Config properties are not filled in correctly, API key is missing.')
return False
# This is where v4 and v5 begin to differ
if(self.conf('version') == 'v4'):
if not self.conf('api_key'):
log.error('Config properties are not filled in correctly, API key is missing.')
return False
url = 'http://' + str(host[0]) + ':' + str(host[1]) + '/jsonrpc'
client = JsonRpcClient(url, 'Token ' + self.conf('api_key'))
self.hadouken_api = HadoukenAPIv4(client)
self.hadouken_api = HadoukenAPI(host[0], port = host[1], api_key = self.conf('api_key'))
return True
else:
auth_type = self.conf('auth_type')
header = None
return True
if auth_type == 'api_key':
header = 'Token ' + self.conf('api_key')
elif auth_type == 'user_pass':
header = 'Basic ' + b64encode(self.conf('auth_user') + ':' + self.conf('auth_pass'))
url = 'http://' + str(host[0]) + ':' + str(host[1]) + '/api'
client = JsonRpcClient(url, header)
self.hadouken_api = HadoukenAPIv5(client)
return True
return False
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
@ -66,6 +86,8 @@ class Hadouken(DownloaderBase):
if self.conf('label'):
torrent_params['label'] = self.conf('label')
# Set the tags array since that is what v5 expects.
torrent_params['tags'] = [self.conf('label')]
torrent_filename = self.createFileName(data, filedata, media)
@ -132,71 +154,25 @@ class Hadouken(DownloaderBase):
if torrent is None:
continue
torrent_filelist = self.hadouken_api.get_files_by_hash(torrent['InfoHash'])
torrent_filelist = self.hadouken_api.get_files_by_hash(torrent.info_hash)
torrent_files = []
save_path = torrent['SavePath']
# The 'Path' key for each file_item contains
# the full path to the single file relative to the
# torrents save path.
# For a single file torrent the result would be,
# - Save path: "C:\Downloads"
# - file_item['Path'] = "file1.iso"
# Resulting path: "C:\Downloads\file1.iso"
# For a multi file torrent the result would be,
# - Save path: "C:\Downloads"
# - file_item['Path'] = "dirname/file1.iso"
# Resulting path: "C:\Downloads\dirname/file1.iso"
for file_item in torrent_filelist:
torrent_files.append(sp(os.path.join(save_path, file_item['Path'])))
torrent_files.append(sp(os.path.join(torrent.save_path, file_item)))
release_downloads.append({
'id': torrent['InfoHash'].upper(),
'name': torrent['Name'],
'status': self.get_torrent_status(torrent),
'seed_ratio': self.get_seed_ratio(torrent),
'original_status': torrent['State'],
'id': torrent.info_hash.upper(),
'name': torrent.name,
'status': torrent.get_status(),
'seed_ratio': torrent.get_seed_ratio(),
'original_status': torrent.state,
'timeleft': -1,
'folder': sp(save_path if len(torrent_files == 1) else os.path.join(save_path, torrent['Name'])),
'folder': sp(torrent.save_path if len(torrent_files == 1) else os.path.join(torrent.save_path, torrent.name)),
'files': torrent_files
})
return release_downloads
def get_seed_ratio(self, torrent):
""" Returns the seed ratio for a given torrent.
Keyword arguments:
torrent -- The torrent to calculate seed ratio for.
"""
up = torrent['TotalUploadedBytes']
down = torrent['TotalDownloadedBytes']
if up > 0 and down > 0:
return up / down
return 0
def get_torrent_status(self, torrent):
""" Returns the CouchPotato status for a given torrent.
Keyword arguments:
torrent -- The torrent to translate status for.
"""
if torrent['IsSeeding'] and torrent['IsFinished'] and torrent['Paused']:
return 'completed'
if torrent['IsSeeding']:
return 'seeding'
return 'busy'
def pause(self, release_download, pause = True):
""" Pauses or resumes the torrent specified by the ID field
in release_download.
@ -243,45 +219,85 @@ class Hadouken(DownloaderBase):
return self.hadouken_api.remove(release_download['id'], remove_data = delete_files)
class HadoukenAPI(object):
def __init__(self, host = 'localhost', port = 7890, api_key = None):
self.url = 'http://' + str(host) + ':' + str(port)
self.api_key = api_key
self.requestId = 0;
class JsonRpcClient(object):
def __init__(self, url, auth_header = None):
self.url = url
self.requestId = 0
self.opener = urllib2.build_opener()
self.opener.addheaders = [('User-agent', 'couchpotato-hadouken-client/1.0'), ('Accept', 'application/json')]
self.opener.addheaders = [
('User-Agent', 'couchpotato-hadouken-client/1.0'),
('Accept', 'application/json'),
('Content-Type', 'application/json')
]
if auth_header:
self.opener.addheaders.append(('Authorization', auth_header))
def invoke(self, method, params):
self.requestId += 1
if not api_key:
log.error('API key missing.')
data = {
'jsonrpc': '2.0',
'id': self.requestId,
'method': method,
'params': params
}
request = urllib2.Request(self.url, data = json.dumps(data))
try:
f = self.opener.open(request)
response = f.read()
f.close()
obj = json.loads(response)
if 'error' in obj.keys():
log.error('JSONRPC error, %s: %s', obj['error']['code'], obj['error']['message'])
return False
def add_file(self, filedata, torrent_params):
if 'result' in obj.keys():
return obj['result']
return True
except httplib.InvalidURL as err:
log.error('Invalid Hadouken host, check your config %s', err)
except urllib2.HTTPError as err:
if err.code == 401:
log.error('Could not authenticate, check your config')
else:
log.error('Hadouken HTTPError: %s', err)
except urllib2.URLError as err:
log.error('Unable to connect to Hadouken %s', err)
return False
class HadoukenAPI(object):
def __init__(self, rpc_client):
self.rpc = rpc_client
if not rpc_client:
log.error('No JSONRPC client specified.')
def add_file(self, data, params):
""" Add a file to Hadouken with the specified parameters.
Keyword arguments:
filedata -- The binary torrent data.
torrent_params -- Additional parameters for the file.
"""
data = {
'method': 'torrents.addFile',
'params': [b64encode(filedata), torrent_params]
}
return self._request(data)
pass
def add_magnet_link(self, magnetLink, torrent_params):
def add_magnet_link(self, link, params):
""" Add a magnet link to Hadouken with the specified parameters.
Keyword arguments:
magnetLink -- The magnet link to send.
torrent_params -- Additional parameters for the magnet link.
"""
data = {
'method': 'torrents.addUrl',
'params': [magnetLink, torrent_params]
}
return self._request(data)
pass
def get_by_hash_list(self, infoHashList):
""" Gets a list of torrents filtered by the given info hash list.
@ -289,12 +305,7 @@ class HadoukenAPI(object):
Keyword arguments:
infoHashList -- A list of info hashes.
"""
data = {
'method': 'torrents.getByInfoHashList',
'params': [infoHashList]
}
return self._request(data)
pass
def get_files_by_hash(self, infoHash):
""" Gets a list of files for the torrent identified by the
@ -303,26 +314,11 @@ class HadoukenAPI(object):
Keyword arguments:
infoHash -- The info hash of the torrent to return files for.
"""
data = {
'method': 'torrents.getFiles',
'params': [infoHash]
}
return self._request(data)
pass
def get_version(self):
""" Gets the version, commitish and build date of Hadouken. """
data = {
'method': 'core.getVersion',
'params': None
}
result = self._request(data)
if not result:
return False
return result['Version']
pass
def pause(self, infoHash, pause):
""" Pauses/unpauses the torrent identified by the given info hash.
@ -331,15 +327,7 @@ class HadoukenAPI(object):
infoHash -- The info hash of the torrent to operate on.
pause -- If true, pauses the torrent. Otherwise resumes.
"""
data = {
'method': 'torrents.pause',
'params': [infoHash]
}
if not pause:
data['method'] = 'torrents.resume'
return self._request(data)
pass
def remove(self, infoHash, remove_data = False):
""" Removes the torrent identified by the given info hash and
@ -349,46 +337,190 @@ class HadoukenAPI(object):
infoHash -- The info hash of the torrent to remove.
remove_data -- If true, removes the data associated with the torrent.
"""
data = {
'method': 'torrents.remove',
'params': [infoHash, remove_data]
}
pass
return self._request(data)
class TorrentItem(object):
@property
def info_hash(self):
pass
def _request(self, data):
self.requestId += 1
@property
def save_path(self):
pass
data['jsonrpc'] = '2.0'
data['id'] = self.requestId
@property
def name(self):
pass
request = urllib2.Request(self.url + '/jsonrpc', data = json.dumps(data))
request.add_header('Authorization', 'Token ' + self.api_key)
request.add_header('Content-Type', 'application/json')
@property
def state(self):
pass
try:
f = self.opener.open(request)
response = f.read()
f.close()
def get_status(self):
""" Returns the CouchPotato status for a given torrent."""
pass
obj = json.loads(response)
def get_seed_ratio(self):
""" Returns the seed ratio for a given torrent."""
pass
if not 'error' in obj.keys():
return obj['result']
log.error('JSONRPC error, %s: %s', obj['error']['code'], obj['error']['message'])
except httplib.InvalidURL as err:
log.error('Invalid Hadouken host, check your config %s', err)
except urllib2.HTTPError as err:
if err.code == 401:
log.error('Invalid Hadouken API key, check your config')
else:
log.error('Hadouken HTTPError: %s', err)
except urllib2.URLError as err:
log.error('Unable to connect to Hadouken %s', err)
class TorrentItemv5(TorrentItem):
def __init__(self, obj):
self.obj = obj
def info_hash(self):
return self.obj['infoHash']
def save_path(self):
return self.obj['savePath']
def name(self):
return self.obj['name']
def state(self):
return self.obj['state']
def get_status(self):
if self.obj['isSeeding'] and self.obj['isFinished'] and self.obj['isPaused']:
return 'completed'
if self.obj['isSeeding']:
return 'seeding'
return 'busy'
def get_seed_ratio(self):
up = self.obj['uploadedBytesTotal']
down = self.obj['downloadedBytesTotal']
if up > 0 and down > 0:
return up / down
return 0
class HadoukenAPIv5(HadoukenAPI):
def add_file(self, data, params):
return self.rpc.invoke('session.addTorrentFile', [b64encode(data), params])
def add_magnet_link(self, link, params):
return self.rpc.invoke('session.addTorrentUri', [link, params])
def get_by_hash_list(self, infoHashList):
torrents = self.rpc.invoke('session.getTorrents')
result = []
for torrent in torrents.values():
if torrent['infoHash'] in infoHashList:
result.append(TorrentItemv5(torrent))
return result
def get_files_by_hash(self, infoHash):
files = self.rpc.invoke('torrent.getFiles', [infoHash])
result = []
for file in files:
result.append(file['path'])
return result
def get_version(self):
result = self.rpc.invoke('core.getSystemInfo', None)
if not result:
return False
return result['versions']['hadouken']
def pause(self, infoHash, pause):
if pause:
return self.rpc.invoke('torrent.pause', [infoHash])
return self.rpc.invoke('torrent.resume', [infoHash])
def remove(self, infoHash, remove_data = False):
return self.rpc.invoke('session.removeTorrent', [infoHash, remove_data])
return False
class TorrentItemv4(TorrentItem):
def __init__(self, obj):
self.obj = obj
def info_hash(self):
return self.obj['InfoHash']
def save_path(self):
return self.obj['SavePath']
def name(self):
return self.obj['Name']
def state(self):
return self.obj['State']
def get_status(self):
if self.obj['IsSeeding'] and self.obj['IsFinished'] and self.obj['Paused']:
return 'completed'
if self.obj['IsSeeding']:
return 'seeding'
return 'busy'
def get_seed_ratio(self):
up = self.obj['TotalUploadedBytes']
down = self.obj['TotalDownloadedBytes']
if up > 0 and down > 0:
return up / down
return 0
class HadoukenAPIv4(object):
def add_file(self, data, params):
return self.rpc.invoke('torrents.addFile', [b64encode(data), params])
def add_magnet_link(self, link, params):
return self.rpc.invoke('torrents.addUrl', [link, params])
def get_by_hash_list(self, infoHashList):
torrents = self.rpc.invoke('torrents.getByInfoHashList', [infoHashList])
result = []
for torrent in torrents:
result.append(TorrentItemv4(torrent))
return result
def get_files_by_hash(self, infoHash):
files = self.rpc.invoke('torrents.getFiles', [infoHash])
result = []
for file in files:
result.append(file['Path'])
return result
def get_version(self):
result = self.rpc.invoke('core.getVersion', None)
if not result:
return False
return result['Version']
def pause(self, infoHash, pause):
if pause:
return self.rpc.invoke('torrents.pause', [infoHash])
return self.rpc.invoke('torrents.resume', [infoHash])
def remove(self, infoHash, remove_data = False):
return self.rpc.invoke('torrents.remove', [infoHash, remove_data])
config = [{
@ -409,15 +541,42 @@ config = [{
'radio_group': 'torrent'
},
{
'name': 'version',
'label': 'Version',
'type': 'dropdown',
'default': 'v4',
'values': [('v4.x', 'v4'), ('v5.x', 'v5')],
'description': 'Hadouken version.',
},
{
'name': 'host',
'default': 'localhost:7890'
},
{
'name': 'auth_type',
'label': 'Auth. type',
'type': 'dropdown',
'default': 'api_key',
'values': [('None', 'none'), ('API key/Token', 'api_key'), ('Username/Password', 'user_pass')],
'description': 'Type of authentication',
},
{
'name': 'api_key',
'label': 'API key',
'label': 'API key (v4)/Token (v5)',
'type': 'password'
},
{
'name': 'auth_user',
'label': 'Username',
'description': '(only for v5)'
},
{
'name': 'auth_pass',
'label': 'Password',
'type': 'password',
'description': '(only for v5)'
},
{
'name': 'label',
'description': 'Label to add torrent as.'
}

4
couchpotato/core/downloaders/nzbget.py

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

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

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

5
couchpotato/core/downloaders/sabnzbd.py

@ -73,10 +73,11 @@ class Sabnzbd(DownloaderBase):
return False
log.debug('Result from SAB: %s', sab_data)
if sab_data.get('status') and not sab_data.get('error'):
nzo_ids = sab_data.get('nzo_ids', [])
if sab_data.get('status') and not sab_data.get('error') and isinstance(nzo_ids, list) and len(nzo_ids) > 0:
log.info('NZB sent to SAB successfully.')
if filedata:
return self.downloadReturnId(sab_data.get('nzo_ids')[0])
return self.downloadReturnId(nzo_ids[0])
else:
return True
else:

4
couchpotato/core/downloaders/transmission.py

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

3
couchpotato/core/helpers/variable.py

@ -41,7 +41,8 @@ def symlink(src, dst):
def getUserDir():
try:
import pwd
os.environ['HOME'] = sp(pwd.getpwuid(os.geteuid()).pw_dir)
if not os.environ['HOME']:
os.environ['HOME'] = sp(pwd.getpwuid(os.geteuid()).pw_dir)
except:
pass

7
couchpotato/core/media/__init__.py

@ -88,8 +88,13 @@ class MediaBase(Plugin):
if len(existing_files) == 0:
del existing_files[file_type]
images = image_urls.get(image_type, [])
for y in ['SX300', 'tmdb']:
initially_try = [x for x in images if y in x]
images[:-1] = initially_try
# Loop over type
for image in image_urls.get(image_type, []):
for image in images:
if not isinstance(image, (str, unicode)):
continue

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

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

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

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

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

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

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

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

@ -16,9 +16,9 @@ class Base(TorrentProvider):
urls = {
'test': 'https://iptorrents.eu/',
'base_url': 'https://iptorrents.eu',
'login': 'https://iptorrents.eu/torrents/',
'login': 'https://iptorrents.eu/',
'login_check': 'https://iptorrents.eu/inbox.php',
'search': 'https://iptorrents.eu/torrents/?%s%%s&q=%s&qf=ti&p=%%d',
'search': 'https://iptorrents.eu/t?%s%%s&q=%s&qf=#torrents&p=%%d',
}
http_time_between_calls = 1 # Seconds

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

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

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
proxy_list = [
'https://dieroschtibay.org',
'https://thebay.al',
'https://thepiratebay.se',
'http://thepiratebay.se.net',
'http://thebootlegbay.com',
'http://tpb.ninja.so',
'https://thepiratebay.mn',
'https://thepiratebay.gd',
'https://thepiratebay.bg',
'https://thepiratebay.la',
'https://thepiratebay.am',
'https://thepiratebay.gs',
'http://proxybay.fr',
'http://pirateproxy.in',
'http://piratebay.skey.sk',
'http://pirateproxy.be',
'http://bayproxy.li',
'http://proxybay.pw',
'https://pirateproxy.sx',
'https://tpbproxy.co',
'https://arrr.xyz',
'http://tpb.dashitz.com',
]
def _search(self, media, quality, results):

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

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

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

@ -18,9 +18,14 @@ class Base(TorrentProvider):
http_time_between_calls = 1 # seconds
proxy_list = [
'https://yts.re',
'https://yts.wf',
'https://yts.im',
'https://yts.to',
'https://yify.ml',
'https://yify.link',
'https://yifytorrent.link',
'https://yts.ch',
'https://yts.click',
'https://yify.me',
]
def search(self, movie, quality):
@ -38,7 +43,7 @@ class Base(TorrentProvider):
search_url = self.urls['search'] % (domain, getIdentifier(movie))
data = self.getJsonData(search_url)
data = self.getJsonData(search_url) or {}
data = data.get('data')
if isinstance(data, dict) and data.get('movies'):

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

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

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

@ -34,6 +34,7 @@ class MovieBase(MovieTypeBase):
'params': {
'identifier': {'desc': 'IMDB id of the movie your want to add.'},
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
'force_readd': {'desc': 'Force re-add even if movie already in wanted or manage. Default: True'},
'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'},
'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
}
@ -78,6 +79,11 @@ class MovieBase(MovieTypeBase):
if not info or (info and len(info.get('titles', [])) == 0):
info = fireEvent('movie.info', merge = True, extended = False, identifier = params.get('identifier'))
# Allow force re-add overwrite from param
if 'force_readd' in params:
fra = params.get('force_readd')
force_readd = fra.lower() not in ['0', '-1'] if not isinstance(fra, bool) else fra
# Set default title
default_title = toUnicode(info.get('title'))
titles = info.get('titles', [])
@ -224,11 +230,11 @@ class MovieBase(MovieTypeBase):
try:
m = db.get('id', media_id)
m['profile_id'] = kwargs.get('profile_id')
m['profile_id'] = kwargs.get('profile_id') or m['profile_id']
cat_id = kwargs.get('category_id')
if cat_id is not None:
m['category_id'] = cat_id if len(cat_id) > 0 else None
m['category_id'] = cat_id if len(cat_id) > 0 else m['category_id']
# Remove releases
for rel in fireEvent('release.for_media', m['_id'], single = True):
@ -249,6 +255,7 @@ class MovieBase(MovieTypeBase):
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id))
except:
print traceback.format_exc()
log.error('Can\'t edit non-existing media')
return {

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

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

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

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

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

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

@ -1,4 +1,4 @@
Page.Wanted = new Class({
var MoviesWanted = new Class({
Extends: PageBase,
@ -10,7 +10,7 @@ Page.Wanted = new Class({
indexAction: function(){
var self = this;
if(!self.wanted){
if(!self.list){
self.manual_search = new Element('a', {
'title': 'Force a search for the full wanted list',
@ -20,25 +20,24 @@ Page.Wanted = new Class({
}
});
self.scan_folder = new Element('a', {
'title': 'Scan a folder and rename all movies in it',
'text': 'Manual folder scan',
'events':{
'click': self.scanFolder.bind(self)
}
});
self.scan_folder = new Element('a', {
'title': 'Scan a folder and rename all movies in it',
'text': 'Manual folder scan',
'events':{
'click': self.scanFolder.bind(self)
}
});
// Wanted movies
self.wanted = new MovieList({
self.list = new MovieList({
'identifier': 'wanted',
'status': 'active',
'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Edit, MA.Refresh, MA.Readd, MA.Delete],
'actions': [MA.IMDB, MA.Release, MA.Trailer, MA.Refresh, MA.Readd, MA.Delete, MA.Category, MA.Profile],
'add_new': true,
'menu': [self.manual_search, self.scan_folder],
'on_empty_element': App.createUserscriptButtons().addClass('empty_wanted')
});
$(self.wanted).inject(self.el);
$(self.list).inject(self.content);
// Check if search is in progress
self.startProgressInterval.delay(4000, self);
@ -82,43 +81,55 @@ Page.Wanted = new Class({
},
scanFolder: function(e) {
(e).stop();
var self = this;
var options = {
'name': 'Scan_folder'
};
scanFolder: function(e) {
(e).stop();
if(!self.folder_browser){
self.folder_browser = new Option['Directory']("Scan", "folder", "", options);
self.folder_browser.save = function() {
var folder = self.folder_browser.getValue();
Api.request('renamer.scan', {
'data': {
'base_folder': folder
var self = this;
var options = {
'name': 'Scan_folder'
};
if(!self.folder_browser){
self.folder_browser = new Option.Directory("Scan", "folder", "", options);
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.fireEvent('injected');
// Hide the settings box
self.folder_browser.directory_inlay.hide();
self.folder_browser.el.removeChild(self.folder_browser.el.firstChild);
self.folder_browser.showBrowser();
// Make adjustments to the browser
self.folder_browser.browser.getElements('.clear.button').hide();
self.folder_browser.save_button.text = "Select";
self.folder_browser.browser.style.zIndex=1000;
}
else{
self.folder_browser.showBrowser();
}
}
};
self.folder_browser.inject(self.content, 'top');
self.folder_browser.fireEvent('injected');
// Hide the settings box
self.folder_browser.directory_inlay.hide();
self.folder_browser.el.removeChild(self.folder_browser.el.firstChild);
self.folder_browser.showBrowser();
// Make adjustments to the browser
self.folder_browser.browser.getElements('.clear.button').hide();
self.folder_browser.save_button.text = "Select";
self.folder_browser.browser.setStyles({
'z-index': 1000,
'right': 20,
'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',
'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.api import addApiView
from couchpotato.core.event import addEvent,fireEvent
from couchpotato.core.event import fireEvent
from couchpotato.core.plugins.base import Plugin
@ -11,51 +13,72 @@ log = CPLog(__name__)
class Charts(Plugin):
update_in_progress = False
update_interval = 72 # hours
def __init__(self):
addApiView('charts.view', self.automationView)
addEvent('app.load', self.setCrons)
def setCrons(self):
fireEvent('schedule.interval', 'charts.update_cache', self.updateViewCache, hours = self.update_interval)
addApiView('charts.ignore', self.ignoreView)
def automationView(self, force_update = False, **kwargs):
if force_update:
charts = self.updateViewCache()
else:
charts = self.getCache('charts_cached')
if not charts:
charts = self.updateViewCache()
db = get_db()
charts = fireEvent('automation.get_chart_list', merge = True)
ignored = splitString(Env.prop('charts_ignore', default = ''))
# 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 {
'success': True,
'count': len(charts),
'charts': charts
'charts': charts,
'ignored': ignored,
}
def updateViewCache(self):
if self.update_in_progress:
while self.update_in_progress:
time.sleep(1)
catched_charts = self.getCache('charts_cached')
if catched_charts:
return catched_charts
charts = []
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
def ignoreView(self, imdb = None, **kwargs):
ignored = splitString(Env.prop('charts_ignore', default = ''))
if imdb:
ignored.append(imdb)
Env.prop('charts_ignore', ','.join(set(ignored)))
return {
'result': True
}

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();
}
else
self.el.hide();
self.show();
self.fireEvent.delay(0, self, 'created');
},
@ -59,94 +54,39 @@ var Charts = new Class({
self.el_refreshing_text.hide();
self.el_refresh_link.show();
if(!json || json.count == 0){
if(!json || json.count === 0){
self.el_no_charts_enabled.show();
self.el_refresh_link.show();
self.el_refreshing_text.hide();
self.el_refresh_link.show();
self.el_refreshing_text.hide();
}
else {
self.el_no_charts_enabled.hide();
json.charts.sort(function(a, b) {
json.charts.sort(function(a, b) {
return a.order - b.order;
});
Object.each(json.charts, function(chart){
var c = new Element('div.chart.tiny_scroll').grab(
new Element('h3').grab( new Element('a', {
'text': chart.name,
'href': chart.url
}))
);
var it = 1;
Object.each(chart.list, function(movie){
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);
var chart_list = new MovieList({
'navigation': false,
'identifier': chart.name.toLowerCase().replace(/[^a-z0-9]+/g, '_'),
'title': chart.name,
'description': '<a href="'+chart.url+'">See source</a>',
'actions': [MA.Add, MA.ChartIgnore, MA.IMDB, MA.Trailer],
'load_more': false,
'view': 'thumb',
'force_view': true,
'api_call': null
});
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();
if(!self.shown_once){
self.api_request = Api.request('charts.view', {
'onComplete': self.fill.bind(self)
});
setTimeout(function(){
self.api_request = Api.request('charts.view', {
'onComplete': self.fill.bind(self)
});
}, 100);
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(){
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'):
cookie = {'Cookie': 'listlayout_7=full'}
page = 0
while True:
page += 1
url = self.backlog_url % page
data = self.getHTMLData(url)
data = self.getHTMLData(url, headers = cookie)
soup = BeautifulSoup(data)
try:
@ -104,41 +105,49 @@ class Bluray(Automation, RSS):
return movies
def getChartList(self):
# Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id)
movie_list = {'name': 'Blu-ray.com - New Releases', 'url': self.display_url, 'order': self.chart_order, 'list': []}
movie_ids = []
max_items = int(self.conf('max_items', section='charts', default=5))
rss_movies = self.getRSSData(self.rss_url)
cache_key = 'bluray.charts'
movie_list = {
'name': 'Blu-ray.com - New Releases',
'url': self.display_url,
'order': self.chart_order,
'list': self.getCache(cache_key) or []
}
for movie in rss_movies:
name = self.getTextElement(movie, 'title').lower().split('blu-ray')[0].strip('(').rstrip()
year = self.getTextElement(movie, 'description').split('|')[1].strip('(').strip()
if not movie_list['list']:
movie_ids = []
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
continue
for movie in rss_movies:
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:
continue
if movie:
is_movie = fireEvent('movie.is_movie', identifier = movie.get('imdb'), single = True)
if not is_movie:
continue
if movie.get('imdb') in movie_ids:
continue
movie_ids.append(movie.get('imdb'))
movie_list['list'].append( movie )
if len(movie_list['list']) >= max_items:
break
is_movie = fireEvent('movie.is_movie', identifier = movie.get('imdb'), single = True)
if not is_movie:
continue
if not movie_list['list']:
return
movie_ids.append(movie.get('imdb'))
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 = [{

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):
def getTypes(self):
return [IMDBWatchlist, IMDBAutomation]
return [IMDBWatchlist, IMDBAutomation, IMDBCharts]
class IMDBBase(Automation, RSS):
@ -126,30 +126,6 @@ class IMDBAutomation(IMDBBase):
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):
movies = []
@ -175,20 +151,53 @@ class IMDBAutomation(IMDBBase):
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)
movie_lists = []
max_items = int(self.conf('max_items', section = 'charts', default=5))
for name in self.charts:
chart = self.charts[name].copy()
url = chart.get('url')
cache_key = 'imdb.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)
try:
@ -206,10 +215,11 @@ class IMDBAutomation(IMDBBase):
except:
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']:
movie_lists.append(chart)
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.logger import CPLog
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)
for movie in rss_movies:
imdb = self.search(self.getTextElement(movie, 'title'))
title = self.getTextElement(movie, 'title')
name_year = fireEvent('scanner.name_year', title, single = True)
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)
if imdb and self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
return movies

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

@ -17,11 +17,12 @@ class PopularMovies(Automation):
movies = []
retrieved_movies = self.getJsonData(self.url)
for movie in retrieved_movies.get('movies'):
imdb_id = movie.get('imdb_id')
info = fireEvent('movie.info', identifier = imdb_id, extended = False, merge = True)
if self.isMinimalMovie(info):
movies.append(imdb_id)
if retrieved_movies:
for movie in retrieved_movies.get('movies'):
imdb_id = movie.get('imdb_id')
info = fireEvent('movie.info', identifier = imdb_id, extended = False, merge = True)
if self.isMinimalMovie(info):
movies.append(imdb_id)
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 = ''
try:
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:
log.debug('Failed getting %s.%s for "%s"', (type, size, ss(str(movie))))
@ -196,7 +197,7 @@ class TheMovieDb(MovieProvider):
params = tryUrlencode(params)
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)
except:
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
autoload = 'Filmweb'
class Filmweb(UserscriptBase):
version = 2
version = 3
includes = ['http://www.filmweb.pl/film/*']
@ -21,14 +21,10 @@ class Filmweb(UserscriptBase):
except:
return
name = re.search("<h2.*?class=\"text-large caption\">(?P<name>[^<]+)</h2>", data)
if name is None:
name = re.search("<a.*?property=\"v:name\".*?>(?P<name>[^<]+)</a>", data)
name = name.group('name').decode('string_escape')
year = re.search("<span.*?id=filmYear.*?>\((?P<year>[^\)]+)\).*?</span>", data)
year = year.group('year')
html = BeautifulSoup(data)
name = html.find('meta', {'name': 'title'})['content'][:-9].strip()
name_year = fireEvent('scanner.name_year', name, single = True)
name = name_year.get('name')
year = name_year.get('year')
return self.search(name, year)

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

@ -12,6 +12,8 @@ autoload = 'Flickchart'
class Flickchart(UserscriptBase):
version = 2
includes = ['http://www.flickchart.com/movie/*']
def getMovie(self, url):
@ -24,11 +26,11 @@ class Flickchart(UserscriptBase):
try:
start = data.find('<title>')
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)
return self.search(**year_name)
return self.search(year_name.get('name'), year_name.get('year'))
except:
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
autoload = 'MovieMeter'
@ -6,3 +7,16 @@ autoload = 'MovieMeter'
class MovieMeter(UserscriptBase):
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 traceback
from couchpotato import fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
@ -15,7 +16,7 @@ class RottenTomatoes(UserscriptBase):
includes = ['*://www.rottentomatoes.com/m/*']
excludes = ['*://www.rottentomatoes.com/m/*/*/']
version = 2
version = 4
def getMovie(self, url):
@ -25,16 +26,12 @@ class RottenTomatoes(UserscriptBase):
return
try:
name = None
year = None
metas = re.findall("property=\"(video:release_date|og:title)\" content=\"([^\"]*)\"", data)
for meta in metas:
mname, mvalue = meta
if mname == 'og:title':
name = mvalue.decode('unicode_escape')
elif mname == 'video:release_date':
year = mvalue[:4]
title = re.findall("<title>(.*)</title>", data)
title = title[0].split(' - Rotten')[0].replace('&nbsp;', ' ').decode('unicode_escape')
name_year = fireEvent('scanner.name_year', title, single = True)
name = name_year.get('name')
year = name_year.get('year')
if name and 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):
includes = ['http://www.themoviedb.org/movie/*']
version = 2
includes = ['*://www.themoviedb.org/movie/*']
def getMovie(self, 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.core.event import fireEvent
from couchpotato.core.helpers.variable import splitString, removeDuplicate, getIdentifier
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.variable import splitString, removeDuplicate, getIdentifier, getTitle
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
@ -15,6 +16,12 @@ class Suggestion(Plugin):
addApiView('suggestion.view', self.suggestView)
addApiView('suggestion.ignore', self.ignoreView)
def test():
time.sleep(1)
self.suggestView()
addEvent('app.load', test)
def suggestView(self, limit = 6, **kwargs):
movies = splitString(kwargs.get('movies', ''))
@ -38,10 +45,31 @@ class Suggestion(Plugin):
suggestions = fireEvent('movie.suggest', movies = movies, ignore = ignored, single = True)
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 {
'success': True,
'count': len(suggestions),
'suggestions': suggestions[:int(limit)]
'movies': medias
}
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)
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 {
'result': True,
'ignore_count': len(ignored),
'suggestions': new_suggestions[limit - 1:limit]
'movie': media
}
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 = [];
App.addEvent('load', function(){
App.block.notification = new Block.Menu(self, {
'button_class': 'icon2.eye-open',
App.block.notification = new BlockMenu(self, {
'button_class': 'icon-notifications',
'class': 'notification_menu',
'onOpen': self.markAsRead.bind(self)
});
@ -32,7 +32,7 @@ var NotificationBase = new Class({
window.addEvent('load', function(){
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.message', {'html': result.message}),
new Element('span.added', {'text': added.timeDiffInWords(), 'title': added})
)
, 'top');
), 'top');
self.notifications.include(result);
if((result.important !== undefined || result.sticky !== undefined) && !result.read){
var sticky = true;
App.trigger('message', [result.message, sticky, result])
App.trigger('message', [result.message, sticky, result]);
}
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){
var self = this;
self.badge.set('text', value);
self.badge[value ? 'show' : 'hide']()
self.badge[value ? 'show' : 'hide']();
},
markAsRead: function(force_ids){
@ -72,13 +71,13 @@ var NotificationBase = new Class({
if(!force_ids) {
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){
ids.include(n._id)
})
ids.include(n._id);
});
}
if(ids.length > 0)
@ -87,9 +86,9 @@ var NotificationBase = new Class({
'ids': ids.join(',')
},
'onSuccess': function(){
self.setBadge('')
self.setBadge('');
}
})
});
},
@ -102,9 +101,9 @@ var NotificationBase = new Class({
}
self.request = Api.request('notification.listener', {
'data': {'init':true},
'onSuccess': function(json){
self.processData(json, true)
'data': {'init':true},
'onSuccess': function(json){
self.processData(json, true);
}
}).send();
@ -112,7 +111,7 @@ var NotificationBase = new Class({
if(self.request && self.request.isRunning()){
self.request.cancel();
self.startPoll()
self.startPoll();
}
}, 120000);
@ -129,16 +128,16 @@ var NotificationBase = new Class({
self.request.cancel();
self.request = Api.request('nonblock/notification.listener', {
'onSuccess': function(json){
self.processData(json, false)
'onSuccess': function(json){
self.processData(json, false);
},
'data': {
'last_id': self.last_id
},
'onFailure': function(){
self.startPoll.delay(2000, self)
}
}).send()
'data': {
'last_id': self.last_id
},
'onFailure': function(){
self.startPoll.delay(2000, self);
}
}).send();
},
@ -160,7 +159,7 @@ var NotificationBase = new Class({
});
if(json.result.length > 0)
self.last_id = json.result.getLast().message_id
self.last_id = json.result.getLast().message_id;
}
// Restart poll
@ -175,11 +174,11 @@ var NotificationBase = new Class({
var new_message = new Element('div', {
'class': 'message' + (sticky ? ' sticky' : ''),
'html': message
'html': '<div class="inner">' + message + '</div>'
}).inject(self.message_container, 'top');
setTimeout(function(){
new_message.addClass('show')
new_message.addClass('show');
}, 10);
var hide_message = function(){
@ -211,8 +210,8 @@ var NotificationBase = new Class({
var setting_page = App.getPage('Settings');
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;
new Element('.ctrlHolder.test_button').adopt(
new Element('.ctrlHolder.test_button').grab(
new Element('a.button', {
'text': button_name,
'events': {
@ -235,20 +234,21 @@ var NotificationBase = new Class({
button.set('text', button_name);
var message;
if(json.success){
var message = new Element('span.success', {
message = new Element('span.success', {
'text': 'Notification successful'
}).inject(button, 'after')
}).inject(button, 'after');
}
else {
var message = new Element('span.failed', {
message = new Element('span.failed', {
'text': 'Notification failed. Check logs for details.'
}).inject(button, 'after')
}).inject(button, 'after');
}
(function(){
message.destroy();
}).delay(3000)
}).delay(3000);
}
});
}
@ -258,7 +258,7 @@ var NotificationBase = new Class({
},
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;
}

14
couchpotato/core/notifications/email_.py

@ -31,12 +31,12 @@ class Email(Notification):
starttls = self.conf('starttls')
# Make the basic message
message = MIMEText(toUnicode(message), _charset = Env.get('encoding'))
message['Subject'] = self.default_title
message['From'] = from_address
message['To'] = to_address
message['Date'] = formatdate(localtime = 1)
message['Message-ID'] = make_msgid()
email = MIMEText(toUnicode(message), _charset = Env.get('encoding'))
email['Subject'] = '%s: %s' % (self.default_title, toUnicode(message))
email['From'] = from_address
email['To'] = to_address
email['Date'] = formatdate(localtime = 1)
email['Message-ID'] = make_msgid()
try:
# Open the SMTP connection, via SSL if requested
@ -58,7 +58,7 @@ class Email(Notification):
# Send the e-mail
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
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):
if not data: data = {}
devices = self.getDevices()
if devices is None:
return False
# Get all the device IDs linked to this user
if not len(devices):
devices = [None]
devices = self.getDevices() or []
successful = 0
for device in devices:
response = self.request(
@ -43,17 +37,30 @@ class Pushbullet(Notification):
else:
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)
def getDevices(self):
return splitString(self.conf('devices'))
def getChannels(self):
return splitString(self.conf('channels'))
def request(self, method, cache = True, **kwargs):
try:
base64string = base64.encodestring('%s:' % self.conf('api_key'))[:-1]
headers = {
"Authorization": "Basic %s" % base64string
'Authorization': 'Basic %s' % base64string
}
if cache:
@ -94,6 +101,12 @@ config = [{
'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',
'default': 0,
'type': 'bool',

31
couchpotato/core/notifications/pushover.py

@ -1,6 +1,4 @@
from httplib import HTTPSConnection
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import getTitle, getIdentifier
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
@ -13,12 +11,11 @@ autoload = 'Pushover'
class Pushover(Notification):
api_url = 'https://api.pushover.net'
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
http_handler = HTTPSConnection("api.pushover.net:443")
api_data = {
'user': self.conf('user_key'),
'token': self.conf('api_token'),
@ -33,25 +30,17 @@ class Pushover(Notification):
'url_title': toUnicode('%s on IMDb' % getTitle(data)),
})
http_handler.request('POST', '/1/messages.json',
headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = tryUrlencode(api_data)
)
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
log.info('Pushover notifications sent.')
try:
data = self.urlopen('%s/%s' % (self.api_url, '1/messages.json'),
headers = {'Content-type': 'application/x-www-form-urlencoded'},
data = api_data)
log.info2('Pushover responded with: %s', data)
return True
elif request_status == 401:
log.error('Pushover auth failed: %s', response.reason)
return False
else:
log.error('Pushover notification failed: %s', request_status)
except:
return False
config = [{
'name': 'pushover',
'groups': [
@ -79,7 +68,7 @@ config = [{
'name': 'priority',
'default': 0,
'type': 'dropdown',
'values': [('Normal', 0), ('High', 1)],
'values': [('Lowest', -2), ('Low', -1), ('Normal', 0), ('High', 1)],
},
{
'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.logger import CPLog
from couchpotato.core.media.movie.providers.automation.trakt.main import TraktBase
from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
@ -7,65 +8,37 @@ log = CPLog(__name__)
autoload = 'Trakt'
class Trakt(Notification):
class Trakt(Notification, TraktBase):
urls = {
'base': 'http://api.trakt.tv/%s',
'library': 'movie/library/%s',
'unwatchlist': 'movie/unwatchlist/%s',
'test': 'account/test/%s',
'library': 'sync/collection',
'unwatchlist': 'sync/watchlist/remove',
'test': 'sync/last_activities',
}
listen_to = ['movie.snatched']
listen_to = ['renamer.after']
enabled_option = 'notification_enabled'
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
if listener == '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)
result = self.call((self.urls['test']))
return result
else:
post_data = {
'username': self.conf('automation_username'),
'password': self.conf('automation_password'),
'movies': [{
'imdb_id': getIdentifier(data),
'title': getTitle(data),
'year': data['info']['year']
}] if data else []
'movies': [{'ids': {'imdb': getIdentifier(data)}}] 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'):
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
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 = [{
'name': 'trakt',
@ -75,7 +48,7 @@ config = [{
'list': 'notification_providers',
'name': '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': [
{
'name': 'notification_enabled',

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

@ -16,7 +16,7 @@ var TwitterNotification = new Class({
var twitter_set = 0;
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');
})
});
}

28
couchpotato/core/notifications/xbmc.py

@ -83,7 +83,7 @@ class XBMC(Notification):
# v6 (as of XBMC v12(Frodo)) is required to send notifications
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
self.use_json_notifications[host] = False
@ -96,7 +96,7 @@ class XBMC(Notification):
success = True
break
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
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']['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
self.use_json_notifications[host] = True
@ -119,12 +119,12 @@ class XBMC(Notification):
success = True
break
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
# error getting version info (we do have contact with XBMC though)
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)
@ -173,10 +173,10 @@ class XBMC(Notification):
return [{'result': 'Error'}]
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'}]
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'}]
def request(self, host, do_requests):
@ -209,10 +209,10 @@ class XBMC(Notification):
return response
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 []
except:
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
log.error('Failed sending request to Kodi: %s', traceback.format_exc())
return []
@ -223,8 +223,8 @@ config = [{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'xbmc',
'label': 'XBMC',
'description': 'v11 (Eden), v12 (Frodo), v13 (Gotham)',
'label': 'Kodi',
'description': 'v14 (Helix), v15 (Isengard)',
'options': [
{
'name': 'enabled',
@ -249,7 +249,7 @@ config = [{
'default': 0,
'type': 'bool',
'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',
@ -257,7 +257,7 @@ config = [{
'default': 0,
'type': 'bool',
'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',
@ -265,7 +265,7 @@ config = [{
'default': 0,
'type': 'bool',
'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',

92
couchpotato/core/plugins/base.py

@ -1,17 +1,14 @@
import threading
from urllib import quote
from urllib import quote, getproxies
from urlparse import urlparse
import glob
import inspect
import os.path
import re
import time
import traceback
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import ss, toSafeString, \
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
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
@ -19,8 +16,6 @@ import requests
from requests.packages.urllib3 import Timeout
from requests.packages.urllib3.exceptions import MaxRetryError
from tornado import template
from tornado.web import StaticFileHandler
log = CPLog(__name__)
@ -32,7 +27,6 @@ class Plugin(object):
plugin_path = None
enabled_option = 'enabled'
auto_register_static = True
_needs_shutdown = False
_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'
http_last_use = {}
http_last_use_queue = {}
http_time_between_calls = 0
http_failed_request = {}
http_failed_disabled = {}
@ -56,9 +51,6 @@ class Plugin(object):
addEvent('plugin.running', self.isRunning)
self._running = []
if self.auto_register_static:
self.registerStatic(inspect.getfile(self.__class__))
# Setup database
if self._database:
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())
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):
path = sp(path)
@ -144,7 +110,7 @@ class Plugin(object):
f.close()
os.chmod(path, Env.getPermission('file'))
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):
os.remove(path)
@ -199,6 +165,23 @@ class Plugin(object):
headers['Connection'] = headers.get('Connection', 'keep-alive')
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')
# Don't try for failed requests
@ -213,7 +196,7 @@ class Plugin(object):
del self.http_failed_request[host]
del self.http_failed_disabled[host]
self.wait(host)
self.wait(host, url)
status_code = None
try:
@ -224,6 +207,7 @@ class Plugin(object):
'files': files,
'verify': False, #verify_ssl, Disable for now as to many wrongly implemented certificates..
'stream': stream,
'proxies': proxy_url,
}
method = 'post' if len(data) > 0 or files else 'get'
@ -267,20 +251,34 @@ class Plugin(object):
return data
def wait(self, host = ''):
def wait(self, host = '', url = ''):
if self.http_time_between_calls == 0:
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)
if last_use > 0:
self.http_last_use_queue[host].append(url)
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):
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': {
'click': function(){
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)
Array.each(self.categories, function(category){
$(category).inject(self.category_container)
$(category).inject(self.category_container);
});
},
getCategory: function(id){
return this.categories.filter(function(category){
return category.data._id == id
}).pick()
return category.data._id == id;
}).pick();
},
getAll: function(){
@ -97,7 +97,7 @@ var CategoryListBase = new Class({
createCategory: function(data){
var self = this;
var data = data || {'id': randomString()};
data = data || {'id': randomString()};
var category = new Category(data);
self.categories.include(category);
@ -115,7 +115,7 @@ var CategoryListBase = new Class({
new Element('label[text=Order]'),
category_list = new Element('ul'),
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);
@ -125,7 +125,7 @@ var CategoryListBase = new Class({
new Element('span.category_label', {
'text': category.data.label
}),
new Element('span.handle')
new Element('span.handle.icon-handle')
).inject(category_list);
});
@ -192,7 +192,7 @@ var Category = new Class({
}),
new Element('.category_label.ctrlHolder').adopt(
new Element('label', {'text':'Name'}),
new Element('input.inlay', {
new Element('input', {
'type':'text',
'value': data.label,
'placeholder': 'Example: Kids, Horror or His'
@ -201,7 +201,7 @@ var Category = new Class({
),
new Element('.category_preferred.ctrlHolder').adopt(
new Element('label', {'text':'Preferred'}),
new Element('input.inlay', {
new Element('input', {
'type':'text',
'value': data.preferred,
'placeholder': 'Blu-ray, DTS'
@ -209,7 +209,7 @@ var Category = new Class({
),
new Element('.category_required.ctrlHolder').adopt(
new Element('label', {'text':'Required'}),
new Element('input.inlay', {
new Element('input', {
'type':'text',
'value': data.required,
'placeholder': 'Example: DTS, AC3 & English'
@ -217,7 +217,7 @@ var Category = new Class({
),
new Element('.category_ignored.ctrlHolder').adopt(
new Element('label', {'text':'Ignored'}),
new Element('input.inlay', {
new Element('input', {
'type':'text',
'value': data.ignored,
'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'),
'ignored' : self.el.getElement('.category_ignored input').get('value'),
'destination': self.data.destination
}
};
},
del: function(){
var self = this;
if(self.data.label == undefined){
if(self.data.label === undefined){
self.el.destroy();
return;
}
@ -318,11 +318,11 @@ var Category = new Class({
},
get: function(attr){
return this.data[attr]
return this.data[attr];
},
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 {
padding: 20px;
display: block;
text-align: center;
font-size: 20px;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
.category {
border-bottom: 1px solid rgba(255,255,255,0.2);
margin-bottom: 20px;
position: relative;
}
@ -28,8 +29,6 @@
}
.category .formHint {
width: 250px !important;
margin: 0 !important;
opacity: 0.1;
}
.category:hover .formHint {
@ -48,11 +47,10 @@
}
#category_ordering li {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
border-bottom: 1px solid rgba(255,255,255,0.2);
padding: 0 5px;
border-bottom: 1px solid $theme_off;
padding: 5px;
list-style: none;
}
#category_ordering li:last-child { border: 0; }
@ -69,14 +67,9 @@
}
#category_ordering li .handle {
background: url('../../images/handle.png') center;
width: 20px;
float: right;
}
#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 time
from CodernityDB.database import RecordDeleted
from CodernityDB.database import RecordDeleted, RecordNotFound
from couchpotato import get_db
from couchpotato.api import addApiView
@ -65,6 +65,10 @@ class Dashboard(Plugin):
log.debug('Record already deleted: %s', media_id)
continue
except RecordNotFound:
log.debug('Record not found: %s', media_id)
continue
pp = profile_pre.get(media.get('profile_id'))
if not pp: continue
@ -92,7 +96,7 @@ class Dashboard(Plugin):
if late:
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']:
add = False
break

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

@ -131,12 +131,13 @@ class Logging(Plugin):
def toList(self, log_content = ''):
logs_raw = toUnicode(log_content).split('[0m\n')
logs_raw = re.split(r'\[0m\n', toUnicode(log_content))
logs = []
re_split = r'\x1b'
for log_line in logs_raw:
split = splitString(log_line, '\x1b')
if split:
split = re.split(re_split, log_line)
if split and len(split) == 3:
try:
date, time, log_type = splitString(split[0], ' ')
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.',
has_tab: false,
navigation: null,
log_items: [],
report_text: '\
### Steps to reproduce:\n\
1. ..\n\
2. ..\n\
\n\
### Information:\n\
Movie(s) I have this with: ...\n\
Quality of the movie being searched: ...\n\
Providers I use: ...\n\
Version of CouchPotato: {version}\n\
Running on: ...\n\
\n\
### Logs:\n\
```\n{issue}```',
report_text: '### Steps to reproduce:\n'+
'1. ..\n'+
'2. ..\n'+
'\n'+
'### Information:\n'+
'Movie(s) I have this with: ...\n'+
'Quality of the movie being searched: ...\n'+
'Providers I use: ...\n'+
'Version of CouchPotato: {version}\n'+
'Running on: ...\n'+
'\n'+
'### Logs:\n'+
'```\n{issue}```',
indexAction: function () {
var self = this;
@ -34,6 +34,7 @@ Running on: ...\n\
var self = this;
if (self.log) self.log.destroy();
self.log = new Element('div.container.loading', {
'text': 'loading...',
'events': {
@ -41,9 +42,17 @@ Running on: ...\n\
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': {
'nr': nr
},
@ -52,65 +61,68 @@ Running on: ...\n\
self.log_items = self.createLogElements(json.log);
self.log.adopt(self.log_items);
self.log.removeClass('loading');
self.scrollToBottom();
var nav = new Element('ul.nav', {
'events': {
'click:relay(li.select)': function (e, el) {
self.getLogs(parseInt(el.get('text')) - 1);
}
}
});
if(!self.navigation){
self.navigation = new Element('div.navigation').adopt(
new Element('h2[text=Logs]'),
new Element('div.hint', {
'text': 'Select multiple lines & report an issue'
})
);
// Type selection
new Element('li.filter').grab(
new Element('select', {
var nav = new Element('ul.nav', {
'events': {
'change': function () {
var type_filter = this.getSelected()[0].get('value');
self.el.set('data-filter', type_filter);
self.scrollToBottom();
'click:relay(li.select)': function (e, el) {
self.getLogs(parseInt(el.get('text')) - 1);
}
}
}).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);
}
// Clear button
new Element('li.clear', {
'text': 'clear',
'events': {
'click': function () {
Api.request('logging.clear', {
'onComplete': function () {
self.getLogs(0);
}).inject(self.navigation);
// Type selection
new Element('li.filter').grab(
new Element('select', {
'events': {
'change': function () {
var type_filter = this.getSelected()[0].get('value');
self.content.set('data-filter', type_filter);
self.scrollToBottom();
}
});
}
}
}).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
new Element('li.hint', {
'text': 'Select multiple lines & report an issue'
}).inject(nav);
// Clear button
new Element('li.clear', {
'text': 'clear',
'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', {
'text': log.message
})
))
));
});
return elements;
},
scrollToBottom: function () {
new Fx.Scroll(window, {'duration': 0}).toBottom();
new Fx.Scroll(this.content, {'duration': 0}).toBottom();
},
showSelectionButton: function(e){
@ -213,7 +225,7 @@ Running on: ...\n\
.replace('{version}', version ? version.version.repr : '...'),
textarea;
var overlay = new Element('div.report', {
var overlay = new Element('div.mask.report_popup', {
'method': 'post',
'events': {
'click': function(e){
@ -245,12 +257,7 @@ Running on: ...\n\
})
),
textarea = new Element('textarea', {
'text': body,
'events': {
'click': function(){
this.select();
}
}
'text': body
}),
new Element('a.button', {
'target': '_blank',
@ -270,7 +277,7 @@ Running on: ...\n\
)
);
overlay.inject(self.log);
overlay.inject(document.body);
},
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('label', {'text':'Name'}),
new Element('input.inlay', {
new Element('input', {
'type':'text',
'value': data.label,
'placeholder': 'Profile name'
@ -47,7 +47,7 @@ var Profile = new Class({
new Element('div.wait_for.ctrlHolder').adopt(
// "Wait the entered number of days for a checked quality, before downloading a lower quality release."
new Element('span', {'text':'Wait'}),
new Element('input.inlay.wait_for_input.xsmall', {
new Element('input.wait_for_input.xsmall', {
'type':'text',
'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'}),
// "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',
'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
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,
'type':'text',
'value': data.minimum_score || 1
@ -81,7 +81,7 @@ var Profile = new Class({
'quality': quality,
'finish': data.finish[nr] || 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){
@ -177,7 +177,7 @@ var Profile = new Class({
var self = this;
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){
return this.data[attr]
return this.data[attr];
},
isCore: function(){
return this.data.core
return this.data.core;
},
toElement: function(){
return this.el
return this.el;
}
});
@ -270,47 +270,42 @@ Profile.Type = new Class({
var data = self.data;
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.finish_container = new Element('label.finish').adopt(
new Element('span.finish').grab(
self.finish = new Element('input.inlay.finish[type=checkbox]', {
'checked': data.finish !== undefined ? data.finish : 1,
'events': {
'change': function(){
if(self.el == self.el.getParent().getElement(':first-child')){
self.finish_class.check();
alert('Top quality always finishes the search');
return;
}
self.fireEvent('change');
self.finish = new Element('input.finish[type=checkbox]', {
'checked': data.finish !== undefined ? data.finish : 1,
'events': {
'change': function(){
if(self.el == self.el.getParent().getElement(':first-child')){
alert('Top quality always finishes the search');
return;
}
self.fireEvent('change');
}
})
),
}
}),
new Element('span.check_label[text=finish]')
),
self['3d_container'] = new Element('label.threed').adopt(
new Element('span.3d').grab(
self['3d'] = new Element('input.inlay.3d[type=checkbox]', {
'checked': data['3d'] !== undefined ? data['3d'] : 0,
'events': {
'change': function(){
self.fireEvent('change');
}
self['3d'] = new Element('input.3d[type=checkbox]', {
'checked': data['3d'] !== undefined ? data['3d'] : 0,
'events': {
'change': function(){
self.fireEvent('change');
}
})
),
}
}),
new Element('span.check_label[text=3D]')
),
new Element('span.delete.icon2', {
new Element('span.delete.icon-cancel', {
'events': {
'click': self.del.bind(self)
}
}),
new Element('span.handle')
new Element('span.handle.icon-handle')
);
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)
self.el.addClass('allow_3d');
self.finish_class = new Form.Check(self.finish);
self['3d_class'] = new Form.Check(self['3d']);
},
fillQualities: function(){
@ -342,7 +334,7 @@ Profile.Type = new Class({
'text': q.label,
'value': q.identifier,
'data-allow_3d': q.allow_3d
}).inject(self.qualities)
}).inject(self.qualities);
});
self.qualities.set('value', self.data.quality);
@ -358,7 +350,7 @@ Profile.Type = new Class({
'quality': self.qualities.get('value'),
'finish': +self.finish.checked,
'3d': +self['3d'].checked
}
};
},
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 = [
{'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': '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': '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', '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': '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')]},
@ -271,8 +271,8 @@ class QualityPlugin(Plugin):
words = words[:-1]
points = {
'identifier': 20,
'label': 20,
'identifier': 25,
'label': 25,
'alternative': 20,
'tags': 11,
'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 (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.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.1080p.HDCAM.-.ReleaseGroup': {'size': 5300, 'quality': 'cam'},
'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

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 = [];
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){
return this.profiles.filter(function(profile){
return profile.data._id == id
}).pick()
return profile.data._id == id;
}).pick();
},
// Hide items when getting profiles
getActiveProfiles: function(){
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){}
return {}
return {};
},
addSettings: function(){
@ -58,7 +58,7 @@ var QualityBase = new Class({
self.createProfileOrdering();
self.createSizes();
})
});
},
@ -68,7 +68,7 @@ var QualityBase = new Class({
createProfiles: function(){
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;
self.settings.createGroup({
@ -81,7 +81,7 @@ var QualityBase = new Class({
'events': {
'click': function(){
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)
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){
var self = this;
var data = data || {'id': randomString()};
data = data || {'id': randomString()};
var profile = new Profile(data);
self.profiles.include(profile);
@ -110,7 +110,7 @@ var QualityBase = new Class({
self.settings.createGroup({
'label': 'Profile Defaults',
'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
}).adopt(
}).grab(
new Element('.ctrlHolder#profile_ordering').adopt(
new Element('label[text=Order]'),
self.profiles_list = new Element('ul'),
@ -123,7 +123,7 @@ var QualityBase = new Class({
Array.each(self.profiles, function(profile){
var check;
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,
'events': {
'change': self.saveProfileOrdering.bind(self)
@ -132,11 +132,8 @@ var QualityBase = new Class({
new Element('span.profile_label', {
'text': profile.data.label
}),
new Element('span.handle')
new Element('span.handle.icon-handle')
).inject(self.profiles_list);
new Form.Check(check);
});
// Sortable
@ -190,7 +187,6 @@ var QualityBase = new Class({
'name': 'sizes'
}).inject(self.content);
new Element('div.item.head.ctrlHolder').adopt(
new Element('span.label', {'text': 'Quality'}),
new Element('span.min', {'text': 'Min'}),
@ -200,23 +196,23 @@ var QualityBase = new Class({
Array.each(self.qualities, function(quality){
new Element('div.ctrlHolder.item').adopt(
new Element('span.label', {'text': quality.label}),
new Element('input.min.inlay[type=text]', {
new Element('input.min[type=text]', {
'value': quality.size_min,
'events': {
'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,
'events': {
'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
}
});
}).delay(300)
}).delay(300);
}

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

Loading…
Cancel
Save