Compare commits

..
Author SHA1 Message Date
Ruud 465e7b2abc Merge branch 'refs/heads/develop' into desktop 2012-09-16 12:36:17 +02:00
Ruud 578fb45785 Installer 1 up 2012-09-16 11:35:56 +02:00
Ruud 96995bbbe5 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-09-16 10:45:19 +02:00
Ruud 4cfdafebbc Merge branch 'refs/heads/develop' into desktop 2012-09-14 13:15:47 +02:00
Ruud b97acb8ef5 Merge branch 'refs/heads/develop' into desktop 2012-09-14 13:08:19 +02:00
Ruud d68d2dfdb6 Updated installer 2012-09-09 21:48:38 +02:00
Ruud 39b269a454 Merge branch 'refs/heads/develop' into desktop 2012-09-09 17:32:47 +02:00
Ruud ac081d3e10 Getting ready for build 2012-09-09 17:28:23 +02:00
Ruud 5d4efb60cf Merge branch 'refs/heads/develop' into desktop 2012-09-08 16:01:49 +02:00
Ruud cc408b980c Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-08-05 16:18:35 +02:00
Ruud 59590b3ac9 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-07-14 00:35:00 +02:00
Ruud ff759dacf3 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-07-11 22:43:45 +02:00
Ruud a328e44130 Merge branch 'desktop' of github.com:RuudBurger/CouchPotatoServer into desktop 2012-05-15 23:23:56 +02:00
Ruud 7924cac5f9 Update installer version 2012-05-15 23:21:24 +02:00
Ruud 1cef3b0c93 remove --nogit tag 2012-05-15 23:21:24 +02:00
Ruud 3cd59edc8b Import errors
File icon
2012-05-15 23:21:24 +02:00
Ruud 0d624af01d Working PNG 2012-05-15 23:21:24 +02:00
Ruud a09132570c Change branch to desktop 2012-05-15 23:21:14 +02:00
Ruud ee3fc38432 Better setup 2012-05-15 23:21:14 +02:00
Ruud dbf0192c8e Inno setup, start 2012-05-15 23:21:14 +02:00
Ruud 6962cfc3f5 new Desktop runner 2012-05-15 23:21:14 +02:00
Ruud e096ec3b5b Desktop files 2012-05-15 23:20:05 +02:00
Ruud b30a74ae0c Merge branch 'refs/heads/develop' into desktop 2012-05-15 23:15:17 +02:00
Ruud 978eeb16c9 Update installer version 2012-05-15 23:14:20 +02:00
Ruud e5c9d91657 Merge branch 'refs/heads/develop' into desktop 2012-05-15 22:27:22 +02:00
Ruud fa81c3a07a Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-05-14 22:00:02 +02:00
Ruud 9cdd520d41 Merge branch 'refs/heads/develop' into desktop 2012-05-14 20:22:55 +02:00
Ruud 55d7898771 Merge branch 'refs/heads/develop' into desktop 2012-05-13 12:56:45 +02:00
Ruud b8256bef97 Merge branch 'refs/heads/develop' into desktop 2012-05-12 00:35:52 +02:00
Ruud 5be9dc0b4a Merge branch 'refs/heads/develop' into desktop 2012-05-09 22:20:53 +02:00
Ruud 7d0be0cefb remove --nogit tag 2012-05-07 22:55:54 +02:00
Ruud f7ce1edb13 Merge branch 'refs/heads/develop' into desktop 2012-05-07 22:44:01 +02:00
Ruud 5ad9280b60 Merge branch 'refs/heads/develop' into desktop 2012-05-07 22:27:55 +02:00
Ruud 2b353f1b20 Merge branch 'refs/heads/develop' into desktop 2012-05-04 17:29:15 +02:00
Ruud 75ab90b87b Merge branch 'refs/heads/develop' into desktop 2012-05-02 21:40:19 +02:00
Ruud 0219296120 Import errors
File icon
2012-05-02 21:34:45 +02:00
Ruud 20032b3a31 Working PNG 2012-05-01 07:35:44 +02:00
Ruud ea9e9a8c90 Updater base 2012-05-01 07:35:27 +02:00
Ruud f7b0ee145b Change branch to desktop 2012-04-30 21:37:04 +02:00
Ruud cc866738ee Merge branch 'refs/heads/develop' into desktop 2012-04-30 21:32:56 +02:00
Ruud eadccf6e33 Merge branch 'refs/heads/develop' into desktop 2012-04-29 00:00:25 +02:00
Ruud b70b66e567 Merge branch 'refs/heads/develop' into desktop 2012-04-28 23:14:59 +02:00
Ruud 5b6792dc20 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	CouchPotato.py
	couchpotato/core/plugins/renamer/main.py
	couchpotato/core/plugins/trailer/__init__.py
2012-04-07 21:35:36 +02:00
Ruud f498e7343a Better setup 2012-02-25 01:48:58 +01:00
Ruud 6962f441e6 Inno setup, start 2012-02-21 18:50:34 +01:00
Ruud 1def62b1b1 new Desktop runner 2012-02-19 17:13:37 +01:00
Ruud a4a4a6a185 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	CouchPotato.py
2012-02-19 13:14:56 +01:00
Ruud d4c9469c1a Remove nfo when not renaming as .orig.nfo 2012-02-19 12:53:55 +01:00
Ruud 3e2d4c5d7b Initial trailer support 2012-02-19 12:48:54 +01:00
Ruud d03f711d69 kwargs in file.download for urlopen 2012-02-19 12:45:22 +01:00
Ruud 44dd8d9b96 Merge lists, not overwrite 2012-02-19 12:37:25 +01:00
Ruud 549a3be0d8 Merge branch 'refs/heads/develop' into desktop 2012-02-12 00:10:56 +01:00
Ruud 1bb2edf8ec Merge branch 'refs/heads/develop' into desktop 2012-02-11 23:33:14 +01:00
Ruud 84c6f36315 Desktop files 2012-02-11 23:06:14 +01:00
563 changed files with 7963 additions and 35753 deletions
+3 -2
View File
@@ -62,6 +62,7 @@ class Loader(object):
self.log.logger.addHandler(hdlr) self.log.logger.addHandler(hdlr)
def addSignals(self): def addSignals(self):
signal.signal(signal.SIGINT, self.onExit) signal.signal(signal.SIGINT, self.onExit)
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1)) signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
@@ -73,7 +74,7 @@ class Loader(object):
def onExit(self, signal, frame): def onExit(self, signal, frame):
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
fireEvent('app.shutdown', single = True) fireEvent('app.crappy_shutdown', single = True)
def run(self): def run(self):
@@ -99,7 +100,7 @@ class Loader(object):
logging.shutdown() logging.shutdown()
time.sleep(3) time.sleep(3)
args = [sys.executable] + [os.path.join(base_path, os.path.basename(__file__))] + sys.argv[1:] args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:]
subprocess.Popen(args) subprocess.Popen(args)
except: except:
self.log.critical(traceback.format_exc()) self.log.critical(traceback.format_exc())
+218
View File
@@ -0,0 +1,218 @@
from threading import Thread
from wx.lib.softwareupdate import SoftwareUpdate
import os
import sys
import time
import webbrowser
import wx
import subprocess
# Include proper dirs
if hasattr(sys, 'frozen'):
import libs
base_path = os.path.dirname(os.path.dirname(os.path.abspath(libs.__file__)))
else:
base_path = os.path.dirname(os.path.abspath(__file__))
lib_dir = os.path.join(base_path, 'libs')
sys.path.insert(0, base_path)
sys.path.insert(0, lib_dir)
from couchpotato.environment import Env
class TaskBarIcon(wx.TaskBarIcon):
TBMENU_OPEN = wx.NewId()
TBMENU_SETTINGS = wx.NewId()
TBMENU_EXIT = wx.ID_EXIT
closed = False
menu = False
enabled = False
def __init__(self, frame):
wx.TaskBarIcon.__init__(self)
self.frame = frame
icon = wx.Icon('icon.png', wx.BITMAP_TYPE_PNG)
self.SetIcon(icon)
self.Bind(wx.EVT_TASKBAR_LEFT_UP, self.OnTaskBarClick)
self.Bind(wx.EVT_TASKBAR_RIGHT_UP, self.OnTaskBarClick)
self.Bind(wx.EVT_MENU, self.onOpen, id = self.TBMENU_OPEN)
self.Bind(wx.EVT_MENU, self.onSettings, id = self.TBMENU_SETTINGS)
self.Bind(wx.EVT_MENU, self.onTaskBarClose, id = self.TBMENU_EXIT)
def OnTaskBarClick(self, evt):
menu = self.CreatePopupMenu()
self.PopupMenu(menu)
menu.Destroy()
def enable(self):
self.enabled = True
if self.menu:
self.open_menu.Enable(True)
self.setting_menu.Enable(True)
self.open_menu.SetText('Open')
def CreatePopupMenu(self):
if not self.menu:
self.menu = wx.Menu()
self.open_menu = self.menu.Append(self.TBMENU_OPEN, 'Open')
self.setting_menu = self.menu.Append(self.TBMENU_SETTINGS, 'About')
self.exit_menu = self.menu.Append(self.TBMENU_EXIT, 'Quit')
if not self.enabled:
self.open_menu.Enable(False)
self.setting_menu.Enable(False)
self.open_menu.SetText('Loading...')
return self.menu
def onOpen(self, event):
url = self.frame.parent.getSetting('base_url')
webbrowser.open(url)
def onSettings(self, event):
url = self.frame.parent.getSetting('base_url') + '/settings/'
webbrowser.open(url)
def onTaskBarClose(self, evt):
if self.closed:
return
self.closed = True
self.RemoveIcon()
wx.CallAfter(self.frame.Close)
def makeIcon(self, img):
if "wxMSW" in wx.PlatformInfo:
img = img.Scale(16, 16)
elif "wxGTK" in wx.PlatformInfo:
img = img.Scale(22, 22)
icon = wx.IconFromBitmap(img.CopyFromBitmap())
return icon
class MainFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, None, style = wx.FRAME_NO_TASKBAR)
self.parent = parent
self.tbicon = TaskBarIcon(self)
class WorkerThread(Thread):
def __init__(self, desktop):
Thread.__init__(self)
self.daemon = True
self._desktop = desktop
self.start()
def run(self):
# Get options via arg
from couchpotato.runner import getOptions
args = ['--quiet']
self.options = getOptions(base_path, args)
# Load settings
settings = Env.get('settings')
settings.setFile(self.options.config_file)
# Create data dir if needed
self.data_dir = os.path.expanduser(Env.setting('data_dir'))
if self.data_dir == '':
from couchpotato.core.helpers.variable import getDataDir
self.data_dir = getDataDir()
if not os.path.isdir(self.data_dir):
os.makedirs(self.data_dir)
# Create logging dir
self.log_dir = os.path.join(self.data_dir, 'logs');
if not os.path.isdir(self.log_dir):
os.mkdir(self.log_dir)
try:
from couchpotato.runner import runCouchPotato
runCouchPotato(self.options, base_path, args, data_dir = self.data_dir, log_dir = self.log_dir, Env = Env, desktop = self._desktop)
except:
pass
self._desktop.frame.Close()
class CouchPotatoApp(wx.App, SoftwareUpdate):
settings = {}
events = {}
restart = False
closing = False
def OnInit(self):
# Updater
base_url = 'http://couchpota.to/updates/'
self.InitUpdates(base_url, base_url + 'changelog.html',
icon = wx.Icon('icon.png'))
self.frame = MainFrame(self)
self.frame.Bind(wx.EVT_CLOSE, self.onClose)
# CouchPotato thread
self.worker = WorkerThread(self)
return True
def onAppLoad(self):
self.frame.tbicon.enable()
def setSettings(self, settings = {}):
self.settings = settings
def getSetting(self, name):
return self.settings.get(name)
def addEvents(self, events = {}):
for name in events.iterkeys():
self.events[name] = events[name]
def onClose(self, event):
if not self.closing:
self.closing = True
self.frame.tbicon.onTaskBarClose(event)
onClose = self.events.get('onClose')
onClose(event)
def afterShutdown(self, restart = False):
self.frame.Destroy()
self.restart = restart
self.ExitMainLoop()
if __name__ == '__main__':
app = CouchPotatoApp(redirect = False)
app.MainLoop()
time.sleep(1)
if app.restart:
args = [sys.executable] + [os.path.join(base_path, 'Desktop.py')] + sys.argv[1:]
subprocess.Popen(args)
+3 -6
View File
@@ -17,7 +17,6 @@ Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files. * Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`. * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`.
* You can now start CP via `CouchPotatoServer\CouchPotato.py` to start * You can now start CP via `CouchPotatoServer\CouchPotato.py` to start
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
OSx: OSx:
@@ -27,7 +26,6 @@ OSx:
* Go to your App folder `cd /Applications` * Go to your App folder `cd /Applications`
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py` * Then do `python CouchPotatoServer/CouchPotato.py`
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
Linux (ubuntu / debian): Linux (ubuntu / debian):
@@ -35,8 +33,7 @@ Linux (ubuntu / debian):
* 'cd' to the folder of your choosing. * 'cd' to the folder of your choosing.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py` to start * Then do `python CouchPotatoServer/CouchPotato.py` to start
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato` * To run on boot copy the init script. `cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* Change the paths inside the init script. `sudo nano /etc/init.d/couchpotato` * Change the paths inside the init script. `nano /etc/init.d/couchpotato`
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato` * Make it executable. `chmod +x /etc/init.d/couchpotato`
* Add it to defaults. `sudo update-rc.d couchpotato defaults` * Add it to defaults. `sudo update-rc.d couchpotato defaults`
* Open your browser and go to: `http://localhost:5050/`
-15
View File
@@ -1,15 +0,0 @@
#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem!
##Just make sure you think of the following things:
* Search through the existing (and closed) issues first. See if you can get your answer there.
* Double check the result manually, because it could be an external issue.
* Post logs! Without seeing what is going on, I can't reproduce the error.
* What is the movie + quality you are searching for.
* What are you settings for the specific problem.
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby).
* Give me a short step by step of how to reproduce.
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
**If I don't get enough info, the change of the issue getting closed is a lot bigger ;)**
+1 -2
View File
@@ -78,7 +78,6 @@ def page_not_found(error):
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url) r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
return redirect(r) return redirect(r)
else: else:
if not Env.get('dev'): time.sleep(0.1)
time.sleep(0.1)
return 'Wrong API key used', 404 return 'Wrong API key used', 404
+2 -5
View File
@@ -1,5 +1,6 @@
from flask.blueprints import Blueprint from flask.blueprints import Blueprint
from flask.helpers import url_for from flask.helpers import url_for
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, asynchronous from tornado.web import RequestHandler, asynchronous
from werkzeug.utils import redirect from werkzeug.utils import redirect
@@ -10,11 +11,7 @@ api_nonblock = {}
class NonBlockHandler(RequestHandler): class NonBlockHandler(RequestHandler):
stoppers = []
def __init__(self, application, request, **kwargs):
cls = NonBlockHandler
cls.stoppers = []
super(NonBlockHandler, self).__init__(application, request, **kwargs)
@asynchronous @asynchronous
def get(self, route): def get(self, route):
+7 -10
View File
@@ -23,22 +23,19 @@ config = [{
'default': '', 'default': '',
'type': 'password', 'type': 'password',
}, },
{
'name': 'host',
'advanced': True,
'default': '0.0.0.0',
'label': 'IP',
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
},
{ {
'name': 'port', 'name': 'port',
'default': 5050, 'default': 5050,
'type': 'int', 'type': 'int',
'description': 'The port I should listen to.', 'description': 'The port I should listen to.',
}, },
{
'name': 'ssl_cert',
'description': 'Path to SSL server.crt',
'advanced': True,
},
{
'name': 'ssl_key',
'description': 'Path to SSL server.key',
'advanced': True,
},
{ {
'name': 'launch_browser', 'name': 'launch_browser',
'default': True, 'default': True,
+3 -16
View File
@@ -9,7 +9,6 @@ from tornado.ioloop import IOLoop
from uuid import uuid4 from uuid import uuid4
import os import os
import platform import platform
import signal
import time import time
import traceback import traceback
import webbrowser import webbrowser
@@ -52,9 +51,6 @@ class Core(Plugin):
addEvent('setting.save.core.password', self.md5Password) addEvent('setting.save.core.password', self.md5Password)
addEvent('setting.save.core.api_key', self.checkApikey) addEvent('setting.save.core.api_key', self.checkApikey)
# Make sure we can close-down with ctrl+c properly
if not Env.get('desktop'):
self.signalHandler()
def md5Password(self, value): def md5Password(self, value):
return md5(value.encode(Env.get('encoding'))) if value else '' return md5(value.encode(Env.get('encoding'))) if value else ''
@@ -70,7 +66,7 @@ class Core(Plugin):
def available(self): def available(self):
return jsonified({ return jsonified({
'success': True 'succes': True
}) })
def shutdown(self): def shutdown(self):
@@ -102,7 +98,7 @@ class Core(Plugin):
self.shutdown_started = True self.shutdown_started = True
fireEvent('app.do_shutdown') fireEvent('app.shutdown')
log.debug('Every plugin got shutdown event') log.debug('Every plugin got shutdown event')
loop = True loop = True
@@ -152,7 +148,7 @@ class Core(Plugin):
def createBaseUrl(self): def createBaseUrl(self):
host = Env.setting('host') host = Env.setting('host')
if host == '0.0.0.0' or host == '': if host == '0.0.0.0':
host = 'localhost' host = 'localhost'
port = Env.setting('port') port = Env.setting('port')
@@ -174,12 +170,3 @@ class Core(Plugin):
return jsonified({ return jsonified({
'version': self.version() 'version': self.version()
}) })
def signalHandler(self):
if Env.get('daemonized'): return
def signal_handler(signal, frame):
fireEvent('app.shutdown', single = True)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
+12 -125
View File
@@ -1,60 +1,15 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from minify.cssmin import cssmin
from minify.jsmin import jsmin
import os
import traceback
log = CPLog(__name__) log = CPLog(__name__)
class ClientScript(Plugin): class ClientScript(Plugin):
core_static = { urls = {
'style': [ 'style': {},
'style/main.css', 'script': {},
'style/uniform.generic.css',
'style/uniform.css',
'style/settings.css',
],
'script': [
'scripts/library/mootools.js',
'scripts/library/mootools_more.js',
'scripts/library/prefix_free.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/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/wanted.js',
'scripts/page/settings.js',
'scripts/page/about.js',
'scripts/page/manage.js',
],
}
urls = {'style': {}, 'script': {}, }
minified = {'style': {}, 'script': {}, }
paths = {'style': {}, 'script': {}, }
comment = {
'style': '/*** %s:%d ***/\n',
'script': '// %s:%d\n'
} }
html = { html = {
@@ -69,66 +24,6 @@ class ClientScript(Plugin):
addEvent('clientscript.get_styles', self.getStyles) addEvent('clientscript.get_styles', self.getStyles)
addEvent('clientscript.get_scripts', self.getScripts) addEvent('clientscript.get_scripts', self.getScripts)
addEvent('app.load', self.minify)
self.addCore()
def addCore(self):
for static_type in self.core_static:
for rel_path in self.core_static.get(static_type):
file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path)
core_url = 'api/%s/static/%s?%s' % (Env.setting('api_key'), rel_path, tryInt(os.path.getmtime(file_path)))
if static_type == 'script':
self.registerScript(core_url, file_path, position = 'front')
else:
self.registerStyle(core_url, file_path, position = 'front')
def minify(self):
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 = 'minified_' + out
out = os.path.join(cache, out_name)
raw = []
for file_path in files:
f = open(file_path, 'r').read()
if file_type == 'script':
data = jsmin(f)
else:
data = cssmin(f)
data = data.replace('../images/', '../static/images/')
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) % (r.get('file'), r.get('date'))
data += r.get('data') + '\n\n'
self.createFile(out, data.strip())
if not self.minified.get(file_type):
self.minified[file_type] = {}
if not self.minified[file_type].get(position):
self.minified[file_type][position] = []
minified_url = 'api/%s/file.cache/%s?%s' % (Env.setting('api_key'), out_name, tryInt(os.path.getmtime(out)))
self.minified[file_type][position].append(minified_url)
def getStyles(self, *args, **kwargs): def getStyles(self, *args, **kwargs):
return self.get('style', *args, **kwargs) return self.get('style', *args, **kwargs)
@@ -140,30 +35,22 @@ class ClientScript(Plugin):
data = '' if as_html else [] data = '' if as_html else []
try: try:
try:
if not Env.get('dev'):
return self.minified[type][location]
except:
pass
return self.urls[type][location] return self.urls[type][location]
except: except Exception, e:
log.error('Error getting minified %s, %s: %s', (type, location, traceback.format_exc())) log.error(e)
return data return data
def registerStyle(self, api_path, file_path, position = 'head'): def registerStyle(self, path, position = 'head'):
self.register(api_path, file_path, 'style', position) self.register(path, 'style', position)
def registerScript(self, api_path, file_path, position = 'head'): def registerScript(self, path, position = 'head'):
self.register(api_path, file_path, 'script', position) self.register(path, 'script', position)
def register(self, api_path, file_path, type, location): def register(self, filepath, type, location):
if not self.urls[type].get(location): if not self.urls[type].get(location):
self.urls[type][location] = [] self.urls[type][location] = []
self.urls[type][location].append(api_path)
if not self.paths[type].get(location): filePath = filepath
self.paths[type][location] = [] self.urls[type][location].append(filePath)
self.paths[type][location].append(file_path)
+1
View File
@@ -2,6 +2,7 @@ from apscheduler.scheduler import Scheduler as Sched
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import logging
log = CPLog(__name__) log = CPLog(__name__)
@@ -1,6 +1,4 @@
from .main import Updater from .main import Updater
from couchpotato.environment import Env
import os
def start(): def start():
return Updater() return Updater()
@@ -35,7 +33,6 @@ config = [{
{ {
'name': 'git_command', 'name': 'git_command',
'default': 'git', 'default': 'git',
'hidden': not os.path.isdir(os.path.join(Env.get('app_dir'), '.git')),
'advanced': True 'advanced': True
}, },
], ],
+1 -6
View File
@@ -106,10 +106,6 @@ class Updater(Plugin):
if success: if success:
fireEventAsync('app.restart') fireEventAsync('app.restart')
# Assume the updater handles things
if not success:
success = True
return jsonified({ return jsonified({
'success': success 'success': success
}) })
@@ -400,7 +396,6 @@ class DesktopUpdater(BaseUpdater):
self.update_failed = True self.update_failed = True
self.desktop._esky.auto_update(callback = do_restart) self.desktop._esky.auto_update(callback = do_restart)
return
except: except:
self.update_failed = True self.update_failed = True
@@ -411,7 +406,7 @@ class DesktopUpdater(BaseUpdater):
'last_check': self.last_check, 'last_check': self.last_check,
'update_version': self.update_version, 'update_version': self.update_version,
'version': self.getVersion(), 'version': self.getVersion(),
'branch': self.branch, 'branch': 'desktop_build',
} }
def check(self): def check(self):
@@ -90,18 +90,17 @@ var UpdaterBase = new Class({
doUpdate: function(){ doUpdate: function(){
var self = this; var self = this;
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
Api.request('updater.update', { Api.request('updater.update', {
'onComplete': function(json){ 'onComplete': function(json){
if(json.success) if(json.success){
self.updating(); self.updating();
else }
App.unBlockPage()
} }
}); });
}, },
updating: function(){ updating: function(){
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
App.checkAvailable.delay(500, App, [1000, function(){ App.checkAvailable.delay(500, App, [1000, function(){
window.location.reload(); window.location.reload();
}]); }]);
-13
View File
@@ -1,13 +0,0 @@
config = {
'name': 'download_providers',
'groups': [
{
'label': 'Downloaders',
'description': 'You can select different downloaders for each type (usenet / torrent)',
'type': 'list',
'name': 'download_providers',
'tab': 'downloaders',
'options': [],
},
],
}
+31 -63
View File
@@ -1,76 +1,51 @@
from base64 import b32decode, b16encode from base64 import b32decode, b16encode
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toSafeString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
import os
import random import random
import re import re
log = CPLog(__name__) log = CPLog(__name__)
class Downloader(Provider): class Downloader(Plugin):
type = [] type = []
http_time_between_calls = 0
torrent_sources = [ torrent_sources = [
'http://torrage.com/torrent/%s.torrent', 'http://torrage.com/torrent/%s.torrent',
'http://torrage.ws/torrent/%s.torrent',
'http://torcache.net/torrent/%s.torrent', 'http://torcache.net/torrent/%s.torrent',
] ]
torrent_trackers = [
'http://tracker.publicbt.com/announce',
'udp://tracker.istole.it:80/announce',
'udp://fr33domtracker.h33t.com:3310/announce',
'http://tracker.istole.it/announce',
'http://tracker.ccc.de/announce',
'udp://tracker.publicbt.com:80/announce',
'udp://tracker.ccc.de:80/announce',
'http://exodus.desync.com/announce',
'http://exodus.desync.com:6969/announce',
'http://tracker.publichd.eu/announce',
'http://tracker.openbittorrent.com/announce',
]
def __init__(self): def __init__(self):
addEvent('download', self._download) addEvent('download', self.download)
addEvent('download.enabled', self._isEnabled) addEvent('download.status', self.getDownloadStatus)
addEvent('download.enabled_types', self.getEnabledDownloadType)
addEvent('download.status', self._getAllDownloadStatus)
addEvent('download.remove_failed', self._removeFailed)
def getEnabledDownloadType(self): def download(self, data = {}, movie = {}, manual = False, filedata = None):
for download_type in self.type: pass
if self.isEnabled(manual = True, data = {'type': download_type}):
return self.type
return []
def _download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual, data):
return
return self.download(data = data, movie = movie, filedata = filedata)
def _getAllDownloadStatus(self):
if self.isDisabled(manual = True, data = {}):
return
return self.getAllDownloadStatus()
def getAllDownloadStatus(self):
return
def _removeFailed(self, item):
if self.isDisabled(manual = True, data = {}):
return
if self.conf('delete_failed', default = True):
return self.removeFailed(item)
def getDownloadStatus(self, data = {}, movie = {}):
return False return False
def removeFailed(self, item): def createNzbName(self, data, movie):
return tag = self.cpTag(movie)
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie))
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('type'))
def cpTag(self, movie):
if Env.setting('enabled', 'renamer'):
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
return ''
def isCorrectType(self, item_type): def isCorrectType(self, item_type):
is_correct = item_type in self.type is_correct = item_type in self.type
@@ -81,7 +56,7 @@ class Downloader(Provider):
return is_correct return is_correct
def magnetToTorrent(self, magnet_link): def magnetToTorrent(self, magnet_link):
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0].upper() torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0]
# Convert base 32 to hex # Convert base 32 to hex
if len(torrent_hash) == 32: if len(torrent_hash) == 32:
@@ -103,16 +78,9 @@ class Downloader(Provider):
log.error('Failed converting magnet url to torrent: %s', (torrent_hash)) log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
return False return False
def isDisabled(self, manual, data): def isDisabled(self, manual):
return not self.isEnabled(manual, data) return not self.isEnabled(manual)
def _isEnabled(self, manual, data = {}): def isEnabled(self, manual):
if not self.isEnabled(manual, data):
return
return True
def isEnabled(self, manual, data = {}):
d_manual = self.conf('manual', default = False) d_manual = self.conf('manual', default = False)
return super(Downloader, self).isEnabled() and \ return super(Downloader, self).isEnabled() and ((d_manual and manual) or (d_manual is False))
((d_manual and manual) or (d_manual is False)) and \
(not data or self.isCorrectType(data.get('type')))
@@ -1,5 +1,4 @@
from .main import Blackhole from .main import Blackhole
from couchpotato.core.helpers.variable import getDownloadDir
def start(): def start():
return Blackhole() return Blackhole()
@@ -10,7 +9,6 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'downloaders', 'tab': 'downloaders',
'list': 'download_providers',
'name': 'blackhole', 'name': 'blackhole',
'label': 'Black hole', 'label': 'Black hole',
'description': 'Download the NZB/Torrent to a specific folder.', 'description': 'Download the NZB/Torrent to a specific folder.',
@@ -18,7 +16,7 @@ config = [{
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
'default': True, 'default': 0,
'type': 'enabler', 'type': 'enabler',
'radio_group': 'nzb,torrent', 'radio_group': 'nzb,torrent',
}, },
@@ -26,7 +24,6 @@ config = [{
'name': 'directory', 'name': 'directory',
'type': 'directory', 'type': 'directory',
'description': 'Directory where the .nzb (or .torrent) file is saved to.', 'description': 'Directory where the .nzb (or .torrent) file is saved to.',
'default': getDownloadDir()
}, },
{ {
'name': 'use_for', 'name': 'use_for',
+5 -22
View File
@@ -1,7 +1,6 @@
from __future__ import with_statement from __future__ import with_statement
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
import os import os
import traceback import traceback
@@ -11,7 +10,11 @@ class Blackhole(Downloader):
type = ['nzb', 'torrent', 'torrent_magnet'] type = ['nzb', 'torrent', 'torrent_magnet']
def download(self, data = {}, movie = {}, filedata = None): def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or \
(not self.isCorrectType(data.get('type')) or \
(not self.conf('use_for') in ['both', 'torrent' if 'torrent' in data.get('type') else data.get('type')])):
return
directory = self.conf('directory') directory = self.conf('directory')
if not directory or not os.path.isdir(directory): if not directory or not os.path.isdir(directory):
@@ -37,7 +40,6 @@ class Blackhole(Downloader):
log.info('Downloading %s to %s.', (data.get('type'), fullPath)) log.info('Downloading %s to %s.', (data.get('type'), fullPath))
with open(fullPath, 'wb') as f: with open(fullPath, 'wb') as f:
f.write(filedata) f.write(filedata)
os.chmod(fullPath, Env.getPermission('file'))
return True return True
else: else:
log.info('File %s already exists.', fullPath) log.info('File %s already exists.', fullPath)
@@ -50,23 +52,4 @@ class Blackhole(Downloader):
except: except:
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc())) log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
return False return False
return False return False
def getEnabledDownloadType(self):
if self.conf('use_for') == 'both':
return super(Blackhole, self).getEnabledDownloadType()
elif self.conf('use_for') == 'torrent':
return ['torrent', 'torrent_magnet']
else:
return ['nzb']
def isEnabled(self, manual, data = {}):
for_type = ['both']
if data and 'torrent' in data.get('type'):
for_type.append('torrent')
elif data:
for_type.append(data.get('type'))
return super(Blackhole, self).isEnabled(manual, data) and \
((self.conf('use_for') in for_type))
@@ -8,10 +8,10 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'downloaders', 'tab': 'downloaders',
'list': 'download_providers',
'name': 'nzbget', 'name': 'nzbget',
'label': 'NZBGet', 'label': 'NZBGet',
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.', 'description': 'Send NZBs to your NZBGet installation.',
'wizard': True,
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@@ -26,7 +26,6 @@ config = [{
}, },
{ {
'name': 'password', 'name': 'password',
'type': 'password',
'description': 'Default NZBGet password is <i>tegbzn6789</i>', 'description': 'Default NZBGet password is <i>tegbzn6789</i>',
}, },
{ {
@@ -34,13 +33,6 @@ config = [{
'default': 'Movies', 'default': 'Movies',
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>', 'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
}, },
{
'name': 'priority',
'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',
},
{ {
'name': 'manual', 'name': 'manual',
'default': 0, 'default': 0,
+7 -11
View File
@@ -1,9 +1,7 @@
from base64 import standard_b64encode from base64 import standard_b64encode
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import re from inspect import isfunction
import socket import socket
import traceback import traceback
import xmlrpclib import xmlrpclib
@@ -16,7 +14,10 @@ class NZBGet(Downloader):
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc' url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
def download(self, data = {}, movie = {}, filedata = None): def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return
if not filedata: if not filedata:
log.error('Unable to get NZB file: %s', traceback.format_exc()) log.error('Unable to get NZB file: %s', traceback.format_exc())
@@ -25,7 +26,7 @@ class NZBGet(Downloader):
log.info('Sending "%s" to NZBGet.', data.get('name')) log.info('Sending "%s" to NZBGet.', data.get('name'))
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')} url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie)) nzb_name = '%s.nzb' % self.createNzbName(data, movie)
rpc = xmlrpclib.ServerProxy(url) rpc = xmlrpclib.ServerProxy(url)
try: try:
@@ -43,12 +44,7 @@ class NZBGet(Downloader):
log.error('Protocol Error: %s', e) log.error('Protocol Error: %s', e)
return False return False
if re.search(r"^0", rpc.version()): if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())):
xml_response = rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip()))
else:
xml_response = rpc.append(nzb_name, self.conf('category'), tryInt(self.conf('priority')), False, standard_b64encode(filedata.strip()))
if xml_response:
log.info('NZB sent successfully to NZBGet') log.info('NZB sent successfully to NZBGet')
return True return True
else: else:
@@ -1,47 +0,0 @@
from .main import NZBVortex
def start():
return NZBVortex()
config = [{
'name': 'nzbvortex',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'nzbvortex',
'label': 'NZBVortex',
'description': 'Use <a href="http://www.nzbvortex.com/landing/" target="_blank">NZBVortex</a> to download NZBs.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'nzb',
},
{
'name': 'host',
'default': 'https://localhost:4321',
},
{
'name': 'api_key',
'label': 'Api Key',
},
{
'name': 'manual',
'default': False,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
{
'name': 'delete_failed',
'default': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],
}]
@@ -1,170 +0,0 @@
from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from urllib2 import URLError
from uuid import uuid4
import hashlib
import httplib
import json
import socket
import ssl
import sys
import traceback
import urllib2
log = CPLog(__name__)
class NZBVortex(Downloader):
type = ['nzb']
api_level = None
session_id = None
def download(self, data = {}, movie = {}, filedata = None):
# Send the nzb
try:
nzb_filename = self.createFileName(data, filedata, movie)
self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
return True
except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False
def getAllDownloadStatus(self):
raw_statuses = self.call('nzb')
statuses = []
for item in raw_statuses.get('nzbs', []):
# Check status
status = 'busy'
if item['state'] == 20:
status = 'completed'
elif item['state'] in [21, 22, 24]:
status = 'failed'
statuses.append({
'id': item['id'],
'name': item['uiTitle'],
'status': status,
'original_status': item['state'],
'timeleft':-1,
})
return statuses
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
try:
self.call('nzb/%s/cancel' % item['id'])
except:
log.error('Failed deleting: %s', traceback.format_exc(0))
return False
return True
def login(self):
nonce = self.call('auth/nonce', auth = False).get('authNonce')
cnonce = uuid4().hex
hashed = b64encode(hashlib.sha256('%s:%s:%s' % (nonce, cnonce, self.conf('api_key'))).digest())
params = {
'nonce': nonce,
'cnonce': cnonce,
'hash': hashed
}
login_data = self.call('auth/login', parameters = params, auth = False)
# Save for later
if login_data.get('loginResult') == 'successful':
self.session_id = login_data.get('sessionID')
return True
log.error('Login failed, please check you api-key')
return False
def call(self, call, parameters = {}, repeat = False, auth = True, *args, **kwargs):
# Login first
if not self.session_id and auth:
self.login()
# Always add session id to request
if self.session_id:
parameters['sessionid'] = self.session_id
params = tryUrlencode(parameters)
url = cleanHost(self.conf('host')) + 'api/' + call
url_opener = urllib2.build_opener(HTTPSHandler())
try:
data = self.urlopen('%s?%s' % (url, params), opener = url_opener, *args, **kwargs)
if data:
return json.loads(data)
except URLError, e:
if hasattr(e, 'code') and e.code == 403:
# Try login and do again
if not repeat:
self.login()
return self.call(call, parameters = parameters, repeat = True, *args, **kwargs)
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return {}
def getApiLevel(self):
if not self.api_level:
url = cleanHost(self.conf('host')) + 'api/app/apilevel'
url_opener = urllib2.build_opener(HTTPSHandler())
try:
data = self.urlopen(url, opener = url_opener, show_error = False)
self.api_level = float(json.loads(data).get('apilevel'))
except URLError, e:
if hasattr(e, 'code') and e.code == 403:
log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher')
else:
log.error('NZBVortex doesn\'t seem to be running or maybe the remote option isn\'t enabled yet: %s', traceback.format_exc(1))
return self.api_level
def isEnabled(self, manual, data):
return super(NZBVortex, self).isEnabled(manual, data) and self.getApiLevel()
class HTTPSConnection(httplib.HTTPSConnection):
def __init__(self, *args, **kwargs):
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
def connect(self):
sock = socket.create_connection((self.host, self.port), self.timeout)
if sys.version_info < (2, 6, 7):
if hasattr(self, '_tunnel_host'):
self.sock = sock
self._tunnel()
else:
if self._tunnel_host:
self.sock = sock
self._tunnel()
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1)
class HTTPSHandler(urllib2.HTTPSHandler):
def https_open(self, req):
return self.do_open(HTTPSConnection, req)
@@ -9,10 +9,10 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'downloaders', 'tab': 'downloaders',
'list': 'download_providers',
'name': 'pneumatic', 'name': 'pneumatic',
'label': 'Pneumatic', 'label': 'Pneumatic',
'description': 'Use <a href="http://forum.xbmc.org/showthread.php?tid=97657" target="_blank">Pneumatic</a> to download .strm files.', 'description': 'Download the .strm file to a specific folder.',
'wizard': True,
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@@ -11,7 +11,9 @@ class Pneumatic(Downloader):
type = ['nzb'] type = ['nzb']
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s' strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
def download(self, data = {}, movie = {}, filedata = None): def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type'))):
return
directory = self.conf('directory') directory = self.conf('directory')
if not directory or not os.path.isdir(directory): if not directory or not os.path.isdir(directory):
@@ -8,10 +8,9 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'downloaders', 'tab': 'downloaders',
'list': 'download_providers',
'name': 'sabnzbd', 'name': 'sabnzbd',
'label': 'Sabnzbd', 'label': 'Sabnzbd',
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> to download NZBs.', 'description': 'Send NZBs to your Sabnzbd installation.',
'wizard': True, 'wizard': True,
'options': [ 'options': [
{ {
+99 -91
View File
@@ -1,11 +1,9 @@
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import tryUrlencode, ss from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost, mergeDicts from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from urllib2 import URLError
import json
import traceback import traceback
import json
log = CPLog(__name__) log = CPLog(__name__)
@@ -13,7 +11,10 @@ class Sabnzbd(Downloader):
type = ['nzb'] type = ['nzb']
def download(self, data = {}, movie = {}, filedata = None): def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return
log.info('Sending "%s" to SABnzbd.', data.get('name')) log.info('Sending "%s" to SABnzbd.', data.get('name'))
@@ -26,7 +27,7 @@ class Sabnzbd(Downloader):
if filedata: if filedata:
if len(filedata) < 50: if len(filedata) < 50:
log.error('No proper nzb available: %s', (filedata)) log.error('No proper nzb available!')
return False return False
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb # If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
@@ -35,119 +36,126 @@ class Sabnzbd(Downloader):
else: else:
params['name'] = data.get('url') params['name'] = data.get('url')
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params) url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try: try:
if params.get('mode') is 'addfile': if params.get('mode') is 'addfile':
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True, show_error = False, headers = {'User-Agent': Env.getIdentifier()}) sab = self.urlopen(url, timeout = 60, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False)
else: else:
sab = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}) sab = self.urlopen(url, timeout = 60, show_error = False)
except URLError:
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
return False
except: except:
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0)) log.error('Failed sending release: %s', traceback.format_exc())
return False return False
result = sab.strip() result = sab.strip()
if not result: if not result:
log.error('SABnzbd didn\'t return anything.') log.error("SABnzbd didn't return anything.")
return False return False
log.debug('Result text from SAB: %s', result[:40]) log.debug("Result text from SAB: " + result[:40])
if result[:2] == 'ok': if result == "ok":
log.info('NZB sent to SAB successfully.') log.info("NZB sent to SAB successfully.")
return True return True
elif result == "Missing authentication":
log.error("Incorrect username/password.")
return False
else: else:
log.error(result[:40]) log.error("Unknown error: " + result[:40])
return False return False
def getAllDownloadStatus(self): def getDownloadStatus(self, data = {}, movie = {}):
if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')):
return
log.debug('Checking SABnzbd download status.') nzbname = self.createNzbName(data, movie)
log.info('Checking download status of "%s" at SABnzbd.', nzbname)
# Go through Queue # Go through Queue
params = {
'apikey': self.conf('api_key'),
'mode': 'queue',
'output': 'json'
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try: try:
queue = self.call({ sab = self.urlopen(url, timeout = 60, show_error = False)
'mode': 'queue',
})
except: except:
log.error('Failed getting queue: %s', traceback.format_exc(1)) log.error('Failed checking status: %s', traceback.format_exc())
return False return False
try:
history = json.loads(sab)
except:
log.debug("Result text from SAB: " + sab[:40])
log.error('Failed parsing json status: %s', traceback.format_exc())
return False
for slot in history['queue']['slots']:
log.debug('Found %s in SabNZBd queue, which is %s, with %s left', (slot['filename'], slot['status'], slot['timeleft']))
if slot['filename'] == nzbname:
return slot['status'].lower()
# Go through history items # Go through history items
try: params = {
history = self.call({ 'apikey': self.conf('api_key'),
'mode': 'history', 'mode': 'history',
'limit': 15, 'output': 'json'
}) }
except: url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
log.error('Failed getting history json: %s', traceback.format_exc(1))
return False
statuses = []
# Get busy releases
for item in queue.get('slots', []):
statuses.append({
'id': item['nzo_id'],
'name': item['filename'],
'status': 'busy',
'original_status': item['status'],
'timeleft': item['timeleft'] if not queue['paused'] else -1,
})
# Get old releases
for item in history.get('slots', []):
status = 'busy'
if item['status'] == 'Failed' or (item['status'] == 'Completed' and item['fail_message'].strip()):
status = 'failed'
elif item['status'] == 'Completed':
status = 'completed'
statuses.append({
'id': item['nzo_id'],
'name': item['name'],
'status': status,
'original_status': item['status'],
'timeleft': 0,
})
return statuses
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
try: try:
self.call({ sab = self.urlopen(url, timeout = 60, show_error = False)
'mode': 'history',
'name': 'delete',
'del_files': '1',
'value': item['id']
}, use_json = False)
except: except:
log.error('Failed deleting: %s', traceback.format_exc(0)) log.error('Failed getting history: %s', traceback.format_exc())
return False return
return True try:
history = json.loads(sab)
except:
log.debug("Result text from SAB: " + sab[:40])
log.error('Failed parsing history json: %s', traceback.format_exc())
return
def call(self, params, use_json = True): for slot in history['history']['slots']:
log.debug('Found %s in SabNZBd history, which has %s', (slot['name'], slot['status']))
if slot['name'] == nzbname:
# Note: if post process even if failed is on in SabNZBd, it will complete with a fail message
if slot['status'] == 'Failed' or (slot['status'] == 'Completed' and slot['fail_message'].strip()):
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, { # Delete failed download
'apikey': self.conf('api_key'), if self.conf('delete_failed', default = True):
'output': 'json'
}))
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}) log.info('%s failed downloading, deleting...', slot['name'])
if use_json: params = {
d = json.loads(data) 'apikey': self.conf('api_key'),
if d.get('error'): 'mode': 'history',
log.error('Error getting data from SABNZBd: %s', d.get('error')) 'name': 'delete',
return {} 'del_files': '1',
'value': slot['nzo_id']
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
return d[params['mode']] try:
else: sab = self.urlopen(url, timeout = 60, show_error = False)
return data except:
log.error('Failed deleting: %s', traceback.format_exc())
return False
result = sab.strip()
if not result:
log.error("SABnzbd didn't return anything.")
log.debug("Result text from SAB: " + result[:40])
if result == "ok":
log.info('SabNZBd deleted failed release %s successfully.', slot['name'])
elif result == "Missing authentication":
log.error("Incorrect username/password or API?.")
else:
log.error("Unknown error: " + result[:40])
return 'failed'
else:
return slot['status'].lower()
return 'not_found'
@@ -1,45 +0,0 @@
from .main import Synology
def start():
return Synology()
config = [{
'name': 'synology',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'synology',
'label': 'Synology',
'description': 'Use <a href="http://www.synology.com/dsm/home_home_applications_download_station.php" target="_blank">Synology Download Station</a> to download.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'localhost:5000',
'description': 'Hostname with port. Usually <strong>localhost:5000</strong>',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
}]
@@ -1,105 +0,0 @@
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog
import httplib
import json
import urllib
import urllib2
log = CPLog(__name__)
class Synology(Downloader):
type = ['torrent_magnet']
log = CPLog(__name__)
def download(self, data, movie, filedata = None):
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type')))
# Load host from config and split out port.
host = self.conf('host').split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
if data.get('type') == 'torrent':
log.error('Can\'t add binary torrent file')
return False
try:
# Send request to Transmission
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
remote_torrent = srpc.add_torrent_uri(data.get('url'))
log.info('Response: %s', remote_torrent)
return remote_torrent['success']
except Exception, err:
log.error('Exception while adding torrent: %s', err)
return False
class SynologyRPC(object):
'''SynologyRPC lite library'''
def __init__(self, host = 'localhost', port = 5000, username = None, password = None):
super(SynologyRPC, self).__init__()
self.download_url = 'http://%s:%s/webapi/DownloadStation/task.cgi' % (host, port)
self.auth_url = 'http://%s:%s/webapi/auth.cgi' % (host, port)
self.username = username
self.password = password
self.session_name = 'DownloadStation'
def _login(self):
if self.username and self.password:
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
'method': 'login', 'session': self.session_name, 'format': 'sid'}
response = self._req(self.auth_url, args)
if response['success'] == True:
self.sid = response['data']['sid']
log.debug('Sid=%s', self.sid)
return response
elif self.username or self.password:
log.error('User or password missing, not using authentication.')
return False
def _logout(self):
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
return self._req(self.auth_url, args)
def _req(self, url, args):
req_url = url + '?' + urllib.urlencode(args)
try:
req_open = urllib2.urlopen(req_url)
response = json.loads(req_open.read())
if response['success'] == True:
log.info('Synology action successfull')
return response
except httplib.InvalidURL, err:
log.error('Invalid Transmission host, check your config %s', err)
return False
except urllib2.HTTPError, err:
log.error('SynologyRPC HTTPError: %s', err)
return False
except urllib2.URLError, err:
log.error('Unable to connect to Synology %s', err)
return False
def add_torrent_uri(self, torrent):
log.info('Adding torrent URL %s', torrent)
response = {}
# login
login = self._login()
if len(login) > 0 and login['success'] == True:
log.info('Login success, adding torrent')
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid}
response = self._req(self.download_url, args)
self._logout()
else:
log.error('Couldn\'t login to Synology, %s', login)
return response
@@ -8,10 +8,9 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'downloaders', 'tab': 'downloaders',
'list': 'download_providers',
'name': 'transmission', 'name': 'transmission',
'label': 'Transmission', 'label': 'Transmission',
'description': 'Use <a href="http://www.transmissionbt.com/" target="_blank">Transmission</a> to download torrents.', 'description': 'Send torrents to Transmission.',
'wizard': True, 'wizard': True,
'options': [ 'options': [
{ {
@@ -16,7 +16,10 @@ class Transmission(Downloader):
type = ['torrent', 'torrent_magnet'] type = ['torrent', 'torrent_magnet']
log = CPLog(__name__) log = CPLog(__name__)
def download(self, data, movie, filedata = None): def download(self, data, movie, manual = False, filedata = None):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type'))) log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
@@ -27,23 +30,15 @@ class Transmission(Downloader):
return False return False
# Set parameters for Transmission # Set parameters for Transmission
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
# Create the empty folder to download too
self.makeDir(folder_path)
params = { params = {
'paused': self.conf('paused', default = 0), 'paused': self.conf('paused', default = 0),
'download-dir': folder_path 'download-dir': self.conf('directory', default = '').rstrip(os.path.sep)
} }
torrent_params = {} torrent_params = {
if self.conf('ratio'): 'seedRatioLimit': self.conf('ratio'),
torrent_params = { 'seedRatioMode': (0 if self.conf('ratio') else 1)
'seedRatioLimit': self.conf('ratio'), }
'seedRatioMode': self.conf('ratio')
}
if not filedata and data.get('type') == 'torrent': if not filedata and data.get('type') == 'torrent':
log.error('Failed sending torrent, no data') log.error('Failed sending torrent, no data')
@@ -54,13 +49,11 @@ class Transmission(Downloader):
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
if data.get('type') == 'torrent_magnet': if data.get('type') == 'torrent_magnet':
remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params) remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
torrent_params['trackerAdd'] = self.torrent_trackers
else: else:
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params) remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
# Change settings of added torrents # Change settings of added torrents
if torrent_params: trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
return True return True
except Exception, err: except Exception, err:
@@ -1,55 +0,0 @@
from .main import uTorrent
def start():
return uTorrent()
config = [{
'name': 'utorrent',
'groups': [
{
'tab': 'downloaders',
'list': 'download_providers',
'name': 'utorrent',
'label': 'uTorrent',
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> to download torrents.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
'radio_group': 'torrent',
},
{
'name': 'host',
'default': 'localhost:8000',
'description': 'Hostname with port. Usually <strong>localhost:8000</strong>',
},
{
'name': 'username',
},
{
'name': 'password',
'type': 'password',
},
{
'name': 'label',
'description': 'Label to add torrent as.',
},
{
'name': 'paused',
'type': 'bool',
'default': False,
'description': 'Add the torrent paused.',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
}]
@@ -1,197 +0,0 @@
from base64 import b16encode, b32decode
from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.logger import CPLog
from hashlib import sha1
from multipartpost import MultipartPostHandler
import cookielib
import httplib
import json
import re
import time
import urllib
import urllib2
log = CPLog(__name__)
class uTorrent(Downloader):
type = ['torrent', 'torrent_magnet']
utorrent_api = None
def download(self, data, movie, filedata = None):
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type')))
# Load host from config and split out port.
host = self.conf('host').split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
torrent_params = {}
if self.conf('label'):
torrent_params['label'] = self.conf('label')
if not filedata and data.get('type') == 'torrent':
log.error('Failed sending torrent, no data')
return False
if data.get('type') == 'torrent_magnet':
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
else:
info = bdecode(filedata)["info"]
torrent_hash = sha1(bencode(info)).hexdigest().upper()
torrent_filename = self.createFileName(data, filedata, movie)
# Convert base 32 to hex
if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash))
# Send request to uTorrent
try:
if not self.utorrent_api:
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
if data.get('type') == 'torrent_magnet':
self.utorrent_api.add_torrent_uri(data.get('url'))
else:
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
# Change settings of added torrents
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
if self.conf('paused', default = 0):
self.utorrent_api.pause_torrent(torrent_hash)
return True
except Exception, err:
log.error('Failed to send torrent to uTorrent: %s', err)
return False
def getAllDownloadStatus(self):
log.debug('Checking uTorrent download status.')
# Load host from config and split out port.
host = self.conf('host').split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
try:
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
except Exception, err:
log.error('Failed to get uTorrent object: %s', err)
return False
data = ''
try:
data = self.utorrent_api.get_status()
queue = json.loads(data)
if queue.get('error'):
log.error('Error getting data from uTorrent: %s', queue.get('error'))
return False
except Exception, err:
log.error('Failed to get status from uTorrent: %s', err)
return False
if queue.get('torrents', []) == []:
log.debug('Nothing in queue')
return False
statuses = []
# Get torrents
for item in queue.get('torrents', []):
# item[21] = Paused | Downloading | Seeding | Finished
status = 'busy'
if item[21] == 'Finished' or item[21] == 'Seeding':
status = 'completed'
statuses.append({
'id': item[0],
'name': item[2],
'status': status,
'original_status': item[1],
'timeleft': item[10],
})
return statuses
class uTorrentAPI(object):
def __init__(self, host = 'localhost', port = 8000, username = None, password = None):
super(uTorrentAPI, self).__init__()
self.url = 'http://' + str(host) + ':' + str(port) + '/gui/'
self.token = ''
self.last_time = time.time()
cookies = cookielib.CookieJar()
self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
self.opener.addheaders = [('User-agent', 'couchpotato-utorrent-client/1.0')]
if username and password:
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
self.opener.add_handler(urllib2.HTTPBasicAuthHandler(password_manager))
self.opener.add_handler(urllib2.HTTPDigestAuthHandler(password_manager))
elif username or password:
log.debug('User or password missing, not using authentication.')
self.token = self.get_token()
def _request(self, action, data = None):
if time.time() > self.last_time + 1800:
self.last_time = time.time()
self.token = self.get_token()
request = urllib2.Request(self.url + "?token=" + self.token + "&" + action, data)
try:
open_request = self.opener.open(request)
response = open_request.read()
if response:
return response
else:
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
except httplib.InvalidURL, err:
log.error('Invalid uTorrent host, check your config %s', err)
except urllib2.HTTPError, err:
if err.code == 401:
log.error('Invalid uTorrent Username or Password, check your config')
else:
log.error('uTorrent HTTPError: %s', err)
except urllib2.URLError, err:
log.error('Unable to connect to uTorrent %s', err)
return False
def get_token(self):
request = self.opener.open(self.url + "token.html")
token = re.findall("<div.*?>(.*?)</", request.read())[0]
return token
def add_torrent_uri(self, torrent):
action = "action=add-url&s=%s" % urllib.quote(torrent)
return self._request(action)
def add_torrent_file(self, filename, filedata):
action = "action=add-file"
return self._request(action, {"torrent_file": (ss(filename), filedata)})
def set_torrent(self, hash, params):
action = "action=setprops&hash=%s" % hash
for k, v in params.iteritems():
action += "&s=%s&v=%s" % (k, v)
return self._request(action)
def pause_torrent(self, hash):
action = "action=pause&hash=%s" % hash
return self._request(action)
def get_status(self):
action = "list=1"
return self._request(action)
+46 -40
View File
@@ -12,14 +12,14 @@ def runHandler(name, handler, *args, **kwargs):
return handler(*args, **kwargs) return handler(*args, **kwargs)
except: except:
from couchpotato.environment import Env from couchpotato.environment import Env
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all() if not Env.get('dev') else '')) log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all()))
def addEvent(name, handler, priority = 100): def addEvent(name, handler, priority = 100):
if events.get(name): if events.get(name):
e = events[name] e = events[name]
else: else:
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock()) e = events[name] = Event(name = name, threads = 20, exc_info = True, traceback = True, lock = threading.RLock())
def createHandle(*args, **kwargs): def createHandle(*args, **kwargs):
@@ -46,40 +46,49 @@ def fireEvent(name, *args, **kwargs):
#log.debug('Firing event %s', name) #log.debug('Firing event %s', name)
try: try:
options = { # Fire after event
'is_after_event': False, # Fire after event is_after_event = False
'on_complete': False, # onComplete event try:
'single': False, # Return single handler del kwargs['is_after_event']
'merge': False, # Merge items is_after_event = True
'in_order': False, # Fire them in specific order, waits for the other to finish except: pass
}
# Do options # onComplete event
for x in options: on_complete = False
try: try:
val = kwargs[x] on_complete = kwargs['on_complete']
del kwargs[x] del kwargs['on_complete']
options[x] = val except: pass
except: pass
# Return single handler
single = False
try:
del kwargs['single']
single = True
except: pass
# Merge items
merge = False
try:
del kwargs['merge']
merge = True
except: pass
# Merge items
in_order = False
try:
del kwargs['in_order']
in_order = True
except: pass
e = events[name] e = events[name]
if not in_order: e.lock.acquire()
# Lock this event
e.lock.acquire()
e.asynchronous = False e.asynchronous = False
e.in_order = in_order
# Make sure only 1 event is fired at a time when order is wanted
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
kwargs['event_return_on_result'] = options['single']
# Fire
result = e(*args, **kwargs) result = e(*args, **kwargs)
if not in_order: e.lock.release()
# Release lock for this event if single and not merge:
e.lock.release()
if options['single'] and not options['merge']:
results = None results = None
# Loop over results, stop when first not None result is found. # Loop over results, stop when first not None result is found.
@@ -103,22 +112,19 @@ def fireEvent(name, *args, **kwargs):
errorHandler(r[1]) errorHandler(r[1])
# Merge # Merge
if options['merge'] and len(results) > 0: if merge and len(results) > 0:
results.reverse() # Priority 1 is higher then 100
# Dict # Dict
if isinstance(results[0], dict): if type(results[0]) == dict:
merged = {} merged = {}
for result in results: for result in results:
merged = mergeDicts(merged, result) merged = mergeDicts(merged, result)
results = merged results = merged
# Lists # Lists
elif isinstance(results[0], list): elif type(results[0]) == list:
merged = [] merged = []
for result in results: for result in results:
if result not in merged: merged += result
merged += result
results = merged results = merged
@@ -127,11 +133,11 @@ def fireEvent(name, *args, **kwargs):
log.debug('Return modified results for %s', name) log.debug('Return modified results for %s', name)
results = modified_results results = modified_results
if not options['is_after_event']: if not is_after_event:
fireEvent('%s.after' % name, is_after_event = True) fireEvent('%s.after' % name, is_after_event = True)
if options['on_complete']: if on_complete:
options['on_complete']() on_complete()
return results return results
except KeyError, e: except KeyError, e:
+1 -1
View File
@@ -68,7 +68,7 @@ def tryUrlencode(s):
return new[1:] return new[1:]
else: else:
for letter in ss(s): for letter in toUnicode(s):
try: try:
new += quote_plus(letter) new += quote_plus(letter)
except: except:
+8 -50
View File
@@ -1,4 +1,3 @@
from couchpotato.core.helpers.encoding import simplifyString, toSafeString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import hashlib import hashlib
import os.path import os.path
@@ -10,34 +9,15 @@ import sys
log = CPLog(__name__) log = CPLog(__name__)
def getUserDir():
try:
import pwd
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
except:
pass
return os.path.expanduser('~')
def getDownloadDir():
user_dir = getUserDir()
# OSX
if 'darwin' in platform.platform().lower():
return os.path.join(user_dir, 'Downloads')
if os.name == 'nt':
return os.path.join(user_dir, 'Downloads')
return user_dir
def getDataDir(): def getDataDir():
# Windows # Windows
if os.name == 'nt': if os.name == 'nt':
return os.path.join(os.environ['APPDATA'], 'CouchPotato') return os.path.join(os.environ['APPDATA'], 'CouchPotato')
user_dir = getUserDir() import pwd
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
user_dir = os.path.expanduser('~')
# OSX # OSX
if 'darwin' in platform.platform().lower(): if 'darwin' in platform.platform().lower():
@@ -104,7 +84,7 @@ def cleanHost(host):
return host return host
def getImdb(txt, check_inside = True, multiple = False): def getImdb(txt, check_inside = True):
if check_inside and os.path.isfile(txt): if check_inside and os.path.isfile(txt):
output = open(txt, 'r') output = open(txt, 'r')
@@ -112,10 +92,8 @@ def getImdb(txt, check_inside = True, multiple = False):
output.close() output.close()
try: try:
ids = re.findall('(tt\d{7})', txt) id = re.findall('(tt\d{7})', txt)[0]
if multiple: return id
return ids if len(ids) > 0 else []
return ids[0]
except IndexError: except IndexError:
pass pass
@@ -140,32 +118,12 @@ def getTitle(library_dict):
try: try:
return library_dict['titles'][0]['title'] return library_dict['titles'][0]['title']
except: except:
try: log.error('Could not get title for %s', library_dict['identifier'])
for title in library_dict.titles: return None
if title.default:
return title.title
except:
log.error('Could not get title for %s', library_dict.identifier)
return None
log.error('Could not get title for %s', library_dict['identifier'])
return None
except: except:
log.error('Could not get title for library item: %s', library_dict) log.error('Could not get title for library item: %s', library_dict)
return None return None
def possibleTitles(raw_title):
titles = []
titles.append(toSafeString(raw_title).lower())
titles.append(raw_title.lower())
titles.append(simplifyString(raw_title))
return list(set(titles))
def randomString(size = 8, chars = string.ascii_uppercase + string.digits): def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size)) return ''.join(random.choice(chars) for x in range(size))
def splitString(str, split_on = ','):
return [x.strip() for x in str.split(split_on)] if str else []
-12
View File
@@ -67,18 +67,6 @@ class Loader(object):
def addFromDir(self, plugin_type, priority, module, dir_name): def addFromDir(self, plugin_type, priority, module, dir_name):
# Load dir module
try:
m = __import__(module)
splitted = module.split('.')
for sub in splitted[1:]:
m = getattr(m, sub)
if hasattr(m, 'config'):
fireEvent('settings.options', splitted[-1] + '_config', getattr(m, 'config'))
except:
raise
for cur_file in glob.glob(os.path.join(dir_name, '*')): for cur_file in glob.glob(os.path.join(dir_name, '*')):
name = os.path.basename(cur_file) name = os.path.basename(cur_file)
if os.path.isdir(os.path.join(dir_name, name)): if os.path.isdir(os.path.join(dir_name, name)):
+2 -6
View File
@@ -5,7 +5,7 @@ import traceback
class CPLog(object): class CPLog(object):
context = '' context = ''
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key'] replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h']
def __init__(self, context = ''): def __init__(self, context = ''):
if context.endswith('.main'): if context.endswith('.main'):
@@ -17,9 +17,6 @@ class CPLog(object):
def info(self, msg, replace_tuple = ()): def info(self, msg, replace_tuple = ()):
self.logger.info(self.addContext(msg, replace_tuple)) self.logger.info(self.addContext(msg, replace_tuple))
def info2(self, msg, replace_tuple = ()):
self.logger.log(19, self.addContext(msg, replace_tuple))
def debug(self, msg, replace_tuple = ()): def debug(self, msg, replace_tuple = ()):
self.logger.debug(self.addContext(msg, replace_tuple)) self.logger.debug(self.addContext(msg, replace_tuple))
@@ -56,8 +53,7 @@ class CPLog(object):
if not Env.get('dev'): if not Env.get('dev'):
for replace in self.replace_private: for replace in self.replace_private:
msg = re.sub('(\?%s=)[^\&]+' % replace, '?%s=xxx' % replace, msg) msg = re.sub('(%s=)[^\&]+' % replace, '%s=xxx' % replace, msg)
msg = re.sub('(&%s=)[^\&]+' % replace, '&%s=xxx' % replace, msg)
# Replace api key # Replace api key
try: try:
@@ -1,25 +0,0 @@
from migrate.changeset.schema import create_column
from sqlalchemy.schema import MetaData, Column, Table, Index
from sqlalchemy.types import Integer
meta = MetaData()
def upgrade(migrate_engine):
meta.bind = migrate_engine
# Change release, add last_edit and index
last_edit_column = Column('last_edit', Integer)
release = Table('release', meta, last_edit_column)
create_column(last_edit_column, release)
Index('ix_release_last_edit', release.c.last_edit).create()
# Change movie last_edit
last_edit_column = Column('last_edit', Integer)
movie = Table('movie', meta, last_edit_column)
Index('ix_movie_last_edit', movie.c.last_edit).create()
def downgrade(migrate_engine):
pass
@@ -1,13 +0,0 @@
config = {
'name': 'notification_providers',
'groups': [
{
'label': 'Notifications',
'description': 'Notify when movies are done or snatched',
'type': 'list',
'name': 'notification_providers',
'tab': 'notifications',
'options': [],
},
],
}
+3 -3
View File
@@ -14,7 +14,7 @@ class Notification(Plugin):
test_message = 'ZOMG Lazors Pewpewpew!' test_message = 'ZOMG Lazors Pewpewpew!'
listen_to = [ listen_to = [
'renamer.after', 'movie.snatched', 'movie.downloaded', 'movie.snatched',
'updater.available', 'updater.updated', 'updater.available', 'updater.updated',
] ]
dont_listen_to = [] dont_listen_to = []
@@ -30,10 +30,10 @@ class Notification(Plugin):
addEvent(listener, self.createNotifyHandler(listener)) addEvent(listener, self.createNotifyHandler(listener))
def createNotifyHandler(self, listener): def createNotifyHandler(self, listener):
def notify(message = None, group = {}, data = None): def notify(message, data):
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched': if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
return return
return self.notify(message = message, data = data if data else group, listener = listener) return self.notify(message = message, data = data, listener = listener)
return notify return notify
@@ -8,7 +8,6 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'boxcar', 'name': 'boxcar',
'options': [ 'options': [
{ {
+7 -4
View File
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif from couchpotato.core.settings.model import Notification as Notif
@@ -22,7 +22,7 @@ class CoreNotifier(Notification):
listeners = [] listeners = []
listen_to = [ listen_to = [
'renamer.after', 'movie.snatched', 'movie.downloaded', 'movie.snatched',
'updater.available', 'updater.updated', 'updater.available', 'updater.updated',
] ]
@@ -67,7 +67,7 @@ class CoreNotifier(Notification):
ids = None ids = None
if getParam('ids'): if getParam('ids'):
ids = splitString(getParam('ids')) ids = [x.strip() for x in getParam('ids').split(',')]
db = get_session() db = get_session()
@@ -79,6 +79,7 @@ class CoreNotifier(Notification):
q.update({Notif.read: True}) q.update({Notif.read: True})
db.commit() db.commit()
#db.close()
return jsonified({ return jsonified({
'success': True 'success': True
@@ -92,7 +93,7 @@ class CoreNotifier(Notification):
q = db.query(Notif) q = db.query(Notif)
if limit_offset: if limit_offset:
splt = splitString(limit_offset) splt = [x.strip() for x in limit_offset.split(',')]
limit = splt[0] limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1] offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset) q = q.limit(limit).offset(offset)
@@ -106,6 +107,7 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
notifications.append(ndict) notifications.append(ndict)
#db.close()
return jsonified({ return jsonified({
'success': True, 'success': True,
'empty': len(notifications) == 0, 'empty': len(notifications) == 0,
@@ -131,6 +133,7 @@ class CoreNotifier(Notification):
self.frontend(type = listener, data = data) self.frontend(type = listener, data = data)
#db.close()
return True return True
def frontend(self, type = 'notification', data = {}, message = None): def frontend(self, type = 'notification', data = {}, message = None):
@@ -178,14 +178,11 @@ var NotificationBase = new Class({
}, },
addTestButton: function(fieldset, plugin_name){ addTestButton: function(fieldset, plugin_name){
var self = this, var self = this;
button_name = self.testButtonName(fieldset);
if(button_name.contains('Notifications')) return;
new Element('.ctrlHolder.test_button').adopt( new Element('.ctrlHolder.test_button').adopt(
new Element('a.button', { new Element('a.button', {
'text': button_name, 'text': self.testButtonName(fieldset),
'events': { 'events': {
'click': function(){ 'click': function(){
var button = fieldset.getElement('.test_button .button'); var button = fieldset.getElement('.test_button .button');
@@ -194,7 +191,7 @@ var NotificationBase = new Class({
Api.request('notify.'+plugin_name+'.test', { Api.request('notify.'+plugin_name+'.test', {
'onComplete': function(json){ 'onComplete': function(json){
button.set('text', button_name); button.set('text', self.testButtonName(fieldset));
if(json.success){ if(json.success){
var message = new Element('span.success', { var message = new Element('span.success', {
@@ -1,56 +0,0 @@
from .main import Email
def start():
return Email()
config = [{
'name': 'email',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'email',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'from',
'label': 'Send e-mail from',
},
{
'name': 'to',
'label': 'Send e-mail to',
},
{
'name': 'smtp_server',
'label': 'SMTP server',
},
{
'name': 'ssl',
'label': 'Enable SSL',
'default': 0,
'type': 'bool',
},
{
'name': 'smtp_user',
'label': 'SMTP user',
},
{
'name': 'smtp_pass',
'label': 'SMTP password',
'type': 'password',
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]
@@ -1,55 +0,0 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from email.mime.text import MIMEText
import smtplib
import traceback
log = CPLog(__name__)
class Email(Notification):
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
# Extract all the settings from settings
from_address = self.conf('from')
to_address = self.conf('to')
ssl = self.conf('ssl')
smtp_server = self.conf('smtp_server')
smtp_user = self.conf('smtp_user')
smtp_pass = self.conf('smtp_pass')
# Make the basic message
message = MIMEText(toUnicode(message))
message['Subject'] = self.default_title
message['From'] = from_address
message['To'] = to_address
try:
# Open the SMTP connection, via SSL if requested
log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled"))
mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server)
# Check too see if an login attempt should be attempted
if len(smtp_user) > 0:
log.debug("Logging on to SMTP server using username \'%s\'%s", (smtp_user, " and a password" if len(smtp_pass) > 0 else ""))
mailserver.login(smtp_user, smtp_pass)
# Send the e-mail
log.debug("Sending the email")
mailserver.sendmail(from_address, splitString(to_address), message.as_string())
# Close the SMTP connection
mailserver.quit()
log.info('Email notification sent')
return True
except:
log.error('E-mail failed: %s', traceback.format_exc())
return False
return False
@@ -8,7 +8,6 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'growl', 'name': 'growl',
'description': 'Version 1.4+', 'description': 'Version 1.4+',
'options': [ 'options': [
+2 -5
View File
@@ -37,11 +37,8 @@ class Growl(Notification):
) )
self.growl.register() self.growl.register()
self.registered = True self.registered = True
except Exception, e: except:
if 'timed out' in str(e): log.error('Failed register of growl: %s', traceback.format_exc())
self.registered = True
else:
log.error('Failed register of growl: %s', traceback.format_exc())
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return if self.isDisabled(): return
@@ -8,7 +8,6 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'nmj', 'name': 'nmj',
'label': 'NMJ', 'label': 'NMJ',
'options': [ 'options': [
+1 -1
View File
@@ -69,7 +69,7 @@ class NMJ(Notification):
'mount': mount, 'mount': mount,
}) })
def addToLibrary(self, message = None, group = {}): def addToLibrary(self, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
host = self.conf('host') host = self.conf('host')
@@ -8,7 +8,6 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'notifo', 'name': 'notifo',
'description': 'Keep in mind that Notifo service will end soon.', 'description': 'Keep in mind that Notifo service will end soon.',
'options': [ 'options': [
@@ -8,7 +8,6 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'notifymyandroid', 'name': 'notifymyandroid',
'label': 'Notify My Android', 'label': 'Notify My Android',
'options': [ 'options': [
@@ -1,4 +1,3 @@
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import pynma import pynma
@@ -12,7 +11,7 @@ class NotifyMyAndroid(Notification):
if self.isDisabled(): return if self.isDisabled(): return
nma = pynma.PyNMA() nma = pynma.PyNMA()
keys = splitString(self.conf('api_key')) keys = [x.strip() for x in self.conf('api_key').split(',')]
nma.addkey(keys) nma.addkey(keys)
nma.developerkey(self.conf('dev_key')) nma.developerkey(self.conf('dev_key'))
@@ -8,9 +8,8 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'notifymywp', 'name': 'notifymywp',
'label': 'Windows Phone', 'label': 'Notify My Windows Phone',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@@ -1,4 +1,3 @@
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from pynmwp import PyNMWP from pynmwp import PyNMWP
@@ -11,7 +10,7 @@ class NotifyMyWP(Notification):
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return if self.isDisabled(): return
keys = splitString(self.conf('api_key')) keys = [x.strip() for x in self.conf('api_key').split(',')]
p = PyNMWP(keys, self.conf('dev_key')) p = PyNMWP(keys, self.conf('dev_key'))
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1) response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
@@ -8,7 +8,6 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'plex', 'name': 'plex',
'options': [ 'options': [
{ {
+1 -1
View File
@@ -17,7 +17,7 @@ class Plex(Notification):
super(Plex, self).__init__() super(Plex, self).__init__()
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, message = None, group = {}): def addToLibrary(self, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
log.info('Sending notification to Plex') log.info('Sending notification to Plex')
@@ -8,7 +8,6 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'prowl', 'name': 'prowl',
'options': [ 'options': [
{ {
+19 -15
View File
@@ -1,35 +1,39 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import traceback from httplib import HTTPSConnection
log = CPLog(__name__) log = CPLog(__name__)
class Prowl(Notification): class Prowl(Notification):
urls = {
'api': 'https://api.prowlapp.com/publicapi/add'
}
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return if self.isDisabled(): return
http_handler = HTTPSConnection('api.prowlapp.com')
data = { data = {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
'application': self.default_title, 'application': self.default_title,
'description': toUnicode(message), 'description': toUnicode(message),
'priority': self.conf('priority'), 'priority': self.conf('priority'),
} }
headers = {
'Content-type': 'application/x-www-form-urlencoded'
}
try: http_handler.request('POST',
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False) '/publicapi/add',
headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = tryUrlencode(data)
)
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
log.info('Prowl notifications sent.') log.info('Prowl notifications sent.')
return True return True
except: elif request_status == 401:
log.error('Prowl failed: %s', traceback.format_exc()) log.error('Prowl auth failed: %s', response.reason)
return False
return False else:
log.error('Prowl notification failed.')
return False
@@ -1,48 +0,0 @@
from .main import Pushalot
def start():
return Pushalot()
config = [{
'name': 'pushalot',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'pushalot',
'description': 'for Windows Phone and Windows 8',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'auth_token',
'label': 'Auth Token',
},
{
'name': 'silent',
'label': 'Silent',
'default': 0,
'type': 'bool',
'description': 'Don\'t send Toast notifications. Only update Live Tile',
},
{
'name': 'important',
'label': 'High Priority',
'default': 0,
'type': 'bool',
'description': 'Send message with High priority.',
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]
@@ -1,37 +0,0 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import traceback
log = CPLog(__name__)
class Pushalot(Notification):
urls = {
'api': 'https://pushalot.com/api/sendmessage'
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = {
'AuthorizationToken': self.conf('auth_token'),
'Title': self.default_title,
'Body': toUnicode(message),
'LinkTitle': toUnicode("CouchPotato"),
'link': toUnicode("https://couchpota.to/"),
'IsImportant': self.conf('important'),
'IsSilent': self.conf('silent'),
}
headers = {
'Content-type': 'application/x-www-form-urlencoded'
}
try:
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
return True
except:
log.error('PushAlot failed: %s', traceback.format_exc())
return False
@@ -8,7 +8,6 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'pushover', 'name': 'pushover',
'options': [ 'options': [
{ {
@@ -8,7 +8,6 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'synoindex', 'name': 'synoindex',
'description': 'Automaticly adds index to Synology Media Server.', 'description': 'Automaticly adds index to Synology Media Server.',
'options': [ 'options': [
@@ -1,8 +1,6 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import os
import subprocess import subprocess
log = CPLog(__name__) log = CPLog(__name__)
@@ -10,17 +8,14 @@ log = CPLog(__name__)
class Synoindex(Notification): class Synoindex(Notification):
index_path = '/usr/syno/bin/synoindex'
def __init__(self): def __init__(self):
super(Synoindex, self).__init__()
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, message = None, group = {}): def addToLibrary(self, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
command = [self.index_path, '-A', group.get('destination_dir')] command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
log.info('Executing synoindex command: %s ', command) log.info(u'Executing synoindex command: %s ', command)
try: try:
p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT) p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
out = p.communicate() out = p.communicate()
@@ -31,6 +26,3 @@ class Synoindex(Notification):
return False return False
return True return True
def test(self):
return jsonified({'success': os.path.isfile(self.index_path)})
@@ -1,33 +0,0 @@
from .main import Toasty
def start():
return Toasty()
config = [{
'name': 'toasty',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'toasty',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'api_key',
'label': 'Device ID',
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]
@@ -1,30 +0,0 @@
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import traceback
log = CPLog(__name__)
class Toasty(Notification):
urls = {
'api': 'http://api.supertoasty.com/notify/%s?%s'
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = {
'title': self.default_title,
'text': toUnicode(message),
'sender': toUnicode("CouchPotato"),
'image': 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/homescreen.png',
}
try:
self.urlopen(self.urls['api'] % (self.conf('api_key'), tryUrlencode(data)), show_error = False)
return True
except:
log.error('Toasty failed: %s', traceback.format_exc())
return False
@@ -8,7 +8,6 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'twitter', 'name': 'twitter',
'options': [ 'options': [
{ {
+4 -15
View File
@@ -40,31 +40,20 @@ class Twitter(Notification):
direct_message_users = self.conf('screen_name') direct_message_users = self.conf('screen_name')
mention = self.conf('mention') mention = self.conf('mention')
mention_tag = None
if mention: if mention:
if direct_message: if direct_message:
direct_message_users = '%s %s' % (direct_message_users, mention) direct_message_users = '%s %s' % (direct_message_users, mention)
direct_message_users = direct_message_users.replace('@', ' ') direct_message_users = direct_message_users.replace('@',' ')
direct_message_users = direct_message_users.replace(',', ' ') direct_message_users = direct_message_users.replace(',',' ')
else: else:
mention_tag = '@%s' % mention.lstrip('@') message = '%s @%s' % (message, mention.lstrip('@'))
message = '%s %s' % (message, mention_tag)
try: try:
if direct_message: if direct_message:
for user in direct_message_users.split(): for user in direct_message_users.split():
api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message)) api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message))
else: else:
update_message = '[%s] %s' % (self.default_title, message) api.PostUpdate('[%s] %s' % (self.default_title, message))
if len(update_message) > 140:
if mention_tag:
api.PostUpdate(update_message[:135 - len(mention_tag)] + ('%s 1/2 ' % mention_tag))
api.PostUpdate(update_message[135 - len(mention_tag):] + ('%s 2/2 ' % mention_tag))
else:
api.PostUpdate(update_message[:135] + ' 1/2')
api.PostUpdate(update_message[135:] + ' 2/2')
else:
api.PostUpdate(update_message)
except Exception, e: except Exception, e:
log.error('Error sending tweet: %s', e) log.error('Error sending tweet: %s', e)
return False return False
@@ -8,10 +8,8 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers',
'name': 'xbmc', 'name': 'xbmc',
'label': 'XBMC', 'label': 'XBMC',
'description': 'v11 (Eden) and v12 (Frodo)',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
+19 -165
View File
@@ -1,189 +1,43 @@
from couchpotato.core.helpers.variable import splitString from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from flask.helpers import json
import base64 import base64
import traceback
import urllib
log = CPLog(__name__) log = CPLog(__name__)
class XBMC(Notification): class XBMC(Notification):
listen_to = ['renamer.after'] listen_to = ['movie.downloaded']
use_json_notifications = {}
couch_logo_url = 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/xbmc-notify.png'
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return if self.isDisabled(): return
hosts = splitString(self.conf('host')) hosts = [x.strip() for x in self.conf('host').split(",")]
successful = 0 successful = 0
for host in hosts: for host in hosts:
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
successful += 1
if self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host):
successful += 1
if self.use_json_notifications.get(host) is None: return successful == len(hosts)*2
self.getXBMCJSONversion(host, message = message)
if self.use_json_notifications.get(host): def send(self, command, host):
response = self.request(host, [
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.couch_logo_url}),
('VideoLibrary.Scan', {}),
])
else:
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
response += self.request(host, [('VideoLibrary.Scan', {})])
try: url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
for result in response:
if (result.get('result') and result['result'] == 'OK'):
successful += 1
elif (result.get('error')):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
except: headers = {}
log.error('Failed parsing results: %s', traceback.format_exc())
return successful == len(hosts) * 2
def getXBMCJSONversion(self, host, message = ''):
success = False
# XBMC JSON-RPC version request
response = self.request(host, [
('JSONRPC.Version', {})
])
for result in response:
if (result.get('result') and type(result['result']['version']).__name__ == 'int'):
# only v2 and v4 return an int object
# 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)
# disable JSON use
self.use_json_notifications[host] = False
# send the text message
resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
for result in resp:
if (result.get('result') and result['result'] == 'OK'):
log.debug('Message delivered successfully!')
success = True
break
elif (result.get('error')):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
break
elif (result.get('result') and type(result['result']['version']).__name__ == 'dict'):
# XBMC JSON-RPC v6 returns an array object containing
# major, minor and patch number
xbmc_rpc_version = str(result['result']['version']['major'])
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)
# ok, XBMC version is supported
self.use_json_notifications[host] = True
# send the text message
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image':self.couch_logo_url})])
for result in resp:
if (result.get('result') and result['result'] == 'OK'):
log.debug('Message delivered successfully!')
success = True
break
elif (result.get('error')):
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['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.debug('Use JSON notifications: %s ', self.use_json_notifications)
return success
def notifyXBMCnoJSON(self, host, data):
server = 'http://%s/xbmcCmds/' % host
# Notification(title, message [, timeout , image])
cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.couch_logo_url))
server += cmd
# I have no idea what to set to, just tried text/plain and seems to be working :)
headers = {
'Content-Type': 'text/plain',
}
# authentication support
if self.conf('password'): if self.conf('password'):
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '') headers = {
headers['Authorization'] = 'Basic %s' % base64string 'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password')))[:-1]
}
try: try:
log.debug('Sending non-JSON-type request to %s: %s', (host, data)) self.urlopen(url, headers = headers, show_error = False)
# response wil either be 'OK':
# <html>
# <li>OK
# </html>
#
# or 'Error':
# <html>
# <li>Error:<message>
# </html>
#
response = self.urlopen(server, headers = headers)
if 'OK' in response:
log.debug('Returned from non-JSON-type request %s: %s', (host, response))
# manually fake expected response array
return [{'result': 'OK'}]
else:
log.error('Returned from non-JSON-type request %s: %s', (host, response))
# manually fake expected response array
return [{'result': 'Error'}]
except: except:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc()) log.error("Couldn't sent command to XBMC")
return [{'result': 'Error'}] return False
def request(self, host, requests):
server = 'http://%s/jsonrpc' % host
data = []
for req in requests:
method, kwargs = req
data.append({
'method': method,
'params': kwargs,
'jsonrpc': '2.0',
'id': method,
})
data = json.dumps(data)
headers = {
'Content-Type': 'application/json',
}
if self.conf('password'):
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
headers['Authorization'] = 'Basic %s' % base64string
try:
log.debug('Sending request to %s: %s', (host, data))
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
response = json.loads(rdata)
log.debug('Returned from request %s: %s', (host, response))
return response
except:
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return []
log.info('XBMC notification to %s successful.', host)
return True
@@ -5,12 +5,13 @@ def start():
config = [{ config = [{
'name': 'automation', 'name': 'automation',
'order': 101, 'order': 30,
'groups': [ 'groups': [
{ {
'tab': 'automation', 'tab': 'automation',
'name': 'automation', 'name': 'automation',
'label': 'Minimal movie requirements', 'label': 'Automation',
'description': 'Minimal movie requirements',
'options': [ 'options': [
{ {
'name': 'year', 'name': 'year',
+1 -9
View File
@@ -18,17 +18,9 @@ class Automation(Plugin):
def addMovies(self): def addMovies(self):
movies = fireEvent('automation.get_movies', merge = True) movies = fireEvent('automation.get_movies', merge = True)
movie_ids = []
for imdb_id in movies: for imdb_id in movies:
prop_name = 'automation.added.%s' % imdb_id prop_name = 'automation.added.%s' % imdb_id
added = Env.prop(prop_name, default = False) added = Env.prop(prop_name, default = False)
if not added: if not added:
added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_library = True, single = True) fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False)
if added_movie:
movie_ids.append(added_movie['id'])
Env.prop(prop_name, True) Env.prop(prop_name, True)
for movie_id in movie_ids:
movie_dict = fireEvent('movie.get', movie_id, single = True)
fireEvent('searcher.single', movie_dict)
+13 -38
View File
@@ -1,8 +1,8 @@
from StringIO import StringIO from StringIO import StringIO
from couchpotato import addView from couchpotato import addView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString, ss
from couchpotato.core.helpers.variable import getExt, md5 from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.templating import render_template_string from flask.templating import render_template_string
@@ -34,7 +34,7 @@ class Plugin(object):
http_failed_disabled = {} http_failed_disabled = {}
def registerPlugin(self): def registerPlugin(self):
addEvent('app.do_shutdown', self.doShutdown) addEvent('app.shutdown', self.doShutdown)
addEvent('plugin.running', self.isRunning) addEvent('plugin.running', self.isRunning)
def conf(self, attr, value = None, default = None): def conf(self, attr, value = None, default = None):
@@ -64,7 +64,7 @@ class Plugin(object):
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')): for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
ext = getExt(f) ext = getExt(f)
if ext in ['js', 'css']: if ext in ['js', 'css']:
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f) fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f))
def showStatic(self, filename): def showStatic(self, filename):
d = os.path.join(self.plugin_path, 'static') d = os.path.join(self.plugin_path, 'static')
@@ -78,7 +78,7 @@ class Plugin(object):
self.makeDir(os.path.dirname(path)) self.makeDir(os.path.dirname(path))
try: try:
f = open(path, 'w+' if not binary else 'w+b') f = open(path, 'w' if not binary else 'wb')
f.write(content) f.write(content)
f.close() f.close()
os.chmod(path, Env.getPermission('file')) os.chmod(path, Env.getPermission('file'))
@@ -98,7 +98,6 @@ class Plugin(object):
# http request # http request
def urlopen(self, url, timeout = 30, params = None, headers = None, opener = None, multipart = False, show_error = True): def urlopen(self, url, timeout = 30, params = None, headers = None, opener = None, multipart = False, show_error = True):
url = ss(url)
if not headers: headers = {} if not headers: headers = {}
if not params: params = {} if not params: params = {}
@@ -114,11 +113,8 @@ class Plugin(object):
# Don't try for failed requests # Don't try for failed requests
if self.http_failed_disabled.get(host, 0) > 0: if self.http_failed_disabled.get(host, 0) > 0:
if self.http_failed_disabled[host] > (time.time() - 900): if self.http_failed_disabled[host] > (time.time() - 900):
log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host) log.info('Disabled calls to %s for 15 minutes because so many failed requests.', host)
if not show_error: raise Exception
raise
else:
return ''
else: else:
del self.http_failed_request[host] del self.http_failed_request[host]
del self.http_failed_disabled[host] del self.http_failed_disabled[host]
@@ -127,14 +123,11 @@ class Plugin(object):
try: try:
if multipart: if multipart:
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data')) log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()]))
request = urllib2.Request(url, params, headers) request = urllib2.Request(url, params, headers)
if opener: cookies = cookielib.CookieJar()
opener.add_handler(MultipartPostHandler()) opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
else:
cookies = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
response = opener.open(request, timeout = timeout) response = opener.open(request, timeout = timeout)
else: else:
@@ -225,7 +218,7 @@ class Plugin(object):
def getCache(self, cache_key, url = None, **kwargs): def getCache(self, cache_key, url = None, **kwargs):
cache_key = md5(ss(cache_key)) cache_key = simplifyString(cache_key)
cache = Env.get('cache').get(cache_key) cache = Env.get('cache').get(cache_key)
if cache: if cache:
if not Env.get('dev'): log.debug('Getting cache %s', cache_key) if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
@@ -240,36 +233,18 @@ class Plugin(object):
del kwargs['cache_timeout'] del kwargs['cache_timeout']
data = self.urlopen(url, **kwargs) data = self.urlopen(url, **kwargs)
if data: if data:
self.setCache(cache_key, data, timeout = cache_timeout) self.setCache(cache_key, data, timeout = cache_timeout)
return data return data
except: except:
if not kwargs.get('show_error', True): pass
raise
return ''
def setCache(self, cache_key, value, timeout = 300): def setCache(self, cache_key, value, timeout = 300):
log.debug('Setting cache %s', cache_key) log.debug('Setting cache %s', cache_key)
Env.get('cache').set(cache_key, value, timeout) Env.get('cache').set(cache_key, value, timeout)
return value return value
def createNzbName(self, data, movie):
tag = self.cpTag(movie)
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie))
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('type'))
def cpTag(self, movie):
if Env.setting('enabled', 'renamer'):
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
return ''
def isDisabled(self): def isDisabled(self):
return not self.isEnabled() return not self.isEnabled()
+3 -19
View File
@@ -1,6 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.request import getParam, jsonified from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.helpers.variable import getUserDir
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import ctypes import ctypes
import os import os
@@ -15,7 +14,7 @@ if os.name == 'nt':
raise ImportError("Missing the win32file module, which is a part of the prerequisite \ raise ImportError("Missing the win32file module, which is a part of the prerequisite \
pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/"); pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/");
else: else:
import win32file #@UnresolvedImport import win32file
class FileBrowser(Plugin): class FileBrowser(Plugin):
@@ -28,8 +27,6 @@ class FileBrowser(Plugin):
}, },
'return': {'type': 'object', 'example': """{ 'return': {'type': 'object', 'example': """{
'is_root': bool, //is top most folder 'is_root': bool, //is top most folder
'parent': string, //parent folder of requested path
'home': string, //user home folder
'empty': bool, //directory is empty 'empty': bool, //directory is empty
'dirs': array, //directory names 'dirs': array, //directory names
}"""} }"""}
@@ -66,28 +63,15 @@ class FileBrowser(Plugin):
def view(self): def view(self):
path = getParam('path', '/') path = getParam('path', '/')
home = getUserDir()
if not path:
path = home
try: try:
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True)) dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
except: except:
dirs = [] dirs = []
parent = os.path.dirname(path.rstrip(os.path.sep))
if parent == path.rstrip(os.path.sep):
parent = '/'
elif parent != '/' and parent[-2:] != ':\\':
parent += os.path.sep
return jsonified({ return jsonified({
'is_root': path == '/', 'is_root': path == '/' or not path,
'empty': len(dirs) == 0, 'empty': len(dirs) == 0,
'parent': parent,
'home': home + os.path.sep,
'platform': os.name,
'dirs': dirs, 'dirs': dirs,
}) })
@@ -98,7 +82,7 @@ class FileBrowser(Plugin):
def has_hidden_attribute(self, filepath): def has_hidden_attribute(self, filepath):
try: try:
attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(filepath)) #@UndefinedVariable attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(filepath))
assert attrs != -1 assert attrs != -1
result = bool(attrs & 2) result = bool(attrs & 2)
except (AttributeError, AssertionError): except (AttributeError, AssertionError):
@@ -1,6 +0,0 @@
from .main import Dashboard
def start():
return Dashboard()
config = []
-134
View File
@@ -1,134 +0,0 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import splitString, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie
from sqlalchemy.orm import joinedload_all
import random
import time
log = CPLog(__name__)
class Dashboard(Plugin):
def __init__(self):
addApiView('dashboard.suggestions', self.suggestView)
addApiView('dashboard.soon', self.getSoonView)
def newSuggestions(self):
movies = fireEvent('movie.list', status = ['active', 'done'], limit_offset = (20, 0), single = True)
movie_identifiers = [m['library']['identifier'] for m in movies[1]]
ignored_movies = fireEvent('movie.list', status = ['ignored', 'deleted'], limit_offset = (100, 0), single = True)
ignored_identifiers = [m['library']['identifier'] for m in ignored_movies[1]]
suggestions = fireEvent('movie.suggest', movies = movie_identifiers, ignore = ignored_identifiers, single = True)
suggest_status = fireEvent('status.get', 'suggest', single = True)
for suggestion in suggestions:
fireEvent('movie.add', params = {'identifier': suggestion}, force_readd = False, search_after = False, status_id = suggest_status.get('id'))
def suggestView(self):
db = get_session()
movies = db.query(Movie).limit(20).all()
identifiers = [m.library.identifier for m in movies]
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
return jsonified({
'result': True,
'suggestions': suggestions
})
def getSoonView(self):
params = getParams()
db = get_session()
now = time.time()
# Get profiles first, determine pre or post theater
profiles = fireEvent('profile.all', single = True)
qualities = fireEvent('quality.all', single = True)
pre_releases = fireEvent('quality.pre_releases', single = True)
id_pre = {}
for quality in qualities:
id_pre[quality.get('id')] = quality.get('identifier') in pre_releases
# See what the profile contain and cache it
profile_pre = {}
for profile in profiles:
contains = {}
for profile_type in profile.get('types', []):
contains['theater' if id_pre.get(profile_type.get('quality_id')) else 'dvd'] = True
profile_pre[profile.get('id')] = contains
# Get all active movies
active_status = fireEvent('status.get', 'active', single = True)
subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery()
q = db.query(Movie).join((subq, subq.c.id == Movie.id)) \
.options(joinedload_all('releases')) \
.options(joinedload_all('profile.types')) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files'))
# Add limit
limit_offset = params.get('limit_offset')
limit = 12
if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
limit = tryInt(splt[0])
all_movies = q.all()
if params.get('random', False):
random.shuffle(all_movies)
movies = []
for movie in all_movies:
pp = profile_pre.get(movie.profile.id)
eta = movie.library.info.get('release_date', {}) or {}
coming_soon = False
# Theater quality
if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, single = True):
coming_soon = True
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
coming_soon = True
if coming_soon:
temp = movie.to_dict({
'profile': {'types': {}},
'releases': {'files':{}, 'info': {}},
'library': {'titles': {}, 'files':{}},
'files': {},
})
# Don't list older movies
if ((not params.get('late') and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
movies.append(temp)
if len(movies) >= limit:
break
return jsonified({
'success': True,
'empty': len(movies) == 0,
'movies': movies,
})
getLateView = getSoonView
+2 -57
View File
@@ -2,15 +2,12 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import md5, getExt from couchpotato.core.helpers.variable import md5, getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.scanner.main import Scanner
from couchpotato.core.settings.model import FileType, File from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env from couchpotato.environment import Env
import os.path import os.path
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -31,54 +28,6 @@ class FileManager(Plugin):
'return': {'type': 'file'} 'return': {'type': 'file'}
}) })
addApiView('file.types', self.getTypesView, docs = {
'desc': 'Return a list of all the file types and their ids.',
'return': {'type': 'object', 'example': """{
'types': [
{
"identifier": "poster_original",
"type": "image",
"id": 1,
"name": "Poster_original"
},
{
"identifier": "poster",
"type": "image",
"id": 2,
"name": "Poster"
},
etc
]
}"""}
})
addEvent('app.load', self.cleanup)
addEvent('app.load', self.init)
def init(self):
for type_tuple in Scanner.file_types.values():
self.getType(type_tuple)
def cleanup(self):
# Wait a bit after starting before cleanup
time.sleep(3)
log.debug('Cleaning up unused files')
python_cache = Env.get('cache')._path
try:
db = get_session()
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
for filename in walk_files:
if root == python_cache or 'minified' in filename: continue
file_path = os.path.join(root, filename)
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
if not f:
os.remove(file_path)
except:
log.error('Failed removing unused file: %s', traceback.format_exc())
def showCacheFile(self, filename = ''): def showCacheFile(self, filename = ''):
cache_dir = Env.get('cache_dir') cache_dir = Env.get('cache_dir')
@@ -140,6 +89,7 @@ class FileManager(Plugin):
db.commit() db.commit()
type_dict = ft.to_dict() type_dict = ft.to_dict()
#db.close()
return type_dict return type_dict
def getTypes(self): def getTypes(self):
@@ -152,10 +102,5 @@ class FileManager(Plugin):
for type_object in results: for type_object in results:
types.append(type_object.to_dict()) types.append(type_object.to_dict())
#db.close()
return types return types
def getTypesView(self):
return jsonified({
'types': self.getTypes()
})
@@ -4,7 +4,6 @@ var File = new Class({
var self = this; var self = this;
if(!file){ if(!file){
self.empty = true;
self.el = new Element('div'); self.el = new Element('div');
return return
} }
+15 -16
View File
@@ -1,7 +1,6 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, LibraryTitle, File from couchpotato.core.settings.model import Library, LibraryTitle, File
@@ -38,7 +37,7 @@ class LibraryPlugin(Plugin):
title = LibraryTitle( title = LibraryTitle(
title = toUnicode(attrs.get('title')), title = toUnicode(attrs.get('title')),
simple_title = self.simplifyTitle(attrs.get('title')), simple_title = self.simplifyTitle(attrs.get('title'))
) )
l.titles.append(title) l.titles.append(title)
@@ -53,6 +52,7 @@ class LibraryPlugin(Plugin):
library_dict = l.to_dict(self.default_dict) library_dict = l.to_dict(self.default_dict)
#db.close()
return library_dict return library_dict
def update(self, identifier, default_title = '', force = False): def update(self, identifier, default_title = '', force = False):
@@ -96,7 +96,6 @@ class LibraryPlugin(Plugin):
titles = info.get('titles', []) titles = info.get('titles', [])
log.debug('Adding titles: %s', titles) log.debug('Adding titles: %s', titles)
counter = 0
for title in titles: for title in titles:
if not title: if not title:
continue continue
@@ -104,29 +103,26 @@ class LibraryPlugin(Plugin):
t = LibraryTitle( t = LibraryTitle(
title = title, title = title,
simple_title = self.simplifyTitle(title), simple_title = self.simplifyTitle(title),
default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title) default = title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
) )
library.titles.append(t) library.titles.append(t)
counter += 1
db.commit() db.commit()
# Files # Files
images = info.get('images', []) images = info.get('images', [])
for image_type in ['poster']: for type in images:
for image in images.get(image_type, []): for image in images[type]:
if not isinstance(image, (str, unicode)): if not isinstance(image, str):
continue continue
file_path = fireEvent('file.download', url = image, single = True) file_path = fireEvent('file.download', url = image, single = True)
if file_path: if file_path:
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True) file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True)
try: try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one() file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj) library.files.append(file_obj)
db.commit() db.commit()
break
except: except:
log.debug('Failed to attach to library: %s', traceback.format_exc()) log.debug('Failed to attach to library: %s', traceback.format_exc())
@@ -140,23 +136,26 @@ class LibraryPlugin(Plugin):
library = db.query(Library).filter_by(identifier = identifier).first() library = db.query(Library).filter_by(identifier = identifier).first()
if not library.info: if not library.info:
library_dict = self.update(identifier, force = True) library_dict = self.update(identifier)
dates = library_dict.get('info', {}).get('release_date') dates = library_dict.get('info', {}).get('release_dates')
else: else:
dates = library.info.get('release_date') dates = library.info.get('release_date')
if dates and dates.get('expires', 0) < time.time() or not dates: if dates and dates.get('expires', 0) < time.time():
dates = fireEvent('movie.release_date', identifier = identifier, merge = True) dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
library.info = mergeDicts(library.info, {'release_date': dates }) library.info['release_date'] = dates
library.info = library.info
db.commit() db.commit()
dates = library.info.get('release_date', {})
#db.close()
return dates return dates
def simplifyTitle(self, title): def simplifyTitle(self, title):
title = toUnicode(title) title = toUnicode(title)
nr_prefix = '' if title[0] in ascii_letters else '#' nr_prefix = '' if title[0] in ascii_letters else '#'
title = simplifyString(title) title = simplifyString(title)
+31 -188
View File
@@ -2,35 +2,22 @@ from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import splitString, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
import ctypes
import os import os
import sys
import time import time
import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Manage(Plugin): class Manage(Plugin):
in_progress = False
def __init__(self): def __init__(self):
fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2) fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
addEvent('manage.update', self.updateLibrary) addEvent('manage.update', self.updateLibrary)
addEvent('manage.diskspace', self.getDiskSpace)
# Add files after renaming
def after_rename(message = None, group = {}):
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
addEvent('renamer.after', after_rename, priority = 110)
addApiView('manage.update', self.updateLibraryView, docs = { addApiView('manage.update', self.updateLibraryView, docs = {
'desc': 'Update the library by scanning for new movies', 'desc': 'Update the library by scanning for new movies',
'params': { 'params': {
@@ -38,23 +25,11 @@ class Manage(Plugin):
} }
}) })
addApiView('manage.progress', self.getProgress, docs = {
'desc': 'Get the progress of current manage update',
'return': {'type': 'object', 'example': """{
'progress': False || object, total & to_go,
}"""},
})
if not Env.get('dev'): if not Env.get('dev'):
def updateLibrary(): def updateLibrary():
self.updateLibrary(full = False) self.updateLibrary(full = False)
addEvent('app.load', updateLibrary) addEvent('app.load', updateLibrary)
def getProgress(self):
return jsonified({
'progress': self.in_progress
})
def updateLibraryView(self): def updateLibraryView(self):
full = getParam('full', default = 1) full = getParam('full', default = 1)
@@ -68,181 +43,49 @@ class Manage(Plugin):
def updateLibrary(self, full = True): def updateLibrary(self, full = True):
last_update = float(Env.prop('manage.last_update', default = 0)) last_update = float(Env.prop('manage.last_update', default = 0))
if self.in_progress: if self.isDisabled() or (last_update > time.time() - 20):
log.info('Already updating library: %s', self.in_progress)
return
elif self.isDisabled() or (last_update > time.time() - 20):
return return
self.in_progress = {} directories = self.directories()
fireEvent('notify.frontend', type = 'manage.updating', data = True) added_identifiers = []
try: for directory in directories:
directories = self.directories() if not os.path.isdir(directory):
added_identifiers = [] if len(directory) > 0:
log.error('Directory doesn\'t exist: %s', directory)
continue
# Add some progress log.info('Updating manage library: %s', directory)
self.in_progress = {} identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update if not full else 0, single = True)
for directory in directories: if identifiers:
self.in_progress[os.path.normpath(directory)] = { added_identifiers.extend(identifiers)
'total': None,
'to_go': None,
}
for directory in directories: # Break if CP wants to shut down
folder = os.path.normpath(directory) if self.shuttingDown():
if not os.path.isdir(folder):
if len(directory) > 0:
log.error('Directory doesn\'t exist: %s', folder)
continue
log.info('Updating manage library: %s', folder)
fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder)
onFound = self.createAddToLibrary(folder, added_identifiers)
fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, on_found = onFound, single = True)
# Break if CP wants to shut down
if self.shuttingDown():
break
# If cleanup option is enabled, remove offline files from database
if self.conf('cleanup') and full and not self.shuttingDown():
# Get movies with done status
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
for done_movie in done_movies:
if done_movie['library']['identifier'] not in added_identifiers:
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
else:
for release in done_movie.get('releases', []):
if len(release.get('files', [])) == 0:
fireEvent('release.delete', release['id'])
else:
for release_file in release.get('files', []):
# Remove release not available anymore
if not os.path.isfile(ss(release_file['path'])):
fireEvent('release.clean', release['id'])
break
# Check if there are duplicate releases (different quality) use the last one, delete the rest
if len(done_movie.get('releases', [])) > 1:
used_files = {}
for release in done_movie.get('releases', []):
for release_file in release.get('files', []):
already_used = used_files.get(release_file['path'])
if already_used:
if already_used < release['id']:
fireEvent('release.delete', release['id'], single = True) # delete this one
else:
fireEvent('release.delete', already_used, single = True) # delete previous one
break
else:
used_files[release_file['path']] = release.get('id')
del used_files
Env.prop('manage.last_update', time.time())
except:
log.error('Failed updating library: %s', (traceback.format_exc()))
while True and not self.shuttingDown():
delete_me = {}
for folder in self.in_progress:
if self.in_progress[folder]['to_go'] <= 0:
delete_me[folder] = True
for delete in delete_me:
del self.in_progress[delete]
if len(self.in_progress) == 0:
break break
time.sleep(1) # If cleanup option is enabled, remove offline files from database
if self.conf('cleanup') and full and not self.shuttingDown():
fireEvent('notify.frontend', type = 'manage.updating', data = False) # Get movies with done status
self.in_progress = False total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
def createAddToLibrary(self, folder, added_identifiers = []): for done_movie in done_movies:
def addToLibrary(group, total_found, to_go): if done_movie['library']['identifier'] not in added_identifiers:
if self.in_progress[folder]['total'] is None: fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
self.in_progress[folder] = { else:
'total': total_found, for release in done_movie.get('releases', []):
'to_go': total_found, for release_file in release.get('files', []):
} # Remove release not available anymore
if not os.path.isfile(ss(release_file['path'])):
fireEvent('release.clean', release['id'])
break
if group['library'] and group['library'].get('identifier'): Env.prop('manage.last_update', time.time())
identifier = group['library'].get('identifier')
added_identifiers.append(identifier)
# Add it to release and update the info
fireEvent('release.add', group = group)
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
return addToLibrary
def createAfterUpdate(self, folder, identifier):
# Notify frontend
def afterUpdate():
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
total = self.in_progress[folder]['total']
movie_dict = fireEvent('movie.get', identifier, single = True)
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library']))
return afterUpdate
def directories(self): def directories(self):
try: try:
if self.conf('library', default = '').strip(): return [x.strip() for x in self.conf('library', default = '').split('::')]
return splitString(self.conf('library', default = ''), '::')
except: except:
pass return []
return []
def scanFilesToLibrary(self, folder = None, files = None):
folder = os.path.normpath(folder)
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
for group in groups.itervalues():
if group['library'] and group['library'].get('identifier'):
fireEvent('release.add', group = group)
def getDiskSpace(self):
free_space = {}
for folder in self.directories():
size = None
if os.path.isdir(folder):
if os.name == 'nt':
_, total, free = ctypes.c_ulonglong(), ctypes.c_ulonglong(), \
ctypes.c_ulonglong()
if sys.version_info >= (3,) or isinstance(folder, unicode):
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExW #@UndefinedVariable
else:
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExA #@UndefinedVariable
ret = fun(folder, ctypes.byref(_), ctypes.byref(total), ctypes.byref(free))
if ret == 0:
raise ctypes.WinError()
used = total.value - free.value
return [total.value, used, free.value]
else:
s = os.statvfs(folder)
size = [s.f_blocks * s.f_frsize / (1024 * 1024), (s.f_bavail * s.f_frsize) / (1024 * 1024)]
free_space[folder] = size
return free_space
+35 -114
View File
@@ -3,16 +3,14 @@ from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.request import getParams, jsonified, getParam from couchpotato.core.helpers.request import getParams, jsonified, getParam
from couchpotato.core.helpers.variable import getImdb, splitString from couchpotato.core.helpers.variable import getImdb
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \ from couchpotato.core.settings.model import Library, LibraryTitle, Movie
Release
from couchpotato.environment import Env from couchpotato.environment import Env
from sqlalchemy.orm import joinedload_all from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_, desc from sqlalchemy.sql.expression import or_, asc, not_
from string import ascii_lowercase from string import ascii_lowercase
import time
log = CPLog(__name__) log = CPLog(__name__)
@@ -43,7 +41,6 @@ class MoviePlugin(Plugin):
'desc': 'List movies in wanted list', 'desc': 'List movies in wanted list',
'params': { 'params': {
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'}, 'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'}, 'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'}, 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
'search': {'desc': 'Search movie title'}, 'search': {'desc': 'Search movie title'},
@@ -97,34 +94,6 @@ class MoviePlugin(Plugin):
addEvent('movie.list', self.list) addEvent('movie.list', self.list)
addEvent('movie.restatus', self.restatus) addEvent('movie.restatus', self.restatus)
# Clean releases that didn't have activity in the last week
addEvent('app.load', self.cleanReleases)
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanReleases, hours = 4)
def cleanReleases(self):
log.debug('Removing releases from dashboard')
now = time.time()
week = 262080
done_status = fireEvent('status.get', 'done', single = True)
available_status = fireEvent('status.get', 'available', single = True)
snatched_status = fireEvent('status.get', 'snatched', single = True)
db = get_session()
# get movies last_edit more than a week ago
movies = db.query(Movie) \
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
.all()
#
for movie in movies:
for rel in movie.releases:
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
fireEvent('release.delete', id = rel.id, single = True)
def getView(self): def getView(self):
movie_id = getParam('id') movie_id = getParam('id')
@@ -138,43 +107,29 @@ class MoviePlugin(Plugin):
def get(self, movie_id): def get(self, movie_id):
db = get_session() db = get_session()
m = db.query(Movie).filter_by(id = movie_id).first()
imdb_id = getImdb(str(movie_id))
if(imdb_id):
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
else:
m = db.query(Movie).filter_by(id = movie_id).first()
results = None results = None
if m: if m:
results = m.to_dict(self.default_dict) results = m.to_dict(self.default_dict)
#db.close()
return results return results
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None): def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None):
db = get_session() db = get_session()
# Make a list from string # Make a list from string
if status and not isinstance(status, (list, tuple)): if not isinstance(status, (list, tuple)):
status = [status] status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
q = db.query(Movie) \ q = db.query(Movie) \
.outerjoin(Movie.releases, Movie.library, Library.titles) \ .join(Movie.library, Library.titles) \
.filter(LibraryTitle.default == True) \ .filter(LibraryTitle.default == True) \
.filter(or_(*[Movie.status.has(identifier = s) for s in status])) \
.group_by(Movie.id) .group_by(Movie.id)
# Filter on movie status
if status and len(status) > 0:
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
# Filter on release status
if release_status and len(release_status) > 0:
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
total_count = q.count() total_count = q.count()
filter_or = [] filter_or = []
@@ -194,10 +149,7 @@ class MoviePlugin(Plugin):
if filter_or: if filter_or:
q = q.filter(or_(*filter_or)) q = q.filter(or_(*filter_or))
if order == 'release_order': q = q.order_by(asc(LibraryTitle.simple_title))
q = q.order_by(desc(Release.last_edit))
else:
q = q.order_by(asc(LibraryTitle.simple_title))
q = q.subquery() q = q.subquery()
q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \ q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \
@@ -209,7 +161,7 @@ class MoviePlugin(Plugin):
.options(joinedload_all('files')) .options(joinedload_all('files'))
if limit_offset: if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset splt = [x.strip() for x in limit_offset.split(',')]
limit = splt[0] limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1] offset = 0 if len(splt) is 1 else splt[1]
q2 = q2.limit(limit).offset(offset) q2 = q2.limit(limit).offset(offset)
@@ -228,7 +180,7 @@ class MoviePlugin(Plugin):
#db.close() #db.close()
return (total_count, movies) return (total_count, movies)
def availableChars(self, status = None, release_status = None): def availableChars(self, status = ['active']):
chars = '' chars = ''
@@ -237,20 +189,11 @@ class MoviePlugin(Plugin):
# Make a list from string # Make a list from string
if not isinstance(status, (list, tuple)): if not isinstance(status, (list, tuple)):
status = [status] status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
q = db.query(Movie) \ q = db.query(Movie) \
.outerjoin(Movie.releases, Movie.library, Library.titles, Movie.status) \ .join(Movie.library, Library.titles, Movie.status) \
.options(joinedload_all('library.titles')) .options(joinedload_all('library.titles')) \
.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
# Filter on movie status
if status and len(status) > 0:
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
# Filter on release status
if release_status and len(release_status) > 0:
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
results = q.all() results = q.all()
@@ -258,29 +201,20 @@ class MoviePlugin(Plugin):
char = movie.library.titles[0].simple_title[0] char = movie.library.titles[0].simple_title[0]
char = char if char in ascii_lowercase else '#' char = char if char in ascii_lowercase else '#'
if char not in chars: if char not in chars:
chars += str(char) chars += char
#db.close() #db.close()
return ''.join(sorted(chars, key = str.lower)) return chars
def listView(self): def listView(self):
params = getParams() params = getParams()
status = splitString(params.get('status', None)) status = params.get('status', ['active'])
release_status = splitString(params.get('release_status', None))
limit_offset = params.get('limit_offset', None) limit_offset = params.get('limit_offset', None)
starts_with = params.get('starts_with', None) starts_with = params.get('starts_with', None)
search = params.get('search', None) search = params.get('search', None)
order = params.get('order', None)
total_movies, movies = self.list( total_movies, movies = self.list(status = status, limit_offset = limit_offset, starts_with = starts_with, search = search)
status = status,
release_status = release_status,
limit_offset = limit_offset,
starts_with = starts_with,
search = search,
order = order
)
return jsonified({ return jsonified({
'success': True, 'success': True,
@@ -292,9 +226,8 @@ class MoviePlugin(Plugin):
def charView(self): def charView(self):
params = getParams() params = getParams()
status = splitString(params.get('status', None)) status = params.get('status', ['active'])
release_status = splitString(params.get('release_status', None)) chars = self.availableChars(status)
chars = self.availableChars(status, release_status)
return jsonified({ return jsonified({
'success': True, 'success': True,
@@ -306,7 +239,7 @@ class MoviePlugin(Plugin):
db = get_session() db = get_session()
for id in splitString(getParam('id')): for id in getParam('id').split(','):
movie = db.query(Movie).filter_by(id = id).first() movie = db.query(Movie).filter_by(id = id).first()
if movie: if movie:
@@ -345,7 +278,7 @@ class MoviePlugin(Plugin):
'movies': movies, 'movies': movies,
}) })
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None): def add(self, params = {}, force_readd = True, search_after = True):
if not params.get('identifier'): if not params.get('identifier'):
msg = 'Can\'t add movie without imdb identifier.' msg = 'Can\'t add movie without imdb identifier.'
@@ -354,8 +287,9 @@ class MoviePlugin(Plugin):
return False return False
else: else:
try: try:
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True) url = 'http://thetvdb.com/api/GetSeriesByRemoteID.php?imdbid=%s' % params.get('identifier')
if not is_movie: tvdb = self.getCache('thetvdb.%s' % params.get('identifier'), url = url, show_error = False)
if tvdb and 'series' in tvdb.lower():
msg = 'Can\'t add movie, seems to be a TV show.' msg = 'Can\'t add movie, seems to be a TV show.'
log.error(msg) log.error(msg)
fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg) fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
@@ -364,13 +298,11 @@ class MoviePlugin(Plugin):
pass pass
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library) library = fireEvent('library.add', single = True, attrs = params, update_after = False)
# Status # Status
status_active = fireEvent('status.add', 'active', single = True) status_active = fireEvent('status.add', 'active', single = True)
snatched_status = fireEvent('status.add', 'snatched', single = True) status_snatched = fireEvent('status.add', 'snatched', single = True)
ignored_status = fireEvent('status.add', 'ignored', single = True)
downloaded_status = fireEvent('status.add', 'downloaded', single = True)
default_profile = fireEvent('profile.default', single = True) default_profile = fireEvent('profile.default', single = True)
@@ -382,7 +314,7 @@ class MoviePlugin(Plugin):
m = Movie( m = Movie(
library_id = library.get('id'), library_id = library.get('id'),
profile_id = params.get('profile_id', default_profile.get('id')), profile_id = params.get('profile_id', default_profile.get('id')),
status_id = status_id if status_id else status_active.get('id'), status_id = status_active.get('id'),
) )
db.add(m) db.add(m)
db.commit() db.commit()
@@ -394,14 +326,10 @@ class MoviePlugin(Plugin):
fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete) fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
search_after = False search_after = False
elif force_readd: elif force_readd:
# Clean snatched history # Clean snatched history
for release in m.releases: for release in m.releases:
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id')]: if release.status_id == status_snatched.get('id'):
if params.get('ignore_previous', False): release.delete()
release.status_id = ignored_status.get('id')
else:
fireEvent('release.delete', release.id, single = True)
m.profile_id = params.get('profile_id', default_profile.get('id')) m.profile_id = params.get('profile_id', default_profile.get('id'))
else: else:
@@ -409,8 +337,7 @@ class MoviePlugin(Plugin):
added = False added = False
if force_readd: if force_readd:
m.status_id = status_id if status_id else status_active.get('id') m.status_id = status_active.get('id')
m.last_edit = int(time.time())
do_search = True do_search = True
db.commit() db.commit()
@@ -454,7 +381,7 @@ class MoviePlugin(Plugin):
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
ids = splitString(params.get('id')) ids = [x.strip() for x in params.get('id').split(',')]
for movie_id in ids: for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first() m = db.query(Movie).filter_by(id = movie_id).first()
@@ -490,7 +417,7 @@ class MoviePlugin(Plugin):
params = getParams() params = getParams()
ids = splitString(params.get('id')) ids = [x.strip() for x in params.get('id').split(',')]
for movie_id in ids: for movie_id in ids:
self.delete(movie_id, delete_from = params.get('delete_from', 'all')) self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
@@ -504,11 +431,9 @@ class MoviePlugin(Plugin):
movie = db.query(Movie).filter_by(id = movie_id).first() movie = db.query(Movie).filter_by(id = movie_id).first()
if movie: if movie:
deleted = False
if delete_from == 'all': if delete_from == 'all':
db.delete(movie) db.delete(movie)
db.commit() db.commit()
deleted = True
else: else:
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
@@ -516,7 +441,7 @@ class MoviePlugin(Plugin):
total_deleted = 0 total_deleted = 0
new_movie_status = None new_movie_status = None
for release in movie.releases: for release in movie.releases:
if delete_from in ['wanted', 'snatched']: if delete_from == 'wanted':
if release.status_id != done_status.get('id'): if release.status_id != done_status.get('id'):
db.delete(release) db.delete(release)
total_deleted += 1 total_deleted += 1
@@ -531,7 +456,6 @@ class MoviePlugin(Plugin):
if total_releases == total_deleted: if total_releases == total_deleted:
db.delete(movie) db.delete(movie)
db.commit() db.commit()
deleted = True
elif new_movie_status: elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True) new_status = fireEvent('status.get', new_movie_status, single = True)
movie.profile_id = None movie.profile_id = None
@@ -540,9 +464,6 @@ class MoviePlugin(Plugin):
else: else:
fireEvent('movie.restatus', movie.id, single = True) fireEvent('movie.restatus', movie.id, single = True)
if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
#db.close() #db.close()
return True return True
+52 -111
View File
@@ -5,7 +5,6 @@ var MovieList = new Class({
options: { options: {
navigation: true, navigation: true,
limit: 50, limit: 50,
load_more: true,
menu: [], menu: [],
add_new: false add_new: false
}, },
@@ -13,67 +12,37 @@ var MovieList = new Class({
movies: [], movies: [],
movies_added: {}, movies_added: {},
letters: {}, letters: {},
filter: null, filter: {
'startswith': null,
'search': null
},
initialize: function(options){ initialize: function(options){
var self = this; var self = this;
self.setOptions(options); self.setOptions(options);
self.offset = 0; self.offset = 0;
self.filter = self.options.filter || {
'startswith': null,
'search': null
}
self.el = new Element('div.movies').adopt( self.el = new Element('div.movies').adopt(
self.title = self.options.title ? new Element('h2', {
'text': self.options.title,
'styles': {'display': 'none'}
}) : null,
self.description = self.options.description ? new Element('div.description', {
'html': self.options.description,
'styles': {'display': 'none'}
}) : null,
self.movie_list = new Element('div'), self.movie_list = new Element('div'),
self.load_more = self.options.load_more ? new Element('a.load_more', { self.load_more = new Element('a.load_more', {
'events': { 'events': {
'click': self.loadMore.bind(self) 'click': self.loadMore.bind(self)
} }
}) : null })
); );
self.changeView(self.getSavedView() || self.options.view || 'details');
self.getMovies(); self.getMovies();
App.addEvent('movie.added', self.movieAdded.bind(self)) if(options.add_new)
App.addEvent('movie.deleted', self.movieDeleted.bind(self)) App.addEvent('movie.added', self.movieAdded.bind(self))
},
movieDeleted: function(notification){
var self = this;
if(self.movies_added[notification.data.id]){
self.movies.each(function(movie){
if(movie.get('id') == notification.data.id){
movie.destroy();
delete self.movies_added[notification.data.id]
}
})
}
self.checkIfEmpty();
}, },
movieAdded: function(notification){ movieAdded: function(notification){
var self = this; var self = this;
window.scroll(0,0);
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){ if(!self.movies_added[notification.data.id])
window.scroll(0,0);
self.createMovie(notification.data, 'top'); self.createMovie(notification.data, 'top');
self.checkIfEmpty();
}
}, },
create: function(){ create: function(){
@@ -83,14 +52,22 @@ var MovieList = new Class({
if(self.options.navigation) if(self.options.navigation)
self.createNavigation(); self.createNavigation();
if(self.options.load_more) self.movie_list.addEvents({
self.scrollspy = new ScrollSpy({ 'mouseenter:relay(.movie)': function(e, el){
min: function(){ el.addClass('hover');
var c = self.load_more.getCoordinates() },
return c.top - window.document.getSize().y - 300 'mouseleave:relay(.movie)': function(e, el){
}, el.removeClass('hover');
onEnter: self.loadMore.bind(self) }
}); });
self.scrollspy = new ScrollSpy({
min: function(){
var c = self.load_more.getCoordinates()
return c.top - window.document.getSize().y - 300
},
onEnter: self.loadMore.bind(self)
});
self.created = true; self.created = true;
}, },
@@ -101,7 +78,7 @@ var MovieList = new Class({
if(!self.created) self.create(); if(!self.created) self.create();
// do scrollspy // do scrollspy
if(movies.length < self.options.limit && self.scrollspy){ if(movies.length < self.options.limit){
self.load_more.hide(); self.load_more.hide();
self.scrollspy.stop(); self.scrollspy.stop();
} }
@@ -110,7 +87,6 @@ var MovieList = new Class({
self.createMovie(movie); self.createMovie(movie);
}); });
self.total_movies = total;
self.setCounter(total); self.setCounter(total);
}, },
@@ -126,14 +102,18 @@ var MovieList = new Class({
createMovie: function(movie, inject_at){ createMovie: function(movie, inject_at){
var self = this; var self = this;
// Attach proper actions
var a = self.options.actions,
status = Status.get(movie.status_id);
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
var m = new Movie(self, { var m = new Movie(self, {
'actions': self.options.actions, 'actions': actions,
'view': self.current_view, 'view': self.current_view,
'onSelect': self.calculateSelected.bind(self) 'onSelect': self.calculateSelected.bind(self)
}, movie); }, movie);
$(m).inject(self.movie_list, inject_at || 'bottom'); $(m).inject(self.movie_list, inject_at || 'bottom');
m.fireEvent('injected'); m.fireEvent('injected');
self.movies.include(m) self.movies.include(m)
@@ -217,7 +197,7 @@ var MovieList = new Class({
}); });
// Actions // Actions
['mass_edit', 'details', 'list'].each(function(view){ ['mass_edit', 'thumbs', 'list'].each(function(view){
self.navigation_actions.adopt( self.navigation_actions.adopt(
new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', { new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', {
'events': { 'events': {
@@ -301,11 +281,10 @@ var MovieList = new Class({
}, },
deleteSelected: function(){ deleteSelected: function(){
var self = this, var self = this;
ids = self.getSelectedMovies(), var ids = self.getSelectedMovies()
help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list';
var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{ var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!', [{
'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'), 'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'),
'class': 'delete', 'class': 'delete',
'events': { 'events': {
@@ -330,8 +309,6 @@ var MovieList = new Class({
erase_movies.each(function(movie){ erase_movies.each(function(movie){
self.movies.erase(movie); self.movies.erase(movie);
movie.destroy()
}); });
self.calculateSelected(); self.calculateSelected();
@@ -399,16 +376,11 @@ var MovieList = new Class({
var self = this; var self = this;
self.movies = [] self.movies = []
if(self.mass_edit_select) self.calculateSelected()
self.calculateSelected() self.navigation_alpha.getElements('.active').removeClass('active')
if(self.navigation_alpha)
self.navigation_alpha.getElements('.active').removeClass('active')
self.offset = 0; self.offset = 0;
if(self.scrollspy){ self.load_more.show();
self.load_more.show(); self.scrollspy.start();
self.scrollspy.start();
}
}, },
activateLetter: function(letter){ activateLetter: function(letter){
@@ -424,6 +396,10 @@ var MovieList = new Class({
changeView: function(new_view){ changeView: function(new_view){
var self = this; var self = this;
self.movies.each(function(movie){
movie.changeView(new_view)
});
self.el self.el
.removeClass(self.current_view+'_list') .removeClass(self.current_view+'_list')
.addClass(new_view+'_list') .addClass(new_view+'_list')
@@ -434,7 +410,7 @@ var MovieList = new Class({
getSavedView: function(){ getSavedView: function(){
var self = this; var self = this;
return Cookie.read(self.options.identifier+'_view') || 'details'; return Cookie.read(self.options.identifier+'_view') || 'thumbs';
}, },
search: function(){ search: function(){
@@ -470,12 +446,9 @@ var MovieList = new Class({
getMovies: function(){ getMovies: function(){
var self = this; var self = this;
if(self.scrollspy){ if(self.scrollspy) self.scrollspy.stop();
self.scrollspy.stop(); self.load_more.set('text', 'loading...');
self.load_more.set('text', 'loading...'); Api.request('movie.list', {
}
Api.request(self.options.api_call || 'movie.list', {
'data': Object.merge({ 'data': Object.merge({
'status': self.options.status, 'status': self.options.status,
'limit_offset': self.options.limit + ',' + self.offset 'limit_offset': self.options.limit + ',' + self.offset
@@ -483,12 +456,8 @@ var MovieList = new Class({
'onComplete': function(json){ 'onComplete': function(json){
self.store(json.movies); self.store(json.movies);
self.addMovies(json.movies, json.total); self.addMovies(json.movies, json.total);
if(self.scrollspy) { self.load_more.set('text', 'load more movies');
self.load_more.set('text', 'load more movies'); if(self.scrollspy) self.scrollspy.start();
self.scrollspy.start();
}
self.checkIfEmpty()
} }
}); });
}, },
@@ -506,34 +475,6 @@ var MovieList = new Class({
}, },
checkIfEmpty: function(){
var self = this;
var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
if(self.title)
self.title[is_empty ? 'hide' : 'show']()
if(self.description)
self.description[is_empty ? 'hide' : 'show']()
if(is_empty && self.options.on_empty_element){
self.el.grab(self.options.on_empty_element);
if(self.navigation)
self.navigation.hide();
self.empty_element = self.options.on_empty_element;
}
else if(self.empty_element){
self.empty_element.destroy();
if(self.navigation)
self.navigation.show();
}
},
toElement: function(){ toElement: function(){
return this.el; return this.el;
} }
@@ -1,699 +0,0 @@
var MovieAction = new Class({
class_name: 'action icon',
initialize: function(movie){
var self = this;
self.movie = movie;
self.create();
if(self.el)
self.el.addClass(self.class_name)
},
create: function(){},
disable: function(){
this.el.addClass('disable')
},
enable: function(){
this.el.removeClass('disable')
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.movie, 'top').fade('hide');
//self.positionMask();
},
positionMask: function(){
var self = this,
movie = $(self.movie),
s = movie.getSize()
return;
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': movie
})
},
toElement: function(){
return this.el || null
}
});
var MA = {};
MA.IMDB = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.id = self.movie.get('identifier');
self.el = new Element('a.imdb', {
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank'
});
if(!self.id) self.disable();
}
});
MA.Release = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.releases.icon.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': {
'click': self.show.bind(self)
}
});
if(self.movie.data.releases.length == 0){
self.el.hide()
}
else {
var buttons_done = false;
self.movie.data.releases.sortBy('-info.score').each(function(release){
if(buttons_done) return;
var status = Status.get(release.status_id);
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
self.hide_on_click = false;
self.show();
buttons_done = true;
}
});
}
},
show: function(e){
var self = this;
if(e)
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.release_container = new Element('div.releases.table').adopt(
self.trynext_container = new Element('div.buttons.try_container')
)
).inject(self.movie, 'top');
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'Release name'}),
new Element('span.status', {'text': 'Status'}),
new Element('span.quality', {'text': 'Quality'}),
new Element('span.size', {'text': 'Size'}),
new Element('span.age', {'text': 'Age'}),
new Element('span.score', {'text': 'Score'}),
new Element('span.provider', {'text': 'Provider'})
).inject(self.release_container)
self.movie.data.releases.sortBy('-info.score').each(function(release){
var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id) || {},
info = release.info,
provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : '');
release.status = status;
var release_name = self.get(release, 'name');
if(release.files && release.files.length > 0){
try {
var movie_file = release.files.filter(function(file){
var type = File.Type.get(file.type_id);
return type && type.identifier == 'movie'
}).pick();
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
}
catch(e){}
}
// Create release
new Element('div', {
'class': 'item '+status.identifier,
'id': 'release_'+release.id
}).adopt(
new Element('span.name', {'text': release_name, 'title': release_name}),
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', { 'text': provider, 'title': provider }),
release.info['detail_url'] ? new Element('a.info.icon', {
'href': release.info['detail_url'],
'target': '_blank'
}) : null,
new Element('a.download.icon', {
'events': {
'click': function(e){
(e).preventDefault();
if(!this.hasClass('completed'))
self.download(release);
}
}
}),
new Element('a.delete.icon', {
'events': {
'click': function(e){
(e).preventDefault();
self.ignore(release);
this.getParent('.item').toggleClass('ignored')
}
}
})
).inject(self.release_container)
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
});
if(self.last_release){
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
}
if(self.next_release){
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
}
if(self.next_release || self.last_release){
self.trynext_container.adopt(
new Element('span.or', {
'text': 'This movie is snatched, if anything went wrong, download'
}),
self.last_release ? new Element('a.button.orange', {
'text': 'the same release again',
'events': {
'click': self.trySameRelease.bind(self)
}
}) : null,
self.next_release && self.last_release ? new Element('span.or', {
'text': ','
}) : null,
self.next_release ? [new Element('a.button.green', {
'text': self.last_release ? 'another release' : 'the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('span.or', {
'text': 'or pick one below'
})] : null
)
}
}
self.movie.slide('in', self.options_container);
},
get: function(release, type){
return release.info[type] || 'n/a'
},
download: function(release){
var self = this;
var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon');
icon.addClass('spinner');
Api.request('release.download', {
'data': {
'id': release.id
},
'onComplete': function(json){
icon.removeClass('spinner')
if(json.success)
icon.addClass('completed');
else
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
}
});
},
ignore: function(release){
var self = this;
Api.request('release.ignore', {
'data': {
'id': release.id
}
})
},
tryNextRelease: function(movie_id){
var self = this;
if(self.last_release)
self.ignore(self.last_release);
if(self.next_release)
self.download(self.next_release);
},
trySameRelease: function(movie_id){
var self = this;
if(self.last_release)
self.download(self.last_release);
}
});
MA.Trailer = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.el = new Element('a.trailer', {
'title': 'Watch the trailer of ' + self.movie.getTitle(),
'events': {
'click': self.watch.bind(self)
}
});
},
watch: function(offset){
var self = this;
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
var url = data_url.substitute({
'title': encodeURI(self.movie.getTitle()),
'year': self.movie.get('year'),
'offset': offset || 1
}),
size = $(self.movie).getSize(),
height = (size.x/16)*9,
id = 'trailer-'+randomString();
self.player_container = new Element('div[id='+id+']');
self.container = new Element('div.hide.trailer_container')
.adopt(self.player_container)
.inject($(self.movie), 'top');
self.container.setStyle('height', 0);
self.container.removeClass('hide');
self.close_button = new Element('a.hide.hide_trailer', {
'text': 'Hide trailer',
'events': {
'click': self.stop.bind(self)
}
}).inject(self.movie);
self.container.setStyle('height', height);
$(self.movie).setStyle('height', height);
new Request.JSONP({
'url': url,
'onComplete': function(json){
var video_url = json.feed.entry[0].id.$t.split('/'),
video_id = video_url[video_url.length-1];
self.player = new YT.Player(id, {
'height': height,
'width': size.x,
'videoId': video_id,
'playerVars': {
'autoplay': 1,
'showsearch': 0,
'wmode': 'transparent',
'iv_load_policy': 3
}
});
self.close_button.removeClass('hide');
var quality_set = false;
var change_quality = function(state){
if(!quality_set && (state.data == 1 || state.data || 2)){
try {
self.player.setPlaybackQuality('hd720');
quality_set = true;
}
catch(e){
}
}
}
self.player.addEventListener('onStateChange', change_quality);
}
}).send()
},
stop: function(){
var self = this;
self.player.stopVideo();
self.container.addClass('hide');
self.close_button.addClass('hide');
$(self.movie).setStyle('height', null);
setTimeout(function(){
self.container.destroy()
self.close_button.destroy();
}, 1800)
}
});
MA.Edit = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.edit', {
'title': 'Change movie information, like title and quality.',
'events': {
'click': self.editMovie.bind(self)
}
});
},
editMovie: function(e){
var self = this;
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
new Element('div.form').adopt(
self.title_select = new Element('select', {
'name': 'title'
}),
self.profile_select = new Element('select', {
'name': 'profile'
}),
new Element('a.button.edit', {
'text': 'Save & Search',
'events': {
'click': self.save.bind(self)
}
})
)
).inject(self.movie, 'top');
Array.each(self.movie.data.library.titles, function(alt){
new Element('option', {
'text': alt.title
}).inject(self.title_select);
if(alt['default'])
self.title_select.set('value', alt.title);
});
Quality.getActiveProfiles().each(function(profile){
var profile_id = profile.id ? profile.id : profile.data.id;
new Element('option', {
'value': profile_id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select);
if(self.movie.profile && self.movie.profile.data && self.movie.profile.data.id == profile_id)
self.profile_select.set('value', profile_id);
});
}
self.movie.slide('in', self.options_container);
},
save: function(e){
(e).preventDefault();
var self = this;
Api.request('movie.edit', {
'data': {
'id': self.movie.get('id'),
'default_title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value')
},
'useSpinner': true,
'spinnerTarget': $(self.movie),
'onComplete': function(){
self.movie.quality.set('text', self.profile_select.getSelected()[0].get('text'));
self.movie.title.set('text', self.title_select.getSelected()[0].get('text'));
}
});
self.movie.slide('out');
}
})
MA.Refresh = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.refresh', {
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.doRefresh.bind(self)
}
});
},
doRefresh: function(e){
var self = this;
(e).preventDefault();
Api.request('movie.refresh', {
'data': {
'id': self.movie.get('id')
}
});
}
});
MA.Readd = new Class({
Extends: MovieAction,
create: function(){
var self = this;
var movie_done = Status.get(self.movie.data.status_id).identifier == 'done';
if(!movie_done)
var snatched = self.movie.data.releases.filter(function(release){
return release.status && (release.status.identifier == 'snatched' || release.status.identifier == 'downloaded' || release.status.identifier == 'done');
}).length;
if(movie_done || snatched && snatched > 0)
self.el = new Element('a.readd', {
'title': 'Readd the movie and mark all previous snatched/downloaded as ignored',
'events': {
'click': self.doReadd.bind(self)
}
});
},
doReadd: function(e){
var self = this;
(e).preventDefault();
Api.request('movie.add', {
'data': {
'identifier': self.movie.get('identifier'),
'ignore_previous': 1
}
});
}
});
MA.Delete = new Class({
Extends: MovieAction,
Implements: [Chain],
create: function(){
var self = this;
self.el = new Element('a.delete', {
'title': 'Remove the movie from this CP list',
'events': {
'click': self.showConfirm.bind(self)
}
});
},
showConfirm: function(e){
var self = this;
(e).preventDefault();
if(!self.delete_container){
self.delete_container = new Element('div.buttons.delete_container').adopt(
new Element('a.cancel', {
'text': 'Cancel',
'events': {
'click': self.hideConfirm.bind(self)
}
}),
new Element('span.or', {
'text': 'or'
}),
new Element('a.button.delete', {
'text': 'Delete ' + self.movie.title.get('text'),
'events': {
'click': self.del.bind(self)
}
})
).inject(self.movie, 'top');
}
self.movie.slide('in', self.delete_container);
},
hideConfirm: function(e){
var self = this;
(e).preventDefault();
self.movie.slide('out');
},
del: function(e){
(e).preventDefault();
var self = this;
var movie = $(self.movie);
self.chain(
function(){
self.callChain();
},
function(){
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id'),
'delete_from': self.movie.list.options.identifier
},
'onComplete': function(){
movie.set('tween', {
'duration': 300,
'onComplete': function(){
self.movie.destroy()
}
});
movie.tween('height', 0);
}
});
}
);
self.callChain();
}
});
MA.Files = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.directory', {
'title': 'Available files',
'events': {
'click': self.showFiles.bind(self)
}
});
},
showFiles: function(e){
var self = this;
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.files_container = new Element('div.files.table')
).inject(self.movie, 'top');
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'File'}),
new Element('span.type', {'text': 'Type'}),
new Element('span.is_available', {'text': 'Available'})
).inject(self.files_container)
Array.each(self.movie.data.releases, function(release){
var rel = new Element('div.release').inject(self.files_container);
Array.each(release.files, function(file){
new Element('div.file.item').adopt(
new Element('span.name', {'text': file.path}),
new Element('span.type', {'text': File.Type.get(file.type_id).name}),
new Element('span.available', {'text': file.available})
).inject(rel)
});
});
}
self.movie.slide('in', self.options_container);
},
});
+81 -288
View File
@@ -1,33 +1,7 @@
.movies { .movies {
padding: 60px 0 20px; padding: 60px 0 20px;
position: relative;
z-index: 3;
} }
.movies h2 {
margin-bottom: 20px;
}
.movies > .description {
position: absolute;
top: 30px;
right: 0;
font-style: italic;
text-shadow: none;
opacity: 0.8;
}
.movies:hover > .description {
opacity: 1;
}
.movies.thumbs_list {
padding: 20px 0 20px;
}
.home .movies {
padding-top: 6px;
}
.movies.mass_edit_list { .movies.mass_edit_list {
padding-top: 90px; padding-top: 90px;
} }
@@ -38,58 +12,33 @@
margin: 10px 0; margin: 10px 0;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
height: 180px;
transition: all 0.2s linear; transition: all 0.2s linear;
} }
.movies .movie.list_view, .movies .movie.mass_edit_view {
.movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie {
height: 32px;
}
.movies.thumbs_list .movie {
width: 153px;
height: 230px;
display: inline-block;
margin: 0 8px 0 0;
}
.movies.thumbs_list .movie:nth-child(6n+6) {
margin: 0;
}
.movies .movie .mask {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
.movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie {
margin: 1px 0; margin: 1px 0;
border-radius: 0; border-radius: 0;
background: no-repeat; background: no-repeat;
box-shadow: none; box-shadow: none;
border-bottom: 1px solid rgba(255,255,255,0.05); border-bottom: 1px solid rgba(255,255,255,0.05);
} }
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
.movies.list_list .movie:hover:not(.details_view),
.movies.mass_edit_list .movie {
background: rgba(255,255,255,0.03); background: rgba(255,255,255,0.03);
} }
.movies .movie_container {
overflow: hidden;
}
.movies .data { .movies .data {
padding: 20px; padding: 20px;
height: 100%; height: 180px;
width: 840px; width: 840px;
position: absolute; position: relative;
right: 0; float: right;
border-radius: 0; border-radius: 0;
transition: all .6s cubic-bezier(0.9,0,0.1,1); transition: all 0.2s linear;
} }
.movies.list_list .movie:not(.details_view) .data, .movies .list_view .data, .movies .mass_edit_view .data {
.movies.mass_edit_list .movie .data {
height: 30px; height: 30px;
padding: 3px 0 3px 10px; padding: 3px 0 3px 10px;
width: 938px; width: 938px;
@@ -98,147 +47,78 @@
background: none; background: none;
} }
.movies.thumbs_list .data {
left: 0;
width: 100%;
padding: 10px;
height: 100%;
background: none;
transition: none;
}
.movies.thumbs_list .movie.no_thumbnail .data { background-image: linear-gradient(-30deg, rgba(255, 0, 85, .2) 0,rgba(125, 185, 235, .2) 100%);
}
.movies.thumbs_list .movie.no_thumbnail:nth-child(2n+6) .data { background-image: linear-gradient(-20deg, rgba(125, 0, 215, .2) 0, rgba(4, 55, 5, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(3n+6) .data { background-image: linear-gradient(-30deg, rgba(155, 0, 85, .2) 0,rgba(25, 185, 235, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(4n+6) .data { background-image: linear-gradient(-30deg, rgba(115, 5, 235, .2) 0, rgba(55, 180, 5, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(5n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 215, 115, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(6n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 15, 115, .7) 100%); }
.movies.thumbs_list .movie:hover .data {
background: rgba(0,0,0,0.9);
}
.movies .data.hide_right {
right: -100%;
}
.movies .movie .check { .movies .movie .check {
display: none; display: none;
} }
.movies.mass_edit_list .movie .check { .movies.mass_edit_list .movie .check {
position: absolute; float: left;
left: 0;
top: 0;
display: block; display: block;
margin: 7px 0 0 5px; margin: 7px 0 0 5px;
} }
.movies .poster { .movies .poster {
position: absolute; float: left;
left: 0;
width: 120px; width: 120px;
line-height: 0; line-height: 0;
overflow: hidden; overflow: hidden;
height: 100%; height: 180px;
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
transition: all .6s cubic-bezier(0.9,0,0.1,1); transition: all 0.2s linear;
} }
.movies.list_list .movie:not(.details_view) .poster, .movies .list_view .poster, .movies .mass_edit_view .poster {
.movies.mass_edit_list .poster {
width: 20px; width: 20px;
height: 30px; height: 30px;
border-radius: 1px 0 0 1px;
} }
.movies.mass_edit_list .poster { .movies.mass_edit_list .poster {
display: none; display: none;
} }
.movies.thumbs_list .poster { .movies .poster img, .options .poster img {
width: 100%;
height: 100%;
}
.movies .poster img,
.options .poster img {
width: 101%; width: 101%;
height: 101%; height: 101%;
} }
.movies .info {
position: relative;
height: 100%;
}
.movies .info .title { .movies .info .title {
display: inline; font-size: 30px;
position: absolute;
font-size: 28px;
font-weight: bold; font-weight: bold;
margin-bottom: 10px; margin-bottom: 10px;
left: 0; float: left;
top: 0;
width: 90%; width: 90%;
transition: all 0.2s linear; transition: all 0.2s linear;
} }
.movies.list_list .movie:not(.details_view) .info .title, .movies .list_view .info .title, .movies .mass_edit_view .info .title {
.movies.mass_edit_list .info .title {
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
text-overflow: ellipsis; text-overflow: ellipsis;
width: auto; width: auto;
overflow: hidden;
} }
.movies.thumbs_list .movie:not(.no_thumbnail) .info {
display: none;
}
.movies.thumbs_list .movie:hover .info {
display: block;
}
.movies.thumbs_list .info .title {
font-size: 21px;
text-shadow: 0 0 10px #000;
word-wrap: break-word;
}
.movies .info .year { .movies .info .year {
position: absolute;
font-size: 30px; font-size: 30px;
margin-bottom: 10px; margin-bottom: 10px;
float: right;
color: #bbb; color: #bbb;
width: 10%; width: 10%;
right: 0;
top: 0;
text-align: right; text-align: right;
transition: all 0.2s linear; transition: all 0.2s linear;
} }
.movies.list_list .movie:not(.details_view) .info .year, .movies .list_view .info .year, .movies .mass_edit_view .info .year {
.movies.mass_edit_list .info .year {
font-size: 16px; font-size: 16px;
width: 6%; width: 6%;
right: 10px;
} }
.movies.thumbs_list .info .year { .movies .info .rating {
font-size: 23px; font-size: 30px;
margin: 0; margin-bottom: 10px;
bottom: 0; color: #444;
left: 0; float: left;
top: auto; width: 5%;
right: auto; padding: 0 0 0 3%;
color: #FFF; }
text-shadow: none;
text-shadow: 0 0 6px #000;
}
.movies .info .description { .movies .info .description {
position: absolute;
top: 30px;
clear: both; clear: both;
height: 80px; height: 80px;
overflow: hidden; overflow: hidden;
@@ -246,82 +126,63 @@
.movies .data:hover .description { .movies .data:hover .description {
overflow: auto; overflow: auto;
} }
.movies.list_list .movie:not(.details_view) .info .description, .movies .list_view .info .description, .movies .mass_edit_view .info .description {
.movies.mass_edit_list .info .description,
.movies.thumbs_list .info .description {
display: none; display: none;
} }
.movies .data .quality { .movies .data .quality {
position: absolute;
bottom: 0;
display: block; display: block;
min-height: 20px; min-height: 20px;
vertical-align: mid; vertical-align: mid;
} }
.movies .status_suggest .data .quality, .movies .data .quality span {
.movies.thumbs_list .data .quality { padding: 2px 3px;
display: none; font-weight: bold;
opacity: 0.5;
font-size: 10px;
height: 16px;
line-height: 12px;
vertical-align: middle;
display: inline-block;
text-transform: uppercase;
text-shadow: none;
font-weight: normal;
margin: 0 2px;
border-radius: 2px;
background-color: rgba(255,255,255,0.1);
}
.movies .list_view .data .quality, .movies .mass_edit_view .data .quality {
text-align: right;
float: right;
} }
.movies .data .quality span { .movies .data .quality .available, .movies .data .quality .snatched {
padding: 2px 3px; opacity: 1;
font-weight: bold; box-shadow: 1px 1px 0 rgba(0,0,0,0.2);
opacity: 0.5; cursor: pointer;
font-size: 10px; }
height: 16px;
line-height: 12px;
vertical-align: middle;
display: inline-block;
text-transform: uppercase;
text-shadow: none;
font-weight: normal;
margin: 0 2px;
border-radius: 2px;
background-color: rgba(255,255,255,0.1);
}
.movies.list_list .data .quality,
.movies.mass_edit_list .data .quality {
text-align: right;
right: 0;
margin-right: 50px;
z-index: 1;
}
.movies .data .quality .available, .movies .data .quality .available { background-color: #578bc3; }
.movies .data .quality .snatched { .movies .data .quality .snatched { background-color: #369545; }
opacity: 1; .movies .data .quality .done {
box-shadow: 1px 1px 0 rgba(0,0,0,0.2); background-color: #369545;
cursor: pointer; opacity: 1;
} }
.movies .data .quality .finish {
.movies .data .quality .available { background-color: #578bc3; } background-image: url('../images/sprite.png');
.movies .data .quality .snatched { background-color: #369545; } background-repeat: no-repeat;
.movies .data .quality .done { background-position: 0 2px;
background-color: #369545; padding-left: 14px;
opacity: 1; background-size: 14px
} }
.movies .data .quality .finish {
background-image: url('../images/sprite.png');
background-repeat: no-repeat;
background-position: 0 2px;
padding-left: 14px;
background-size: 14px
}
.movies .data .actions { .movies .data .actions {
position: absolute;
bottom: 20px;
right: 20px;
line-height: 0; line-height: 0;
clear: both;
float: right;
margin-top: -25px; margin-top: -25px;
} }
.movies.thumbs_list .data .actions {
bottom: 8px;
right: 10px;
}
.movies .data:hover .action { opacity: 0.6; } .movies .data:hover .action { opacity: 0.6; }
.movies .data:hover .action:hover { opacity: 1; } .movies .data:hover .action:hover { opacity: 1; }
.movies.mass_edit_list .data .actions { .movies.mass_edit_list .data .actions {
@@ -338,14 +199,10 @@
opacity: 0; opacity: 0;
} }
.movies.list_list .movie:not(.details_view) .data:hover .actions, .movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions {
.movies.mass_edit_list .data:hover .actions { margin: -34px 2px 0 0;
margin: 0;
background: #4e5969; background: #4e5969;
top: 2px; position: relative;
bottom: 2px;
right: 5px;
z-index: 3;
} }
.movies .delete_container { .movies .delete_container {
@@ -427,7 +284,6 @@
.movies .options .table .provider { .movies .options .table .provider {
width: 120px; width: 120px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden;
} }
.movies .options .table .name { .movies .options .table .name {
width: 350px; width: 350px;
@@ -479,11 +335,11 @@
padding: 3px 10px; padding: 3px 10px;
background: #4e5969; background: #4e5969;
border-radius: 0 0 2px 2px; border-radius: 0 0 2px 2px;
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s; transition: all .6s cubic-bezier(0.9,0,0.1,1) .2s;
}
.movies .movie .hide_trailer.hide {
top: -30px;
} }
.movies .movie .hide_trailer.hide {
top: -30px;
}
.movies .movie .try_container { .movies .movie .try_container {
padding: 5px 10px; padding: 5px 10px;
@@ -524,7 +380,7 @@
.movies .alph_nav { .movies .alph_nav {
transition: box-shadow .4s linear; transition: box-shadow .4s linear;
position: fixed; position: fixed;
z-index: 4; z-index: 2;
top: 0; top: 0;
padding: 100px 60px 7px; padding: 100px 60px 7px;
width: 1080px; width: 1080px;
@@ -553,8 +409,7 @@
text-align: center; text-align: center;
} }
.movies .alph_nav .numbers li, .movies .alph_nav .numbers li, .movies .alph_nav .actions li {
.movies .alph_nav .actions li {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
width: 20px; width: 20px;
@@ -617,7 +472,7 @@
background-position: 3px -95px; background-position: 3px -95px;
} }
.movies .alph_nav .actions li.details span { .movies .alph_nav .actions li.thumbs span {
background-position: 3px -74px; background-position: 3px -74px;
} }
@@ -679,67 +534,5 @@
} }
.movies .alph_nav .more_menu > a { .movies .alph_nav .more_menu > a {
background-position: center -158px; background-position: center -157px;
} }
.movies .empty_wanted {
background-image: url('../images/emptylist.png');
height: 750px;
width: 800px;
padding-top: 260px;
margin-top: -50px;
}
.movies .empty_manage {
text-align: center;
font-size: 25px;
line-height: 150%;
}
.movies .empty_manage .after_manage {
margin-top: 30px;
font-size: 16px;
}
.movies .progress {
border-radius: 2px;
padding: 10px;
margin: 5px 0;
text-align: left;
}
.movies .progress > div {
padding: 5px 10px;
font-size: 12px;
line-height: 12px;
text-align: left;
display: inline-block;
width: 49%;
background: rgba(255, 255, 255, 0.05);
margin: 2px 0.5%;
border-radius: 3px;
}
.movies .progress > div .folder {
display: inline-block;
padding: 5px 20px 5px 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 85%;
direction: rtl;
vertical-align: middle;
}
.movies .progress > div .percentage {
font-weight: bold;
display: inline-block;
text-transform: uppercase;
text-shadow: none;
font-weight: normal;
font-size: 20px;
border-left: 1px solid rgba(255, 255, 255, .2);
width: 15%;
text-align: right;
vertical-align: middle;
}
+428 -84
View File
@@ -8,7 +8,7 @@ var Movie = new Class({
var self = this; var self = this;
self.data = data; self.data = data;
self.view = options.view || 'details'; self.view = options.view || 'thumbs';
self.list = list; self.list = list;
self.el = new Element('div.movie.inlay'); self.el = new Element('div.movie.inlay');
@@ -16,41 +16,14 @@ var Movie = new Class({
self.profile = Quality.getProfile(data.profile_id) || {}; self.profile = Quality.getProfile(data.profile_id) || {};
self.parent(self, options); self.parent(self, options);
self.addEvents(); App.addEvent('movie.update.'+data.id, self.update.bind(self));
},
addEvents: function(){
var self = this;
App.addEvent('movie.update.'+self.data.id, self.update.bind(self));
['movie.busy', 'searcher.started'].each(function(listener){ ['movie.busy', 'searcher.started'].each(function(listener){
App.addEvent(listener+'.'+self.data.id, function(notification){ App.addEvent(listener+'.'+data.id, function(notification){
if(notification.data) if(notification.data)
self.busy(true) self.busy(true)
}); });
}) })
App.addEvent('searcher.ended.'+self.data.id, function(notification){
if(notification.data)
self.busy(false)
});
},
destroy: function(){
var self = this;
self.el.destroy();
delete self.list.movies_added[self.get('id')];
self.list.movies.erase(self)
self.list.checkIfEmpty();
// Remove events
App.removeEvents('movie.update.'+self.data.id);
['movie.busy', 'searcher.started'].each(function(listener){
App.removeEvents(listener+'.'+self.data.id);
})
}, },
busy: function(set_busy){ busy: function(set_busy){
@@ -72,6 +45,7 @@ var Movie = new Class({
else if(!self.spinner) { else if(!self.spinner) {
self.createMask(); self.createMask();
self.spinner = createSpinner(self.mask); self.spinner = createSpinner(self.mask);
self.positionMask();
self.mask.fade('in'); self.mask.fade('in');
} }
}, },
@@ -80,9 +54,10 @@ var Movie = new Class({
var self = this; var self = this;
self.mask = new Element('div.mask', { self.mask = new Element('div.mask', {
'styles': { 'styles': {
'z-index': 4 'z-index': '1'
} }
}).inject(self.el, 'top').fade('hide'); }).inject(self.el, 'top').fade('hide');
self.positionMask();
}, },
positionMask: function(){ positionMask: function(){
@@ -101,7 +76,7 @@ var Movie = new Class({
var self = this; var self = this;
self.data = notification.data; self.data = notification.data;
self.el.empty(); self.container.destroy();
self.profile = Quality.getProfile(self.data.profile_id) || {}; self.profile = Quality.getProfile(self.data.profile_id) || {};
self.create(); self.create();
@@ -112,50 +87,52 @@ var Movie = new Class({
create: function(){ create: function(){
var self = this; var self = this;
var s = Status.get(self.get('status_id'));
self.el.addClass('status_'+s.identifier);
self.el.adopt( self.el.adopt(
self.select_checkbox = new Element('input[type=checkbox].inlay', { self.container = new Element('div.movie_container').adopt(
'events': { self.select_checkbox = new Element('input[type=checkbox].inlay', {
'change': function(){ 'events': {
self.fireEvent('select') 'change': function(){
} self.fireEvent('select')
}
}),
self.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light').adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
}),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', {
'text': self.data.library.plot
}),
self.quality = new Element('div.quality', {
'events': {
'click': function(e){
var releases = self.el.getElement('.actions .releases');
if(releases)
releases.fireEvent('click', [e])
}
} }
}) }
), }),
self.actions = new Element('div.actions') self.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light', {
'tween': {
duration: 400,
transition: 'quint:in:out',
onComplete: self.fireEvent.bind(self, 'slideEnd')
}
}).adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
}),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', {
'text': self.data.library.plot
}),
self.quality = new Element('div.quality', {
'events': {
'click': function(e){
var releases = self.el.getElement('.actions .releases');
if(releases)
releases.fireEvent('click', [e])
}
}
})
),
self.actions = new Element('div.actions')
)
) )
); );
if(self.thumbnail.empty) self.changeView(self.view);
self.el.addClass('no_thumbnail');
//self.changeView(self.view);
self.select_checkbox_class = new Form.Check(self.select_checkbox); self.select_checkbox_class = new Form.Check(self.select_checkbox);
// Add profile // Add profile
@@ -170,7 +147,7 @@ var Movie = new Class({
}); });
// Add releases // Add done releases
self.data.releases.each(function(release){ self.data.releases.each(function(release){
var q = self.quality.getElement('.q_id'+ release.quality_id), var q = self.quality.getElement('.q_id'+ release.quality_id),
@@ -237,23 +214,23 @@ var Movie = new Class({
if(direction == 'in'){ if(direction == 'in'){
self.temp_view = self.view; self.temp_view = self.view;
self.changeView('details') self.changeView('thumbs')
self.el.addEvent('outerClick', function(){ self.el.addEvent('outerClick', function(){
self.removeView() self.changeView(self.temp_view)
self.slide('out') self.slide('out')
}) })
el.show(); el.show();
self.data_container.addClass('hide_right'); self.data_container.tween('right', 0, -840);
} }
else { else {
self.el.removeEvents('outerClick') self.el.removeEvents('outerClick')
setTimeout(function(){ self.addEvent('slideEnd:once', function(){
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide(); self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
}, 600); });
self.data_container.removeClass('hide_right'); self.data_container.tween('right', -840, 0);
} }
}, },
@@ -267,12 +244,6 @@ var Movie = new Class({
self.view = new_view; self.view = new_view;
}, },
removeView: function(){
var self = this;
self.el.removeClass(self.view+'_view')
},
get: function(attr){ get: function(attr){
return this.data[attr] || this.data.library[attr] return this.data[attr] || this.data.library[attr]
}, },
@@ -291,3 +262,376 @@ var Movie = new Class({
} }
}); });
var MovieAction = new Class({
class_name: 'action icon',
initialize: function(movie){
var self = this;
self.movie = movie;
self.create();
if(self.el)
self.el.addClass(self.class_name)
},
create: function(){},
disable: function(){
this.el.addClass('disable')
},
enable: function(){
this.el.removeClass('disable')
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.movie, 'top').fade('hide');
self.positionMask();
},
positionMask: function(){
var self = this,
movie = $(self.movie),
s = movie.getSize()
return;
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': movie
})
},
toElement: function(){
return this.el || null
}
});
var IMDBAction = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.id = self.movie.get('identifier');
self.el = new Element('a.imdb', {
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank'
});
if(!self.id) self.disable();
}
});
var ReleaseAction = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.releases.icon.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': {
'click': self.show.bind(self)
}
});
var buttons_done = false;
self.movie.data.releases.sortBy('-info.score').each(function(release){
if(buttons_done) return;
var status = Status.get(release.status_id);
if((status.identifier == 'ignored' || status.identifier == 'failed') || (!self.next_release && status.identifier == 'available')){
self.hide_on_click = false;
self.show();
buttons_done = true;
}
});
},
show: function(e){
var self = this;
if(e)
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.release_container = new Element('div.releases.table').adopt(
self.trynext_container = new Element('div.buttons.try_container')
)
).inject(self.movie, 'top');
// Header
new Element('div.item.head').adopt(
new Element('span.name', {'text': 'Release name'}),
new Element('span.status', {'text': 'Status'}),
new Element('span.quality', {'text': 'Quality'}),
new Element('span.size', {'text': 'Size'}),
new Element('span.age', {'text': 'Age'}),
new Element('span.score', {'text': 'Score'}),
new Element('span.provider', {'text': 'Provider'})
).inject(self.release_container)
self.movie.data.releases.sortBy('-info.score').each(function(release){
var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id) || {},
info = release.info;
if( status.identifier == 'ignored' || status.identifier == 'failed'){
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
// Create release
new Element('div', {
'class': 'item '+status.identifier +
(self.next_release && self.next_release.id == release.id ? ' next_release' : '') +
(self.last_release && self.last_release.id == release.id ? ' last_release' : ''),
'id': 'release_'+release.id
}).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', {'text': self.get(release, 'provider')}),
release.info['detail_url'] ? new Element('a.info.icon', {
'href': release.info['detail_url'],
'target': '_blank'
}) : null,
new Element('a.download.icon', {
'events': {
'click': function(e){
(e).preventDefault();
if(!this.hasClass('completed'))
self.download(release);
}
}
}),
new Element('a.delete.icon', {
'events': {
'click': function(e){
(e).preventDefault();
self.ignore(release);
this.getParent('.item').toggleClass('ignored')
}
}
})
).inject(self.release_container)
});
self.trynext_container.adopt(
new Element('span.or', {
'text': 'Download'
}),
self.last_release ? new Element('a.button.orange', {
'text': 'the same release again',
'events': {
'click': self.trySameRelease.bind(self)
}
}) : null,
self.next_release && self.last_release ? new Element('span.or', {
'text': 'or'
}) : null,
self.next_release ? [new Element('a.button.green', {
'text': self.last_release ? 'another release' : 'the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('span.or', {
'text': 'or pick one below'
})] : null
)
}
self.movie.slide('in', self.options_container);
},
get: function(release, type){
return release.info[type] || 'n/a'
},
download: function(release){
var self = this;
var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon');
icon.addClass('spinner');
Api.request('release.download', {
'data': {
'id': release.id
},
'onComplete': function(json){
icon.removeClass('spinner')
if(json.success)
icon.addClass('completed');
else
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
}
});
},
ignore: function(release){
var self = this;
Api.request('release.ignore', {
'data': {
'id': release.id
}
})
},
tryNextRelease: function(movie_id){
var self = this;
if(self.last_release)
self.ignore(self.last_release);
if(self.next_release)
self.download(self.next_release);
},
trySameRelease: function(movie_id){
var self = this;
if(self.last_release)
self.download(self.last_release);
}
});
var TrailerAction = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.el = new Element('a.trailer', {
'title': 'Watch the trailer of ' + self.movie.getTitle(),
'events': {
'click': self.watch.bind(self)
}
});
},
watch: function(offset){
var self = this;
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
var url = data_url.substitute({
'title': encodeURI(self.movie.getTitle()),
'year': self.movie.get('year'),
'offset': offset || 1
}),
size = $(self.movie).getSize(),
height = (size.x/16)*9,
id = 'trailer-'+randomString();
self.player_container = new Element('div[id='+id+']');
self.container = new Element('div.hide.trailer_container')
.adopt(self.player_container)
.inject(self.movie.container, 'top');
self.container.setStyle('height', 0);
self.container.removeClass('hide');
self.close_button = new Element('a.hide.hide_trailer', {
'text': 'Hide trailer',
'events': {
'click': self.stop.bind(self)
}
}).inject(self.movie);
setTimeout(function(){
$(self.movie).setStyle('max-height', height);
self.container.setStyle('height', height);
}, 100)
new Request.JSONP({
'url': url,
'onComplete': function(json){
var video_url = json.feed.entry[0].id.$t.split('/'),
video_id = video_url[video_url.length-1];
self.player = new YT.Player(id, {
'height': height,
'width': size.x,
'videoId': video_id,
'playerVars': {
'autoplay': 1,
'showsearch': 0,
'wmode': 'transparent',
'iv_load_policy': 3
}
});
self.close_button.removeClass('hide');
var quality_set = false;
var change_quality = function(state){
if(!quality_set && (state.data == 1 || state.data || 2)){
try {
self.player.setPlaybackQuality('hd720');
quality_set = true;
}
catch(e){
}
}
}
self.player.addEventListener('onStateChange', change_quality);
}
}).send()
},
stop: function(){
var self = this;
self.player.stopVideo();
self.container.addClass('hide');
self.close_button.addClass('hide');
setTimeout(function(){
self.container.destroy()
self.close_button.destroy();
}, 1800)
}
});
@@ -91,15 +91,10 @@
.movie_result { .movie_result {
overflow: hidden; overflow: hidden;
height: 140px; height: 140px;
position: relative;
} }
.movie_result .options { .movie_result .options {
position: absolute; height: 140px;
height: 100%;
width: 100%;
top: 0;
left: 0;
border: 1px solid transparent; border: 1px solid transparent;
border-width: 1px 0; border-width: 1px 0;
border-radius: 0; border-radius: 0;
@@ -138,21 +133,17 @@
.movie_result .data { .movie_result .data {
padding: 0 15px; padding: 0 15px;
position: absolute; width: 470px;
height: 100%; position: relative;
width: 100%; height: 140px;
top: 0; top: 0;
left: 0; margin: -140px 0 0 0;
background: #5c697b; background: #5c697b;
cursor: pointer; cursor: pointer;
border-bottom: 1px solid #333; border-bottom: 1px solid #333;
border-top: 1px solid rgba(255,255,255, 0.15); border-top: 1px solid rgba(255,255,255, 0.15);
transition: all .6s cubic-bezier(0.9,0,0.1,1);
} }
.movie_result .data.open {
left: 100%;
}
.movie_result:last-child .data { border-bottom: 0; } .movie_result:last-child .data { border-bottom: 0; }
@@ -191,8 +182,6 @@
.movie_result .info h2 { .movie_result .info h2 {
margin: 0; margin: 0;
font-size: 17px;
line-height: 20px;
} }
.movie_result .info h2 span { .movie_result .info h2 span {
@@ -202,12 +191,6 @@
.movie_result .info h2 span:before { content: "("; } .movie_result .info h2 span:before { content: "("; }
.movie_result .info h2 span:after { content: ")"; } .movie_result .info h2 span:after { content: ")"; }
.search_form .mask, .search_form .mask {
.movie_result .mask {
border-radius: 3px; border-radius: 3px;
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
} }
+50 -26
View File
@@ -19,9 +19,7 @@ Block.Search = new Class({
self.hideResults(false) self.hideResults(false)
}, },
'blur': function(){ 'blur': function(){
(function(){ self.el.removeClass('focused')
self.el.removeClass('focused')
}).delay(2000);
} }
} }
}), }),
@@ -119,7 +117,7 @@ Block.Search = new Class({
self.hideResults(false); self.hideResults(false);
if(!cache){ if(!cache){
self.mask.fade('in'); self.positionMask().fade('in');
if(!self.spinner) if(!self.spinner)
self.spinner = createSpinner(self.mask); self.spinner = createSpinner(self.mask);
@@ -141,6 +139,7 @@ Block.Search = new Class({
fill: function(q, json){ fill: function(q, json){
var self = this; var self = this;
self.positionMask()
self.cache[q] = json self.cache[q] = json
self.movies = {} self.movies = {}
@@ -169,6 +168,19 @@ Block.Search = new Class({
}, },
positionMask: function(){
var self = this;
var s = self.result_container.getSize()
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': self.result_container
})
},
loading: function(bool){ loading: function(bool){
this.el[bool ? 'addClass' : 'removeClass']('loading') this.el[bool ? 'addClass' : 'removeClass']('loading')
}, },
@@ -181,7 +193,7 @@ Block.Search = new Class({
Block.Search.Item = new Class({ Block.Search.Item = new Class({
initialize: function(info, options){ initialize: function(info){
var self = this; var self = this;
self.info = info; self.info = info;
@@ -191,13 +203,14 @@ Block.Search.Item = new Class({
}, },
create: function(){ create: function(){
var self = this, var self = this;
info = self.info;
var info = self.info;
self.el = new Element('div.movie_result', { self.el = new Element('div.movie_result', {
'id': info.imdb 'id': info.imdb
}).adopt( }).adopt(
self.options_el = new Element('div.options.inlay'), self.options = new Element('div.options.inlay'),
self.data_container = new Element('div.data', { self.data_container = new Element('div.data', {
'tween': { 'tween': {
duration: 400, duration: 400,
@@ -260,7 +273,11 @@ Block.Search.Item = new Class({
self.createOptions(); self.createOptions();
self.data_container.addClass('open'); if(!self.width)
self.width = self.data_container.getCoordinates().width
self.data_container.tween('left', 0, self.width);
self.el.addEvent('outerClick', self.closeOptions.bind(self)) self.el.addEvent('outerClick', self.closeOptions.bind(self))
}, },
@@ -268,7 +285,7 @@ Block.Search.Item = new Class({
closeOptions: function(){ closeOptions: function(){
var self = this; var self = this;
self.data_container.removeClass('open'); self.data_container.tween('left', self.width, 0);
self.el.removeEvents('outerClick') self.el.removeEvents('outerClick')
}, },
@@ -285,31 +302,28 @@ Block.Search.Item = new Class({
'profile_id': self.profile_select.get('value') 'profile_id': self.profile_select.get('value')
}, },
'onComplete': function(json){ 'onComplete': function(json){
self.options_el.empty(); self.options.empty();
self.options_el.adopt( self.options.adopt(
new Element('div.message', { new Element('div.message', {
'text': json.added ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs' 'text': json.added ? 'Movie succesfully added.' : 'Movie didn\'t add properly. Check logs'
}) })
); );
self.mask.fade('out');
}, },
'onFailure': function(){ 'onFailure': function(){
self.options_el.empty(); self.options.empty();
self.options_el.adopt( self.options.adopt(
new Element('div.message', { new Element('div.message', {
'text': 'Something went wrong, check the logs for more info.' 'text': 'Something went wrong, check the logs for more info.'
}) })
); );
self.mask.fade('out');
} }
}); });
}, },
createOptions: function(){ createOptions: function(){
var self = this, var self = this;
info = self.info;
if(!self.options_el.hasClass('set')){ if(!self.options.hasClass('set')){
if(self.info.in_library){ if(self.info.in_library){
var in_library = []; var in_library = [];
@@ -318,14 +332,14 @@ Block.Search.Item = new Class({
}); });
} }
self.options_el.grab( self.options.adopt(
new Element('div').adopt( new Element('div').adopt(
self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', { self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0], 'src': self.info.images.poster[0],
'height': null, 'height': null,
'width': null 'width': null
}) : null, }) : null,
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', { self.info.in_wanted ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label 'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
}) : (in_library ? new Element('span.in_library', { }) : (in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + in_library.join(', ') 'text': 'Already in library: ' + in_library.join(', ')
@@ -358,7 +372,7 @@ Block.Search.Item = new Class({
}).inject(self.profile_select) }).inject(self.profile_select)
}); });
self.options_el.addClass('set'); self.options.addClass('set');
} }
}, },
@@ -366,7 +380,17 @@ Block.Search.Item = new Class({
loadingMask: function(){ loadingMask: function(){
var self = this; var self = this;
self.mask = new Element('div.mask').inject(self.el).fade('hide') var s = self.options.getSize();
self.mask = new Element('span.mask', {
'styles': {
'position': 'relative',
'width': s.x,
'height': s.y,
'top': -s.y,
'display': 'block'
}
}).inject(self.options).fade('hide')
createSpinner(self.mask) createSpinner(self.mask)
self.mask.fade('in') self.mask.fade('in')
+8 -2
View File
@@ -47,6 +47,7 @@ class ProfilePlugin(Plugin):
for profile in profiles: for profile in profiles:
temp.append(profile.to_dict(self.to_dict)) temp.append(profile.to_dict(self.to_dict))
#db.close()
return temp return temp
def save(self): def save(self):
@@ -83,6 +84,7 @@ class ProfilePlugin(Plugin):
profile_dict = p.to_dict(self.to_dict) profile_dict = p.to_dict(self.to_dict)
#db.close()
return jsonified({ return jsonified({
'success': True, 'success': True,
'profile': profile_dict 'profile': profile_dict
@@ -93,6 +95,7 @@ class ProfilePlugin(Plugin):
db = get_session() db = get_session()
default = db.query(Profile).first() default = db.query(Profile).first()
default_dict = default.to_dict(self.to_dict) default_dict = default.to_dict(self.to_dict)
#db.close()
return default_dict return default_dict
@@ -110,6 +113,7 @@ class ProfilePlugin(Plugin):
order += 1 order += 1
db.commit() db.commit()
#db.close()
return jsonified({ return jsonified({
'success': True 'success': True
@@ -133,6 +137,8 @@ class ProfilePlugin(Plugin):
except Exception, e: except Exception, e:
message = log.error('Failed deleting Profile: %s', e) message = log.error('Failed deleting Profile: %s', e)
#db.close()
return jsonified({ return jsonified({
'success': success, 'success': success,
'message': message 'message': message
@@ -175,10 +181,10 @@ class ProfilePlugin(Plugin):
) )
p.types.append(profile_type) p.types.append(profile_type)
db.commit()
quality_order += 1 quality_order += 1
order += 1 order += 1
db.commit() #db.close()
return True return True
@@ -86,10 +86,7 @@ var Profile = new Class({
}, },
'onComplete': function(json){ 'onComplete': function(json){
if(json.success){ if(json.success){
self.data = json.profile; self.data = json.profile
self.type_container.getElement('li:first-child input[type=checkbox]')
.set('checked', true)
.getParent().addClass('checked');
} }
} }
}); });
@@ -242,17 +239,9 @@ Profile.Type = new Class({
), ),
new Element('span.finish').adopt( new Element('span.finish').adopt(
self.finish = new Element('input.inlay.finish[type=checkbox]', { self.finish = new Element('input.inlay.finish[type=checkbox]', {
'checked': data.finish !== undefined ? data.finish : 1, 'checked': data.finish,
'events': { 'events': {
'change': function(e){ 'change': self.fireEvent.bind(self, 'change')
if(self.el == self.el.getParent().getElement(':first-child')){
self.finish_class.check();
alert('Top quality always finishes the search')
return;
}
self.fireEvent('change');
}
} }
}) })
), ),
@@ -266,7 +255,7 @@ Profile.Type = new Class({
self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty'); self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
self.finish_class = new Form.Check(self.finish); new Form.Check(self.finish);
}, },
+31 -37
View File
@@ -7,10 +7,8 @@ from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Quality, Profile, ProfileType from couchpotato.core.settings.model import Quality, Profile, ProfileType
from sqlalchemy.sql.expression import or_
import os.path import os.path
import re import re
import time
log = CPLog(__name__) log = CPLog(__name__)
@@ -19,15 +17,15 @@ class QualityPlugin(Plugin):
qualities = [ qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']}, {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']}, {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']} {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
] ]
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr'] pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
@@ -70,6 +68,7 @@ class QualityPlugin(Plugin):
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict()) q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
temp.append(q) temp.append(q)
#db.close()
return temp return temp
def single(self, identifier = ''): def single(self, identifier = ''):
@@ -77,10 +76,11 @@ class QualityPlugin(Plugin):
db = get_session() db = get_session()
quality_dict = {} quality_dict = {}
quality = db.query(Quality).filter(or_(Quality.identifier == identifier, Quality.id == identifier)).first() quality = db.query(Quality).filter_by(identifier = identifier).first()
if quality: if quality:
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict()) quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
#db.close()
return quality_dict return quality_dict
def getQuality(self, identifier): def getQuality(self, identifier):
@@ -100,6 +100,7 @@ class QualityPlugin(Plugin):
setattr(quality, params.get('value_type'), params.get('value')) setattr(quality, params.get('value_type'), params.get('value'))
db.commit() db.commit()
#db.close()
return jsonified({ return jsonified({
'success': True 'success': True
}) })
@@ -112,48 +113,46 @@ class QualityPlugin(Plugin):
for q in self.qualities: for q in self.qualities:
# Create quality # Create quality
qual = db.query(Quality).filter_by(identifier = q.get('identifier')).first() quality = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
if not qual: if not quality:
log.info('Creating quality: %s', q.get('label')) log.info('Creating quality: %s', q.get('label'))
qual = Quality() quality = Quality()
qual.order = order db.add(quality)
qual.identifier = q.get('identifier')
qual.label = toUnicode(q.get('label'))
qual.size_min, qual.size_max = q.get('size')
db.add(qual) quality.order = order
quality.identifier = q.get('identifier')
quality.label = toUnicode(q.get('label'))
quality.size_min, quality.size_max = q.get('size')
# Create single quality profile # Create single quality profile
prof = db.query(Profile).filter( profile = db.query(Profile).filter(
Profile.core == True Profile.core == True
).filter( ).filter(
Profile.types.any(quality = qual) Profile.types.any(quality = quality)
).all() ).all()
if not prof: if not profile:
log.info('Creating profile: %s', q.get('label')) log.info('Creating profile: %s', q.get('label'))
prof = Profile( profile = Profile(
core = True, core = True,
label = toUnicode(qual.label), label = toUnicode(quality.label),
order = order order = order
) )
db.add(prof) db.add(profile)
profile_type = ProfileType( profile_type = ProfileType(
quality = qual, quality = quality,
profile = prof, profile = profile,
finish = True, finish = True,
order = 0 order = 0
) )
prof.types.append(profile_type) profile.types.append(profile_type)
order += 1 order += 1
db.commit()
db.commit() #db.close()
time.sleep(0.3) # Wait a moment
return True return True
def guess(self, files, extra = {}): def guess(self, files, extra = {}):
@@ -199,14 +198,9 @@ class QualityPlugin(Plugin):
for quality in self.all(): for quality in self.all():
# Check width resolution, range 20 # Last check on resolution only
if (quality.get('width', 720) - 20) <= extra.get('resolution_width', 0) <= (quality.get('width', 720) + 20): if quality.get('width', 480) == extra.get('resolution_width', 0):
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 720), extra.get('resolution_width', 0))) log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0)))
return self.setCache(hash, quality)
# Check height resolution, range 20
if (quality.get('height', 480) - 20) <= extra.get('resolution_height', 0) <= (quality.get('height', 480) + 20):
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height', 480), extra.get('resolution_height', 0)))
return self.setCache(hash, quality) return self.setCache(hash, quality)
if 480 <= extra.get('resolution_width', 0) <= 720: if 480 <= extra.get('resolution_width', 0) <= 720:
+6 -8
View File
@@ -88,6 +88,8 @@ class Release(Plugin):
fireEvent('movie.restatus', movie.id) fireEvent('movie.restatus', movie.id)
#db.close()
return True return True
@@ -106,6 +108,7 @@ class Release(Plugin):
release_id = getParam('id') release_id = getParam('id')
#db.close()
return jsonified({ return jsonified({
'success': self.delete(release_id) 'success': self.delete(release_id)
}) })
@@ -133,9 +136,6 @@ class Release(Plugin):
db.delete(release_file) db.delete(release_file)
db.commit() db.commit()
if len(rel.files) == 0:
self.delete(id)
return True return True
return False return False
@@ -152,6 +152,7 @@ class Release(Plugin):
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id') rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
db.commit() db.commit()
#db.close()
return jsonified({ return jsonified({
'success': True 'success': True
}) })
@@ -160,7 +161,6 @@ class Release(Plugin):
db = get_session() db = get_session()
id = getParam('id') id = getParam('id')
status_snatched = fireEvent('status.add', 'snatched', single = True)
rel = db.query(Relea).filter_by(id = id).first() rel = db.query(Relea).filter_by(id = id).first()
if rel: if rel:
@@ -181,16 +181,14 @@ class Release(Plugin):
'files': {} 'files': {}
}), manual = True, single = True) }), manual = True, single = True)
if success: #db.close()
rel.status_id = status_snatched.get('id')
db.commit()
return jsonified({ return jsonified({
'success': success 'success': success
}) })
else: else:
log.error('Couldn\'t find release with id: %s', id) log.error('Couldn\'t find release with id: %s', id)
#db.close()
return jsonified({ return jsonified({
'success': False 'success': False
}) })
+7 -19
View File
@@ -1,5 +1,4 @@
from couchpotato.core.plugins.renamer.main import Renamer from couchpotato.core.plugins.renamer.main import Renamer
import os
def start(): def start():
return Renamer() return Renamer()
@@ -83,15 +82,6 @@ config = [{
'unit': 'min(s)', 'unit': 'min(s)',
'description': 'Detect movie status every X minutes. Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled', 'description': 'Detect movie status every X minutes. Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled',
}, },
{
'advanced': True,
'name': 'force_every',
'label': 'Force every',
'default': 2,
'type': 'int',
'unit': 'hour(s)',
'description': 'Forces the renamer to scan every X hours',
},
{ {
'advanced': True, 'advanced': True,
'name': 'next_on_failed', 'name': 'next_on_failed',
@@ -112,15 +102,6 @@ config = [{
'label': 'Separator', 'label': 'Separator',
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
}, },
{
'advanced': True,
'name': 'ntfs_permission',
'label': 'NTFS Permission',
'type': 'bool',
'hidden': os.name != 'nt',
'description': 'Set permission of moved files to that of destination folder (Windows NTFS only).',
'default': False,
},
], ],
}, { }, {
'tab': 'renamer', 'tab': 'renamer',
@@ -143,6 +124,13 @@ config = [{
'type': 'choice', 'type': 'choice',
'options': rename_options 'options': rename_options
}, },
{
'name': 'trailer_name',
'label': 'Trailer naming',
'default': '<filename>-trailer.<ext>',
'type': 'choice',
'options': rename_options
},
], ],
}, },
], ],
+77 -125
View File
@@ -3,8 +3,7 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss from couchpotato.core.helpers.encoding import toUnicode, ss
from couchpotato.core.helpers.request import jsonified from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle
getImdb
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, File, Profile, Release from couchpotato.core.settings.model import Library, File, Profile, Release
@@ -13,7 +12,6 @@ import errno
import os import os
import re import re
import shutil import shutil
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -22,7 +20,6 @@ log = CPLog(__name__)
class Renamer(Plugin): class Renamer(Plugin):
renaming_started = False renaming_started = False
checking_snatched = False
def __init__(self): def __init__(self):
@@ -34,13 +31,8 @@ class Renamer(Plugin):
addEvent('renamer.check_snatched', self.checkSnatched) addEvent('renamer.check_snatched', self.checkSnatched)
addEvent('app.load', self.scan) addEvent('app.load', self.scan)
addEvent('app.load', self.checkSnatched)
if self.conf('run_every') > 0: fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
if self.conf('force_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'))
def scanView(self): def scanView(self):
@@ -56,7 +48,7 @@ class Renamer(Plugin):
return return
if self.renaming_started is True: if self.renaming_started is True:
log.info('Renamer is already running, if you see this often, check the logs above for errors.') log.info('Renamer is disabled to avoid infinite looping of the same error.')
return return
# Check to see if the "to" folder is inside the "from" folder. # Check to see if the "to" folder is inside the "from" folder.
@@ -135,8 +127,6 @@ class Renamer(Plugin):
'resolution_width': group['meta_data'].get('resolution_width'), 'resolution_width': group['meta_data'].get('resolution_width'),
'resolution_height': group['meta_data'].get('resolution_height'), 'resolution_height': group['meta_data'].get('resolution_height'),
'imdb_id': library['identifier'], 'imdb_id': library['identifier'],
'cd': '',
'cd_nr': '',
} }
for file_type in group['files']: for file_type in group['files']:
@@ -154,7 +144,7 @@ class Renamer(Plugin):
continue continue
# Move other files # Move other files
multiple = len(group['files'][file_type]) > 1 and not group['is_dvd'] multiple = len(group['files']['movie']) > 1 and not group['is_dvd']
cd = 1 if multiple else 0 cd = 1 if multiple else 0
for current_file in sorted(list(group['files'][file_type])): for current_file in sorted(list(group['files'][file_type])):
@@ -167,19 +157,23 @@ class Renamer(Plugin):
replacements['ext'] = getExt(current_file) replacements['ext'] = getExt(current_file)
# cd # # cd #
replacements['cd'] = ' cd%d' % cd if multiple else '' replacements['cd'] = ' cd%d' % cd if cd else ''
replacements['cd_nr'] = cd if multiple else '' replacements['cd_nr'] = cd
# Naming # Naming
final_folder_name = self.doReplace(folder_name, replacements).lstrip('. ') final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements).lstrip('. ') final_file_name = self.doReplace(file_name, replacements)
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)] replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
# Group filename without cd extension
replacements['cd'] = ''
replacements['cd_nr'] = ''
# Meta naming # Meta naming
if file_type is 'trailer': if file_type is 'trailer':
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True).lstrip('. ') final_file_name = self.doReplace(trailer_name, replacements)
elif file_type is 'nfo': elif file_type is 'nfo':
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True).lstrip('. ') final_file_name = self.doReplace(nfo_name, replacements)
# Seperator replace # Seperator replace
if separator: if separator:
@@ -210,15 +204,9 @@ class Renamer(Plugin):
# Check for extra subtitle files # Check for extra subtitle files
if file_type is 'subtitle': if file_type is 'subtitle':
remove_multiple = False
if len(group['files']['movie']) == 1:
remove_multiple = True
sub_langs = group['subtitle_language'].get(current_file, [])
# rename subtitles with or without language # rename subtitles with or without language
sub_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple) rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name) sub_langs = group['subtitle_language'].get(current_file, [])
rename_extras = self.getRenameExtras( rename_extras = self.getRenameExtras(
extra_type = 'subtitle_extra', extra_type = 'subtitle_extra',
@@ -227,19 +215,20 @@ class Renamer(Plugin):
file_name = file_name, file_name = file_name,
destination = destination, destination = destination,
group = group, group = group,
current_file = current_file, current_file = current_file
remove_multiple = remove_multiple,
) )
# Don't add language if multiple languages in 1 subtitle file # Don't add language if multiple languages in 1 file
if len(sub_langs) == 1: if len(sub_langs) > 1:
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
elif len(sub_langs) == 1:
sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext'])) sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name) rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
rename_files = mergeDicts(rename_files, rename_extras) rename_files = mergeDicts(rename_files, rename_extras)
# Filename without cd etc # Filename without cd etc
elif file_type is 'movie': if file_type is 'movie':
rename_extras = self.getRenameExtras( rename_extras = self.getRenameExtras(
extra_type = 'movie_extra', extra_type = 'movie_extra',
replacements = replacements, replacements = replacements,
@@ -251,7 +240,7 @@ class Renamer(Plugin):
) )
rename_files = mergeDicts(rename_files, rename_extras) rename_files = mergeDicts(rename_files, rename_extras)
group['filename'] = self.doReplace(file_name, replacements, remove_multiple = True)[:-(len(getExt(final_file_name)) + 1)] group['filename'] = self.doReplace(file_name, replacements)[:-(len(getExt(final_file_name)) + 1)]
group['destination_dir'] = os.path.join(destination, final_folder_name) group['destination_dir'] = os.path.join(destination, final_folder_name)
if multiple: if multiple:
@@ -276,7 +265,6 @@ class Renamer(Plugin):
for profile_type in movie.profile.types: for profile_type in movie.profile.types:
if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish: if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish:
movie.status_id = done_status.get('id') movie.status_id = done_status.get('id')
movie.last_edit = int(time.time())
db.commit() db.commit()
except Exception, e: except Exception, e:
log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc())) log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
@@ -316,12 +304,7 @@ class Renamer(Plugin):
elif release.status_id is snatched_status.get('id'): elif release.status_id is snatched_status.get('id'):
if release.quality.id is group['meta_data']['quality']['id']: if release.quality.id is group['meta_data']['quality']['id']:
log.debug('Marking release as downloaded') log.debug('Marking release as downloaded')
try: release.status_id = downloaded_status.get('id')
release.status_id = downloaded_status.get('id')
release.last_edit = int(time.time())
except Exception, e:
log.error('Failed marking release as finished: %s %s', (e, traceback.format_exc()))
db.commit() db.commit()
# Remove leftover files # Remove leftover files
@@ -345,7 +328,6 @@ class Renamer(Plugin):
log.info('Removing "%s"', src) log.info('Removing "%s"', src)
try: try:
src = ss(src)
if os.path.isfile(src): if os.path.isfile(src):
os.remove(src) os.remove(src)
@@ -359,10 +341,7 @@ class Renamer(Plugin):
# Delete leftover folder from older releases # Delete leftover folder from older releases
for delete_folder in delete_folders: for delete_folder in delete_folders:
try: self.deleteEmptyFolder(delete_folder, show_error = False)
self.deleteEmptyFolder(delete_folder, show_error = False)
except Exception, e:
log.error('Failed to delete folder: %s %s', (e, traceback.format_exc()))
# Rename all files marked # Rename all files marked
group['renamed_files'] = [] group['renamed_files'] = []
@@ -396,22 +375,22 @@ class Renamer(Plugin):
except: except:
log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc())) log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
# Notify on download, search for trailers etc # Search for trailers etc
fireEventAsync('renamer.after', group)
# Notify on download
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality']) download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
try: fireEventAsync('movie.downloaded', message = download_message, data = group)
fireEvent('renamer.after', message = download_message, group = group, in_order = True)
except:
log.error('Failed firing (some) of the renamer.after events: %s', traceback.format_exc())
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
break break
#db.close()
self.renaming_started = False self.renaming_started = False
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = '', remove_multiple = False): def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = ''):
replacements = replacements.copy()
rename_files = {} rename_files = {}
def test(s): def test(s):
@@ -420,8 +399,8 @@ class Renamer(Plugin):
for extra in set(filter(test, group['files'][extra_type])): for extra in set(filter(test, group['files'][extra_type])):
replacements['ext'] = getExt(extra) replacements['ext'] = getExt(extra)
final_folder_name = self.doReplace(folder_name, replacements, remove_multiple = remove_multiple) final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple) final_file_name = self.doReplace(file_name, replacements)
rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name) rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name)
return rename_files return rename_files
@@ -459,8 +438,6 @@ class Renamer(Plugin):
try: try:
os.chmod(dest, Env.getPermission('file')) os.chmod(dest, Env.getPermission('file'))
if os.name == 'nt' and self.conf('ntfs_permission'):
os.popen('icacls "' + dest + '"* /reset /T')
except: except:
log.error('Failed setting permissions for file: %s, %s', (dest, traceback.format_exc(1))) log.error('Failed setting permissions for file: %s, %s', (dest, traceback.format_exc(1)))
@@ -474,20 +451,15 @@ class Renamer(Plugin):
except: except:
log.error('Couldn\'t move file "%s" to "%s": %s', (old, dest, traceback.format_exc())) log.error('Couldn\'t move file "%s" to "%s": %s', (old, dest, traceback.format_exc()))
raise raise Exception
return True return True
def doReplace(self, string, replacements, remove_multiple = False): def doReplace(self, string, replacements):
''' '''
replace confignames with the real thing replace confignames with the real thing
''' '''
replacements = replacements.copy()
if remove_multiple:
replacements['cd'] = ''
replacements['cd_nr'] = ''
replaced = toUnicode(string) replaced = toUnicode(string)
for x, r in replacements.iteritems(): for x, r in replacements.iteritems():
if r is not None: if r is not None:
@@ -505,7 +477,6 @@ class Renamer(Plugin):
return string.replace(' ', ' ').replace(' .', '.') return string.replace(' ', ' ').replace(' .', '.')
def deleteEmptyFolder(self, folder, show_error = True): def deleteEmptyFolder(self, folder, show_error = True):
folder = ss(folder)
loge = log.error if show_error else log.debug loge = log.error if show_error else log.debug
for root, dirs, files in os.walk(folder): for root, dirs, files in os.walk(folder):
@@ -524,11 +495,6 @@ class Renamer(Plugin):
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc())) loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
def checkSnatched(self): def checkSnatched(self):
if self.checking_snatched:
log.debug('Already checking snatched')
self.checking_snatched = True
snatched_status = fireEvent('status.get', 'snatched', single = True) snatched_status = fireEvent('status.get', 'snatched', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True) ignored_status = fireEvent('status.get', 'ignored', single = True)
failed_status = fireEvent('status.get', 'failed', single = True) failed_status = fireEvent('status.get', 'failed', single = True)
@@ -538,71 +504,57 @@ class Renamer(Plugin):
db = get_session() db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all() rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
scan_required = False
if rels: if rels:
self.checking_snatched = True
log.debug('Checking status snatched releases...') log.debug('Checking status snatched releases...')
statuses = fireEvent('download.status', merge = True) scan_required = False
if not statuses:
for rel in rels:
# Get current selected title
default_title = ''
for title in rel.movie.library.titles:
if title.default: default_title = title.title
# Check if movie has already completed and is manage tab (legacy db correction)
if rel.movie.status_id == done_status.get('id'):
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
rel.status_id = ignored_status.get('id')
db.commit()
continue
item = {}
for info in rel.info:
item[info.identifier] = info.value
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
# check status
downloadstatus = fireEvent('download.status', data = item, movie = movie_dict, single = True)
if not downloadstatus:
log.debug('Download status functionality is not implemented for active downloaders.') log.debug('Download status functionality is not implemented for active downloaders.')
scan_required = True scan_required = True
else: else:
try: log.debug('Download status: %s' , downloadstatus)
for rel in rels:
rel_dict = rel.to_dict({'info': {}})
# Get current selected title if downloadstatus == 'failed':
default_title = getTitle(rel.movie.library) if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
else:
rel.status_id = failed_status.get('id')
db.commit()
# Check if movie has already completed and is manage tab (legacy db correction) log.info('Download of %s failed.', item['name'])
if rel.movie.status_id == done_status.get('id'):
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
rel.status_id = ignored_status.get('id')
rel.last_edit = int(time.time())
db.commit()
continue
movie_dict = fireEvent('movie.get', rel.movie_id, single = True) elif downloadstatus == 'completed':
log.info('Download of %s completed!', item['name'])
scan_required = True
# check status elif downloadstatus == 'not_found':
nzbname = self.createNzbName(rel_dict['info'], movie_dict) log.info('%s not found in downloaders', item['name'])
rel.status_id = ignored_status.get('id')
found = False db.commit()
for item in statuses:
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
if item['status'] == 'busy':
pass
elif item['status'] == 'failed':
fireEvent('download.remove_failed', item, single = True)
if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
else:
rel.status_id = failed_status.get('id')
rel.last_edit = int(time.time())
db.commit()
elif item['status'] == 'completed':
log.info('Download of %s completed!', item['name'])
scan_required = True
found = True
break
if not found:
log.info('%s not found in downloaders', nzbname)
except:
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
# Note that Queued, Downloading, Paused, Repair and Unpackimg are also available as status for SabNZBd
if scan_required: if scan_required:
fireEvent('renamer.scan') fireEvent('renamer.scan')
self.checking_snatched = False
return True
+69 -70
View File
@@ -23,7 +23,7 @@ class Scanner(Plugin):
'media': 314572800, # 300MB 'media': 314572800, # 300MB
'trailer': 1048576, # 1MB 'trailer': 1048576, # 1MB
} }
ignored_in_path = [os.path.sep + 'extracted' + os.path.sep, 'extracting', '_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate'] ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
extensions = { extensions = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'], 'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
@@ -34,7 +34,6 @@ class Scanner(Plugin):
'subtitle_extra': ['idx'], 'subtitle_extra': ['idx'],
'trailer': ['mov', 'mp4', 'flv'] 'trailer': ['mov', 'mp4', 'flv']
} }
file_types = { file_types = {
'subtitle': ('subtitle', 'subtitle'), 'subtitle': ('subtitle', 'subtitle'),
'subtitle_extra': ('subtitle', 'subtitle_extra'), 'subtitle_extra': ('subtitle', 'subtitle_extra'),
@@ -43,8 +42,6 @@ class Scanner(Plugin):
'movie': ('video', 'movie'), 'movie': ('video', 'movie'),
'movie_extra': ('movie', 'movie_extra'), 'movie_extra': ('movie', 'movie_extra'),
'backdrop': ('image', 'backdrop'), 'backdrop': ('image', 'backdrop'),
'poster': ('image', 'poster'),
'thumbnail': ('image', 'thumbnail'),
'leftover': ('leftover', 'leftover'), 'leftover': ('leftover', 'leftover'),
} }
@@ -53,20 +50,6 @@ class Scanner(Plugin):
'video': ['x264', 'h264', 'divx', 'xvid'] 'video': ['x264', 'h264', 'divx', 'xvid']
} }
audio_codec_map = {
0x2000: 'ac3',
0x2001: 'dts',
0x0055: 'mp3',
0x0050: 'mp2',
0x0001: 'pcm',
0x003: 'pcm',
0x77a1: 'tta1',
0x5756: 'wav',
0x6750: 'vorbis',
0xF1AC: 'flac',
0x00ff: 'aac',
}
source_media = { source_media = {
'bluray': ['bluray', 'blu-ray', 'brrip', 'br-rip'], 'bluray': ['bluray', 'blu-ray', 'brrip', 'br-rip'],
'hddvd': ['hddvd', 'hd-dvd'], 'hddvd': ['hddvd', 'hd-dvd'],
@@ -89,7 +72,7 @@ class Scanner(Plugin):
'()([ab])(\.....?)$' #*a.mkv '()([ab])(\.....?)$' #*a.mkv
] ]
cp_imdb = '(.cp.(?P<id>tt[0-9{7}]+).)' cp_imdb = '(\.cp\((?P<id>tt[0-9{7}]+)\))'
def __init__(self): def __init__(self):
@@ -97,10 +80,55 @@ class Scanner(Plugin):
addEvent('scanner.remove_cptag', self.removeCPTag) addEvent('scanner.remove_cptag', self.removeCPTag)
addEvent('scanner.scan', self.scan) addEvent('scanner.scan', self.scan)
addEvent('scanner.files', self.scanFilesToLibrary)
addEvent('scanner.folder', self.scanFolderToLibrary)
addEvent('scanner.name_year', self.getReleaseNameYear) addEvent('scanner.name_year', self.getReleaseNameYear)
addEvent('scanner.partnumber', self.getPartNumber) addEvent('scanner.partnumber', self.getPartNumber)
def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None): def after_rename(group):
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
addEvent('renamer.after', after_rename)
def scanFilesToLibrary(self, folder = None, files = None):
folder = os.path.normpath(folder)
groups = self.scan(folder = folder, files = files)
for group in groups.itervalues():
if group['library']:
fireEvent('release.add', group = group)
def scanFolderToLibrary(self, folder = None, newer_than = 0, simple = True):
folder = os.path.normpath(folder)
if not os.path.isdir(folder):
return
groups = self.scan(folder = folder, simple = simple, newer_than = newer_than)
added_identifier = []
while True and not self.shuttingDown():
try:
identifier, group = groups.popitem()
except:
break
# Save to DB
if group['library']:
# Add release
fireEvent('release.add', group = group)
library_item = fireEvent('library.update', identifier = group['library'].get('identifier'), single = True)
if library_item:
added_identifier.append(library_item['identifier'])
return added_identifier
def scan(self, folder = None, files = [], simple = False, newer_than = 0):
folder = ss(os.path.normpath(folder)) folder = ss(os.path.normpath(folder))
@@ -113,8 +141,7 @@ class Scanner(Plugin):
leftovers = [] leftovers = []
# Scan all files of the folder if no files are set # Scan all files of the folder if no files are set
if not files: if len(files) == 0:
check_file_date = True
try: try:
files = [] files = []
for root, dirs, walk_files in os.walk(folder): for root, dirs, walk_files in os.walk(folder):
@@ -123,7 +150,6 @@ class Scanner(Plugin):
except: except:
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc())) log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
else: else:
check_file_date = False
files = [ss(x) for x in files] files = [ss(x) for x in files]
db = get_session() db = get_session()
@@ -253,8 +279,8 @@ class Scanner(Plugin):
del path_identifiers[identifier] del path_identifiers[identifier]
del delete_identifiers del delete_identifiers
# Make sure we remove older / still extracting files # Determine file types
valid_files = {} processed_movies = {}
while True and not self.shuttingDown(): while True and not self.shuttingDown():
try: try:
identifier, group = movie_files.popitem() identifier, group = movie_files.popitem()
@@ -276,7 +302,7 @@ class Scanner(Plugin):
if file_too_new: if file_too_new:
break break
if check_file_date and file_too_new: if file_too_new:
try: try:
time_string = time.ctime(file_time[0]) time_string = time.ctime(file_time[0])
except: except:
@@ -294,33 +320,17 @@ class Scanner(Plugin):
# Only process movies newer than x # Only process movies newer than x
if newer_than and newer_than > 0: if newer_than and newer_than > 0:
has_new_files = False
for cur_file in group['unsorted_files']: for cur_file in group['unsorted_files']:
file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)] file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
if file_time[0] > newer_than or file_time[1] > newer_than: if file_time[0] > time.time() or file_time[1] > time.time():
has_new_files = True
break break
if not has_new_files: log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
# Delete the unsorted list # Delete the unsorted list
del group['unsorted_files'] del group['unsorted_files']
continue continue
valid_files[identifier] = group
del movie_files
# Determine file types
processed_movies = {}
total_found = len(valid_files)
while True and not self.shuttingDown():
try:
identifier, group = valid_files.popitem()
except:
break
# Group extra (and easy) files first # Group extra (and easy) files first
# images = self.getImages(group['unsorted_files']) # images = self.getImages(group['unsorted_files'])
@@ -341,11 +351,11 @@ class Scanner(Plugin):
group['files']['movie'] = self.getMediaFiles(group['unsorted_files']) group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
if len(group['files']['movie']) == 0: if len(group['files']['movie']) == 0:
log.error('Couldn\'t find any movie files for %s', identifier) log.error('Couldn\t find any movie files for %s', identifier)
continue continue
log.debug('Getting metadata for %s', identifier) log.debug('Getting metadata for %s', identifier)
group['meta_data'] = self.getMetaData(group, folder = folder) group['meta_data'] = self.getMetaData(group)
# Subtitle meta # Subtitle meta
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {} group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
@@ -382,11 +392,8 @@ class Scanner(Plugin):
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first() movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
group['movie_id'] = None if not movie else movie.id group['movie_id'] = None if not movie else movie.id
processed_movies[identifier] = group
# Notify parent & progress on something found processed_movies[identifier] = group
if on_found:
on_found(group, total_found, total_found - len(processed_movies))
if len(processed_movies) > 0: if len(processed_movies) > 0:
log.info('Found %s movies in the folder %s', (len(processed_movies), folder)) log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
@@ -395,7 +402,7 @@ class Scanner(Plugin):
return processed_movies return processed_movies
def getMetaData(self, group, folder = ''): def getMetaData(self, group):
data = {} data = {}
files = list(group['files']['movie']) files = list(group['files']['movie'])
@@ -421,10 +428,10 @@ class Scanner(Plugin):
if not data['quality']: if not data['quality']:
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True) data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD' data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 else 'SD'
filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0]) filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
data['group'] = self.getGroup(filename[len(folder):]) data['group'] = self.getGroup(filename)
data['source'] = self.getSourceMedia(filename) data['source'] = self.getSourceMedia(filename)
return data return data
@@ -433,18 +440,9 @@ class Scanner(Plugin):
try: try:
p = enzyme.parse(filename) p = enzyme.parse(filename)
# Video codec
vc = ('h264' if p.video[0].codec == 'AVC1' else p.video[0].codec).lower()
# Audio codec
ac = p.audio[0].codec
try: ac = self.audio_codec_map.get(p.audio[0].codec)
except: pass
return { return {
'video': vc, 'video': p.video[0].codec,
'audio': ac, 'audio': p.audio[0].codec,
'resolution_width': tryInt(p.video[0].width), 'resolution_width': tryInt(p.video[0].width),
'resolution_height': tryInt(p.video[0].height), 'resolution_height': tryInt(p.video[0].height),
} }
@@ -541,6 +539,7 @@ class Scanner(Plugin):
break break
except: except:
pass pass
#db.close()
# Search based on OpenSubtitleHash # Search based on OpenSubtitleHash
if not imdb_id and not group['is_dvd']: if not imdb_id and not group['is_dvd']:
@@ -761,8 +760,8 @@ class Scanner(Plugin):
def getGroup(self, file): def getGroup(self, file):
try: try:
match = re.findall('\-([A-Z0-9]+)[\.\/]', file, re.I) match = re.search('-(?P<group>[A-Z0-9]+).', file, re.I)
return match[-1] or '' return match.group('group') or ''
except: except:
return '' return ''
@@ -775,7 +774,7 @@ class Scanner(Plugin):
return None return None
def findYear(self, text): def findYear(self, text):
matches = re.search('(?P<year>19[0-9]{2}|20[0-9]{2})', text) matches = re.search('(?P<year>[12]{1}[0-9]{3})', text)
if matches: if matches:
return matches.group('year') return matches.group('year')
+2 -2
View File
@@ -27,9 +27,9 @@ class Score(Plugin):
score += sizeScore(nzb['size']) score += sizeScore(nzb['size'])
# Torrents only # Torrents only
if nzb.get('seeders'): if nzb.get('seeds'):
try: try:
score += nzb.get('seeders') / 5 score += nzb.get('seeds') / 5
score += nzb.get('leechers') / 10 score += nzb.get('leechers') / 10
except: except:
pass pass
+2 -2
View File
@@ -10,7 +10,7 @@ name_scores = [
# Video # Video
'x264:1', 'h264:1', 'x264:1', 'h264:1',
# Audio # Audio
'dts:4', 'ac3:2', 'DTS:4', 'AC3:2',
# Quality # Quality
'720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1', '720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1',
# Language / Subs # Language / Subs
@@ -116,7 +116,7 @@ def sizeScore(size):
def providerScore(provider): def providerScore(provider):
if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']: if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']:
return 20 return 20
if provider in ['Newznab']: if provider in ['Newznab']:
@@ -24,19 +24,18 @@ config = [{
'name': 'required_words', 'name': 'required_words',
'label': 'Required words', 'label': 'Required words',
'default': '', 'default': '',
'placeholder': 'Example: DTS, AC3 & English', 'description': 'Ignore releases that don\'t contain at least one of these words.'
'description': 'Ignore releases that don\'t contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
}, },
{ {
'name': 'ignored_words', 'name': 'ignored_words',
'label': 'Ignored words', 'label': 'Ignored words',
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs', 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
}, },
{ {
'name': 'preferred_method', 'name': 'preferred_method',
'label': 'First search', 'label': 'First search',
'description': 'Which of the methods do you prefer', 'description': 'Which of the methods do you prefer',
'default': 'both', 'default': 'nzb',
'type': 'dropdown', 'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')], 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
}, },
+106 -250
View File
@@ -1,10 +1,9 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \ from couchpotato.core.helpers.variable import md5, getTitle
possibleTitles
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
@@ -12,7 +11,6 @@ from couchpotato.environment import Env
from inspect import ismethod, isfunction from inspect import ismethod, isfunction
from sqlalchemy.exc import InterfaceError from sqlalchemy.exc import InterfaceError
import datetime import datetime
import random
import re import re
import time import time
import traceback import traceback
@@ -30,7 +28,6 @@ class Searcher(Plugin):
addEvent('searcher.correct_movie', self.correctMovie) addEvent('searcher.correct_movie', self.correctMovie)
addEvent('searcher.download', self.download) addEvent('searcher.download', self.download)
addEvent('searcher.try_next_release', self.tryNextRelease) addEvent('searcher.try_next_release', self.tryNextRelease)
addEvent('searcher.could_be_released', self.couldBeReleased)
addApiView('searcher.try_next', self.tryNextReleaseView, docs = { addApiView('searcher.try_next', self.tryNextReleaseView, docs = {
'desc': 'Marks the snatched results as ignored and try the next best release', 'desc': 'Marks the snatched results as ignored and try the next best release',
@@ -39,38 +36,9 @@ class Searcher(Plugin):
}, },
}) })
addApiView('searcher.full_search', self.allMoviesView, docs = {
'desc': 'Starts a full search for all wanted movies',
})
addApiView('searcher.progress', self.getProgress, docs = {
'desc': 'Get the progress of current full search',
'return': {'type': 'object', 'example': """{
'progress': False || object, total & to_go,
}"""},
})
# Schedule cronjob # Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
def allMoviesView(self):
in_progress = self.in_progress
if not in_progress:
fireEventAsync('searcher.all')
fireEvent('notify.frontend', type = 'searcher.started', data = True, message = 'Full search started')
else:
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
return jsonified({
'success': not in_progress
})
def getProgress(self):
return jsonified({
'progress': self.in_progress
})
def allMovies(self): def allMovies(self):
@@ -85,51 +53,30 @@ class Searcher(Plugin):
movies = db.query(Movie).filter( movies = db.query(Movie).filter(
Movie.status.has(identifier = 'active') Movie.status.has(identifier = 'active')
).all() ).all()
random.shuffle(movies)
self.in_progress = { for movie in movies:
'total': len(movies), movie_dict = movie.to_dict({
'to_go': len(movies), 'profile': {'types': {'quality': {}}},
} 'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
})
try: try:
search_types = self.getSearchTypes() self.single(movie_dict)
except IndexError:
fireEvent('library.update', movie_dict['library']['identifier'], force = True)
except:
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
for movie in movies: # Break if CP wants to shut down
movie_dict = movie.to_dict({ if self.shuttingDown():
'profile': {'types': {'quality': {}}}, break
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
})
try:
self.single(movie_dict, search_types)
except IndexError:
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
fireEvent('library.update', movie_dict['library']['identifier'], force = True)
except:
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
self.in_progress['to_go'] -= 1
# Break if CP wants to shut down
if self.shuttingDown():
break
except SearchSetupError:
pass
#db.close()
self.in_progress = False self.in_progress = False
def single(self, movie, search_types = None): def single(self, movie):
# Find out search type
try:
if not search_types:
search_types = self.getSearchTypes()
except SearchSetupError:
return
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
@@ -144,8 +91,6 @@ class Searcher(Plugin):
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True) ignored_status = fireEvent('status.get', 'ignored', single = True)
found_releases = []
default_title = getTitle(movie['library']) default_title = getTitle(movie['library'])
if not default_title: if not default_title:
log.error('No proper info found for movie, removing it from library to cause it from having more issues.') log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
@@ -154,10 +99,9 @@ class Searcher(Plugin):
fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title) fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
ret = False ret = False
for quality_type in movie['profile']['types']: for quality_type in movie['profile']['types']:
if not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates): if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title)) log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title))
continue continue
@@ -165,7 +109,7 @@ class Searcher(Plugin):
# See if better quality is available # See if better quality is available
for release in movie['releases']: for release in movie['releases']:
if release['quality']['order'] < quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]: if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]:
has_better_quality += 1 has_better_quality += 1
# Don't search for quality lower then already available. # Don't search for quality lower then already available.
@@ -174,11 +118,7 @@ class Searcher(Plugin):
log.info('Search for %s in %s', (default_title, quality_type['quality']['label'])) log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True) quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
results = [] results = fireEvent('yarr.search', movie, quality, merge = True)
for search_type in search_types:
type_results = fireEvent('%s.search' % search_type, movie, quality, merge = True)
if type_results:
results += type_results
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True) sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
if len(sorted_results) == 0: if len(sorted_results) == 0:
@@ -195,23 +135,19 @@ class Searcher(Plugin):
# Add them to this movie releases list # Add them to this movie releases list
for nzb in sorted_results: for nzb in sorted_results:
nzb_identifier = md5(nzb['url']) rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first()
found_releases.append(nzb_identifier)
rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
if not rls: if not rls:
rls = Release( rls = Release(
identifier = nzb_identifier, identifier = md5(nzb['url']),
movie_id = movie.get('id'), movie_id = movie.get('id'),
quality_id = quality_type.get('quality_id'), quality_id = quality_type.get('quality_id'),
status_id = available_status.get('id') status_id = available_status.get('id')
) )
db.add(rls) db.add(rls)
db.commit()
else: else:
[db.delete(old_info) for old_info in rls.info] [db.delete(info) for info in rls.info]
rls.last_edit = int(time.time()) db.commit()
db.commit()
for info in nzb: for info in nzb:
try: try:
@@ -223,19 +159,14 @@ class Searcher(Plugin):
value = toUnicode(nzb[info]) value = toUnicode(nzb[info])
) )
rls.info.append(rls_info) rls.info.append(rls_info)
db.commit()
except InterfaceError: except InterfaceError:
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc())) log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
db.commit()
nzb['status_id'] = rls.status_id nzb['status_id'] = rls.status_id
for nzb in sorted_results: for nzb in sorted_results:
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
continue
if nzb['status_id'] == ignored_status.get('id'): if nzb['status_id'] == ignored_status.get('id'):
log.info('Ignored: %s', nzb['name']) log.info('Ignored: %s', nzb['name'])
continue continue
@@ -250,12 +181,6 @@ class Searcher(Plugin):
break break
elif downloaded != 'try_next': elif downloaded != 'try_next':
break break
# Remove releases that aren't found anymore
for release in movie.get('releases', []):
if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases:
fireEvent('release.delete', release.get('id'), single = True)
else: else:
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title)) log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
fireEvent('movie.restatus', movie['id']) fireEvent('movie.restatus', movie['id'])
@@ -267,149 +192,111 @@ class Searcher(Plugin):
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True) fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
#db.close()
return ret return ret
def download(self, data, movie, manual = False): def download(self, data, movie, manual = False):
# Test to see if any downloaders are enabled for this type snatched_status = fireEvent('status.get', 'snatched', single = True)
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
if downloader_enabled: # Download movie to temp
filedata = None
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
if filedata is 'try_next':
return filedata
snatched_status = fireEvent('status.get', 'snatched', single = True) successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
# Download movie to temp if successful:
filedata = None
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
if filedata == 'try_next':
return filedata
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True) # Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
rls.status_id = snatched_status.get('id')
db.commit()
if successful: log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message)
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
# If renamer isn't used, mark movie done
if not Env.setting('enabled', 'renamer'):
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
try: try:
# Mark release as snatched if movie['status_id'] == active_status.get('id'):
db = get_session() for profile_type in movie['profile']['types']:
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
if rls: log.info('Renamer disabled, marking movie as finished: %s', log_movie)
renamer_enabled = Env.setting('enabled', 'renamer')
done_status = fireEvent('status.get', 'done', single = True) # Mark release done
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id') rls.status_id = done_status.get('id')
db.commit() db.commit()
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label) # Mark movie done
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) mvie = db.query(Movie).filter_by(id = movie['id']).first()
log.info(snatch_message) mvie.status_id = done_status.get('id')
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict()) db.commit()
except Exception, e:
log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
# If renamer isn't used, mark movie done #db.close()
if not renamer_enabled: return True
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
try:
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# Mark release done
rls.status_id = done_status.get('id')
rls.last_edit = int(time.time())
db.commit()
# Mark movie done
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = done_status.get('id')
mvie.last_edit = int(time.time())
db.commit()
except:
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
except:
log.error('Failed marking movie finished: %s', traceback.format_exc())
return True
log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', '')))
log.info('Tried to download, but none of the downloaders are enabled')
return False return False
def getSearchTypes(self):
download_types = fireEvent('download.enabled_types', merge = True)
provider_types = fireEvent('provider.enabled_types', merge = True)
if download_types and len(list(set(provider_types) & set(download_types))) == 0:
log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_types))
raise NoProviders
for useless_provider in list(set(provider_types) - set(download_types)):
log.debug('Provider for "%s" enabled, but no downloader.', useless_provider)
search_types = download_types
if len(search_types) == 0:
log.error('There aren\'t any downloaders enabled. Please pick one in settings.')
raise NoDownloaders
return search_types
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs): def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
imdb_results = kwargs.get('imdb_results', False) imdb_results = kwargs.get('imdb_results', False)
retention = Env.setting('retention', section = 'nzb') retention = Env.setting('retention', section = 'nzb')
if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0): if nzb.get('seeds') is None and retention < nzb.get('age', 0):
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name'])) log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
return False return False
movie_name = getTitle(movie['library']) movie_name = getTitle(movie['library'])
movie_words = re.split('\W+', simplifyString(movie_name)) movie_words = re.split('\W+', simplifyString(movie_name))
nzb_name = simplifyString(nzb['name']) nzb_name = simplifyString(nzb['name'])
nzb_words = re.split('\W+', nzb_name) nzb_words = re.split('\W+', nzb_name)
required_words = splitString(self.conf('required_words').lower()) required_words = [x.strip().lower() for x in self.conf('required_words').lower().split(',')]
req_match = 0 if self.conf('required_words') and not list(set(nzb_words) & set(required_words)):
for req_set in required_words: log.info("Wrong: Required word missing: %s" % nzb['name'])
req = splitString(req_set, '&')
req_match += len(list(set(nzb_words) & set(req))) == len(req)
if self.conf('required_words') and req_match == 0:
log.info2("Wrong: Required word missing: %s" % nzb['name'])
return False return False
ignored_words = splitString(self.conf('ignored_words').lower()) ignored_words = [x.strip().lower() for x in self.conf('ignored_words').split(',')]
blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words)) blacklisted = list(set(nzb_words) & set(ignored_words))
if self.conf('ignored_words') and blacklisted: if self.conf('ignored_words') and blacklisted:
log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted))) log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
return False return False
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic'] pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words)) for p_tag in pron_tags:
if pron_words: if p_tag in nzb_words and p_tag not in movie_words:
log.info('Wrong: %s, probably pr0n', (nzb['name'])) log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False return False
#qualities = fireEvent('quality.all', single = True) #qualities = fireEvent('quality.all', single = True)
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True) preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string # Contains lower quality string
if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality): if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality):
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label'])) log.info('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
return False return False
# File to small # File to small
if nzb['size'] and preferred_quality['size_min'] > nzb['size']: if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min'])) log.info('"%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
return False return False
# File to large # File to large
if nzb['size'] and preferred_quality.get('size_max') < nzb['size']: if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max'])) log.info('"%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
return False return False
@@ -427,21 +314,20 @@ class Searcher(Plugin):
return True return True
# Check if nzb contains imdb link # Check if nzb contains imdb link
if self.checkIMDB([nzb.get('description', '')], movie['library']['identifier']): if self.checkIMDB([nzb['description']], movie['library']['identifier']):
return True return True
for raw_title in movie['library']['titles']: for movie_title in movie['library']['titles']:
for movie_title in possibleTitles(raw_title['title']): movie_words = re.split('\W+', simplifyString(movie_title['title']))
movie_words = re.split('\W+', simplifyString(movie_title))
if self.correctName(nzb['name'], movie_title): if self.correctName(nzb['name'], movie_title['title']):
# if no IMDB link, at least check year range 1 # if no IMDB link, at least check year range 1
if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1): if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1):
return True return True
# if no IMDB link, at least check year # if no IMDB link, at least check year
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0): if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
return True return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year'])) log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
return False return False
@@ -464,20 +350,13 @@ class Searcher(Plugin):
if list(set(nzb_words) & set(quality['alternative'])): if list(set(nzb_words) & set(quality['alternative'])):
found[quality['identifier']] = True found[quality['identifier']] = True
# Try guessing via quality tags
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
if guess:
found[guess['identifier']] = True
# Hack for older movies that don't contain quality tag # Hack for older movies that don't contain quality tag
year_name = fireEvent('scanner.name_year', name, single = True) year_name = fireEvent('scanner.name_year', name, single = True)
if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): if movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
if size > 3000: # Assume dvdr if size > 3000: # Assume dvdr
log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size)) return 'dvdr' == preferred_quality['identifier']
found['dvdr'] = True
else: # Assume dvdrip else: # Assume dvdrip
log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size)) return 'dvdrip' == preferred_quality['identifier']
found['dvdrip'] = True
# Allow other qualities # Allow other qualities
for allowed in preferred_quality.get('allow'): for allowed in preferred_quality.get('allow'):
@@ -494,32 +373,23 @@ class Searcher(Plugin):
return False return False
def correctYear(self, haystack, year, year_range): def correctYear(self, haystack, year, range):
for string in haystack: for string in haystack:
if str(year) in string or str(int(year) + range) in string or str(int(year) - range) in string: # 1 year of is fine too
year_name = fireEvent('scanner.name_year', string, single = True)
if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)):
log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year))
return True return True
log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year))
return False return False
def correctName(self, check_name, movie_name): def correctName(self, check_name, movie_name):
check_names = [check_name] check_names = [check_name]
try:
check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
except:
pass
# Match names between " for check_name in check_names:
try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
except: pass
# Match longest name between []
try: check_names.append(max(check_name.split('['), key = len))
except: pass
for check_name in list(set(check_names)):
check_movie = fireEvent('scanner.name_year', check_name, single = True) check_movie = fireEvent('scanner.name_year', check_name, single = True)
try: try:
@@ -533,19 +403,14 @@ class Searcher(Plugin):
return False return False
def couldBeReleased(self, is_pre_release, dates): def couldBeReleased(self, wanted_quality, dates, pre_releases):
now = int(time.time()) now = int(time.time())
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0): if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
return True return True
else: else:
if wanted_quality in pre_releases:
# For movies before 1972
if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
return True
if is_pre_release:
# Prerelease 1 week before theaters # Prerelease 1 week before theaters
if dates.get('theater') - 604800 < now: if dates.get('theater') - 604800 < now:
return True return True
@@ -600,12 +465,3 @@ class Searcher(Plugin):
except: except:
log.error('Failed searching for next release: %s', traceback.format_exc()) log.error('Failed searching for next release: %s', traceback.format_exc())
return False return False
class SearchSetupError(Exception):
pass
class NoDownloaders(SearchSetupError):
pass
class NoProviders(SearchSetupError):
pass
-2
View File
@@ -22,8 +22,6 @@ class StatusPlugin(Plugin):
'failed': 'Failed', 'failed': 'Failed',
'deleted': 'Deleted', 'deleted': 'Deleted',
'ignored': 'Ignored', 'ignored': 'Ignored',
'available': 'Available',
'suggest': 'Suggest',
} }
def __init__(self): def __init__(self):
@@ -8,9 +8,9 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'renamer', 'tab': 'renamer',
'subtab': 'subtitles',
'name': 'subtitle', 'name': 'subtitle',
'label': 'Download subtitles', 'label': 'Download subtitles after rename',
'description': 'after rename',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
+4 -5
View File
@@ -1,7 +1,6 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, FileType from couchpotato.core.settings.model import Library, FileType
@@ -14,7 +13,7 @@ log = CPLog(__name__)
class Subtitle(Plugin): class Subtitle(Plugin):
services = ['opensubtitles', 'thesubdb', 'subswiki', 'podnapisi'] services = ['opensubtitles', 'thesubdb', 'subswiki']
def __init__(self): def __init__(self):
addEvent('renamer.before', self.searchSingle) addEvent('renamer.before', self.searchSingle)
@@ -41,6 +40,8 @@ class Subtitle(Plugin):
# get subtitles for those files # get subtitles for those files
subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services) subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
#db.close()
def searchSingle(self, group): def searchSingle(self, group):
if self.isDisabled(): return if self.isDisabled(): return
@@ -49,7 +50,6 @@ class Subtitle(Plugin):
available_languages = sum(group['subtitle_language'].itervalues(), []) available_languages = sum(group['subtitle_language'].itervalues(), [])
downloaded = [] downloaded = []
files = [toUnicode(x) for x in group['files']['movie']] files = [toUnicode(x) for x in group['files']['movie']]
log.debug('Searching for subtitles for: %s', files)
for lang in self.getLanguages(): for lang in self.getLanguages():
if lang not in available_languages: if lang not in available_languages:
@@ -58,7 +58,6 @@ class Subtitle(Plugin):
downloaded.extend(download[subtitle]) downloaded.extend(download[subtitle])
for d_sub in downloaded: for d_sub in downloaded:
log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files))
group['files']['subtitle'].add(d_sub.path) group['files']['subtitle'].add(d_sub.path)
group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2] group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2]
@@ -70,4 +69,4 @@ class Subtitle(Plugin):
return False return False
def getLanguages(self): def getLanguages(self):
return splitString(self.conf('languages')) return [x.strip() for x in self.conf('languages').split(',')]
+1 -17
View File
@@ -1,22 +1,6 @@
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
class Suggestion(Plugin): class Suggestion(Plugin):
def __init__(self): pass
addApiView('suggestion.view', self.getView)
def getView(self):
limit_offset = getParam('limit_offset', None)
total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True)
return jsonified({
'success': True,
'empty': len(movies) == 0,
'total': total_movies,
'movies': movies,
})

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