Compare commits

..
161 Commits
Author SHA1 Message Date
Ruud d08c7c57a8 One up! 2013-09-20 17:46:54 +02:00
Ruud eeeb845ef3 Simplify string before checking on imdb 2013-09-20 17:30:11 +02:00
Ruud 651a063f94 Fix about submenu 2013-09-20 16:33:01 +02:00
Ruud f20aaa2d9d Hide IE clear button on search 2013-09-20 16:23:42 +02:00
Ruud ba925ec191 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	couchpotato/core/plugins/suggestion/main.py
2013-09-20 16:12:40 +02:00
Ruud 3b7376fd18 One up 2013-07-06 01:01:26 +02:00
Ruud c31b10c798 Ignore current suggested results 2013-07-06 00:49:11 +02:00
Ruud acda664686 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2013-07-05 22:43:54 +02:00
Ruud e2852407ea One up 2013-06-03 22:22:44 +02:00
Ruud 88e738c6cd Don't show double updater name 2013-06-03 22:22:35 +02:00
Ruud eaae8bdb0b Merge branch 'refs/heads/develop' into desktop 2013-06-03 22:00:21 +02:00
Ruud 821f68909d One up 2013-05-05 21:19:10 +02:00
Ruud 2b8dfed475 Merge branch 'refs/heads/master' into desktop
Conflicts:
	version.py
2013-05-05 20:31:28 +02:00
Ruud 0a749ce913 Merge branch 'refs/heads/develop' 2013-05-05 20:24:40 +02:00
Ruud dfd2c33657 Extend files, not append 2013-05-05 10:15:19 +02:00
Ruud 7aad27c3d2 Last message check 0 after first message 2013-05-03 23:05:17 +02:00
Ruud 7a5588d5de Merge branch 'refs/heads/develop' 2013-05-03 22:51:35 +02:00
Ruud f1dde5c925 Merge branch 'refs/heads/develop' 2013-04-14 11:09:32 +02:00
Ruud 0eff4f0096 Merge branch 'master' of github.com:RuudBurger/CouchPotatoServer 2013-04-05 23:59:56 +02:00
Ruud 4d7fa08805 Merge branch 'refs/heads/develop' 2013-04-05 23:57:54 +02:00
Ruud f0af184262 Merge branch 'refs/heads/develop' 2013-04-02 11:32:20 +02:00
Ruud 5a23be2224 Merge branch 'refs/heads/develop' 2013-03-26 21:42:25 +01:00
Ruud 7f87b255f9 Merge branch 'refs/heads/develop' 2013-03-26 21:10:27 +01:00
Ruud 5ac1118db3 Merge branch 'refs/heads/develop' 2013-03-20 20:32:57 +01:00
Ruud 2c46279617 Merge branch 'refs/heads/develop' 2013-03-20 19:37:15 +01:00
Ruud 5d6a9ad2d0 Merge branch 'refs/heads/develop' 2013-03-19 22:55:39 +01:00
Ruud 607b5ea766 Run exe after install 2013-03-19 21:22:07 +01:00
Ruud 88579cd71a One up 2013-03-19 20:52:07 +01:00
Ruud 6c57316ce6 Use https for changelog 2013-03-19 20:46:00 +01:00
Ruud 6702683da3 Merge branch 'refs/heads/develop' into desktop 2013-03-19 20:34:38 +01:00
Ruud b9c2b42725 Merge branch 'refs/heads/develop' 2013-03-19 20:28:46 +01:00
Ruud 1ed58586a1 Force install install in AppData
Add images to installer
2013-03-18 23:56:54 +01:00
Ruud a8369b4e93 Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2013-03-18 21:57:58 +01:00
Ruud f08ccd4fd8 One up installer 2013-03-17 22:34:04 +01:00
Ruud 312562a9f5 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2013-03-17 16:42:53 +01:00
Ruud fab8e66fe1 One up
Conflicts:
	version.py
2013-03-17 16:40:22 +01:00
Ruud 4db1b57c70 Merge branch 'refs/heads/develop' 2013-03-17 16:31:31 +01:00
Ruud b06dbd3069 Merge branch 'refs/heads/develop' 2013-03-12 21:12:18 +01:00
Ruud f84aa8c638 Merge branch 'refs/heads/develop' 2013-03-09 18:15:26 +01:00
Ruud 8e07dfc730 Merge branch 'refs/heads/develop' 2013-03-08 14:46:01 +01:00
Ruud a49a00a25f Host to 0.0.0.0 2013-02-14 23:02:44 +01:00
Ruud 673843fb66 Merge branch 'refs/heads/develop' 2013-02-12 23:25:11 +01:00
Ruud 811f35b028 Merge branch 'refs/heads/develop' 2013-02-04 23:11:39 +01:00
Ruud ec6e2c240f Merge branch 'refs/heads/develop' 2013-01-28 23:21:52 +01:00
Ruud 9e260a89af One up 2013-01-26 14:51:39 +01:00
Ruud d233e4d22e Merge branch 'refs/heads/develop' into desktop 2013-01-26 13:54:56 +01:00
Ruud 23893dbcb9 Merge branch 'refs/heads/develop' into desktop 2013-01-25 20:13:58 +01:00
Ruud 3187a0f820 Merge branch 'refs/heads/develop' 2013-01-25 15:52:54 +01:00
Ruud f86b9299c4 Merge branch 'refs/heads/develop' 2013-01-25 14:21:11 +01:00
Ruud d27d0abeb0 Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2013-01-24 23:35:37 +01:00
Ruud 506871b506 One up 2013-01-23 23:10:55 +01:00
Ruud 6115917660 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2013-01-23 22:57:07 +01:00
Ruud 21df8819d3 Merge branch 'refs/heads/develop' into desktop 2013-01-23 22:55:09 +01:00
Ruud 7c59348138 Merge branch 'refs/heads/develop' 2013-01-23 22:54:29 +01:00
Ruud ab53f44157 Remove non-int backup folders. closes #1298 2013-01-23 22:23:52 +01:00
Ruud b35f325d94 Merge branch 'refs/heads/develop' 2013-01-23 22:16:26 +01:00
Ruud fb3f3e11f6 Merge branch 'refs/heads/develop' into desktop 2013-01-22 21:40:40 +01:00
Ruud 178c8942c3 Merge branch 'refs/heads/develop' into desktop 2013-01-14 19:54:22 +01:00
Ruud 393c14de54 Urlencode spotweb id. fix #1213 2013-01-07 23:12:08 +01:00
Ruud 51e747049d One up 2013-01-07 23:10:42 +01:00
Ruud 0582f7d694 Urlencode spotweb id. fix #1213 2013-01-07 23:10:06 +01:00
Ruud fa7cac7538 Merge branch 'refs/heads/develop' into desktop 2013-01-07 22:41:55 +01:00
Ruud bff17c0b95 Merge branch 'refs/heads/develop' 2013-01-07 22:40:37 +01:00
Ruud d172828ac5 Merge branch 'refs/heads/develop' 2013-01-02 14:12:07 +01:00
Ruud 9500ac73fc Link to downloaders 2013-01-02 13:52:44 +01:00
Ruud e2cf7e4421 Merge branch 'refs/heads/develop' 2013-01-02 13:44:34 +01:00
Ruud 9a314cfbc4 One up 2012-12-29 00:03:45 +01:00
Ruud 5941d0bf77 Add version to update url 2012-12-29 00:03:36 +01:00
Ruud d326c1c25c Merge branch 'refs/heads/master' into desktop
Conflicts:
	version.py
2012-12-28 23:31:08 +01:00
Ruud 7e6234298d Merge branch 'refs/heads/develop' 2012-12-28 23:25:40 +01:00
Ruud d4da206f93 Merge branch 'refs/heads/develop' 2012-12-22 16:33:47 +01:00
Ruud 985a168724 Merge branch 'refs/heads/develop' 2012-12-21 23:18:00 +01:00
Ruud 173c6194ed Merge branch 'refs/heads/develop' 2012-12-19 11:12:26 +01:00
Ruud bcd23ad10c Merge branch 'refs/heads/develop' 2012-12-17 15:13:00 +01:00
Ruud 898e6f487d Merge branch 'refs/heads/develop' 2012-12-16 23:52:06 +01:00
Ruud 96472a9a8f One up 2012-12-16 23:51:58 +01:00
Ruud 27252561e2 Merge branch 'refs/heads/develop' into desktop 2012-12-16 23:51:24 +01:00
Ruud 6618c3927c Merge branch 'refs/heads/develop' 2012-12-11 23:15:06 +01:00
Ruud c9e732651f One up 2012-12-01 12:16:58 +01:00
Ruud 7849e7170d Uninstall only create files, no wildcard *.* 2012-12-01 12:16:51 +01:00
Ruud 087894eb4e Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-12-01 11:50:08 +01:00
Ruud 4b58b40226 Merge branch 'refs/heads/develop' 2012-12-01 11:48:54 +01:00
Ruud 3ecc826629 Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2012-11-11 22:06:48 +01:00
Ruud 25f1b8c7a7 Fedora init fix #1009 2012-11-02 18:32:15 +01:00
Ruud e71da1f14d Use proper description for binary build. fix #1005 2012-11-02 18:24:13 +01:00
Ruud 938b14ba18 One up installer 2012-10-29 20:45:17 +01:00
Ruud d6522d8f38 One up installer 2012-10-27 18:49:44 +02:00
Ruud 78eab890e7 Merge branch 'refs/heads/develop' into desktop 2012-10-27 18:25:36 +02:00
Ruud 1a56191f83 Don't unzip 2012-10-27 18:22:50 +02:00
Ruud 41c0f34d95 Properly restart 2012-10-27 18:22:40 +02:00
Ruud 37bf205d7a Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-10-27 11:56:57 +02:00
Ruud 32fe3796e4 Merge branch 'refs/heads/develop' 2012-10-26 22:22:47 +02:00
Ruud 359d1aaafa Merge branch 'refs/heads/develop' 2012-10-26 14:54:12 +02:00
Ruud fb5d336351 Merge branch 'refs/heads/develop' 2012-10-26 14:36:04 +02:00
Ruud eb30dff986 Merge branch 'refs/heads/develop' 2012-10-13 00:00:44 +02:00
Ruud 9312336962 Merge branch 'refs/heads/develop' 2012-09-24 09:36:59 +02:00
Ruud aa1fa3eb9a Add description 2012-09-19 15:42:33 +02:00
Ruud 0e2f8a612c Extract zip after build, for testing 2012-09-19 15:29:07 +02:00
Ruud ade4338ea6 Merge branch 'refs/heads/develop' 2012-09-16 21:32:16 +02:00
Ruud 55b20324c0 Merge branch 'refs/heads/develop' 2012-09-16 12:36:48 +02:00
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 c0fb28301d Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2012-09-16 10:46:39 +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 f9c2503f81 Merge branch 'refs/heads/develop' 2012-09-14 13:15:35 +02:00
Ruud b97acb8ef5 Merge branch 'refs/heads/develop' into desktop 2012-09-14 13:08:19 +02:00
Ruud 5b4cdf05b1 Merge branch 'refs/heads/develop' 2012-09-14 13:06:56 +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 6f25a6bdfd Merge branch 'refs/heads/develop' 2012-09-03 10:32:09 +02:00
Ruud 23427e95f7 Merge branch 'refs/heads/develop' 2012-08-26 23:09:51 +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 90a09e573b Merge branch 'refs/heads/develop'
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-08-05 16:15:53 +02:00
Ruud e1d7440b9d Wrong branch in master 2012-07-15 00:23:44 +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
446 changed files with 16235 additions and 47384 deletions
+231
View File
@@ -0,0 +1,231 @@
from esky.util import appdir_from_executable #@UnresolvedImport
from threading import Thread
from version import VERSION
from wx.lib.softwareupdate import SoftwareUpdate
import os
import sys
import time
import webbrowser
import wx
# 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/about/'
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 = 'https://couchpota.to/updates/%s'
self.InitUpdates(base_url % VERSION + '/', 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:
def appexe_from_executable(exepath):
appdir = appdir_from_executable(exepath)
exename = os.path.basename(exepath)
if sys.platform == "darwin":
if os.path.isdir(os.path.join(appdir, "Contents", "MacOS")):
return os.path.join(appdir, "Contents", "MacOS", exename)
return os.path.join(appdir, exename)
exe = appexe_from_executable(sys.executable)
os.chdir(os.path.dirname(exe))
os.execv(exe, [exe] + sys.argv[1:])
+9 -19
View File
@@ -1,25 +1,15 @@
## Got a issue/feature request or submitting a pull request? #So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem!
Make sure you think of the following things: ##Just make sure you think of the following things:
## Issue * Search through the existing (and closed) issues first. See if you can get your answer there.
* 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. * 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. * Post logs! Without seeing what is going on, I can't reproduce the error.
* Also check the logs before submitting, obvious errors like permission or http errors are often not related to CP. * What is the movie + quality you are searching for.
* What is the movie + quality you are searching for? * What are you settings for the specific problem.
* What are you're 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).
* What providers are you using? (While you're logs include these, scanning through hundred of lines of log isn't our hobby) * Give me a short step by step of how to reproduce.
* Post the logs from config directory, please do not copy paste the UI. Use pastebin to store these logs!
* Give a short step by step of how to reproduce the error.
* 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. * 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 "why closed" if it clearly says the issue in the tag ;) * 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 you're running on a NAS (QNAP, Austor etc..) with pre-made packages, make sure these are setup to use our source repo (RuudBurger/CouchPotatoServer) and nothing else!!
## Pull Request **If I don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
* Make sure you're pull request is made for develop branch (or relevant feature branch)
* Have you tested your PR? If not, why?
* Are there any limitations of your PR we should know of?
* Make sure to keep you're PR up-to-date with the branch you're trying to push into.
**If we don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
+1 -3
View File
@@ -44,13 +44,12 @@ class NonBlockHandler(RequestHandler):
def onNewMessage(self, response): def onNewMessage(self, response):
if self.request.connection.stream.closed(): if self.request.connection.stream.closed():
self.on_connection_close()
return return
try: try:
self.finish(response) self.finish(response)
except: except:
log.debug('Failed doing nonblock request, probably already closed: %s', (traceback.format_exc())) log.error('Failed doing nonblock request: %s', (traceback.format_exc()))
try: self.finish({'success': False, 'error': 'Failed returning results'}) try: self.finish({'success': False, 'error': 'Failed returning results'})
except: pass except: pass
@@ -110,7 +109,6 @@ class ApiHandler(RequestHandler):
if jsonp_callback: if jsonp_callback:
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')') self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
self.set_header("Content-Type", "text/javascript")
elif isinstance(result, tuple) and result[0] == 'redirect': elif isinstance(result, tuple) and result[0] == 'redirect':
self.redirect(result[1]) self.redirect(result[1])
else: else:
-4
View File
@@ -55,10 +55,6 @@ class Core(Plugin):
if not Env.get('desktop'): if not Env.get('desktop'):
self.signalHandler() self.signalHandler()
# Set default urlopen timeout
import socket
socket.setdefaulttimeout(30)
def md5Password(self, value): def md5Password(self, value):
return md5(value) if value else '' return md5(value) if value else ''
@@ -34,8 +34,6 @@ class ClientScript(Plugin):
'scripts/library/question.js', 'scripts/library/question.js',
'scripts/library/scrollspy.js', 'scripts/library/scrollspy.js',
'scripts/library/spin.js', 'scripts/library/spin.js',
'scripts/library/Array.stableSort.js',
'scripts/library/async.js',
'scripts/couchpotato.js', 'scripts/couchpotato.js',
'scripts/api.js', 'scripts/api.js',
'scripts/library/history.js', 'scripts/library/history.js',
+2 -2
View File
@@ -31,13 +31,13 @@ class Scheduler(Plugin):
pass pass
def doShutdown(self): def doShutdown(self):
super(Scheduler, self).doShutdown()
self.stop() self.stop()
return super(Scheduler, self).doShutdown()
def stop(self): def stop(self):
if self.started: if self.started:
log.debug('Stopping scheduler') log.debug('Stopping scheduler')
self.sched.shutdown(wait = False) self.sched.shutdown()
log.debug('Scheduler stopped') log.debug('Scheduler stopped')
self.started = False self.started = False
+9 -10
View File
@@ -183,6 +183,9 @@ class GitUpdater(BaseUpdater):
def doUpdate(self): def doUpdate(self):
try: try:
log.debug('Stashing local changes')
self.repo.saveStash()
log.info('Updating to latest version') log.info('Updating to latest version')
self.repo.pull() self.repo.pull()
@@ -295,7 +298,6 @@ class SourceUpdater(BaseUpdater):
def replaceWith(self, path): def replaceWith(self, path):
app_dir = ss(Env.get('app_dir')) app_dir = ss(Env.get('app_dir'))
data_dir = ss(Env.get('data_dir'))
# Get list of files we want to overwrite # Get list of files we want to overwrite
self.deletePyc() self.deletePyc()
@@ -327,15 +329,12 @@ class SourceUpdater(BaseUpdater):
log.error('Failed overwriting file "%s": %s', (tofile, traceback.format_exc())) log.error('Failed overwriting file "%s": %s', (tofile, traceback.format_exc()))
return False return False
for still_exists in existing_files: if Env.get('app_dir') not in Env.get('data_dir'):
for still_exists in existing_files:
if data_dir in still_exists: try:
continue os.remove(still_exists)
except:
try: log.error('Failed removing non-used file: %s', traceback.format_exc())
os.remove(still_exists)
except:
log.error('Failed removing non-used file: %s', traceback.format_exc())
return True return True
@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
self.doUpdate(); self.doUpdate();
else { else {
App.unBlockPage(); App.unBlockPage();
App.on('message', 'No updates available'); App.fireEvent('message', 'No updates available');
} }
} }
}) })
+2 -2
View File
@@ -1,4 +1,4 @@
config = [{ config = {
'name': 'download_providers', 'name': 'download_providers',
'groups': [ 'groups': [
{ {
@@ -10,4 +10,4 @@ config = [{
'options': [], 'options': [],
}, },
], ],
}] }
+24 -32
View File
@@ -13,7 +13,6 @@ class Downloader(Provider):
protocol = [] protocol = []
http_time_between_calls = 0 http_time_between_calls = 0
status_support = True
torrent_sources = [ torrent_sources = [
'http://torrage.com/torrent/%s.torrent', 'http://torrage.com/torrent/%s.torrent',
@@ -50,58 +49,53 @@ class Downloader(Provider):
return [] return []
def _download(self, data = None, media = None, manual = False, filedata = None): def _download(self, data = None, movie = None, manual = False, filedata = None):
if not media: media = {} if not movie: movie = {}
if not data: data = {} if not data: data = {}
if self.isDisabled(manual, data): if self.isDisabled(manual, data):
return return
return self.download(data = data, media = media, filedata = filedata) return self.download(data = data, movie = movie, filedata = filedata)
def _getAllDownloadStatus(self, download_ids): def _getAllDownloadStatus(self):
if self.isDisabled(manual = True, data = {}): if self.isDisabled(manual = True, data = {}):
return return
ids = [download_id['id'] for download_id in download_ids if download_id['downloader'] == self.getName()] return self.getAllDownloadStatus()
if ids: def getAllDownloadStatus(self):
return self.getAllDownloadStatus(ids) return
else:
return
def getAllDownloadStatus(self, ids): def _removeFailed(self, item):
return []
def _removeFailed(self, release_download):
if self.isDisabled(manual = True, data = {}): if self.isDisabled(manual = True, data = {}):
return return
if release_download and release_download.get('downloader') == self.getName(): if item and item.get('downloader') == self.getName():
if self.conf('delete_failed'): if self.conf('delete_failed'):
return self.removeFailed(release_download) return self.removeFailed(item)
return False return False
return return
def removeFailed(self, release_download): def removeFailed(self, item):
return return
def _processComplete(self, release_download): def _processComplete(self, item):
if self.isDisabled(manual = True, data = {}): if self.isDisabled(manual = True, data = {}):
return return
if release_download and release_download.get('downloader') == self.getName(): if item and item.get('downloader') == self.getName():
if self.conf('remove_complete', default = False): if self.conf('remove_complete', default = False):
return self.processComplete(release_download = release_download, delete_files = self.conf('delete_files', default = False)) return self.processComplete(item = item, delete_files = self.conf('delete_files', default = False))
return False return False
return return
def processComplete(self, release_download, delete_files): def processComplete(self, item, delete_files):
return return
def isCorrectProtocol(self, protocol): def isCorrectProtocol(self, item_protocol):
is_correct = protocol in self.protocol is_correct = item_protocol in self.protocol
if not is_correct: if not is_correct:
log.debug("Downloader doesn't support this protocol") log.debug("Downloader doesn't support this protocol")
@@ -134,7 +128,6 @@ class Downloader(Provider):
def downloadReturnId(self, download_id): def downloadReturnId(self, download_id):
return { return {
'downloader': self.getName(), 'downloader': self.getName(),
'status_support': self.status_support,
'id': download_id 'id': download_id
} }
@@ -158,20 +151,20 @@ class Downloader(Provider):
(d_manual and manual or d_manual is False) and \ (d_manual and manual or d_manual is False) and \
(not data or self.isCorrectProtocol(data.get('protocol'))) (not data or self.isCorrectProtocol(data.get('protocol')))
def _pause(self, release_download, pause = True): def _pause(self, item, pause = True):
if self.isDisabled(manual = True, data = {}): if self.isDisabled(manual = True, data = {}):
return return
if release_download and release_download.get('downloader') == self.getName(): if item and item.get('downloader') == self.getName():
self.pause(release_download, pause) self.pause(item, pause)
return True return True
return False return False
def pause(self, release_download, pause): def pause(self, item, pause):
return return
class ReleaseDownloadList(list): class StatusList(list):
provider = None provider = None
@@ -180,7 +173,7 @@ class ReleaseDownloadList(list):
self.provider = provider self.provider = provider
self.kwargs = kwargs self.kwargs = kwargs
super(ReleaseDownloadList, self).__init__() super(StatusList, self).__init__()
def extend(self, results): def extend(self, results):
for r in results: for r in results:
@@ -188,7 +181,7 @@ class ReleaseDownloadList(list):
def append(self, result): def append(self, result):
new_result = self.fillResult(result) new_result = self.fillResult(result)
super(ReleaseDownloadList, self).append(new_result) super(StatusList, self).append(new_result)
def fillResult(self, result): def fillResult(self, result):
@@ -197,7 +190,6 @@ class ReleaseDownloadList(list):
'status': 'busy', 'status': 'busy',
'downloader': self.provider.getName(), 'downloader': self.provider.getName(),
'folder': '', 'folder': '',
'files': '',
} }
return mergeDicts(defaults, result) return mergeDicts(defaults, result)
@@ -13,7 +13,7 @@ config = [{
'list': 'download_providers', 'list': 'download_providers',
'name': 'blackhole', 'name': 'blackhole',
'label': 'Black hole', 'label': 'Black hole',
'description': 'Download the NZB/Torrent to a specific folder. <em>Note: Seeding and copying/linking features do <strong>not</strong> work with Black hole</em>.', 'description': 'Download the NZB/Torrent to a specific folder.',
'wizard': True, 'wizard': True,
'options': [ 'options': [
{ {
@@ -35,13 +35,6 @@ config = [{
'type': 'dropdown', 'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')], 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
}, },
{
'name': 'create_subdir',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Create a sub directory when saving the .nzb (or .torrent).',
},
{ {
'name': 'manual', 'name': 'manual',
'default': 0, 'default': 0,
+10 -21
View File
@@ -11,10 +11,9 @@ log = CPLog(__name__)
class Blackhole(Downloader): class Blackhole(Downloader):
protocol = ['nzb', 'torrent', 'torrent_magnet'] protocol = ['nzb', 'torrent', 'torrent_magnet']
status_support = False
def download(self, data = None, media = None, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not media: media = {} if not movie: movie = {}
if not data: data = {} if not data: data = {}
directory = self.conf('directory') directory = self.conf('directory')
@@ -34,28 +33,18 @@ class Blackhole(Downloader):
log.error('No nzb/torrent available: %s', data.get('url')) log.error('No nzb/torrent available: %s', data.get('url'))
return False return False
file_name = self.createFileName(data, filedata, media) fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
full_path = os.path.join(directory, file_name)
if self.conf('create_subdir'):
try:
new_path = os.path.splitext(full_path)[0]
if not os.path.exists(new_path):
os.makedirs(new_path)
full_path = os.path.join(new_path, file_name)
except:
log.error('Couldnt create sub dir, reverting to old one: %s', full_path)
try: try:
if not os.path.isfile(full_path): if not os.path.isfile(fullPath):
log.info('Downloading %s to %s.', (data.get('protocol'), full_path)) log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
with open(full_path, 'wb') as f: with open(fullPath, 'wb') as f:
f.write(filedata) f.write(filedata)
os.chmod(full_path, Env.getPermission('file')) os.chmod(fullPath, Env.getPermission('file'))
return self.downloadReturnId('') return True
else: else:
log.info('File %s already exists.', full_path) log.info('File %s already exists.', fullPath)
return self.downloadReturnId('') return True
except: except:
log.error('Failed to download to blackhole %s', traceback.format_exc()) log.error('Failed to download to blackhole %s', traceback.format_exc())
+48 -79
View File
@@ -1,14 +1,12 @@
from base64 import b64encode, b16encode, b32decode from base64 import b64encode
from bencode import bencode as benc, bdecode from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import tryFloat from couchpotato.core.helpers.variable import tryFloat
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from datetime import timedelta from datetime import timedelta
from hashlib import sha1
from synchronousdeluge import DelugeClient from synchronousdeluge import DelugeClient
import os.path import os.path
import re
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -32,10 +30,7 @@ class Deluge(Downloader):
return self.drpc return self.drpc
def download(self, data = None, media = None, filedata = None): def download(self, data, movie, filedata = None):
if not media: media = {}
if not data: data = {}
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol'))) log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
if not self.connect(): if not self.connect():
@@ -76,8 +71,8 @@ class Deluge(Downloader):
if data.get('protocol') == 'torrent_magnet': if data.get('protocol') == 'torrent_magnet':
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options) remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
else: else:
filename = self.createFileName(data, filedata, media) filename = self.createFileName(data, filedata, movie)
remote_torrent = self.drpc.add_torrent_file(filename, filedata, options) remote_torrent = self.drpc.add_torrent_file(filename, b64encode(filedata), options)
if not remote_torrent: if not remote_torrent:
log.error('Failed sending torrent to Deluge') log.error('Failed sending torrent to Deluge')
@@ -86,71 +81,70 @@ class Deluge(Downloader):
log.info('Torrent sent to Deluge successfully.') log.info('Torrent sent to Deluge successfully.')
return self.downloadReturnId(remote_torrent) return self.downloadReturnId(remote_torrent)
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self):
log.debug('Checking Deluge download status.') log.debug('Checking Deluge download status.')
if not os.path.isdir(Env.setting('from', 'renamer')):
log.error('Renamer "from" folder doesn\'t to exist.')
return
if not self.connect(): if not self.connect():
return [] return False
release_downloads = ReleaseDownloadList(self) statuses = StatusList(self)
queue = self.drpc.get_alltorrents(ids) queue = self.drpc.get_alltorrents()
if not queue: if not queue:
log.debug('Nothing in queue or error') log.debug('Nothing in queue or error')
return [] return False
for torrent_id in queue: for torrent_id in queue:
torrent = queue[torrent_id] item = queue[torrent_id]
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused'])) log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (item['name'], item['hash'], item['save_path'], item['move_completed_path'], item['hash'], item['progress'], item['state'], item['eta'], item['ratio'], item['stop_ratio'], item['is_seed'], item['is_finished'], item['paused']))
# Deluge has no easy way to work out if a torrent is stalled or failing. # Deluge has no easy way to work out if a torrent is stalled or failing.
#status = 'failed' #status = 'failed'
status = 'busy' status = 'busy'
if torrent['is_seed'] and tryFloat(torrent['ratio']) < tryFloat(torrent['stop_ratio']): if item['is_seed'] and tryFloat(item['ratio']) < tryFloat(item['stop_ratio']):
# We have torrent['seeding_time'] to work out what the seeding time is, but we do not # We have item['seeding_time'] to work out what the seeding time is, but we do not
# have access to the downloader seed_time, as with deluge we have no way to pass it # have access to the downloader seed_time, as with deluge we have no way to pass it
# when the torrent is added. So Deluge will only look at the ratio. # when the torrent is added. So Deluge will only look at the ratio.
# See above comment in download(). # See above comment in download().
status = 'seeding' status = 'seeding'
elif torrent['is_seed'] and torrent['is_finished'] and torrent['paused'] and torrent['state'] == 'Paused': elif item['is_seed'] and item['is_finished'] and item['paused'] and item['state'] == 'Paused':
status = 'completed' status = 'completed'
download_dir = sp(torrent['save_path']) download_dir = item['save_path']
if torrent['move_on_completed']: if item['move_on_completed']:
download_dir = torrent['move_completed_path'] download_dir = item['move_completed_path']
torrent_files = [] statuses.append({
for file_item in torrent['files']: 'id': item['hash'],
torrent_files.append(sp(os.path.join(download_dir, file_item['path']))) 'name': item['name'],
release_downloads.append({
'id': torrent['hash'],
'name': torrent['name'],
'status': status, 'status': status,
'original_status': torrent['state'], 'original_status': item['state'],
'seed_ratio': torrent['ratio'], 'seed_ratio': item['ratio'],
'timeleft': str(timedelta(seconds = torrent['eta'])), 'timeleft': str(timedelta(seconds = item['eta'])),
'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])), 'folder': ss(os.path.join(download_dir, item['name'])),
'files': '|'.join(torrent_files),
}) })
return release_downloads return statuses
def pause(self, release_download, pause = True): def pause(self, item, pause = True):
if pause: if pause:
return self.drpc.pause_torrent([release_download['id']]) return self.drpc.pause_torrent([item['id']])
else: else:
return self.drpc.resume_torrent([release_download['id']]) return self.drpc.resume_torrent([item['id']])
def removeFailed(self, release_download): def removeFailed(self, item):
log.info('%s failed downloading, deleting...', release_download['name']) log.info('%s failed downloading, deleting...', item['name'])
return self.drpc.remove_torrent(release_download['id'], True) return self.drpc.remove_torrent(item['id'], True)
def processComplete(self, release_download, delete_files = False): def processComplete(self, item, delete_files = False):
log.debug('Requesting Deluge to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else '')) log.debug('Requesting Deluge to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
return self.drpc.remove_torrent(release_download['id'], remove_local_data = delete_files) return self.drpc.remove_torrent(item['id'], remove_local_data = delete_files)
class DelugeRPC(object): class DelugeRPC(object):
@@ -177,10 +171,7 @@ class DelugeRPC(object):
try: try:
self.connect() self.connect()
torrent_id = self.client.core.add_torrent_magnet(torrent, options).get() torrent_id = self.client.core.add_torrent_magnet(torrent, options).get()
if not torrent_id: if options['label']:
torrent_id = self._check_torrent(True, torrent)
if torrent_id and options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get() self.client.label.set_torrent(torrent_id, options['label']).get()
except Exception, err: except Exception, err:
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc())) log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
@@ -194,11 +185,8 @@ class DelugeRPC(object):
torrent_id = False torrent_id = False
try: try:
self.connect() self.connect()
torrent_id = self.client.core.add_torrent_file(filename, b64encode(torrent), options).get() torrent_id = self.client.core.add_torrent_file(filename, torrent, options).get()
if not torrent_id: if options['label']:
torrent_id = self._check_torrent(False, torrent)
if torrent_id and options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get() self.client.label.set_torrent(torrent_id, options['label']).get()
except Exception, err: except Exception, err:
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc())) log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
@@ -208,11 +196,11 @@ class DelugeRPC(object):
return torrent_id return torrent_id
def get_alltorrents(self, ids): def get_alltorrents(self):
ret = False ret = False
try: try:
self.connect() self.connect()
ret = self.client.core.get_torrents_status({'id': ids}, {}).get() ret = self.client.core.get_torrents_status({}, {}).get()
except Exception, err: except Exception, err:
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc())) log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
finally: finally:
@@ -254,22 +242,3 @@ class DelugeRPC(object):
def disconnect(self): def disconnect(self):
self.client.disconnect() self.client.disconnect()
def _check_torrent(self, magnet, torrent):
# Torrent not added, check if it already existed.
if magnet:
torrent_hash = re.findall('urn:btih:([\w]{32,40})', torrent)[0]
else:
info = bdecode(torrent)["info"]
torrent_hash = sha1(benc(info)).hexdigest()
# Convert base 32 to hex
if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash))
torrent_hash = torrent_hash.lower()
torrent_check = self.client.core.get_torrent_status(torrent_hash, {}).get()
if torrent_check['hash']:
return torrent_hash
return False
@@ -12,7 +12,6 @@ config = [{
'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': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
'wizard': True,
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@@ -25,13 +24,6 @@ config = [{
'default': 'localhost:6789', 'default': 'localhost:6789',
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>', 'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
}, },
{
'name': 'ssl',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
},
{ {
'name': 'username', 'name': 'username',
'default': 'nzbget', 'default': 'nzbget',
+57 -60
View File
@@ -1,6 +1,6 @@
from base64 import standard_b64encode from base64 import standard_b64encode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import ss, sp from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt, md5 from couchpotato.core.helpers.variable import tryInt, md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta from datetime import timedelta
@@ -17,10 +17,10 @@ class NZBGet(Downloader):
protocol = ['nzb'] protocol = ['nzb']
url = '%(protocol)s://%(username)s:%(password)s@%(host)s/xmlrpc' url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc'
def download(self, data = None, media = None, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not media: media = {} if not movie: movie = {}
if not data: data = {} if not data: data = {}
if not filedata: if not filedata:
@@ -29,8 +29,8 @@ class NZBGet(Downloader):
log.info('Sending "%s" to NZBGet.', data.get('name')) log.info('Sending "%s" to NZBGet.', data.get('name'))
url = self.url % {'protocol': 'https' if self.conf('ssl') else 'http', 'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
nzb_name = ss('%s.nzb' % self.createNzbName(data, media)) nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
rpc = xmlrpclib.ServerProxy(url) rpc = xmlrpclib.ServerProxy(url)
try: try:
@@ -67,11 +67,11 @@ class NZBGet(Downloader):
log.error('NZBGet could not add %s to the queue.', nzb_name) log.error('NZBGet could not add %s to the queue.', nzb_name)
return False return False
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self):
log.debug('Checking NZBGet download status.') log.debug('Checking NZBGet download status.')
url = self.url % {'protocol': 'https' if self.conf('ssl') else 'http', 'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
rpc = xmlrpclib.ServerProxy(url) rpc = xmlrpclib.ServerProxy(url)
try: try:
@@ -81,13 +81,13 @@ class NZBGet(Downloader):
log.info('Successfully connected to NZBGet, but unable to send a message') log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error: except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return [] return False
except xmlrpclib.ProtocolError, e: except xmlrpclib.ProtocolError, e:
if e.errcode == 401: if e.errcode == 401:
log.error('Password is incorrect.') log.error('Password is incorrect.')
else: else:
log.error('Protocol Error: %s', e) log.error('Protocol Error: %s', e)
return [] return False
# Get NZBGet data # Get NZBGet data
try: try:
@@ -97,65 +97,62 @@ class NZBGet(Downloader):
history = rpc.history() history = rpc.history()
except: except:
log.error('Failed getting data: %s', traceback.format_exc(1)) log.error('Failed getting data: %s', traceback.format_exc(1))
return [] return False
release_downloads = ReleaseDownloadList(self) statuses = StatusList(self)
for nzb in groups: for item in groups:
log.debug('Found %s in NZBGet download queue', item['NZBFilename'])
try: try:
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0] nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
except: except:
nzb_id = nzb['NZBID'] nzb_id = item['NZBID']
if nzb_id in ids:
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
timeleft = -1
try:
if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
except:
pass
release_downloads.append({
'id': nzb_id,
'name': nzb['NZBFilename'],
'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED',
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
'timeleft': timeleft,
})
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue timeleft = -1
if nzb['NZBID'] in ids:
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
release_downloads.append({
'id': nzb['NZBID'],
'name': nzb['NZBFilename'],
'original_status': nzb['Stage'],
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
})
for nzb in history:
try: try:
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0] if item['ActiveDownloads'] > 0 and item['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
timeleft = str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
except: except:
nzb_id = nzb['NZBID'] pass
if nzb_id in ids: statuses.append({
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log'])) 'id': nzb_id,
release_downloads.append({ 'name': item['NZBFilename'],
'id': nzb_id, 'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
'name': nzb['NZBFilename'], # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed', 'timeleft': timeleft,
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'], })
'timeleft': str(timedelta(seconds = 0)),
'folder': sp(nzb['DestDir'])
})
return release_downloads for item in queue: # 'Parameters' is not passed in rpc.postqueue
log.debug('Found %s in NZBGet postprocessing queue', item['NZBFilename'])
statuses.append({
'id': item['NZBID'],
'name': item['NZBFilename'],
'original_status': item['Stage'],
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
})
def removeFailed(self, release_download): for item in history:
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log']))
try:
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
except:
nzb_id = item['NZBID']
statuses.append({
'id': nzb_id,
'name': item['NZBFilename'],
'status': 'completed' if item['ParStatus'] in ['SUCCESS','NONE'] and item['ScriptStatus'] in ['SUCCESS','NONE'] else 'failed',
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
'timeleft': str(timedelta(seconds = 0)),
'folder': ss(item['DestDir'])
})
log.info('%s failed downloading, deleting...', release_download['name']) return statuses
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
@@ -182,9 +179,9 @@ class NZBGet(Downloader):
for hist in history: for hist in history:
for param in hist['Parameters']: for param in hist['Parameters']:
if param['Name'] == 'couchpotato' and param['Value'] == release_download['id']: if param['Name'] == 'couchpotato' and param['Value'] == item['id']:
nzb_id = hist['ID'] nzb_id = hist['ID']
path = hist['DestDir'] path = hist['DestDir']
if nzb_id and path and rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]): if nzb_id and path and rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
shutil.rmtree(path, True) shutil.rmtree(path, True)
+33 -35
View File
@@ -1,6 +1,6 @@
from base64 import b64encode from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import tryUrlencode, sp from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from urllib2 import URLError from urllib2 import URLError
@@ -8,11 +8,9 @@ from uuid import uuid4
import hashlib import hashlib
import httplib import httplib
import json import json
import os
import socket import socket
import ssl import ssl
import sys import sys
import time
import traceback import traceback
import urllib2 import urllib2
@@ -25,55 +23,53 @@ class NZBVortex(Downloader):
api_level = None api_level = None
session_id = None session_id = None
def download(self, data = None, media = None, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not media: media = {} if not movie: movie = {}
if not data: data = {} if not data: data = {}
# Send the nzb # Send the nzb
try: try:
nzb_filename = self.createFileName(data, filedata, media) nzb_filename = self.createFileName(data, filedata, movie)
self.call('nzb/add', files = {'file': (nzb_filename, filedata)}) self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
time.sleep(10)
raw_statuses = self.call('nzb') raw_statuses = self.call('nzb')
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(item['nzbFileName']) == nzb_filename][0] nzb_id = [item['id'] for item in raw_statuses.get('nzbs', []) if item['name'] == nzb_filename][0]
return self.downloadReturnId(nzb_id) return self.downloadReturnId(nzb_id)
except: except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc()) log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False return False
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self):
raw_statuses = self.call('nzb') raw_statuses = self.call('nzb')
release_downloads = ReleaseDownloadList(self) statuses = StatusList(self)
for nzb in raw_statuses.get('nzbs', []): for item in raw_statuses.get('nzbs', []):
if nzb['id'] in ids:
# Check status # Check status
status = 'busy' status = 'busy'
if nzb['state'] == 20: if item['state'] == 20:
status = 'completed' status = 'completed'
elif nzb['state'] in [21, 22, 24]: elif item['state'] in [21, 22, 24]:
status = 'failed' status = 'failed'
release_downloads.append({
'id': nzb['id'],
'name': nzb['uiTitle'],
'status': status,
'original_status': nzb['state'],
'timeleft':-1,
'folder': sp(nzb['destinationPath']),
})
return release_downloads statuses.append({
'id': item['id'],
'name': item['uiTitle'],
'status': status,
'original_status': item['state'],
'timeleft':-1,
'folder': ss(item['destinationPath']),
})
def removeFailed(self, release_download): return statuses
log.info('%s failed downloading, deleting...', release_download['name']) def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
try: try:
self.call('nzb/%s/cancel' % release_download['id']) self.call('nzb/%s/cancel' % item['id'])
except: except:
log.error('Failed deleting: %s', traceback.format_exc(0)) log.error('Failed deleting: %s', traceback.format_exc(0))
return False return False
@@ -117,9 +113,10 @@ class NZBVortex(Downloader):
params = tryUrlencode(parameters) params = tryUrlencode(parameters)
url = cleanHost(self.conf('host')) + 'api/' + call url = cleanHost(self.conf('host')) + 'api/' + call
url_opener = urllib2.build_opener(HTTPSHandler())
try: try:
data = self.urlopen('%s?%s' % (url, params), *args, **kwargs) data = self.urlopen('%s?%s' % (url, params), opener = url_opener, *args, **kwargs)
if data: if data:
return json.loads(data) return json.loads(data)
@@ -141,9 +138,10 @@ class NZBVortex(Downloader):
if not self.api_level: if not self.api_level:
url = cleanHost(self.conf('host')) + 'api/app/apilevel' url = cleanHost(self.conf('host')) + 'api/app/apilevel'
url_opener = urllib2.build_opener(HTTPSHandler())
try: try:
data = self.urlopen(url, show_error = False) data = self.urlopen(url, opener = url_opener, show_error = False)
self.api_level = float(json.loads(data).get('apilevel')) self.api_level = float(json.loads(data).get('apilevel'))
except URLError, e: except URLError, e:
if hasattr(e, 'code') and e.code == 403: if hasattr(e, 'code') and e.code == 403:
@@ -11,10 +11,9 @@ class Pneumatic(Downloader):
protocol = ['nzb'] protocol = ['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'
status_support = False
def download(self, data = None, media = None, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not media: media = {} if not movie: movie = {}
if not data: data = {} if not data: data = {}
directory = self.conf('directory') directory = self.conf('directory')
@@ -26,7 +25,7 @@ class Pneumatic(Downloader):
log.error('No nzb available!') log.error('No nzb available!')
return False return False
fullPath = os.path.join(directory, self.createFileName(data, filedata, media)) fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
try: try:
if not os.path.isfile(fullPath): if not os.path.isfile(fullPath):
@@ -34,7 +33,7 @@ class Pneumatic(Downloader):
with open(fullPath, 'wb') as f: with open(fullPath, 'wb') as f:
f.write(filedata) f.write(filedata)
nzb_name = self.createNzbName(data, media) nzb_name = self.createNzbName(data, movie)
strm_path = os.path.join(directory, nzb_name) strm_path = os.path.join(directory, nzb_name)
strm_file = open(strm_path + '.strm', 'wb') strm_file = open(strm_path + '.strm', 'wb')
@@ -42,11 +41,11 @@ class Pneumatic(Downloader):
strm_file.write(strmContent) strm_file.write(strmContent)
strm_file.close() strm_file.close()
return self.downloadReturnId('') return True
else: else:
log.info('File %s already exists.', fullPath) log.info('File %s already exists.', fullPath)
return self.downloadReturnId('') return True
except: except:
log.error('Failed to download .strm: %s', traceback.format_exc()) log.error('Failed to download .strm: %s', traceback.format_exc())
@@ -23,8 +23,6 @@ config = [{
{ {
'name': 'url', 'name': 'url',
'default': 'http://localhost:80/RPC2', 'default': 'http://localhost:80/RPC2',
'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
'or <strong>http://localhost:80/RPC2</strong>'
}, },
{ {
'name': 'username', 'name': 'username',
@@ -37,11 +35,6 @@ config = [{
'name': 'label', 'name': 'label',
'description': 'Label to apply on added torrents.', 'description': 'Label to apply on added torrents.',
}, },
{
'name': 'directory',
'type': 'directory',
'description': 'Download to this directory. Keep empty for default rTorrent download directory.',
},
{ {
'name': 'remove_complete', 'name': 'remove_complete',
'label': 'Remove torrent', 'label': 'Remove torrent',
+38 -66
View File
@@ -1,13 +1,13 @@
from base64 import b16encode, b32decode from base64 import b16encode, b32decode
from bencode import bencode, bdecode from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.encoding import ss
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta from datetime import timedelta
from hashlib import sha1 from hashlib import sha1
from rtorrent import RTorrent from rtorrent import RTorrent
from rtorrent.err import MethodError from rtorrent.err import MethodError
import os import shutil
log = CPLog(__name__) log = CPLog(__name__)
@@ -71,16 +71,13 @@ class rTorrent(Downloader):
group.set_command() group.set_command()
group.disable() group.disable()
except MethodError, err: except MethodError, err:
log.error('Unable to set group options: %s', err.msg) log.error('Unable to set group options: %s', err.message)
return False return False
return True return True
def download(self, data = None, media = None, filedata = None): def download(self, data, movie, filedata = None):
if not media: media = {}
if not data: data = {}
log.debug('Sending "%s" to rTorrent.', (data.get('name'))) log.debug('Sending "%s" to rTorrent.', (data.get('name')))
if not self.connect(): if not self.connect():
@@ -94,7 +91,6 @@ class rTorrent(Downloader):
if self.conf('label'): if self.conf('label'):
torrent_params['label'] = self.conf('label') torrent_params['label'] = self.conf('label')
if not filedata and data.get('protocol') == 'torrent': if not filedata and data.get('protocol') == 'torrent':
log.error('Failed sending torrent, no data') log.error('Failed sending torrent, no data')
return False return False
@@ -120,17 +116,10 @@ class rTorrent(Downloader):
# Send torrent to rTorrent # Send torrent to rTorrent
torrent = self.rt.load_torrent(filedata) torrent = self.rt.load_torrent(filedata)
if not torrent:
log.error('Unable to find the torrent, did it fail to load?')
return False
# Set label # Set label
if self.conf('label'): if self.conf('label'):
torrent.set_custom(1, self.conf('label')) torrent.set_custom(1, self.conf('label'))
if self.conf('directory'):
torrent.set_directory(self.conf('directory'))
# Set Ratio Group # Set Ratio Group
torrent.set_visible(group_name) torrent.set_visible(group_name)
@@ -143,52 +132,46 @@ class rTorrent(Downloader):
log.error('Failed to send torrent to rTorrent: %s', err) log.error('Failed to send torrent to rTorrent: %s', err)
return False return False
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self):
log.debug('Checking rTorrent download status.') log.debug('Checking rTorrent download status.')
if not self.connect(): if not self.connect():
return [] return False
try: try:
torrents = self.rt.get_torrents() torrents = self.rt.get_torrents()
release_downloads = ReleaseDownloadList(self) statuses = StatusList(self)
for torrent in torrents: for item in torrents:
if torrent.info_hash in ids: status = 'busy'
torrent_files = [] if item.complete:
for file_item in torrent.get_files(): if item.active:
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path))) status = 'seeding'
else:
status = 'busy' status = 'completed'
if torrent.complete:
if torrent.active:
status = 'seeding'
else:
status = 'completed'
release_downloads.append({
'id': torrent.info_hash,
'name': torrent.name,
'status': status,
'seed_ratio': torrent.ratio,
'original_status': torrent.state,
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
'folder': sp(torrent.directory),
'files': '|'.join(torrent_files)
})
return release_downloads statuses.append({
'id': item.info_hash,
'name': item.name,
'status': status,
'seed_ratio': item.ratio,
'original_status': item.state,
'timeleft': str(timedelta(seconds = float(item.left_bytes) / item.down_rate)) if item.down_rate > 0 else -1,
'folder': ss(item.directory)
})
return statuses
except Exception, err: except Exception, err:
log.error('Failed to get status from rTorrent: %s', err) log.error('Failed to get status from rTorrent: %s', err)
return [] return False
def pause(self, release_download, pause = True): def pause(self, download_info, pause = True):
if not self.connect(): if not self.connect():
return False return False
torrent = self.rt.find_torrent(release_download['id']) torrent = self.rt.find_torrent(download_info['id'])
if torrent is None: if torrent is None:
return False return False
@@ -196,34 +179,23 @@ class rTorrent(Downloader):
return torrent.pause() return torrent.pause()
return torrent.resume() return torrent.resume()
def removeFailed(self, release_download): def removeFailed(self, item):
log.info('%s failed downloading, deleting...', release_download['name']) log.info('%s failed downloading, deleting...', item['name'])
return self.processComplete(release_download, delete_files = True) return self.processComplete(item, delete_files = True)
def processComplete(self, release_download, delete_files): def processComplete(self, item, delete_files):
log.debug('Requesting rTorrent to remove the torrent %s%s.', log.debug('Requesting rTorrent to remove the torrent %s%s.',
(release_download['name'], ' and cleanup the downloaded files' if delete_files else '')) (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
if not self.connect(): if not self.connect():
return False return False
torrent = self.rt.find_torrent(release_download['id']) torrent = self.rt.find_torrent(item['id'])
if torrent is None: if torrent is None:
return False return False
if delete_files:
for file_item in torrent.get_files(): # will only delete files, not dir/sub-dir
os.unlink(os.path.join(torrent.directory, file_item.path))
if torrent.is_multi_file() and torrent.directory.endswith(torrent.name):
# Remove empty directories bottom up
try:
for path, _, _ in os.walk(torrent.directory, topdown = False):
os.rmdir(path)
except OSError:
log.info('Directory "%s" contains extra files, unable to remove', torrent.directory)
torrent.erase() # just removes the torrent, doesn't delete data torrent.erase() # just removes the torrent, doesn't delete data
if delete_files:
shutil.rmtree(item['folder'], True)
return True return True
+41 -54
View File
@@ -1,12 +1,11 @@
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost, mergeDicts from couchpotato.core.helpers.variable import cleanHost, mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from datetime import timedelta from datetime import timedelta
from urllib2 import URLError from urllib2 import URLError
import json import json
import os
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -16,8 +15,8 @@ class Sabnzbd(Downloader):
protocol = ['nzb'] protocol = ['nzb']
def download(self, data = None, media = None, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not media: media = {} if not movie: movie = {}
if not data: data = {} if not data: data = {}
log.info('Sending "%s" to SABnzbd.', data.get('name')) log.info('Sending "%s" to SABnzbd.', data.get('name'))
@@ -25,7 +24,7 @@ class Sabnzbd(Downloader):
req_params = { req_params = {
'cat': self.conf('category'), 'cat': self.conf('category'),
'mode': 'addurl', 'mode': 'addurl',
'nzbname': self.createNzbName(data, media), 'nzbname': self.createNzbName(data, movie),
'priority': self.conf('priority'), 'priority': self.conf('priority'),
} }
@@ -36,14 +35,14 @@ class Sabnzbd(Downloader):
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
nzb_filename = self.createFileName(data, filedata, media) nzb_filename = self.createFileName(data, filedata, movie)
req_params['mode'] = 'addfile' req_params['mode'] = 'addfile'
else: else:
req_params['name'] = data.get('url') req_params['name'] = data.get('url')
try: try:
if nzb_filename and req_params.get('mode') is 'addfile': if nzb_filename and req_params.get('mode') is 'addfile':
sab_data = self.call(req_params, files = {'nzbfile': (ss(nzb_filename), filedata)}) sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
else: else:
sab_data = self.call(req_params) sab_data = self.call(req_params)
except URLError: except URLError:
@@ -64,7 +63,7 @@ class Sabnzbd(Downloader):
log.error('Error getting data from SABNZBd: %s', sab_data) log.error('Error getting data from SABNZBd: %s', sab_data)
return False return False
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self):
log.debug('Checking SABnzbd download status.') log.debug('Checking SABnzbd download status.')
@@ -75,7 +74,7 @@ class Sabnzbd(Downloader):
}) })
except: except:
log.error('Failed getting queue: %s', traceback.format_exc(1)) log.error('Failed getting queue: %s', traceback.format_exc(1))
return [] return False
# Go through history items # Go through history items
try: try:
@@ -85,61 +84,49 @@ class Sabnzbd(Downloader):
}) })
except: except:
log.error('Failed getting history json: %s', traceback.format_exc(1)) log.error('Failed getting history json: %s', traceback.format_exc(1))
return [] return False
release_downloads = ReleaseDownloadList(self) statuses = StatusList(self)
# Get busy releases # Get busy releases
for nzb in queue.get('slots', []): for item in queue.get('slots', []):
if nzb['nzo_id'] in ids: statuses.append({
status = 'busy' 'id': item['nzo_id'],
if 'ENCRYPTED / ' in nzb['filename']: 'name': item['filename'],
status = 'failed' 'original_status': item['status'],
'timeleft': item['timeleft'] if not queue['paused'] else -1,
release_downloads.append({ })
'id': nzb['nzo_id'],
'name': nzb['filename'],
'status': status,
'original_status': nzb['status'],
'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
})
# Get old releases # Get old releases
for nzb in history.get('slots', []): for item in history.get('slots', []):
if nzb['nzo_id'] in ids:
status = 'busy'
if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()):
status = 'failed'
elif nzb['status'] == 'Completed':
status = 'completed'
release_downloads.append({
'id': nzb['nzo_id'],
'name': nzb['name'],
'status': status,
'original_status': nzb['status'],
'timeleft': str(timedelta(seconds = 0)),
'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']),
})
return release_downloads status = 'busy'
if item['status'] == 'Failed' or (item['status'] == 'Completed' and item['fail_message'].strip()):
status = 'failed'
elif item['status'] == 'Completed':
status = 'completed'
def removeFailed(self, release_download): statuses.append({
'id': item['nzo_id'],
'name': item['name'],
'status': status,
'original_status': item['status'],
'timeleft': str(timedelta(seconds = 0)),
'folder': ss(item['storage']),
})
log.info('%s failed downloading, deleting...', release_download['name']) return statuses
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
try: try:
self.call({
'mode': 'queue',
'name': 'delete',
'del_files': '1',
'value': release_download['id']
}, use_json = False)
self.call({ self.call({
'mode': 'history', 'mode': 'history',
'name': 'delete', 'name': 'delete',
'del_files': '1', 'del_files': '1',
'value': release_download['id'] 'value': item['id']
}, use_json = False) }, use_json = False)
except: except:
log.error('Failed deleting: %s', traceback.format_exc(0)) log.error('Failed deleting: %s', traceback.format_exc(0))
@@ -147,15 +134,15 @@ class Sabnzbd(Downloader):
return True return True
def processComplete(self, release_download, delete_files = False): def processComplete(self, item, delete_files = False):
log.debug('Requesting SabNZBd to remove the NZB %s.', release_download['name']) log.debug('Requesting SabNZBd to remove the NZB %s.', item['name'])
try: try:
self.call({ self.call({
'mode': 'history', 'mode': 'history',
'name': 'delete', 'name': 'delete',
'del_files': '0', 'del_files': '0',
'value': release_download['id'] 'value': item['id']
}, use_json = False) }, use_json = False)
except: except:
log.error('Failed removing: %s', traceback.format_exc(0)) log.error('Failed removing: %s', traceback.format_exc(0))
@@ -3,7 +3,6 @@ from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import json import json
import requests import requests
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -11,10 +10,10 @@ log = CPLog(__name__)
class Synology(Downloader): class Synology(Downloader):
protocol = ['nzb', 'torrent', 'torrent_magnet'] protocol = ['nzb', 'torrent', 'torrent_magnet']
status_support = False log = CPLog(__name__)
def download(self, data = None, media = None, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not media: media = {} if not movie: movie = {}
if not data: data = {} if not data: data = {}
response = False response = False
@@ -35,14 +34,14 @@ class Synology(Downloader):
elif data['protocol'] in ['nzb', 'torrent']: elif data['protocol'] in ['nzb', 'torrent']:
log.info('Adding %s' % data['protocol']) log.info('Adding %s' % data['protocol'])
if not filedata: if not filedata:
log.error('No %s data found', data['protocol']) log.error('No %s data found' % data['protocol'])
else: else:
filename = data['name'] + '.' + data['protocol'] filename = data['name'] + '.' + data['protocol']
response = srpc.create_task(filename = filename, filedata = filedata) response = srpc.create_task(filename = filename, filedata = filedata)
except: except Exception, err:
log.error('Exception while adding torrent: %s', traceback.format_exc()) log.error('Exception while adding torrent: %s', err)
finally: finally:
return self.downloadReturnId('') if response else False return response
def getEnabledProtocol(self): def getEnabledProtocol(self):
if self.conf('use_for') == 'both': if self.conf('use_for') == 'both':
@@ -1,8 +1,9 @@
from base64 import b64encode from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt, sp from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.helpers.variable import tryInt, tryFloat from couchpotato.core.helpers.variable import tryInt, tryFloat
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from datetime import timedelta from datetime import timedelta
import httplib import httplib
import json import json
@@ -31,9 +32,7 @@ class Transmission(Downloader):
return self.trpc return self.trpc
def download(self, data = None, media = None, filedata = None): def download(self, data, movie, filedata = None):
if not media: media = {}
if not data: data = {}
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol'))) log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol')))
@@ -83,67 +82,65 @@ class Transmission(Downloader):
log.info('Torrent sent to Transmission successfully.') log.info('Torrent sent to Transmission successfully.')
return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self):
log.debug('Checking Transmission download status.') log.debug('Checking Transmission download status.')
if not self.connect(): if not self.connect():
return [] return False
release_downloads = ReleaseDownloadList(self) statuses = StatusList(self)
return_params = { return_params = {
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit', 'files'] 'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit']
} }
queue = self.trpc.get_alltorrents(return_params) queue = self.trpc.get_alltorrents(return_params)
if not (queue and queue.get('torrents')): if not (queue and queue.get('torrents')):
log.debug('Nothing in queue or error') log.debug('Nothing in queue or error')
return [] return False
for torrent in queue['torrents']: for item in queue['torrents']:
if torrent['hashString'] in ids: log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s',
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / isStalled=%s / eta=%s / uploadRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], item['isFinished']))
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent.get('isStalled', 'N/A'), torrent['eta'], torrent['uploadRatio'], torrent['isFinished']))
torrent_files = []
for file_item in torrent['files']:
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name'])))
status = 'busy'
if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'):
status = 'failed'
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
status = 'completed'
elif torrent['status'] in [5, 6]:
status = 'seeding'
release_downloads.append({
'id': torrent['hashString'],
'name': torrent['name'],
'status': status,
'original_status': torrent['status'],
'seed_ratio': torrent['uploadRatio'],
'timeleft': str(timedelta(seconds = torrent['eta'])),
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
'files': '|'.join(torrent_files)
})
return release_downloads if not os.path.isdir(Env.setting('from', 'renamer')):
log.error('Renamer "from" folder doesn\'t to exist.')
return
def pause(self, release_download, pause = True): status = 'busy'
if item['isStalled'] and self.conf('stalled_as_failed'):
status = 'failed'
elif item['status'] == 0 and item['percentDone'] == 1:
status = 'completed'
elif item['status'] in [5, 6]:
status = 'seeding'
statuses.append({
'id': item['hashString'],
'name': item['name'],
'status': status,
'original_status': item['status'],
'seed_ratio': item['uploadRatio'],
'timeleft': str(timedelta(seconds = item['eta'])),
'folder': ss(os.path.join(item['downloadDir'], item['name'])),
})
return statuses
def pause(self, item, pause = True):
if pause: if pause:
return self.trpc.stop_torrent(release_download['id']) return self.trpc.stop_torrent(item['id'])
else: else:
return self.trpc.start_torrent(release_download['id']) return self.trpc.start_torrent(item['id'])
def removeFailed(self, release_download): def removeFailed(self, item):
log.info('%s failed downloading, deleting...', release_download['name']) log.info('%s failed downloading, deleting...', item['name'])
return self.trpc.remove_torrent(release_download['id'], True) return self.trpc.remove_torrent(item['hashString'], True)
def processComplete(self, release_download, delete_files = False): def processComplete(self, item, delete_files = False):
log.debug('Requesting Transmission to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else '')) log.debug('Requesting Transmission to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
return self.trpc.remove_torrent(release_download['id'], delete_files) return self.trpc.remove_torrent(item['hashString'], delete_files)
class TransmissionRPC(object): class TransmissionRPC(object):
+106 -96
View File
@@ -1,7 +1,7 @@
from base64 import b16encode, b32decode from base64 import b16encode, b32decode
from bencode import bencode as benc, bdecode from bencode import bencode as benc, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt, ss, sp from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.helpers.variable import tryInt, tryFloat from couchpotato.core.helpers.variable import tryInt, tryFloat
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta from datetime import timedelta
@@ -24,16 +24,6 @@ class uTorrent(Downloader):
protocol = ['torrent', 'torrent_magnet'] protocol = ['torrent', 'torrent_magnet']
utorrent_api = None utorrent_api = None
status_flags = {
'STARTED' : 1,
'CHECKING' : 2,
'CHECK-START' : 4,
'CHECKED' : 8,
'ERROR' : 16,
'PAUSED' : 32,
'QUEUED' : 64,
'LOADED' : 128
}
def connect(self): def connect(self):
# Load host from config and split out port. # Load host from config and split out port.
@@ -46,11 +36,11 @@ class uTorrent(Downloader):
return self.utorrent_api return self.utorrent_api
def download(self, data = None, media = None, filedata = None): def download(self, data = None, movie = None, filedata = None):
if not media: media = {} if not movie: movie = {}
if not data: data = {} if not data: data = {}
log.debug("Sending '%s' (%s) to uTorrent.", (data.get('name'), data.get('protocol'))) log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('protocol')))
if not self.connect(): if not self.connect():
return False return False
@@ -85,10 +75,9 @@ class uTorrent(Downloader):
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper() 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) torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
else: else:
info = bdecode(filedata)['info'] info = bdecode(filedata)["info"]
torrent_hash = sha1(benc(info)).hexdigest().upper() torrent_hash = sha1(benc(info)).hexdigest().upper()
torrent_filename = self.createFileName(data, filedata, movie)
torrent_filename = self.createFileName(data, filedata, media)
if data.get('seed_ratio'): if data.get('seed_ratio'):
torrent_params['seed_override'] = 1 torrent_params['seed_override'] = 1
@@ -113,90 +102,111 @@ class uTorrent(Downloader):
if self.conf('paused', default = 0): if self.conf('paused', default = 0):
self.utorrent_api.pause_torrent(torrent_hash) self.utorrent_api.pause_torrent(torrent_hash)
count = 0
while True:
count += 1
# Check if torrent is saved in subfolder of torrent name
data = self.utorrent_api.get_files(torrent_hash)
torrent_files = json.loads(data)
if torrent_files.get('error'):
log.error('Error getting data from uTorrent: %s', torrent_files.get('error'))
return False
if (torrent_files.get('files') and len(torrent_files['files'][1]) > 0) or count > 60:
break
time.sleep(1)
# Torrent has only one file, so uTorrent wont create a folder for it
if len(torrent_files['files'][1]) == 1:
# Remove torrent and try again
self.utorrent_api.remove_torrent(torrent_hash, remove_data = True)
# Send request to uTorrent
if data.get('protocol') == 'torrent_magnet':
self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'), add_folder = True)
else:
self.utorrent_api.add_torrent_file(torrent_filename, filedata, add_folder = True)
# Change settings of added torrent
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
if self.conf('paused', default = 0):
self.utorrent_api.pause_torrent(torrent_hash)
return self.downloadReturnId(torrent_hash) return self.downloadReturnId(torrent_hash)
def getAllDownloadStatus(self, ids): def getAllDownloadStatus(self):
log.debug('Checking uTorrent download status.') log.debug('Checking uTorrent download status.')
if not self.connect(): if not self.connect():
return [] return False
release_downloads = ReleaseDownloadList(self) statuses = StatusList(self)
data = self.utorrent_api.get_status() data = self.utorrent_api.get_status()
if not data: if not data:
log.error('Error getting data from uTorrent') log.error('Error getting data from uTorrent')
return [] return False
queue = json.loads(data) queue = json.loads(data)
if queue.get('error'): if queue.get('error'):
log.error('Error getting data from uTorrent: %s', queue.get('error')) log.error('Error getting data from uTorrent: %s', queue.get('error'))
return [] return False
if not queue.get('torrents'): if not queue.get('torrents'):
log.debug('Nothing in queue') log.debug('Nothing in queue')
return [] return False
# Get torrents # Get torrents
for torrent in queue['torrents']: for item in queue['torrents']:
if torrent[0] in ids:
#Get files of the torrent # item[21] = Paused | Downloading | Seeding | Finished
torrent_files = [] status = 'busy'
try: if 'Finished' in item[21]:
torrent_files = json.loads(self.utorrent_api.get_files(torrent[0])) status = 'completed'
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]] self.removeReadOnly(item[26])
except: elif 'Seeding' in item[21]:
log.debug('Failed getting files from torrent: %s', torrent[2]) status = 'seeding'
self.removeReadOnly(item[26])
status = 'busy'
if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000:
status = 'seeding'
elif (torrent[1] & self.status_flags['ERROR']):
status = 'failed'
elif torrent[4] == 1000:
status = 'completed'
if not status == 'busy':
self.removeReadOnly(torrent_files)
release_downloads.append({
'id': torrent[0],
'name': torrent[2],
'status': status,
'seed_ratio': float(torrent[7]) / 1000,
'original_status': torrent[1],
'timeleft': str(timedelta(seconds = torrent[10])),
'folder': sp(torrent[26]),
'files': '|'.join(torrent_files)
})
return release_downloads statuses.append({
'id': item[0],
'name': item[2],
'status': status,
'seed_ratio': float(item[7]) / 1000,
'original_status': item[1],
'timeleft': str(timedelta(seconds = item[10])),
'folder': ss(item[26]),
})
def pause(self, release_download, pause = True): return statuses
def pause(self, item, pause = True):
if not self.connect(): if not self.connect():
return False return False
return self.utorrent_api.pause_torrent(release_download['id'], pause) return self.utorrent_api.pause_torrent(item['id'], pause)
def removeFailed(self, release_download): def removeFailed(self, item):
log.info('%s failed downloading, deleting...', release_download['name']) log.info('%s failed downloading, deleting...', item['name'])
if not self.connect(): if not self.connect():
return False return False
return self.utorrent_api.remove_torrent(release_download['id'], remove_data = True) return self.utorrent_api.remove_torrent(item['id'], remove_data = True)
def processComplete(self, release_download, delete_files = False): def processComplete(self, item, delete_files = False):
log.debug('Requesting uTorrent to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else '')) log.debug('Requesting uTorrent to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
if not self.connect(): if not self.connect():
return False return False
return self.utorrent_api.remove_torrent(release_download['id'], remove_data = delete_files) return self.utorrent_api.remove_torrent(item['id'], remove_data = delete_files)
def removeReadOnly(self, files): def removeReadOnly(self, folder):
#Removes all read-on ly flags in a for all files #Removes all read-only flags in a folder
for filepath in files: if folder and os.path.isdir(folder):
if os.path.isfile(filepath): for root, folders, filenames in os.walk(folder):
#Windows only needs S_IWRITE, but we bitwise-or with current perms to preserve other permission bits on Linux for filename in filenames:
os.chmod(filepath, stat.S_IWRITE | os.stat(filepath).st_mode) os.chmod(os.path.join(root, filename), stat.S_IWRITE)
class uTorrentAPI(object): class uTorrentAPI(object):
@@ -223,7 +233,7 @@ class uTorrentAPI(object):
if time.time() > self.last_time + 1800: if time.time() > self.last_time + 1800:
self.last_time = time.time() self.last_time = time.time()
self.token = self.get_token() self.token = self.get_token()
request = urllib2.Request(self.url + '?token=' + self.token + '&' + action, data) request = urllib2.Request(self.url + "?token=" + self.token + "&" + action, data)
try: try:
open_request = self.opener.open(request) open_request = self.opener.open(request)
response = open_request.read() response = open_request.read()
@@ -243,64 +253,64 @@ class uTorrentAPI(object):
return False return False
def get_token(self): def get_token(self):
request = self.opener.open(self.url + 'token.html') request = self.opener.open(self.url + "token.html")
token = re.findall('<div.*?>(.*?)</', request.read())[0] token = re.findall("<div.*?>(.*?)</", request.read())[0]
return token return token
def add_torrent_uri(self, filename, torrent, add_folder = False): def add_torrent_uri(self, filename, torrent, add_folder = False):
action = 'action=add-url&s=%s' % urllib.quote(torrent) action = "action=add-url&s=%s" % urllib.quote(torrent)
if add_folder: if add_folder:
action += '&path=%s' % urllib.quote(filename) action += "&path=%s" % urllib.quote(filename)
return self._request(action) return self._request(action)
def add_torrent_file(self, filename, filedata, add_folder = False): def add_torrent_file(self, filename, filedata, add_folder = False):
action = 'action=add-file' action = "action=add-file"
if add_folder: if add_folder:
action += '&path=%s' % urllib.quote(filename) action += "&path=%s" % urllib.quote(filename)
return self._request(action, {'torrent_file': (ss(filename), filedata)}) return self._request(action, {"torrent_file": (ss(filename), filedata)})
def set_torrent(self, hash, params): def set_torrent(self, hash, params):
action = 'action=setprops&hash=%s' % hash action = "action=setprops&hash=%s" % hash
for k, v in params.iteritems(): for k, v in params.iteritems():
action += '&s=%s&v=%s' % (k, v) action += "&s=%s&v=%s" % (k, v)
return self._request(action) return self._request(action)
def pause_torrent(self, hash, pause = True): def pause_torrent(self, hash, pause = True):
if pause: if pause:
action = 'action=pause&hash=%s' % hash action = "action=pause&hash=%s" % hash
else: else:
action = 'action=unpause&hash=%s' % hash action = "action=unpause&hash=%s" % hash
return self._request(action) return self._request(action)
def stop_torrent(self, hash): def stop_torrent(self, hash):
action = 'action=stop&hash=%s' % hash action = "action=stop&hash=%s" % hash
return self._request(action) return self._request(action)
def remove_torrent(self, hash, remove_data = False): def remove_torrent(self, hash, remove_data = False):
if remove_data: if remove_data:
action = 'action=removedata&hash=%s' % hash action = "action=removedata&hash=%s" % hash
else: else:
action = 'action=remove&hash=%s' % hash action = "action=remove&hash=%s" % hash
return self._request(action) return self._request(action)
def get_status(self): def get_status(self):
action = 'list=1' action = "list=1"
return self._request(action) return self._request(action)
def get_settings(self): def get_settings(self):
action = 'action=getsettings' action = "action=getsettings"
settings_dict = {} settings_dict = {}
try: try:
utorrent_settings = json.loads(self._request(action)) utorrent_settings = json.loads(self._request(action))
# Create settings dict # Create settings dict
for setting in utorrent_settings['settings']: for item in utorrent_settings['settings']:
if setting[1] == 0: # int if item[1] == 0: # int
settings_dict[setting[0]] = int(setting[2] if not setting[2].strip() == '' else '0') settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
elif setting[1] == 1: # bool elif item[1] == 1: # bool
settings_dict[setting[0]] = True if setting[2] == 'true' else False settings_dict[item[0]] = True if item[2] == 'true' else False
elif setting[1] == 2: # string elif item[1] == 2: # string
settings_dict[setting[0]] = setting[2] settings_dict[item[0]] = item[2]
#log.debug('uTorrent settings: %s', settings_dict) #log.debug('uTorrent settings: %s', settings_dict)
@@ -320,5 +330,5 @@ class uTorrentAPI(object):
return self._request(action) return self._request(action)
def get_files(self, hash): def get_files(self, hash):
action = 'action=getfiles&hash=%s' % hash action = "action=getfiles&hash=%s" % hash
return self._request(action) return self._request(action)
+2 -34
View File
@@ -1,7 +1,6 @@
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from string import ascii_letters, digits from string import ascii_letters, digits
from urllib import quote_plus from urllib import quote_plus
import os
import re import re
import traceback import traceback
import unicodedata import unicodedata
@@ -39,39 +38,8 @@ def toUnicode(original, *args):
return toUnicode(ascii_text) return toUnicode(ascii_text)
def ss(original, *args): def ss(original, *args):
from couchpotato.environment import Env
u_original = toUnicode(original, *args) return toUnicode(original, *args).encode(Env.get('encoding'))
try:
from couchpotato.environment import Env
return u_original.encode(Env.get('encoding'))
except Exception, e:
log.debug('Failed ss encoding char, force UTF8: %s', e)
return u_original.encode('UTF-8')
def sp(path, *args):
# Standardise encoding, normalise case, path and strip trailing '/' or '\'
if not path or len(path) == 0:
return path
# convert windows path (from remote box) to *nix path
if os.path.sep == '/' and '\\' in path:
path = '/' + path.replace(':', '').replace('\\', '/')
path = os.path.normcase(os.path.normpath(ss(path, *args)))
# Remove any trailing path separators
if path != os.path.sep:
path = path.rstrip(os.path.sep)
# Add a trailing separator in case it is a root folder on windows (crashes guessit)
if len(path) == 2 and path[1] == ':':
path = path + os.path.sep
# Replace *NIX ambiguous '//' at the beginning of a path with '/' (crashes guessit)
path = re.sub('^//', '/', path)
return path
def ek(original, *args): def ek(original, *args):
if isinstance(original, (str, unicode)): if isinstance(original, (str, unicode)):
+6 -24
View File
@@ -1,8 +1,7 @@
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import collections
import hashlib import hashlib
import os import os.path
import platform import platform
import random import random
import re import re
@@ -11,9 +10,6 @@ import sys
log = CPLog(__name__) log = CPLog(__name__)
def fnEscape(pattern):
return pattern.replace('[','[[').replace(']','[]]').replace('[[','[[]')
def link(src, dst): def link(src, dst):
if os.name == 'nt': if os.name == 'nt':
import ctypes import ctypes
@@ -140,20 +136,18 @@ def getImdb(txt, check_inside = False, multiple = False):
output.close() output.close()
try: try:
ids = re.findall('(tt\d{4,7})', txt) ids = re.findall('(tt\d{7})', txt)
if multiple: if multiple:
return list(set(['tt%07d' % tryInt(x[2:]) for x in ids])) if len(ids) > 0 else [] return list(set(ids)) if len(ids) > 0 else []
return ids[0]
return 'tt%07d' % tryInt(ids[0][2:])
except IndexError: except IndexError:
pass pass
return False return False
def tryInt(s, default = 0): def tryInt(s):
try: return int(s) try: return int(s)
except: return default except: return 0
def tryFloat(s): def tryFloat(s):
try: try:
@@ -169,11 +163,6 @@ def natsortKey(s):
def natcmp(a, b): def natcmp(a, b):
return cmp(natsortKey(a), natsortKey(b)) return cmp(natsortKey(a), natsortKey(b))
def toIterable(value):
if type(value) in [list, tuple]:
return value
return [value]
def getTitle(library_dict): def getTitle(library_dict):
try: try:
try: try:
@@ -216,10 +205,3 @@ def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
def splitString(str, split_on = ',', clean = True): def splitString(str, split_on = ',', clean = True):
list = [x.strip() for x in str.split(split_on)] if str else [] list = [x.strip() for x in str.split(split_on)] if str else []
return filter(None, list) if clean else list return filter(None, list) if clean else list
def dictIsSubset(a, b):
return all([k in b and b[k] == v for k, v in a.items()])
def isSubFolder(sub_folder, base_folder):
# Returns True is sub_folder is the same as or in base_folder
return base_folder.rstrip(os.path.sep) + os.path.sep in sub_folder.rstrip(os.path.sep) + os.path.sep
+29 -30
View File
@@ -1,8 +1,7 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from importlib import import_module import glob
import os import os
import sys
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -13,6 +12,17 @@ class Loader(object):
providers = {} providers = {}
modules = {} modules = {}
def addPath(self, root, base_path, priority, recursive = False):
for filename in os.listdir(os.path.join(root, *base_path)):
path = os.path.join(os.path.join(root, *base_path), filename)
if os.path.isdir(path) and filename[:2] != '__':
if u'__init__.py' in os.listdir(path):
new_base_path = ''.join(s + '.' for s in base_path) + filename
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
if recursive:
self.addPath(root, base_path + [filename], priority, recursive = True)
def preload(self, root = ''): def preload(self, root = ''):
core = os.path.join(root, 'couchpotato', 'core') core = os.path.join(root, 'couchpotato', 'core')
@@ -29,14 +39,6 @@ class Loader(object):
# Add media to loader # Add media to loader
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True) self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
# Add custom plugin folder
from couchpotato.environment import Env
custom_plugin_dir = os.path.join(Env.get('data_dir'), 'custom_plugins')
if os.path.isdir(custom_plugin_dir):
sys.path.insert(0, custom_plugin_dir)
self.paths['custom_plugins'] = (30, '', custom_plugin_dir)
# Loop over all paths and add to module list
for plugin_type, plugin_tuple in self.paths.iteritems(): for plugin_type, plugin_tuple in self.paths.iteritems():
priority, module, dir_name = plugin_tuple priority, module, dir_name = plugin_tuple
self.addFromDir(plugin_type, priority, module, dir_name) self.addFromDir(plugin_type, priority, module, dir_name)
@@ -44,9 +46,8 @@ class Loader(object):
def run(self): def run(self):
did_save = 0 did_save = 0
for priority in sorted(self.modules): for priority in self.modules:
for module_name, plugin in sorted(self.modules[priority].iteritems()): for module_name, plugin in sorted(self.modules[priority].iteritems()):
# Load module # Load module
try: try:
if plugin.get('name')[:2] == '__': if plugin.get('name')[:2] == '__':
@@ -55,6 +56,7 @@ class Loader(object):
m = self.loadModule(module_name) m = self.loadModule(module_name)
if m is None: if m is None:
continue continue
m = getattr(m, plugin.get('name'))
log.info('Loading %s: %s', (plugin['type'], plugin['name'])) log.info('Loading %s: %s', (plugin['type'], plugin['name']))
@@ -76,26 +78,20 @@ class Loader(object):
if did_save: if did_save:
fireEvent('settings.save') fireEvent('settings.save')
def addPath(self, root, base_path, priority, recursive = False):
root_path = os.path.join(root, *base_path)
for filename in os.listdir(root_path):
path = os.path.join(root_path, filename)
if os.path.isdir(path) and filename[:2] != '__':
if u'__init__.py' in os.listdir(path):
new_base_path = ''.join(s + '.' for s in base_path) + filename
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
if recursive:
self.addPath(root, base_path + [filename], priority, recursive = True)
def addFromDir(self, plugin_type, priority, module, dir_name): def addFromDir(self, plugin_type, priority, module, dir_name):
# Load dir module # Load dir module
if module and len(module) > 0: try:
self.addModule(priority, plugin_type, module, os.path.basename(dir_name)) m = __import__(module)
splitted = module.split('.')
for sub in splitted[1:]:
m = getattr(m, sub)
except:
raise
for name in os.listdir(dir_name): for cur_file in glob.glob(os.path.join(dir_name, '*')):
if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(dir_name, name, '__init__.py')): name = os.path.basename(cur_file)
if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(cur_file, '__init__.py')):
module_name = '%s.%s' % (module, name) module_name = '%s.%s' % (module, name)
self.addModule(priority, plugin_type, module_name, name) self.addModule(priority, plugin_type, module_name, name)
@@ -135,7 +131,6 @@ class Loader(object):
if not self.modules.get(priority): if not self.modules.get(priority):
self.modules[priority] = {} self.modules[priority] = {}
module = module.lstrip('.')
self.modules[priority][module] = { self.modules[priority][module] = {
'priority': priority, 'priority': priority,
'module': module, 'module': module,
@@ -145,7 +140,11 @@ class Loader(object):
def loadModule(self, name): def loadModule(self, name):
try: try:
return import_module(name) m = __import__(name)
splitted = name.split('.')
for sub in splitted[1:-1]:
m = getattr(m, sub)
return m
except ImportError: except ImportError:
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc())) log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
return None return None
+1 -40
View File
@@ -1,52 +1,13 @@
from couchpotato import get_session from couchpotato.core.event import addEvent
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Media
class MediaBase(Plugin): class MediaBase(Plugin):
_type = None _type = None
default_dict = {
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
'library': {'titles': {}, 'files':{}},
'files': {},
'status': {},
'category': {},
}
search_dict = mergeDicts({
'library': {
'related_libraries': {},
'root_library': {}
},
}, default_dict)
def initType(self): def initType(self):
addEvent('media.types', self.getType) addEvent('media.types', self.getType)
def getType(self): def getType(self):
return self._type return self._type
def createOnComplete(self, id):
def onComplete():
db = get_session()
media = db.query(Media).filter_by(id = id).first()
fireEventAsync('%s.searcher.single' % media.type, media.to_dict(self.search_dict), on_complete = self.createNotifyFront(id))
db.expire_all()
return onComplete
def createNotifyFront(self, media_id):
def notifyFront():
db = get_session()
media = db.query(Media).filter_by(id = media_id).first()
fireEvent('notify.frontend', type = '%s.update' % media.type, data = media.to_dict(self.default_dict))
db.expire_all()
return notifyFront
@@ -1,6 +1,13 @@
from .main import Library from couchpotato.core.event import addEvent
from couchpotato.core.plugins.base import Plugin
def start():
return Library()
config = [] class LibraryBase(Plugin):
_type = None
def initType(self):
addEvent('library.types', self.getType)
def getType(self):
return self._type
@@ -1,13 +0,0 @@
from couchpotato.core.event import addEvent
from couchpotato.core.plugins.base import Plugin
class LibraryBase(Plugin):
_type = None
def initType(self):
addEvent('library.types', self.getType)
def getType(self):
return self._type
@@ -1,18 +0,0 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.media._base.library.base import LibraryBase
class Library(LibraryBase):
def __init__(self):
addEvent('library.title', self.title)
def title(self, library):
return fireEvent(
'library.query',
library,
condense = False,
include_year = False,
include_identifier = False,
single = True
)
@@ -1,6 +0,0 @@
from .main import Matcher
def start():
return Matcher()
config = []
@@ -1,84 +0,0 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
class MatcherBase(Plugin):
type = None
def __init__(self):
if self.type:
addEvent('%s.matcher.correct' % self.type, self.correct)
def correct(self, chain, release, media, quality):
raise NotImplementedError()
def flattenInfo(self, info):
# Flatten dictionary of matches (chain info)
if isinstance(info, dict):
return dict([(key, self.flattenInfo(value)) for key, value in info.items()])
# Flatten matches
result = None
for match in info:
if isinstance(match, dict):
if result is None:
result = {}
for key, value in match.items():
if key not in result:
result[key] = []
result[key].append(value)
else:
if result is None:
result = []
result.append(match)
return result
def constructFromRaw(self, match):
if not match:
return None
parts = [
''.join([
y for y in x[1:] if y
]) for x in match
]
return ''.join(parts)[:-1].strip()
def simplifyValue(self, value):
if not value:
return value
if isinstance(value, basestring):
return simplifyString(value)
if isinstance(value, list):
return [self.simplifyValue(x) for x in value]
raise ValueError("Unsupported value type")
def chainMatch(self, chain, group, tags):
info = self.flattenInfo(chain.info[group])
found_tags = []
for tag, accepted in tags.items():
values = [self.simplifyValue(x) for x in info.get(tag, [None])]
if any([val in accepted for val in values]):
found_tags.append(tag)
log.debug('tags found: %s, required: %s' % (found_tags, tags.keys()))
if set(tags.keys()) == set(found_tags):
return True
return all([key in found_tags for key, value in tags.items()])
@@ -1,88 +0,0 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import possibleTitles
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.matcher.base import MatcherBase
from caper import Caper
log = CPLog(__name__)
class Matcher(MatcherBase):
def __init__(self):
super(Matcher, self).__init__()
self.caper = Caper()
addEvent('matcher.parse', self.parse)
addEvent('matcher.match', self.match)
addEvent('matcher.flatten_info', self.flattenInfo)
addEvent('matcher.construct_from_raw', self.constructFromRaw)
addEvent('matcher.correct_title', self.correctTitle)
addEvent('matcher.correct_quality', self.correctQuality)
def parse(self, name, parser='scene'):
return self.caper.parse(name, parser)
def match(self, release, media, quality):
match = fireEvent('matcher.parse', release['name'], single = True)
if len(match.chains) < 1:
log.info2('Wrong: %s, unable to parse release name (no chains)', release['name'])
return False
for chain in match.chains:
if fireEvent('%s.matcher.correct' % media['type'], chain, release, media, quality, single = True):
return chain
return False
def correctTitle(self, chain, media):
root_library = media['library']['root_library']
if 'show_name' not in chain.info or not len(chain.info['show_name']):
log.info('Wrong: missing show name in parsed result')
return False
# Get the lower-case parsed show name from the chain
chain_words = [x.lower() for x in chain.info['show_name']]
# Build a list of possible titles of the media we are searching for
titles = root_library['info']['titles']
# Add year suffix titles (will result in ['<name_one>', '<name_one> <suffix_one>', '<name_two>', ...])
suffixes = [None, root_library['info']['year']]
titles = [
title + ((' %s' % suffix) if suffix else '')
for title in titles
for suffix in suffixes
]
# Check show titles match
# TODO check xem names
for title in titles:
for valid_words in [x.split(' ') for x in possibleTitles(title)]:
if valid_words == chain_words:
return True
return False
def correctQuality(self, chain, quality, quality_map):
if quality['identifier'] not in quality_map:
log.info2('Wrong: unknown preferred quality %s', quality['identifier'])
return False
if 'video' not in chain.info:
log.info2('Wrong: no video tags found')
return False
video_tags = quality_map[quality['identifier']]
if not self.chainMatch(chain, 'video', video_tags):
log.info2('Wrong: %s tags not in chain', video_tags)
return False
return True
@@ -1,6 +0,0 @@
from .main import MediaPlugin
def start():
return MediaPlugin()
config = []
-458
View File
@@ -1,458 +0,0 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb
from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase
from couchpotato.core.settings.model import Library, LibraryTitle, Release, \
Media
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_, desc
from string import ascii_lowercase
log = CPLog(__name__)
class MediaPlugin(MediaBase):
def __init__(self):
addApiView('media.refresh', self.refresh, docs = {
'desc': 'Refresh a any media type by ID',
'params': {
'id': {'desc': 'Movie, Show, Season or Episode ID(s) you want to refresh.', 'type': 'int (comma separated)'},
}
})
addApiView('media.list', self.listView, docs = {
'desc': 'List media',
'params': {
'type': {'type': 'string', 'desc': 'Media type to filter on.'},
'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"'},
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
'search': {'desc': 'Search movie title'},
},
'return': {'type': 'object', 'example': """{
'success': True,
'empty': bool, any movies returned or not,
'media': array, media found,
}"""}
})
addApiView('media.get', self.getView, docs = {
'desc': 'Get media by id',
'params': {
'id': {'desc': 'The id of the media'},
}
})
addApiView('media.delete', self.deleteView, docs = {
'desc': 'Delete a media from the wanted list',
'params': {
'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'},
'delete_from': {'desc': 'Delete media from this page', 'type': 'string: all (default), wanted, manage'},
}
})
addApiView('media.available_chars', self.charView)
addEvent('app.load', self.addSingleRefreshView)
addEvent('app.load', self.addSingleListView)
addEvent('app.load', self.addSingleCharView)
addEvent('app.load', self.addSingleDeleteView)
addEvent('media.get', self.get)
addEvent('media.list', self.list)
addEvent('media.delete', self.delete)
addEvent('media.restatus', self.restatus)
def refresh(self, id = '', **kwargs):
db = get_session()
for x in splitString(id):
media = db.query(Media).filter_by(id = x).first()
if media:
# Get current selected title
default_title = ''
for title in media.library.titles:
if title.default: default_title = title.title
fireEvent('notify.frontend', type = '%s.busy' % media.type, data = {'id': x})
fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
db.expire_all()
return {
'success': True,
}
def addSingleRefreshView(self):
for media_type in fireEvent('media.types', merge = True):
addApiView('%s.refresh' % media_type, self.refresh)
def get(self, media_id):
db = get_session()
imdb_id = getImdb(str(media_id))
if imdb_id:
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
else:
m = db.query(Media).filter_by(id = media_id).first()
results = None
if m:
results = m.to_dict(self.default_dict)
db.expire_all()
return results
def getView(self, id = None, **kwargs):
media = self.get(id) if id else None
return {
'success': media is not None,
'media': media,
}
def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
db = get_session()
# Make a list from string
if status and not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
if types and not isinstance(types, (list, tuple)):
types = [types]
# query movie ids
q = db.query(Media) \
.with_entities(Media.id) \
.group_by(Media.id)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Media.status_id.in_(statuses))
# Filter on release status
if release_status and len(release_status) > 0:
q = q.join(Media.releases)
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Release.status_id.in_(statuses))
# Filter on type
if types and len(types) > 0:
try: q = q.filter(Media.type.in_(types))
except: pass
# Only join when searching / ordering
if starts_with or search or order != 'release_order':
q = q.join(Media.library, Library.titles) \
.filter(LibraryTitle.default == True)
# Add search filters
filter_or = []
if starts_with:
starts_with = toUnicode(starts_with.lower())
if starts_with in ascii_lowercase:
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
else:
ignore = []
for letter in ascii_lowercase:
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
filter_or.append(not_(or_(*ignore)))
if search:
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
if len(filter_or) > 0:
q = q.filter(or_(*filter_or))
total_count = q.count()
if total_count == 0:
return 0, []
if order == 'release_order':
q = q.order_by(desc(Release.last_edit))
else:
q = q.order_by(asc(LibraryTitle.simple_title))
if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
# Get all media_ids in sorted order
media_ids = [m.id for m in q.all()]
# List release statuses
releases = db.query(Release) \
.filter(Release.media_id.in_(media_ids)) \
.all()
release_statuses = dict((m, set()) for m in media_ids)
releases_count = dict((m, 0) for m in media_ids)
for release in releases:
release_statuses[release.media_id].add('%d,%d' % (release.status_id, release.quality_id))
releases_count[release.media_id] += 1
# Get main movie data
q2 = db.query(Media) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files'))
q2 = q2.filter(Media.id.in_(media_ids))
results = q2.all()
# Create dict by movie id
movie_dict = {}
for movie in results:
movie_dict[movie.id] = movie
# List movies based on media_ids order
movies = []
for media_id in media_ids:
releases = []
for r in release_statuses.get(media_id):
x = splitString(r)
releases.append({'status_id': x[0], 'quality_id': x[1]})
# Merge releases with movie dict
movies.append(mergeDicts(movie_dict[media_id].to_dict({
'library': {'titles': {}, 'files':{}},
'files': {},
}), {
'releases': releases,
'releases_count': releases_count.get(media_id),
}))
db.expire_all()
return total_count, movies
def listView(self, **kwargs):
types = splitString(kwargs.get('types'))
status = splitString(kwargs.get('status'))
release_status = splitString(kwargs.get('release_status'))
limit_offset = kwargs.get('limit_offset')
starts_with = kwargs.get('starts_with')
search = kwargs.get('search')
order = kwargs.get('order')
total_movies, movies = self.list(
types = types,
status = status,
release_status = release_status,
limit_offset = limit_offset,
starts_with = starts_with,
search = search,
order = order
)
return {
'success': True,
'empty': len(movies) == 0,
'total': total_movies,
'movies': movies,
}
def addSingleListView(self):
for media_type in fireEvent('media.types', merge = True):
def tempList(*args, **kwargs):
return self.listView(types = media_type, *args, **kwargs)
addApiView('%s.list' % media_type, tempList)
def availableChars(self, types = None, status = None, release_status = None):
types = types or []
status = status or []
release_status = release_status or []
db = get_session()
# Make a list from string
if not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
if types and not isinstance(types, (list, tuple)):
types = [types]
q = db.query(Media)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Media.status_id.in_(statuses))
# Filter on release status
if release_status and len(release_status) > 0:
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.join(Media.releases) \
.filter(Release.status_id.in_(statuses))
# Filter on type
if types and len(types) > 0:
try: q = q.filter(Media.type.in_(types))
except: pass
q = q.join(Library, LibraryTitle) \
.with_entities(LibraryTitle.simple_title) \
.filter(LibraryTitle.default == True)
titles = q.all()
chars = set()
for title in titles:
try:
char = title[0][0]
char = char if char in ascii_lowercase else '#'
chars.add(str(char))
except:
log.error('Failed getting title for %s', title.libraries_id)
if len(chars) == 25:
break
db.expire_all()
return ''.join(sorted(chars))
def charView(self, **kwargs):
type = splitString(kwargs.get('type', 'movie'))
status = splitString(kwargs.get('status', None))
release_status = splitString(kwargs.get('release_status', None))
chars = self.availableChars(type, status, release_status)
return {
'success': True,
'empty': len(chars) == 0,
'chars': chars,
}
def addSingleCharView(self):
for media_type in fireEvent('media.types', merge = True):
def tempChar(*args, **kwargs):
return self.charView(types = media_type, *args, **kwargs)
addApiView('%s.available_chars' % media_type, tempChar)
def delete(self, media_id, delete_from = None):
db = get_session()
media = db.query(Media).filter_by(id = media_id).first()
if media:
deleted = False
if delete_from == 'all':
db.delete(media)
db.commit()
deleted = True
else:
done_status = fireEvent('status.get', 'done', single = True)
total_releases = len(media.releases)
total_deleted = 0
new_movie_status = None
for release in media.releases:
if delete_from in ['wanted', 'snatched', 'late']:
if release.status_id != done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'done'
elif delete_from == 'manage':
if release.status_id == done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'active'
db.commit()
if total_releases == total_deleted:
db.delete(media)
db.commit()
deleted = True
elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True)
media.profile_id = None
media.status_id = new_status.get('id')
db.commit()
else:
fireEvent('media.restatus', media.id, single = True)
if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict())
db.expire_all()
return True
def deleteView(self, id = '', **kwargs):
ids = splitString(id)
for media_id in ids:
self.delete(media_id, delete_from = kwargs.get('delete_from', 'all'))
return {
'success': True,
}
def addSingleDeleteView(self):
for media_type in fireEvent('media.types', merge = True):
def tempDelete(*args, **kwargs):
return self.deleteView(types = media_type, *args, **kwargs)
addApiView('%s.delete' % media_type, tempDelete)
def restatus(self, media_id):
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
db = get_session()
m = db.query(Media).filter_by(id = media_id).first()
if not m or len(m.library.titles) == 0:
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
return False
log.debug('Changing status for %s', m.library.titles[0].title)
if not m.profile:
m.status_id = done_status.get('id')
else:
move_to_wanted = True
for t in m.profile.types:
for release in m.releases:
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
move_to_wanted = False
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
db.commit()
return True
@@ -1,6 +0,0 @@
from .main import Search
def start():
return Search()
config = []
@@ -1,59 +0,0 @@
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
class Search(Plugin):
def __init__(self):
addApiView('search', self.search, docs = {
'desc': 'Search the info in providers for a movie',
'params': {
'q': {'desc': 'The (partial) movie name you want to search for'},
'type': {'desc': 'Search for a specific media type. Leave empty to search all.'},
},
'return': {'type': 'object', 'example': """{
'success': True,
'movies': array,
'show': array,
etc
}"""}
})
addEvent('app.load', self.addSingleSearches)
def search(self, q = '', types = None, **kwargs):
# Make sure types is the correct instance
if isinstance(types, (str, unicode)):
types = [types]
elif isinstance(types, (list, tuple, set)):
types = list(types)
if not types:
result = fireEvent('info.search', q = q, merge = True)
else:
result = {}
for media_type in types:
result[media_type] = fireEvent('%s.search' % media_type)
return mergeDicts({
'success': True,
}, result)
def createSingleSearch(self, media_type):
def singleSearch(q, **kwargs):
return self.search(q, type = media_type, **kwargs)
return singleSearch
def addSingleSearches(self):
for media_type in fireEvent('media.types', merge = True):
addApiView('%s.search' % media_type, self.createSingleSearch(media_type))
@@ -1,188 +0,0 @@
Block.Search = new Class({
Extends: BlockBase,
cache: {},
create: function(){
var self = this;
var focus_timer = 0;
self.el = new Element('div.search_form').adopt(
new Element('div.input').adopt(
self.input = new Element('input', {
'placeholder': 'Search & add a new media',
'events': {
'keyup': self.keyup.bind(self),
'focus': function(){
if(focus_timer) clearTimeout(focus_timer);
self.el.addClass('focused')
if(this.get('value'))
self.hideResults(false)
},
'blur': function(){
focus_timer = (function(){
self.el.removeClass('focused')
}).delay(100);
}
}
}),
new Element('a.icon2', {
'events': {
'click': self.clear.bind(self),
'touchend': self.clear.bind(self)
}
})
),
self.result_container = new Element('div.results_container', {
'tween': {
'duration': 200
},
'events': {
'mousewheel': function(e){
(e).stopPropagation();
}
}
}).adopt(
self.results = new Element('div.results')
)
);
self.mask = new Element('div.mask').inject(self.result_container).fade('hide');
},
clear: function(e){
var self = this;
(e).preventDefault();
if(self.last_q === ''){
self.input.blur()
self.last_q = null;
}
else {
self.last_q = '';
self.input.set('value', '');
self.input.focus()
self.media = {}
self.results.empty()
self.el.removeClass('filled')
}
},
hideResults: function(bool){
var self = this;
if(self.hidden == bool) return;
self.el[bool ? 'removeClass' : 'addClass']('shown');
if(bool){
History.removeEvent('change', self.hideResults.bind(self, !bool));
self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool));
}
else {
History.addEvent('change', self.hideResults.bind(self, !bool));
self.el.addEvent('outerClick', self.hideResults.bind(self, !bool));
}
self.hidden = bool;
},
keyup: function(e){
var self = this;
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
if(self.q() != self.last_q){
if(self.api_request && self.api_request.isRunning())
self.api_request.cancel();
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
self.autocomplete_timer = self.autocomplete.delay(300, self)
}
},
autocomplete: function(){
var self = this;
if(!self.q()){
self.hideResults(true)
return
}
self.list()
},
list: function(){
var self = this,
q = self.q(),
cache = self.cache[q];
self.hideResults(false);
if(!cache){
self.mask.fade('in');
if(!self.spinner)
self.spinner = createSpinner(self.mask);
self.api_request = Api.request('search', {
'data': {
'q': q
},
'onComplete': self.fill.bind(self, q)
})
}
else
self.fill(q, cache)
self.last_q = q;
},
fill: function(q, json){
var self = this;
self.cache[q] = json
self.media = {}
self.results.empty()
Object.each(json, function(media, type){
if(typeOf(media) == 'array'){
Object.each(media, function(m){
var m = new Block.Search[m.type.capitalize() + 'Item'](m);
$(m).inject(self.results)
self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
if(q == m.imdb)
m.showOptions()
});
}
})
// Calculate result heights
var w = window.getSize(),
rc = self.result_container.getCoordinates();
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
self.mask.fade('out')
},
loading: function(bool){
this.el[bool ? 'addClass' : 'removeClass']('loading')
},
q: function(){
return this.input.get('value').trim();
}
});
@@ -47,7 +47,7 @@ config = [{
{ {
'name': 'ignored_words', 'name': 'ignored_words',
'label': 'Ignored', 'label': 'Ignored',
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs, vain', 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
'description': 'Ignores releases that match any of these sets. (Works like explained above)' 'description': 'Ignores releases that match any of these sets. (Works like explained above)'
}, },
], ],
+80 -60
View File
@@ -2,10 +2,10 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent 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.variable import md5, getTitle, splitString from couchpotato.core.helpers.variable import md5, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.settings.model import Media, Release, ReleaseInfo from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env from couchpotato.environment import Env
from inspect import ismethod, isfunction from inspect import ismethod, isfunction
import datetime import datetime
@@ -23,8 +23,7 @@ class Searcher(SearcherBase):
addEvent('searcher.contains_other_quality', self.containsOtherQuality) addEvent('searcher.contains_other_quality', self.containsOtherQuality)
addEvent('searcher.correct_year', self.correctYear) addEvent('searcher.correct_year', self.correctYear)
addEvent('searcher.correct_name', self.correctName) addEvent('searcher.correct_name', self.correctName)
addEvent('searcher.correct_words', self.correctWords) addEvent('searcher.download', self.download)
addEvent('searcher.search', self.search)
addApiView('searcher.full_search', self.searchAllView, docs = { addApiView('searcher.full_search', self.searchAllView, docs = {
'desc': 'Starts a full search for all media', 'desc': 'Starts a full search for all media',
@@ -50,21 +49,86 @@ class Searcher(SearcherBase):
progress = fireEvent('searcher.progress', merge = True) progress = fireEvent('searcher.progress', merge = True)
return progress return progress
def search(self, protocols, media, quality): def download(self, data, movie, manual = False):
results = []
for search_protocol in protocols: if not data.get('protocol'):
protocol_results = fireEvent('provider.search.%s.%s' % (search_protocol, media['type']), media, quality, merge = True) data['protocol'] = data['type']
if protocol_results: data['type'] = 'movie'
results += protocol_results
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True) # Test to see if any downloaders are enabled for this type
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
download_preference = self.conf('preferred_method', section = 'searcher') if downloader_enabled:
if download_preference != 'both':
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
return sorted_results snatched_status = fireEvent('status.get', 'snatched', single = True)
# 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 == 'try_next':
return filedata
download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
log.debug('Downloader result: %s', download_result)
if download_result:
try:
# Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
if rls:
renamer_enabled = Env.setting('enabled', 'renamer')
done_status = fireEvent('status.get', 'done', single = True)
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
# Save download-id info if returned
if isinstance(download_result, dict):
for key in download_result:
rls_info = ReleaseInfo(
identifier = 'download_%s' % key,
value = toUnicode(download_result.get(key))
)
rls.info.append(rls_info)
db.commit()
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 renamer_enabled:
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 or gave an error', (data.get('protocol')))
return False
def getSearchProtocols(self): def getSearchProtocols(self):
@@ -153,7 +217,7 @@ class Searcher(SearcherBase):
except: pass except: pass
# Match longest name between [] # Match longest name between []
try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', check_name), key = len).strip()) try: check_names.append(max(check_name.split('['), key = len))
except: pass except: pass
for check_name in list(set(check_names)): for check_name in list(set(check_names)):
@@ -170,49 +234,5 @@ class Searcher(SearcherBase):
return False return False
def correctWords(self, rel_name, media):
media_title = fireEvent('library.title', media['library'], single = True)
media_words = re.split('\W+', simplifyString(media_title))
rel_name = simplifyString(rel_name)
rel_words = re.split('\W+', rel_name)
# Make sure it has required words
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
try: required_words = list(set(required_words + splitString(media['category']['required'].lower())))
except: pass
req_match = 0
for req_set in required_words:
req = splitString(req_set, '&')
req_match += len(list(set(rel_words) & set(req))) == len(req)
if len(required_words) > 0 and req_match == 0:
log.info2('Wrong: Required word missing: %s', rel_name)
return False
# Ignore releases
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
try: ignored_words = list(set(ignored_words + splitString(media['category']['ignored'].lower())))
except: pass
ignored_match = 0
for ignored_set in ignored_words:
ignored = splitString(ignored_set, '&')
ignored_match += len(list(set(rel_words) & set(ignored))) == len(ignored)
if len(ignored_words) > 0 and ignored_match:
log.info2("Wrong: '%s' contains 'ignored words'", rel_name)
return False
# Ignore porn stuff
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic', 'cock', 'dick']
pron_words = list(set(rel_words) & set(pron_tags) - set(media_words))
if pron_words:
log.info('Wrong: %s, probably pr0n', rel_name)
return False
return True
class SearchSetupError(Exception): class SearchSetupError(Exception):
pass pass
+492 -16
View File
@@ -1,11 +1,17 @@
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 fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import splitString, tryInt from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie import MovieTypeBase from couchpotato.core.media.movie import MovieTypeBase
from couchpotato.core.settings.model import Media from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \
Release
from couchpotato.environment import Env
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_, desc
from string import ascii_lowercase
import time import time
log = CPLog(__name__) log = CPLog(__name__)
@@ -13,7 +19,14 @@ log = CPLog(__name__)
class MovieBase(MovieTypeBase): class MovieBase(MovieTypeBase):
_type = 'movie' default_dict = {
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
'library': {'titles': {}, 'files':{}},
'files': {},
'status': {},
'category': {},
}
def __init__(self): def __init__(self):
@@ -21,12 +34,50 @@ class MovieBase(MovieTypeBase):
super(MovieBase, self).__init__() super(MovieBase, self).__init__()
self.initType() self.initType()
addApiView('movie.search', self.search, docs = {
'desc': 'Search the movie providers for a movie',
'params': {
'q': {'desc': 'The (partial) movie name you want to search for'},
},
'return': {'type': 'object', 'example': """{
'success': True,
'empty': bool, any movies returned or not,
'movies': array, movies found,
}"""}
})
addApiView('movie.list', self.listView, docs = {
'desc': 'List movies in wanted list',
'params': {
'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"'},
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
'search': {'desc': 'Search movie title'},
},
'return': {'type': 'object', 'example': """{
'success': True,
'empty': bool, any movies returned or not,
'movies': array, movies found,
}"""}
})
addApiView('movie.get', self.getView, docs = {
'desc': 'Get a movie by id',
'params': {
'id': {'desc': 'The id of the movie'},
}
})
addApiView('movie.refresh', self.refresh, docs = {
'desc': 'Refresh a movie by id',
'params': {
'id': {'desc': 'Movie ID(s) you want to refresh.', 'type': 'int (comma separated)'},
}
})
addApiView('movie.available_chars', self.charView)
addApiView('movie.add', self.addView, docs = { addApiView('movie.add', self.addView, docs = {
'desc': 'Add new movie to the wanted list', 'desc': 'Add new movie to the wanted list',
'params': { 'params': {
'identifier': {'desc': 'IMDB id of the movie your want to add.'}, 'identifier': {'desc': 'IMDB id of the movie your want to add.'},
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'}, 'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'},
'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, 'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
} }
}) })
@@ -35,12 +86,327 @@ class MovieBase(MovieTypeBase):
'params': { 'params': {
'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'}, 'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'},
'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'}, 'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'},
'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'},
'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, 'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
} }
}) })
addApiView('movie.delete', self.deleteView, docs = {
'desc': 'Delete a movie from the wanted list',
'params': {
'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'},
'delete_from': {'desc': 'Delete movie from this page', 'type': 'string: all (default), wanted, manage'},
}
})
addEvent('movie.add', self.add) addEvent('movie.add', self.add)
addEvent('movie.delete', self.delete)
addEvent('movie.get', self.get)
addEvent('movie.list', self.list)
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, available_status, snatched_status = \
fireEvent('status.get', ['done', 'available', '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)
db.expire_all()
def getView(self, id = None, **kwargs):
movie = self.get(id) if id else None
return {
'success': movie is not None,
'movie': movie,
}
def get(self, movie_id):
db = get_session()
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
if m:
results = m.to_dict(self.default_dict)
db.expire_all()
return results
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
db = get_session()
# Make a list from string
if status and not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
# query movie ids
q = db.query(Movie) \
.with_entities(Movie.id) \
.group_by(Movie.id)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Movie.status_id.in_(statuses))
# Filter on release status
if release_status and len(release_status) > 0:
q = q.join(Movie.releases)
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Release.status_id.in_(statuses))
# Only join when searching / ordering
if starts_with or search or order != 'release_order':
q = q.join(Movie.library, Library.titles) \
.filter(LibraryTitle.default == True)
# Add search filters
filter_or = []
if starts_with:
starts_with = toUnicode(starts_with.lower())
if starts_with in ascii_lowercase:
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
else:
ignore = []
for letter in ascii_lowercase:
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
filter_or.append(not_(or_(*ignore)))
if search:
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
if len(filter_or) > 0:
q = q.filter(or_(*filter_or))
total_count = q.count()
if total_count == 0:
return 0, []
if order == 'release_order':
q = q.order_by(desc(Release.last_edit))
else:
q = q.order_by(asc(LibraryTitle.simple_title))
if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
# Get all movie_ids in sorted order
movie_ids = [m.id for m in q.all()]
# List release statuses
releases = db.query(Release) \
.filter(Release.movie_id.in_(movie_ids)) \
.all()
release_statuses = dict((m, set()) for m in movie_ids)
releases_count = dict((m, 0) for m in movie_ids)
for release in releases:
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id))
releases_count[release.movie_id] += 1
# Get main movie data
q2 = db.query(Movie) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files'))
q2 = q2.filter(Movie.id.in_(movie_ids))
results = q2.all()
# Create dict by movie id
movie_dict = {}
for movie in results:
movie_dict[movie.id] = movie
# List movies based on movie_ids order
movies = []
for movie_id in movie_ids:
releases = []
for r in release_statuses.get(movie_id):
x = splitString(r)
releases.append({'status_id': x[0], 'quality_id': x[1]})
# Merge releases with movie dict
movies.append(mergeDicts(movie_dict[movie_id].to_dict({
'library': {'titles': {}, 'files':{}},
'files': {},
}), {
'releases': releases,
'releases_count': releases_count.get(movie_id),
}))
db.expire_all()
return total_count, movies
def availableChars(self, status = None, release_status = None):
status = status or []
release_status = release_status or []
db = get_session()
# Make a list from string
if not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
q = db.query(Movie)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Movie.status_id.in_(statuses))
# Filter on release status
if release_status and len(release_status) > 0:
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.join(Movie.releases) \
.filter(Release.status_id.in_(statuses))
q = q.join(Library, LibraryTitle) \
.with_entities(LibraryTitle.simple_title) \
.filter(LibraryTitle.default == True)
titles = q.all()
chars = set()
for title in titles:
try:
char = title[0][0]
char = char if char in ascii_lowercase else '#'
chars.add(str(char))
except:
log.error('Failed getting title for %s', title.libraries_id)
if len(chars) == 25:
break
db.expire_all()
return ''.join(sorted(chars))
def listView(self, **kwargs):
status = splitString(kwargs.get('status'))
release_status = splitString(kwargs.get('release_status'))
limit_offset = kwargs.get('limit_offset')
starts_with = kwargs.get('starts_with')
search = kwargs.get('search')
order = kwargs.get('order')
total_movies, movies = self.list(
status = status,
release_status = release_status,
limit_offset = limit_offset,
starts_with = starts_with,
search = search,
order = order
)
return {
'success': True,
'empty': len(movies) == 0,
'total': total_movies,
'movies': movies,
}
def charView(self, **kwargs):
status = splitString(kwargs.get('status', None))
release_status = splitString(kwargs.get('release_status', None))
chars = self.availableChars(status, release_status)
return {
'success': True,
'empty': len(chars) == 0,
'chars': chars,
}
def refresh(self, id = '', **kwargs):
db = get_session()
for x in splitString(id):
movie = db.query(Movie).filter_by(id = x).first()
if movie:
# Get current selected title
default_title = ''
for title in movie.library.titles:
if title.default: default_title = title.title
fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
fireEventAsync('library.update.movie', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
db.expire_all()
return {
'success': True,
}
def search(self, q = '', **kwargs):
cache_key = u'%s/%s' % (__name__, simplifyString(q))
movies = Env.get('cache').get(cache_key)
if not movies:
if getImdb(q):
movies = [fireEvent('movie.info', identifier = q, merge = True)]
else:
movies = fireEvent('movie.search', q = q, merge = True)
Env.get('cache').set(cache_key, movies)
return {
'success': True,
'empty': len(movies) == 0 if movies else 0,
'movies': movies,
}
def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None): def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None):
if not params: params = {} if not params: params = {}
@@ -72,12 +438,12 @@ class MovieBase(MovieTypeBase):
cat_id = params.get('category_id') cat_id = params.get('category_id')
db = get_session() db = get_session()
m = db.query(Media).filter_by(library_id = library.get('id')).first() m = db.query(Movie).filter_by(library_id = library.get('id')).first()
added = True added = True
do_search = False do_search = False
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher') search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
if not m: if not m:
m = Media( 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_id if status_id else status_active.get('id'),
@@ -134,12 +500,15 @@ class MovieBase(MovieTypeBase):
db.expire_all() db.expire_all()
return movie_dict return movie_dict
def addView(self, **kwargs): def addView(self, **kwargs):
add_dict = self.add(params = kwargs)
movie_dict = self.add(params = kwargs)
return { return {
'success': True if add_dict else False, 'success': True,
'movie': add_dict, 'added': True if movie_dict else False,
'movie': movie_dict,
} }
def edit(self, id = '', **kwargs): def edit(self, id = '', **kwargs):
@@ -149,9 +518,9 @@ class MovieBase(MovieTypeBase):
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
ids = splitString(id) ids = splitString(id)
for media_id in ids: for movie_id in ids:
m = db.query(Media).filter_by(id = media_id).first() m = db.query(Movie).filter_by(id = movie_id).first()
if not m: if not m:
continue continue
@@ -174,12 +543,119 @@ class MovieBase(MovieTypeBase):
db.commit() db.commit()
fireEvent('media.restatus', m.id) fireEvent('movie.restatus', m.id)
movie_dict = m.to_dict(self.search_dict) movie_dict = m.to_dict(self.default_dict)
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id)) fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
db.expire_all() db.expire_all()
return { return {
'success': True, 'success': True,
} }
def deleteView(self, id = '', **kwargs):
ids = splitString(id)
for movie_id in ids:
self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
return {
'success': True,
}
def delete(self, movie_id, delete_from = None):
db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first()
if movie:
deleted = False
if delete_from == 'all':
db.delete(movie)
db.commit()
deleted = True
else:
done_status = fireEvent('status.get', 'done', single = True)
total_releases = len(movie.releases)
total_deleted = 0
new_movie_status = None
for release in movie.releases:
if delete_from in ['wanted', 'snatched', 'late']:
if release.status_id != done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'done'
elif delete_from == 'manage':
if release.status_id == done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'active'
db.commit()
if total_releases == total_deleted:
db.delete(movie)
db.commit()
deleted = True
elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True)
movie.profile_id = None
movie.status_id = new_status.get('id')
db.commit()
else:
fireEvent('movie.restatus', movie.id, single = True)
if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
db.expire_all()
return True
def restatus(self, movie_id):
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
db = get_session()
m = db.query(Movie).filter_by(id = movie_id).first()
if not m or len(m.library.titles) == 0:
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
return False
log.debug('Changing status for %s', m.library.titles[0].title)
if not m.profile:
m.status_id = done_status.get('id')
else:
move_to_wanted = True
for t in m.profile.types:
for release in m.releases:
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
move_to_wanted = False
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
db.commit()
return True
def createOnComplete(self, movie_id):
def onComplete():
db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first()
fireEventAsync('movie.searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
db.expire_all()
return onComplete
def createNotifyFront(self, movie_id):
def notifyFront():
db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first()
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
db.expire_all()
return notifyFront
@@ -52,8 +52,8 @@ var MovieList = new Class({
self.getMovies(); self.getMovies();
App.on('movie.added', self.movieAdded.bind(self)) App.addEvent('movie.added', self.movieAdded.bind(self))
App.on('movie.deleted', self.movieDeleted.bind(self)) App.addEvent('movie.deleted', self.movieDeleted.bind(self))
}, },
movieDeleted: function(notification){ movieDeleted: function(notification){
@@ -65,7 +65,6 @@ var MovieList = new Class({
movie.destroy(); movie.destroy();
delete self.movies_added[notification.data.id]; delete self.movies_added[notification.data.id];
self.setCounter(self.counter_count-1); self.setCounter(self.counter_count-1);
self.total_movies--;
} }
}) })
} }
@@ -76,7 +75,6 @@ var MovieList = new Class({
movieAdded: function(notification){ movieAdded: function(notification){
var self = this; var self = this;
self.fireEvent('movieAdded', notification);
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){ if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
window.scroll(0,0); window.scroll(0,0);
self.createMovie(notification.data, 'top'); self.createMovie(notification.data, 'top');
@@ -281,7 +279,7 @@ var MovieList = new Class({
// Get available chars and highlight // Get available chars and highlight
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible())) if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
Api.request('media.available_chars', { Api.request('movie.available_chars', {
'data': Object.merge({ 'data': Object.merge({
'status': self.options.status 'status': self.options.status
}, self.filter), }, self.filter),
@@ -372,7 +370,7 @@ var MovieList = new Class({
'click': function(e){ 'click': function(e){
(e).preventDefault(); (e).preventDefault();
this.set('text', 'Deleting..') this.set('text', 'Deleting..')
Api.request('media.delete', { Api.request('movie.delete', {
'data': { 'data': {
'id': ids.join(','), 'id': ids.join(','),
'delete_from': self.options.identifier 'delete_from': self.options.identifier
@@ -392,7 +390,6 @@ var MovieList = new Class({
self.movies.erase(movie); self.movies.erase(movie);
movie.destroy(); movie.destroy();
self.setCounter(self.counter_count-1); self.setCounter(self.counter_count-1);
self.total_movies--;
}); });
self.calculateSelected(); self.calculateSelected();
@@ -425,7 +422,7 @@ var MovieList = new Class({
var self = this; var self = this;
var ids = self.getSelectedMovies() var ids = self.getSelectedMovies()
Api.request('media.refresh', { Api.request('movie.refresh', {
'data': { 'data': {
'id': ids.join(','), 'id': ids.join(','),
} }
@@ -550,9 +547,8 @@ var MovieList = new Class({
} }
Api.request(self.options.api_call || 'media.list', { Api.request(self.options.api_call || 'movie.list', {
'data': Object.merge({ 'data': Object.merge({
'type': 'movie',
'status': self.options.status, 'status': self.options.status,
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null 'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
}, self.filter), }, self.filter),
@@ -18,13 +18,11 @@ var MovieAction = new Class({
create: function(){}, create: function(){},
disable: function(){ disable: function(){
if(this.el) this.el.addClass('disable')
this.el.addClass('disable')
}, },
enable: function(){ enable: function(){
if(this.el) this.el.removeClass('disable')
this.el.removeClass('disable')
}, },
getTitle: function(){ getTitle: function(){
@@ -126,9 +124,7 @@ MA.Release = new Class({
else else
self.showHelper(); self.showHelper();
App.on('movie.searcher.ended', function(notification){ App.addEvent('movie.searcher.ended.'+self.movie.data.id, function(notification){
if(self.movie.data.id != notification.data.id) return;
self.releases = null; self.releases = null;
if(self.options_container){ if(self.options_container){
self.options_container.destroy(); self.options_container.destroy();
@@ -243,6 +239,7 @@ MA.Release = new Class({
} }
}) })
).inject(self.release_container); ).inject(self.release_container);
release['el'] = item; release['el'] = item;
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){ if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
@@ -252,44 +249,18 @@ MA.Release = new Class({
else if(!self.next_release && status.identifier == 'available'){ else if(!self.next_release && status.identifier == 'available'){
self.next_release = release; self.next_release = release;
} }
var update_handle = function(notification) {
if(notification.data.id != release.id) return;
var q = self.movie.quality.getElement('.q_id' + release.quality_id),
status = Status.get(release.status_id),
new_status = Status.get(notification.data.status_id);
release.status_id = new_status.id
release.el.set('class', 'item ' + new_status.identifier);
var status_el = release.el.getElement('.release_status');
status_el.set('class', 'release_status ' + new_status.identifier);
status_el.set('text', new_status.identifier);
if(!q && (new_status.identifier == 'snatched' || new_status.identifier == 'seeding' || new_status.identifier == 'done'))
var q = self.addQuality(release.quality_id);
if(new_status && q && !q.hasClass(new_status.identifier)) {
q.removeClass(status.identifier).addClass(new_status.identifier);
q.set('title', q.get('title').replace(status.label, new_status.label));
}
}
App.on('release.update_status', update_handle);
}); });
if(self.last_release) if(self.last_release)
self.release_container.getElements('#release_'+self.last_release.id).addClass('last_release'); self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
if(self.next_release) if(self.next_release)
self.release_container.getElements('#release_'+self.next_release.id).addClass('next_release'); self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){ if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top'); self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
var nr = self.next_release, var nr = self.next_release,
lr = self.last_release; lr = self.last_release;
@@ -385,7 +356,7 @@ MA.Release = new Class({
}, },
get: function(release, type){ get: function(release, type){
return release.info[type] !== undefined ? release.info[type] : 'n/a' return release.info[type] || 'n/a'
}, },
download: function(release){ download: function(release){
@@ -394,25 +365,21 @@ MA.Release = new Class({
var release_el = self.release_container.getElement('#release_'+release.id), var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon2'); icon = release_el.getElement('.download.icon2');
if(icon) icon.addClass('icon spinner').removeClass('download');
icon.addClass('icon spinner').removeClass('download');
Api.request('release.manual_download', { Api.request('release.download', {
'data': { 'data': {
'id': release.id 'id': release.id
}, },
'onComplete': function(json){ 'onComplete': function(json){
if(icon) icon.removeClass('icon spinner');
icon.removeClass('icon spinner');
if(json.success){ if(json.success){
if(icon) icon.addClass('completed');
icon.addClass('completed');
release_el.getElement('.release_status').set('text', 'snatched'); release_el.getElement('.release_status').set('text', 'snatched');
} }
else else
if(icon) icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
} }
}); });
}, },
@@ -424,6 +391,17 @@ MA.Release = new Class({
'data': { 'data': {
'id': release.id 'id': release.id
}, },
'onComplete': function(){
var el = release.el;
if(el.hasClass('failed') || el.hasClass('ignored')){
el.removeClass('failed').removeClass('ignored');
el.getElement('.release_status').set('text', 'available');
}
else {
el.addClass('ignored');
el.getElement('.release_status').set('text', 'ignored');
}
}
}) })
}, },
@@ -431,7 +409,7 @@ MA.Release = new Class({
markMovieDone: function(){ markMovieDone: function(){
var self = this; var self = this;
Api.request('media.delete', { Api.request('movie.delete', {
'data': { 'data': {
'id': self.movie.get('id'), 'id': self.movie.get('id'),
'delete_from': 'wanted' 'delete_from': 'wanted'
@@ -450,7 +428,7 @@ MA.Release = new Class({
}, },
tryNextRelease: function(){ tryNextRelease: function(movie_id){
var self = this; var self = this;
Api.request('movie.searcher.try_next', { Api.request('movie.searcher.try_next', {
@@ -710,7 +688,7 @@ MA.Refresh = new Class({
var self = this; var self = this;
(e).preventDefault(); (e).preventDefault();
Api.request('media.refresh', { Api.request('movie.refresh', {
'data': { 'data': {
'id': self.movie.get('id') 'id': self.movie.get('id')
} }
@@ -821,7 +799,7 @@ MA.Delete = new Class({
self.callChain(); self.callChain();
}, },
function(){ function(){
Api.request('media.delete', { Api.request('movie.delete', {
'data': { 'data': {
'id': self.movie.get('id'), 'id': self.movie.get('id'),
'delete_from': self.movie.list.options.identifier 'delete_from': self.movie.list.options.identifier
@@ -419,25 +419,22 @@
} }
.movies .data .quality .available, .movies .data .quality .available,
.movies .data .quality .snatched, .movies .data .quality .snatched {
.movies .data .quality .seeding {
opacity: 1; opacity: 1;
cursor: pointer; cursor: pointer;
} }
.movies .data .quality .available { background-color: #578bc3; } .movies .data .quality .available { background-color: #578bc3; }
.movies .data .quality .failed, .movies .data .quality .failed { background-color: #a43d34; }
.movies .data .quality .missing,
.movies .data .quality .ignored { background-color: #a43d34; }
.movies .data .quality .snatched { background-color: #a2a232; } .movies .data .quality .snatched { background-color: #a2a232; }
.movies .data .quality .seeding { background-color: #0a6819; }
.movies .data .quality .done { .movies .data .quality .done {
background-color: #369545; background-color: #369545;
opacity: 1; opacity: 1;
} }
.movies .data .quality .seeding { background-color: #0a6819; }
.movies .data .quality .finish { .movies .data .quality .finish {
background-image: url('../../images/sprite.png'); background-image: url('../images/sprite.png');
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 0 2px; background-position: 0 2px;
padding-left: 14px; padding-left: 14px;
background-size: 14px background-size: 14px
@@ -649,7 +646,7 @@
margin-top: 25px; margin-top: 25px;
} }
} }
.trailer_container.hide { .trailer_container.hide {
height: 0 !important; height: 0 !important;
} }
@@ -992,7 +989,7 @@
} }
.movies .empty_wanted { .movies .empty_wanted {
background-image: url('../../images/emptylist.png'); background-image: url('../images/emptylist.png');
background-position: 80% 0; background-position: 80% 0;
height: 750px; height: 750px;
width: 100%; width: 100%;
@@ -1032,11 +1029,11 @@
.movies .progress > div .folder { .movies .progress > div .folder {
display: inline-block; display: inline-block;
padding: 5px 20px 5px 0; padding: 5px 20px 5px 0;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
width: 85%; width: 85%;
direction: ltr; direction: rtl;
vertical-align: middle; vertical-align: middle;
} }
@@ -23,49 +23,23 @@ var Movie = new Class({
addEvents: function(){ addEvents: function(){
var self = this; var self = this;
self.global_events = {} App.addEvent('movie.update.'+self.data.id, function(notification){
// Do refresh with new data
self.global_events['movie.update'] = function(notification){
if(self.data.id != notification.data.id) return;
self.busy(false); self.busy(false);
self.removeView(); self.removeView();
self.update.delay(2000, self, notification); self.update.delay(2000, self, notification);
} });
App.on('movie.update', self.global_events['movie.update']);
// Add spinner on load / search
['movie.busy', 'movie.searcher.started'].each(function(listener){ ['movie.busy', 'movie.searcher.started'].each(function(listener){
self.global_events[listener] = function(notification){ App.addEvent(listener+'.'+self.data.id, function(notification){
if(notification.data && self.data.id == notification.data.id) if(notification.data)
self.busy(true) self.busy(true)
} });
App.on(listener, self.global_events[listener]);
}) })
// Remove spinner App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){
self.global_events['movie.searcher.ended'] = function(notification){ if(notification.data)
if(notification.data && self.data.id == notification.data.id)
self.busy(false) self.busy(false)
} });
App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
// Reload when releases have updated
self.global_events['release.update_status'] = function(notification){
var data = notification.data
if(data && self.data.id == data.media_id){
if(!self.data.releases)
self.data.releases = [];
self.data.releases.push({'quality_id': data.quality_id, 'status_id': data.status_id});
self.updateReleases();
}
}
App.on('release.update_status', self.global_events['release.update_status']);
}, },
destroy: function(){ destroy: function(){
@@ -78,9 +52,10 @@ var Movie = new Class({
self.list.checkIfEmpty(); self.list.checkIfEmpty();
// Remove events // Remove events
Object.each(self.global_events, function(handle, listener){ App.removeEvents('movie.update.'+self.data.id);
App.off(listener, handle); ['movie.busy', 'movie.searcher.started'].each(function(listener){
}); App.removeEvents(listener+'.'+self.data.id);
})
}, },
busy: function(set_busy, timeout){ busy: function(set_busy, timeout){
@@ -204,7 +179,21 @@ var Movie = new Class({
}); });
// Add releases // Add releases
self.updateReleases(); if(self.data.releases)
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_id'+ release.quality_id),
status = Status.get(release.status_id);
if(!q && (status.identifier == 'snatched' || status.identifier == 'done'))
var q = self.addQuality(release.quality_id)
if (status && q && !q.hasClass(status.identifier)){
q.addClass(status.identifier);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
}
});
Object.each(self.options.actions, function(action, key){ Object.each(self.options.actions, function(action, key){
self.action[key.toLowerCase()] = action = new self.options.actions[key](self) self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
@@ -214,26 +203,6 @@ var Movie = new Class({
}, },
updateReleases: function(){
var self = this;
if(!self.data.releases || self.data.releases.length == 0) return;
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_id'+ release.quality_id),
status = Status.get(release.status_id);
if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done'))
var q = self.addQuality(release.quality_id)
if (status && q && !q.hasClass(status.identifier)){
q.addClass(status.identifier);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
}
});
},
addQuality: function(quality_id){ addQuality: function(quality_id){
var self = this; var self = this;
@@ -287,8 +256,7 @@ var Movie = new Class({
self.el.removeEvents('outerClick') self.el.removeEvents('outerClick')
setTimeout(function(){ setTimeout(function(){
if(self.el) self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
}, 600); }, 600);
self.data_container.removeClass('hide_right'); self.data_container.removeClass('hide_right');
@@ -298,10 +266,9 @@ var Movie = new Class({
changeView: function(new_view){ changeView: function(new_view){
var self = this; var self = this;
if(self.el) self.el
self.el .removeClass(self.view+'_view')
.removeClass(self.view+'_view') .addClass(new_view+'_view')
.addClass(new_view+'_view')
self.view = new_view; self.view = new_view;
}, },
@@ -129,13 +129,13 @@
overflow-x: hidden; overflow-x: hidden;
} }
.media_result { .movie_result {
overflow: hidden; overflow: hidden;
height: 50px; height: 50px;
position: relative; position: relative;
} }
.media_result .options { .movie_result .options {
position: absolute; position: absolute;
height: 100%; height: 100%;
top: 0; top: 0;
@@ -147,48 +147,48 @@
border-radius: 0; border-radius: 0;
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25); box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
} }
.media_result .options > .in_library_wanted { .movie_result .options > .in_library_wanted {
margin-top: -7px; margin-top: -7px;
} }
.media_result .options > div { .movie_result .options > div {
border: 0; border: 0;
} }
.media_result .options .thumbnail { .movie_result .options .thumbnail {
vertical-align: middle; vertical-align: middle;
} }
.media_result .options select { .movie_result .options select {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
} }
.media_result .options select[name=title] { width: 170px; } .movie_result .options select[name=title] { width: 170px; }
.media_result .options select[name=profile] { width: 90px; } .movie_result .options select[name=profile] { width: 90px; }
.media_result .options select[name=category] { width: 80px; } .movie_result .options select[name=category] { width: 80px; }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.media_result .options select[name=title] { width: 90px; } .movie_result .options select[name=title] { width: 90px; }
.media_result .options select[name=profile] { width: 50px; } .movie_result .options select[name=profile] { width: 50px; }
.media_result .options select[name=category] { width: 50px; } .movie_result .options select[name=category] { width: 50px; }
} }
.media_result .options .button { .movie_result .options .button {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
} }
.media_result .options .message { .movie_result .options .message {
height: 100%; height: 100%;
font-size: 20px; font-size: 20px;
color: #fff; color: #fff;
line-height: 20px; line-height: 20px;
} }
.media_result .data { .movie_result .data {
position: absolute; position: absolute;
height: 100%; height: 100%;
top: 0; top: 0;
@@ -199,20 +199,20 @@
border-top: 1px solid rgba(255,255,255, 0.08); border-top: 1px solid rgba(255,255,255, 0.08);
transition: all .4s cubic-bezier(0.9,0,0.1,1); transition: all .4s cubic-bezier(0.9,0,0.1,1);
} }
.media_result .data.open { .movie_result .data.open {
left: 100% !important; left: 100% !important;
} }
.media_result:last-child .data { border-bottom: 0; } .movie_result:last-child .data { border-bottom: 0; }
.media_result .in_wanted, .media_result .in_library { .movie_result .in_wanted, .movie_result .in_library {
position: absolute; position: absolute;
bottom: 2px; bottom: 2px;
left: 14px; left: 14px;
font-size: 11px; font-size: 11px;
} }
.media_result .thumbnail { .movie_result .thumbnail {
width: 34px; width: 34px;
min-height: 100%; min-height: 100%;
display: block; display: block;
@@ -220,7 +220,7 @@
vertical-align: top; vertical-align: top;
} }
.media_result .info { .movie_result .info {
position: absolute; position: absolute;
top: 20%; top: 20%;
left: 15px; left: 15px;
@@ -228,7 +228,7 @@
vertical-align: middle; vertical-align: middle;
} }
.media_result .info h2 { .movie_result .info h2 {
margin: 0; margin: 0;
font-weight: normal; font-weight: normal;
font-size: 20px; font-size: 20px;
@@ -240,7 +240,7 @@
width: 100%; width: 100%;
} }
.media_result .info h2 .title { .movie_result .info h2 .title {
display: block; display: block;
margin: 0; margin: 0;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -253,7 +253,7 @@
width: 88%; width: 88%;
} }
.media_result .info h2 .year { .movie_result .info h2 .year {
padding: 0 5px; padding: 0 5px;
text-align: center; text-align: center;
position: absolute; position: absolute;
@@ -271,7 +271,7 @@
} }
.search_form .mask, .search_form .mask,
.media_result .mask { .movie_result .mask {
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;
@@ -1,4 +1,189 @@
Block.Search.MovieItem = new Class({ Block.Search = new Class({
Extends: BlockBase,
cache: {},
create: function(){
var self = this;
var focus_timer = 0;
self.el = new Element('div.search_form').adopt(
new Element('div.input').adopt(
self.input = new Element('input', {
'placeholder': 'Search & add a new movie',
'events': {
'keyup': self.keyup.bind(self),
'focus': function(){
if(focus_timer) clearTimeout(focus_timer);
self.el.addClass('focused')
if(this.get('value'))
self.hideResults(false)
},
'blur': function(){
focus_timer = (function(){
self.el.removeClass('focused')
}).delay(100);
}
}
}),
new Element('a.icon2', {
'events': {
'click': self.clear.bind(self),
'touchend': self.clear.bind(self)
}
})
),
self.result_container = new Element('div.results_container', {
'tween': {
'duration': 200
},
'events': {
'mousewheel': function(e){
(e).stopPropagation();
}
}
}).adopt(
self.results = new Element('div.results')
)
);
self.mask = new Element('div.mask').inject(self.result_container).fade('hide');
},
clear: function(e){
var self = this;
(e).preventDefault();
if(self.last_q === ''){
self.input.blur()
self.last_q = null;
}
else {
self.last_q = '';
self.input.set('value', '');
self.input.focus()
self.movies = []
self.results.empty()
self.el.removeClass('filled')
}
},
hideResults: function(bool){
var self = this;
if(self.hidden == bool) return;
self.el[bool ? 'removeClass' : 'addClass']('shown');
if(bool){
History.removeEvent('change', self.hideResults.bind(self, !bool));
self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool));
}
else {
History.addEvent('change', self.hideResults.bind(self, !bool));
self.el.addEvent('outerClick', self.hideResults.bind(self, !bool));
}
self.hidden = bool;
},
keyup: function(e){
var self = this;
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
if(self.q() != self.last_q){
if(self.api_request && self.api_request.isRunning())
self.api_request.cancel();
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
self.autocomplete_timer = self.autocomplete.delay(300, self)
}
},
autocomplete: function(){
var self = this;
if(!self.q()){
self.hideResults(true)
return
}
self.list()
},
list: function(){
var self = this,
q = self.q(),
cache = self.cache[q];
self.hideResults(false);
if(!cache){
self.mask.fade('in');
if(!self.spinner)
self.spinner = createSpinner(self.mask);
self.api_request = Api.request('movie.search', {
'data': {
'q': q
},
'onComplete': self.fill.bind(self, q)
})
}
else
self.fill(q, cache)
self.last_q = q;
},
fill: function(q, json){
var self = this;
self.cache[q] = json
self.movies = {}
self.results.empty()
Object.each(json.movies, function(movie){
var m = new Block.Search.Item(movie);
$(m).inject(self.results)
self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
if(q == movie.imdb)
m.showOptions()
});
// Calculate result heights
var w = window.getSize(),
rc = self.result_container.getCoordinates();
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
self.mask.fade('out')
},
loading: function(bool){
this.el[bool ? 'addClass' : 'removeClass']('loading')
},
q: function(){
return this.input.get('value').trim();
}
});
Block.Search.Item = new Class({
Implements: [Options, Events], Implements: [Options, Events],
@@ -16,7 +201,7 @@ Block.Search.MovieItem = new Class({
var self = this, var self = this,
info = self.info; info = self.info;
self.el = new Element('div.media_result', { self.el = new Element('div.movie_result', {
'id': info.imdb 'id': info.imdb
}).adopt( }).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', { self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
@@ -107,7 +292,7 @@ Block.Search.MovieItem = new Class({
self.options_el.empty(); self.options_el.empty();
self.options_el.adopt( self.options_el.adopt(
new Element('div.message', { new Element('div.message', {
'text': json.success ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs' 'text': json.added ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs'
}) })
); );
self.mask.fade('out'); self.mask.fade('out');
@@ -181,7 +366,7 @@ Block.Search.MovieItem = new Class({
if(categories.length == 0) if(categories.length == 0)
self.category_select.hide(); self.category_select.hide();
else { else {
self.category_select.movie(); self.category_select.show();
categories.each(function(category){ categories.each(function(category){
new Element('option', { new Element('option', {
'value': category.data.id, 'value': category.data.id,
@@ -2,7 +2,7 @@ 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.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.library.base import LibraryBase from couchpotato.core.media._base.library import LibraryBase
from couchpotato.core.settings.model import Library, LibraryTitle, File from couchpotato.core.settings.model import Library, LibraryTitle, File
from string import ascii_letters from string import ascii_letters
import time import time
@@ -16,46 +16,27 @@ class MovieLibraryPlugin(LibraryBase):
default_dict = {'titles': {}, 'files':{}} default_dict = {'titles': {}, 'files':{}}
def __init__(self): def __init__(self):
addEvent('library.query', self.query)
addEvent('library.add.movie', self.add) addEvent('library.add.movie', self.add)
addEvent('library.update.movie', self.update) addEvent('library.update.movie', self.update)
addEvent('library.update.movie.release_date', self.updateReleaseDate) addEvent('library.update.movie.release_date', self.updateReleaseDate)
def query(self, library, first = True, include_year = True, **kwargs): def add(self, attrs = None, update_after = True):
if library.get('type') != 'movie': if not attrs: attrs = {}
return
titles = [title['title'] for title in library['titles']]
# Add year identifier to titles
if include_year:
titles = [title + (' %s' % str(library['year'])) for title in titles]
if first:
return titles[0] if titles else None
return titles
def add(self, attrs = {}, update_after = True):
# movies don't yet contain these, so lets make sure to set defaults
type = attrs.get('type', 'movie')
primary_provider = attrs.get('primary_provider', 'imdb') primary_provider = attrs.get('primary_provider', 'imdb')
db = get_session() db = get_session()
l = db.query(Library).filter_by(type = type, identifier = attrs.get('identifier')).first() l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
if not l: if not l:
status = fireEvent('status.get', 'needs_update', single = True) status = fireEvent('status.get', 'needs_update', single = True)
l = Library( l = Library(
type = type,
primary_provider = primary_provider,
year = attrs.get('year'), year = attrs.get('year'),
identifier = attrs.get('identifier'), identifier = attrs.get('identifier'),
plot = toUnicode(attrs.get('plot')), plot = toUnicode(attrs.get('plot')),
tagline = toUnicode(attrs.get('tagline')), tagline = toUnicode(attrs.get('tagline')),
status_id = status.get('id'), status_id = status.get('id'),
info = {}, info = {}
parent = None
) )
title = LibraryTitle( title = LibraryTitle(
@@ -170,7 +151,7 @@ class MovieLibraryPlugin(LibraryBase):
else: else:
dates = library.info.get('release_date') dates = library.info.get('release_date')
if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates: if dates and dates.get('expires', 0) < time.time() or not dates:
dates = fireEvent('movie.release_date', identifier = identifier, merge = True) dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
library.info.update({'release_date': dates }) library.info.update({'release_date': dates })
db.commit() db.commit()
+129 -41
View File
@@ -1,14 +1,16 @@
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, fireEventAsync
from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss
from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
possibleTitles, getImdb
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.media.movie import MovieTypeBase from couchpotato.core.media.movie import MovieTypeBase
from couchpotato.core.settings.model import Media, Release from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env from couchpotato.environment import Env
from datetime import date from datetime import date
from sqlalchemy.exc import InterfaceError
import random import random
import re import re
import time import time
@@ -27,9 +29,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
addEvent('movie.searcher.all', self.searchAll) addEvent('movie.searcher.all', self.searchAll)
addEvent('movie.searcher.all_view', self.searchAllView) addEvent('movie.searcher.all_view', self.searchAllView)
addEvent('movie.searcher.single', self.single) addEvent('movie.searcher.single', self.single)
addEvent('movie.searcher.correct_movie', self.correctMovie)
addEvent('movie.searcher.try_next_release', self.tryNextRelease) addEvent('movie.searcher.try_next_release', self.tryNextRelease)
addEvent('movie.searcher.could_be_released', self.couldBeReleased) addEvent('movie.searcher.could_be_released', self.couldBeReleased)
addEvent('searcher.correct_release', self.correctRelease)
addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = { addApiView('movie.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',
@@ -72,8 +74,8 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
db = get_session() db = get_session()
movies = db.query(Media).filter( movies = db.query(Movie).filter(
Media.status.has(identifier = 'active') Movie.status.has(identifier = 'active')
).all() ).all()
random.shuffle(movies) random.shuffle(movies)
@@ -115,10 +117,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
def single(self, movie, search_protocols = None, manual = False): def single(self, movie, search_protocols = None, manual = False):
# movies don't contain 'type' yet, so just set to default here
if not movie.has_key('type'):
movie['type'] = 'movie'
# Find out search type # Find out search type
try: try:
if not search_protocols: if not search_protocols:
@@ -144,10 +142,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
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.')
fireEvent('media.delete', movie['id'], single = True) fireEvent('movie.delete', movie['id'], single = True)
return return
fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title) fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
ret = False ret = False
@@ -169,20 +167,82 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
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 = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or [] results = []
if len(results) == 0: for search_protocol in search_protocols:
protocol_results = fireEvent('provider.search.%s.movie' % search_protocol, movie, quality, merge = True)
if protocol_results:
results += protocol_results
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
if len(sorted_results) == 0:
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label'])) log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
download_preference = self.conf('preferred_method', section = 'searcher')
if download_preference != 'both':
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
# Check if movie isn't deleted while searching # Check if movie isn't deleted while searching
if not db.query(Media).filter_by(id = movie.get('id')).first(): if not db.query(Movie).filter_by(id = movie.get('id')).first():
break break
# Add them to this movie releases list # Add them to this movie releases list
found_releases += fireEvent('release.create_from_search', results, movie, quality_type, single = True) for nzb in sorted_results:
# Try find a valid result and download it nzb_identifier = md5(nzb['url'])
if fireEvent('release.try_download_result', results, movie, quality_type, manual, single = True): found_releases.append(nzb_identifier)
ret = True
rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
if not rls:
rls = Release(
identifier = nzb_identifier,
movie_id = movie.get('id'),
quality_id = quality_type.get('quality_id'),
status_id = available_status.get('id')
)
db.add(rls)
else:
[db.delete(old_info) for old_info in rls.info]
rls.last_edit = int(time.time())
db.commit()
for info in nzb:
try:
if not isinstance(nzb[info], (str, unicode, int, long, float)):
continue
rls_info = ReleaseInfo(
identifier = info,
value = toUnicode(nzb[info])
)
rls.info.append(rls_info)
except InterfaceError:
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
db.commit()
nzb['status_id'] = rls.status_id
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'] in [ignored_status.get('id'), failed_status.get('id')]:
log.info('Ignored: %s', nzb['name'])
continue
if nzb['score'] <= 0:
log.info('Ignored, score to low: %s', nzb['name'])
continue
downloaded = fireEvent('searcher.download', data = nzb, movie = movie, manual = manual, single = True)
if downloaded is True:
ret = True
break
elif downloaded != 'try_next':
break
# Remove releases that aren't found anymore # Remove releases that aren't found anymore
for release in movie.get('releases', []): for release in movie.get('releases', []):
@@ -191,7 +251,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
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('media.restatus', movie['id']) fireEvent('movie.restatus', movie['id'])
break break
# Break if CP wants to shut down # Break if CP wants to shut down
@@ -201,15 +261,11 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
if len(too_early_to_search) > 0: if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['id']}) fireEvent('notify.frontend', type = 'movie.searcher.ended.%s' % movie['id'], data = True)
return ret return ret
def correctRelease(self, nzb = None, media = None, quality = None, **kwargs): def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
if media.get('type') != 'movie': return
media_title = fireEvent('library.title', media['library'], single = True)
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')
@@ -218,14 +274,50 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name'])) log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
return False return False
# Check for required and ignored words movie_name = getTitle(movie['library'])
if not fireEvent('searcher.correct_words', nzb['name'], media, single = True): movie_words = re.split('\W+', simplifyString(movie_name))
nzb_name = simplifyString(nzb['name'])
nzb_words = re.split('\W+', nzb_name)
# Make sure it has required words
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
try: required_words = list(set(required_words + splitString(movie['category']['required'].lower())))
except: pass
req_match = 0
for req_set in required_words:
req = splitString(req_set, '&')
req_match += len(list(set(nzb_words) & set(req))) == len(req)
if len(required_words) > 0 and req_match == 0:
log.info2('Wrong: Required word missing: %s', nzb['name'])
return False
# Ignore releases
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
try: ignored_words = list(set(ignored_words + splitString(movie['category']['ignored'].lower())))
except: pass
ignored_match = 0
for ignored_set in ignored_words:
ignored = splitString(ignored_set, '&')
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
if len(ignored_words) > 0 and ignored_match:
log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name']))
return False
# Ignore porn stuff
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic', 'cock', 'dick']
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
if pron_words:
log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False return False
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 fireEvent('searcher.contains_other_quality', nzb, movie_year = media['library']['year'], preferred_quality = preferred_quality, single = True): if fireEvent('searcher.contains_other_quality', nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single = True):
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label'])) log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
return False return False
@@ -255,23 +347,23 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
return True return True
# Check if nzb contains imdb link # Check if nzb contains imdb link
if getImdb(nzb.get('description', '')) == media['library']['identifier']: if getImdb(nzb.get('description', '')) == movie['library']['identifier']:
return True return True
for raw_title in media['library']['titles']: for raw_title in movie['library']['titles']:
for movie_title in possibleTitles(raw_title['title']): for movie_title in possibleTitles(raw_title['title']):
movie_words = re.split('\W+', simplifyString(movie_title)) movie_words = re.split('\W+', simplifyString(movie_title))
if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True): if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
# 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 fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 1, single = True): if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 1, single = True):
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 fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 0, single = True): if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 0, single = True):
return True return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], media_title, media['library']['year'])) log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year']))
return False return False
def couldBeReleased(self, is_pre_release, dates, year = None): def couldBeReleased(self, is_pre_release, dates, year = None):
@@ -283,10 +375,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
return True return True
else: else:
# Don't allow movies with years to far in the future
if year is not None and year > now_year + 1:
return False
# For movies before 1972 # For movies before 1972
if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0: if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
return True return True
@@ -321,14 +409,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
'success': trynext 'success': trynext
} }
def tryNextRelease(self, media_id, manual = False): def tryNextRelease(self, movie_id, manual = False):
snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True) snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True)
try: try:
db = get_session() db = get_session()
rels = db.query(Release) \ rels = db.query(Release) \
.filter_by(media_id = media_id) \ .filter_by(movie_id = movie_id) \
.filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \ .filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \
.all() .all()
@@ -336,7 +424,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
rel.status_id = ignored_status.get('id') rel.status_id = ignored_status.get('id')
db.commit() db.commit()
movie_dict = fireEvent('media.get', media_id = media_id, single = True) movie_dict = fireEvent('movie.get', movie_id, single = True)
log.info('Trying next release for: %s', getTitle(movie_dict['library'])) log.info('Trying next release for: %s', getTitle(movie_dict['library']))
fireEvent('movie.searcher.single', movie_dict, manual = manual) fireEvent('movie.searcher.single', movie_dict, manual = manual)
@@ -1,160 +0,0 @@
.suggestions {
}
.suggestions > h2 {
height: 40px;
}
.suggestions .media_result {
display: inline-block;
width: 33.333%;
height: 150px;
}
@media all and (max-width: 960px) {
.suggestions .media_result {
width: 50%;
}
}
@media all and (max-width: 600px) {
.suggestions .media_result {
width: 100%;
}
}
.suggestions .media_result .data {
left: 100px;
background: #4e5969;
border: none;
}
.suggestions .media_result .data .info {
top: 10px;
left: 15px;
right: 15px;
bottom: 10px;
overflow: hidden;
}
.suggestions .media_result .data .info h2 {
white-space: normal;
max-height: 120px;
font-size: 18px;
line-height: 18px;
}
.suggestions .media_result .data .info .rating,
.suggestions .media_result .data .info .genres,
.suggestions .media_result .data .info .year {
position: static;
display: block;
padding: 0;
opacity: .6;
}
.suggestions .media_result .data .info .year {
margin: 10px 0 0;
}
.suggestions .media_result .data .info .rating {
font-size: 20px;
float: right;
margin-top: -20px;
}
.suggestions .media_result .data .info .rating:before {
content: "\e031";
font-family: 'Elusive-Icons';
font-size: 14px;
margin: 0 5px 0 0;
vertical-align: bottom;
}
.suggestions .media_result .data .info .genres {
font-size: 11px;
font-style: italic;
text-align: right;
}
.suggestions .media_result .data .info .plot {
display: block;
font-size: 11px;
overflow: hidden;
text-align: justify;
height: 100%;
z-index: 2;
top: 64px;
position: absolute;
background: #4e5969;
cursor: pointer;
transition: all .4s ease-in-out;
padding: 0 3px 10px 0;
}
.suggestions .media_result .data:before {
bottom: 0;
content: '';
display: block;
height: 10px;
right: 0;
left: 0;
bottom: 10px;
position: absolute;
background: linear-gradient(
0deg,
rgba(78, 89, 105, 1) 0%,
rgba(78, 89, 105, 0) 100%
);
z-index: 3;
pointer-events: none;
}
.suggestions .media_result .data .info .plot.full {
top: 0;
overflow: auto;
}
.suggestions .media_result .data {
cursor: default;
}
.suggestions .media_result .options {
left: 100px;
}
.suggestions .media_result .options select[name=title] { width: 100%; }
.suggestions .media_result .options select[name=profile] { width: 100%; }
.suggestions .media_result .options select[name=category] { width: 100%; }
.suggestions .media_result .button {
position: absolute;
margin: 2px 0 0 0;
right: 15px;
bottom: 15px;
}
.suggestions .media_result .thumbnail {
width: 100px;
}
.suggestions .media_result .actions {
position: absolute;
top: 10px;
right: 10px;
display: none;
width: 140px;
}
.suggestions .media_result:hover .actions {
display: block;
}
.suggestions .media_result:hover h2 .title {
opacity: 0;
}
.suggestions .media_result .data.open .actions {
display: none;
}
.suggestions .media_result .actions a {
margin-left: 10px;
vertical-align: middle;
}
@@ -1,153 +0,0 @@
var SuggestList = new Class({
Implements: [Options, Events],
initialize: function(options){
var self = this;
self.setOptions(options);
self.create();
},
create: function(){
var self = this;
self.el = new Element('div.suggestions', {
'events': {
'click:relay(a.delete)': function(e, el){
(e).stop();
$(el).getParent('.media_result').destroy();
Api.request('suggestion.ignore', {
'data': {
'imdb': el.get('data-ignore')
},
'onComplete': self.fill.bind(self)
});
},
'click:relay(a.eye-open)': function(e, el){
(e).stop();
$(el).getParent('.media_result').destroy();
Api.request('suggestion.ignore', {
'data': {
'imdb': el.get('data-seen'),
'mark_seen': 1
},
'onComplete': self.fill.bind(self)
});
}
}
}).grab(
new Element('h2', {
'text': 'You might like these'
})
);
self.api_request = Api.request('suggestion.view', {
'onComplete': self.fill.bind(self)
});
},
fill: function(json){
var self = this;
if(!json || json.count == 0){
self.el.hide();
}
else {
Object.each(json.suggestions, function(movie){
var m = new Block.Search.MovieItem(movie, {
'onAdded': function(){
self.afterAdded(m, movie)
}
});
m.data_container.grab(
new Element('div.actions').adopt(
new Element('a.add.icon2', {
'title': 'Add movie with your default quality',
'data-add': movie.imdb,
'events': {
'click': m.showOptions.bind(m)
}
}),
$(new MA.IMDB(m)),
$(new MA.Trailer(m, {
'height': 150
})),
new Element('a.delete.icon2', {
'title': 'Don\'t suggest this movie again',
'data-ignore': movie.imdb
}),
new Element('a.eye-open.icon2', {
'title': 'Seen it, like it, don\'t add',
'data-seen': movie.imdb
})
)
);
m.data_container.removeEvents('click');
var plot = false;
if(m.info.plot && m.info.plot.length > 0)
plot = m.info.plot;
// Add rating
m.info_container.adopt(
m.rating = m.info.rating && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', {
'text': parseFloat(m.info.rating.imdb[0]),
'title': parseInt(m.info.rating.imdb[1]) + ' votes'
}) : null,
m.genre = m.info.genres && m.info.genres.length > 0 ? new Element('span.genres', {
'text': m.info.genres.slice(0, 3).join(', ')
}) : null,
m.plot = plot ? new Element('span.plot', {
'text': plot,
'events': {
'click': function(){
this.toggleClass('full')
}
}
}) : null
)
$(m).inject(self.el);
});
}
self.fireEvent('loaded');
},
afterAdded: function(m, movie){
var self = this;
setTimeout(function(){
$(m).destroy();
Api.request('suggestion.ignore', {
'data': {
'imdb': movie.imdb,
'remove_only': true
},
'onComplete': self.fill.bind(self)
});
}, 3000);
},
toElement: function(){
return this.el;
}
})
@@ -1,6 +0,0 @@
from .main import ShowBase
def start():
return ShowBase()
config = []
-239
View File
@@ -1,239 +0,0 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase
from couchpotato.core.settings.model import Media
import time
log = CPLog(__name__)
class ShowBase(MediaBase):
_type = 'show'
def __init__(self):
super(ShowBase, self).__init__()
addApiView('show.add', self.addView, docs = {
'desc': 'Add new movie to the wanted list',
'params': {
'identifier': {'desc': 'IMDB id of the movie your want to add.'},
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
}
})
addEvent('show.add', self.add)
def addView(self, **kwargs):
add_dict = self.add(params = kwargs)
return {
'success': True if add_dict else False,
'show': add_dict,
}
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
"""
params
{'category_id': u'-1',
'identifier': u'tt1519931',
'profile_id': u'12',
'thetvdb_id': u'158661',
'title': u'Haven'}
"""
log.debug("show.add")
# Add show parent to db first; need to update library so maps will be in place (if any)
parent = self.addToDatabase(params = params, update_library = True, type = 'show')
# TODO: add by airdate
# Add by Season/Episode numbers
self.addBySeasonEpisode(parent,
params = params,
force_readd = force_readd,
search_after = search_after,
update_library = update_library,
status_id = status_id
)
def addBySeasonEpisode(self, parent, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
identifier = params.get('id')
# 'tvdb' will always be the master for our purpose. All mapped data can be mapped
# to another source for downloading, but it will always be remapped back to tvdb numbering
# when renamed so media can be used in media players that use tvdb for info provider
#
# This currently means the episode must actually exist in tvdb in order to be found but
# the numbering can be different
#master = 'tvdb'
#destination = 'scene'
#destination = 'anidb'
#destination = 'rage'
#destination = 'trakt'
# TODO: auto mode. if anime exists use it. if scene exists use it else use tvdb
# XXX: We should abort adding show, etc if either tvdb or xem is down or we will have incorrent mappings
# I think if tvdb gets error we wont have anydata anyway, but we must make sure XEM returns!!!!
# Only the master should return results here; all other info providers should just return False
# since we are just interested in the structure at this point.
seasons = fireEvent('season.info', merge = True, identifier = identifier)
if seasons is not None:
for season in seasons:
# Make sure we are only dealing with 'tvdb' responses at this point
if season.get('primary_provider', None) != 'thetvdb':
continue
season_id = season.get('id', None)
if season_id is None: continue
season_params = {'season_identifier': season_id}
# Calling all info providers; merge your info now for individual season
single_season = fireEvent('season.info', merge = True, identifier = identifier, params = season_params)
single_season['category_id'] = params.get('category_id')
single_season['profile_id'] = params.get('profile_id')
single_season['title'] = single_season.get('original_title', None)
single_season['identifier'] = season_id
single_season['parent_identifier'] = identifier
log.info("Adding Season %s" % season_id)
s = self.addToDatabase(params = single_season, type = "season")
episode_params = {'season_identifier': season_id}
episodes = fireEvent('episode.info', merge = True, identifier = identifier, params = episode_params)
if episodes is not None:
for episode in episodes:
# Make sure we are only dealing with 'tvdb' responses at this point
if episode.get('primary_provider', None) != 'thetvdb':
continue
episode_id = episode.get('id', None)
if episode_id is None: continue
try:
episode_number = int(episode.get('episodenumber', None))
except (ValueError, TypeError):
continue
try:
absolute_number = int(episode.get('absolute_number', None))
except (ValueError, TypeError):
absolute_number = None
episode_params = {'season_identifier': season_id,
'episode_identifier': episode_id,
'episode': episode_number}
if absolute_number:
episode_params['absolute'] = absolute_number
# Calling all info providers; merge your info now for individual episode
single_episode = fireEvent('episode.info', merge = True, identifier = identifier, params = episode_params)
single_episode['category_id'] = params.get('category_id')
single_episode['profile_id'] = params.get('profile_id')
single_episode['title'] = single_episode.get('original_title', None)
single_episode['identifier'] = episode_id
single_episode['parent_identifier'] = single_season['identifier']
log.info("Adding [%sx%s] %s - %s" % (season_id,
episode_number,
params['title'],
single_episode.get('original_title', '')))
e = self.addToDatabase(params = single_episode, type = "episode")
# Start searching now that all the media has been added
if search_after:
onComplete = self.createOnComplete(parent['id'])
onComplete()
return parent
def addToDatabase(self, params = {}, type = "show", force_readd = True, search_after = False, update_library = False, status_id = None):
log.debug("show.addToDatabase")
if not params.get('identifier'):
msg = 'Can\'t add show without imdb identifier.'
log.error(msg)
fireEvent('notify.frontend', type = 'show.is_tvshow', message = msg)
return False
#else:
#try:
#is_show = fireEvent('movie.is_show', identifier = params.get('identifier'), single = True)
#if not is_show:
#msg = 'Can\'t add show, seems to be a TV show.'
#log.error(msg)
#fireEvent('notify.frontend', type = 'show.is_tvshow', message = msg)
#return False
#except:
#pass
library = fireEvent('library.add.%s' % type, single = True, attrs = params, update_after = update_library)
if not library:
return False
# Status
status_active, snatched_status, ignored_status, done_status, downloaded_status = \
fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True)
default_profile = fireEvent('profile.default', single = True)
cat_id = params.get('category_id', None)
db = get_session()
m = db.query(Media).filter_by(library_id = library.get('id')).first()
added = True
do_search = False
if not m:
m = Media(
type = type,
library_id = library.get('id'),
profile_id = params.get('profile_id', default_profile.get('id')),
status_id = status_id if status_id else status_active.get('id'),
category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None,
)
db.add(m)
db.commit()
onComplete = None
if search_after:
onComplete = self.createOnComplete(m.id)
fireEventAsync('library.update.%s' % type, params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
search_after = False
elif force_readd:
# Clean snatched history
for release in m.releases:
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
if params.get('ignore_previous', False):
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.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None
else:
log.debug('Show already exists, not updating: %s', params)
added = False
if force_readd:
m.status_id = status_id if status_id else status_active.get('id')
m.last_edit = int(time.time())
do_search = True
db.commit()
# Remove releases
available_status = fireEvent('status.get', 'available', single = True)
for rel in m.releases:
if rel.status_id is available_status.get('id'):
db.delete(rel)
db.commit()
show_dict = m.to_dict(self.default_dict)
if do_search and search_after:
onComplete = self.createOnComplete(m.id)
onComplete()
if added:
fireEvent('notify.frontend', type = 'show.added', data = show_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
db.expire_all()
return show_dict
@@ -1,232 +0,0 @@
Block.Search.ShowItem = new Class({
Implements: [Options, Events],
initialize: function(info, options){
var self = this;
self.setOptions(options);
self.info = info;
self.alternative_titles = [];
self.create();
},
create: function(){
var self = this,
info = self.info;
self.el = new Element('div.media_result', {
'id': info.id
}).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0],
'height': null,
'width': null
}) : null,
self.options_el = new Element('div.options.inlay'),
self.data_container = new Element('div.data', {
'events': {
'click': self.showOptions.bind(self)
}
}).adopt(
self.info_container = new Element('div.info').adopt(
new Element('h2').adopt(
self.title = new Element('span.title', {
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
}),
self.year = info.year ? new Element('span.year', {
'text': info.year
}) : null
)
)
)
)
if(info.titles)
info.titles.each(function(title){
self.alternativeTitle({
'title': title
});
})
},
alternativeTitle: function(alternative){
var self = this;
self.alternative_titles.include(alternative);
},
getTitle: function(){
var self = this;
try {
return self.info.original_title ? self.info.original_title : self.info.titles[0];
}
catch(e){
return 'Unknown';
}
},
get: function(key){
return this.info[key]
},
showOptions: function(){
var self = this;
self.createOptions();
self.data_container.addClass('open');
self.el.addEvent('outerClick', self.closeOptions.bind(self))
},
closeOptions: function(){
var self = this;
self.data_container.removeClass('open');
self.el.removeEvents('outerClick')
},
add: function(e){
var self = this;
if(e)
(e).preventDefault();
self.loadingMask();
Api.request('show.add', {
'data': {
'identifier': self.info.id,
'id': self.info.id,
'type': self.info.type,
'primary_provider': self.info.primary_provider,
'title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value'),
'category_id': self.category_select.get('value')
},
'onComplete': function(json){
self.options_el.empty();
self.options_el.adopt(
new Element('div.message', {
'text': json.added ? 'Show successfully added.' : 'Show didn\'t add properly. Check logs'
})
);
self.mask.fade('out');
self.fireEvent('added');
},
'onFailure': function(){
self.options_el.empty();
self.options_el.adopt(
new Element('div.message', {
'text': 'Something went wrong, check the logs for more info.'
})
);
self.mask.fade('out');
}
});
},
createOptions: function(){
var self = this,
info = self.info;
if(!self.options_el.hasClass('set')){
if(self.info.in_library){
var in_library = [];
self.info.in_library.releases.each(function(release){
in_library.include(release.quality.label)
});
}
self.options_el.grab(
new Element('div', {
'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
}).adopt(
self.info.in_wanted && self.info.in_wanted.profile_id ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + Quality.getProfile(self.info.in_wanted.profile_id).get('label')
}) : (in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + in_library.join(', ')
}) : null),
self.title_select = new Element('select', {
'name': 'title'
}),
self.profile_select = new Element('select', {
'name': 'profile'
}),
self.category_select = new Element('select', {
'name': 'category'
}).grab(
new Element('option', {'value': -1, 'text': 'None'})
),
self.add_button = new Element('a.button', {
'text': 'Add',
'events': {
'click': self.add.bind(self)
}
})
)
);
Array.each(self.alternative_titles, function(alt){
new Element('option', {
'text': alt.title
}).inject(self.title_select)
})
// Fill categories
var categories = CategoryList.getAll();
if(categories.length == 0)
self.category_select.hide();
else {
self.category_select.show();
categories.each(function(category){
new Element('option', {
'value': category.data.id,
'text': category.data.label
}).inject(self.category_select);
});
}
// Fill profiles
var profiles = Quality.getActiveProfiles();
if(profiles.length == 1)
self.profile_select.hide();
profiles.each(function(profile){
new Element('option', {
'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select)
});
self.options_el.addClass('set');
if(categories.length == 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 &&
!(self.info.in_wanted && self.info.in_wanted.profile_id || in_library))
self.add();
}
},
loadingMask: function(){
var self = this;
self.mask = new Element('div.mask').inject(self.el).fade('hide')
createSpinner(self.mask)
self.mask.fade('in')
},
toElement: function(){
return this.el
}
});
@@ -1,6 +0,0 @@
from .main import EpisodeLibraryPlugin
def start():
return EpisodeLibraryPlugin()
config = []
@@ -1,266 +0,0 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.settings.model import EpisodeLibrary, SeasonLibrary, LibraryTitle, File
from couchpotato.core.media._base.library.base import LibraryBase
from couchpotato.core.helpers.variable import tryInt
from string import ascii_letters
import time
import traceback
log = CPLog(__name__)
class EpisodeLibraryPlugin(LibraryBase):
default_dict = {'titles': {}, 'files':{}}
def __init__(self):
addEvent('library.query', self.query)
addEvent('library.identifier', self.identifier)
addEvent('library.add.episode', self.add)
addEvent('library.update.episode', self.update)
addEvent('library.update.episode_release_date', self.updateReleaseDate)
def query(self, library, first = True, condense = True, include_identifier = True, **kwargs):
if library is list or library.get('type') != 'episode':
return
# Get the titles of the season
if not library.get('related_libraries', {}).get('season', []):
log.warning('Invalid library, unable to determine title.')
return
titles = fireEvent(
'library.query',
library['related_libraries']['season'][0],
first=False,
include_identifier=include_identifier,
condense=condense,
single=True
)
identifier = fireEvent('library.identifier', library, single = True)
# Add episode identifier to titles
if include_identifier and identifier.get('episode'):
titles = [title + ('E%02d' % identifier['episode']) for title in titles]
if first:
return titles[0] if titles else None
return titles
def identifier(self, library):
if library.get('type') != 'episode':
return
identifier = {
'season': None,
'episode': None
}
scene_map = library['info'].get('map_episode', {}).get('scene')
if scene_map:
# Use scene mappings if they are available
identifier['season'] = scene_map.get('season')
identifier['episode'] = scene_map.get('episode')
else:
# Fallback to normal season/episode numbers
identifier['season'] = library.get('season_number')
identifier['episode'] = library.get('episode_number')
# Cast identifiers to integers
# TODO this will need changing to support identifiers with trailing 'a', 'b' characters
identifier['season'] = tryInt(identifier['season'], None)
identifier['episode'] = tryInt(identifier['episode'], None)
return identifier
def add(self, attrs = {}, update_after = True):
type = attrs.get('type', 'episode')
primary_provider = attrs.get('primary_provider', 'thetvdb')
db = get_session()
parent_identifier = attrs.get('parent_identifier', None)
parent = None
if parent_identifier:
parent = db.query(SeasonLibrary).filter_by(primary_provider = primary_provider, identifier = attrs.get('parent_identifier')).first()
l = db.query(EpisodeLibrary).filter_by(type = type, identifier = attrs.get('identifier')).first()
if not l:
status = fireEvent('status.get', 'needs_update', single = True)
l = EpisodeLibrary(
type = type,
primary_provider = primary_provider,
year = attrs.get('year'),
identifier = attrs.get('identifier'),
plot = toUnicode(attrs.get('plot')),
tagline = toUnicode(attrs.get('tagline')),
status_id = status.get('id'),
info = {},
parent = parent,
season_number = tryInt(attrs.get('seasonnumber', None)),
episode_number = tryInt(attrs.get('episodenumber', None)),
absolute_number = tryInt(attrs.get('absolute_number', None))
)
title = LibraryTitle(
title = toUnicode(attrs.get('title')),
simple_title = self.simplifyTitle(attrs.get('title')),
)
l.titles.append(title)
db.add(l)
db.commit()
# Update library info
if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent
handle('library.update.episode', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
library_dict = l.to_dict(self.default_dict)
db.expire_all()
return library_dict
def update(self, identifier, default_title = '', force = False):
if self.shuttingDown():
return
db = get_session()
library = db.query(EpisodeLibrary).filter_by(identifier = identifier).first()
done_status = fireEvent('status.get', 'done', single = True)
if library:
library_dict = library.to_dict(self.default_dict)
do_update = True
parent_identifier = None
if library.parent is not None:
parent_identifier = library.parent.identifier
if library.status_id == done_status.get('id') and not force:
do_update = False
episode_params = {'season_identifier': parent_identifier,
'episode_identifier': identifier,
'episode': library.episode_number,
'absolute': library.absolute_number,}
info = fireEvent('episode.info', merge = True, params = episode_params)
# Don't need those here
try: del info['in_wanted']
except: pass
try: del info['in_library']
except: pass
if not info or len(info) == 0:
log.error('Could not update, no movie info to work with: %s', identifier)
return False
# Main info
if do_update:
library.plot = toUnicode(info.get('plot', ''))
library.tagline = toUnicode(info.get('tagline', ''))
library.year = info.get('year', 0)
library.status_id = done_status.get('id')
library.season_number = tryInt(info.get('seasonnumber', None))
library.episode_number = tryInt(info.get('episodenumber', None))
library.absolute_number = tryInt(info.get('absolute_number', None))
try:
library.last_updated = int(info.get('lastupdated'))
except:
library.last_updated = int(time.time())
library.info.update(info)
db.commit()
# Titles
[db.delete(title) for title in library.titles]
db.commit()
titles = info.get('titles', [])
log.debug('Adding titles: %s', titles)
counter = 0
for title in titles:
if not title:
continue
title = toUnicode(title)
t = LibraryTitle(
title = 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)
)
library.titles.append(t)
counter += 1
db.commit()
# Files
images = info.get('images', [])
for image_type in ['poster']:
for image in images.get(image_type, []):
if not isinstance(image, (str, unicode)):
continue
file_path = fireEvent('file.download', url = image, single = True)
if file_path:
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj)
db.commit()
break
except:
log.debug('Failed to attach to library: %s', traceback.format_exc())
library_dict = library.to_dict(self.default_dict)
db.expire_all()
return library_dict
def updateReleaseDate(self, identifier):
'''XXX: Not sure what this is for yet in relation to an episode'''
pass
#db = get_session()
#library = db.query(EpisodeLibrary).filter_by(identifier = identifier).first()
#if not library.info:
#library_dict = self.update(identifier, force = True)
#dates = library_dict.get('info', {}).get('release_date')
#else:
#dates = library.info.get('release_date')
#if dates and dates.get('expires', 0) < time.time() or not dates:
#dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
#library.info.update({'release_date': dates })
#db.commit()
#db.expire_all()
#return dates
#TODO: Add to base class
def simplifyTitle(self, title):
title = toUnicode(title)
nr_prefix = '' if title[0] in ascii_letters else '#'
title = simplifyString(title)
for prefix in ['the ']:
if prefix == title[:len(prefix)]:
title = title[len(prefix):]
break
return nr_prefix + title
@@ -1,6 +0,0 @@
from .main import SeasonLibraryPlugin
def start():
return SeasonLibraryPlugin()
config = []
@@ -1,242 +0,0 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.settings.model import SeasonLibrary, ShowLibrary, LibraryTitle, File
from couchpotato.core.media._base.library.base import LibraryBase
from couchpotato.core.helpers.variable import tryInt
from string import ascii_letters
import time
import traceback
log = CPLog(__name__)
class SeasonLibraryPlugin(LibraryBase):
default_dict = {'titles': {}, 'files':{}}
def __init__(self):
addEvent('library.query', self.query)
addEvent('library.identifier', self.identifier)
addEvent('library.add.season', self.add)
addEvent('library.update.season', self.update)
addEvent('library.update.season_release_date', self.updateReleaseDate)
def query(self, library, first = True, condense = True, include_identifier = True, **kwargs):
if library is list or library.get('type') != 'season':
return
# Get the titles of the show
if not library.get('related_libraries', {}).get('show', []):
log.warning('Invalid library, unable to determine title.')
return
titles = fireEvent(
'library.query',
library['related_libraries']['show'][0],
first=False,
condense=condense,
single=True
)
# Add season map_names if they exist
if 'map_names' in library['info']:
season_names = library['info']['map_names'].get(str(library['season_number']), {})
# Add titles from all locations
# TODO only add name maps from a specific location
for location, names in season_names.items():
titles += [name for name in names if name and name not in titles]
identifier = fireEvent('library.identifier', library, single = True)
# Add season identifier to titles
if include_identifier and identifier.get('season') is not None:
titles = [title + (' S%02d' % identifier['season']) for title in titles]
if first:
return titles[0] if titles else None
return titles
def identifier(self, library):
if library.get('type') != 'season':
return
return {
'season': tryInt(library['season_number'], None)
}
def add(self, attrs = {}, update_after = True):
type = attrs.get('type', 'season')
primary_provider = attrs.get('primary_provider', 'thetvdb')
db = get_session()
parent_identifier = attrs.get('parent_identifier', None)
parent = None
if parent_identifier:
parent = db.query(ShowLibrary).filter_by(primary_provider = primary_provider, identifier = attrs.get('parent_identifier')).first()
l = db.query(SeasonLibrary).filter_by(type = type, identifier = attrs.get('identifier')).first()
if not l:
status = fireEvent('status.get', 'needs_update', single = True)
l = SeasonLibrary(
type = type,
primary_provider = primary_provider,
year = attrs.get('year'),
identifier = attrs.get('identifier'),
plot = toUnicode(attrs.get('plot')),
tagline = toUnicode(attrs.get('tagline')),
status_id = status.get('id'),
info = {},
parent = parent,
)
title = LibraryTitle(
title = toUnicode(attrs.get('title')),
simple_title = self.simplifyTitle(attrs.get('title')),
)
l.titles.append(title)
db.add(l)
db.commit()
# Update library info
if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent
handle('library.update.season', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
library_dict = l.to_dict(self.default_dict)
db.expire_all()
return library_dict
def update(self, identifier, default_title = '', force = False):
if self.shuttingDown():
return
db = get_session()
library = db.query(SeasonLibrary).filter_by(identifier = identifier).first()
done_status = fireEvent('status.get', 'done', single = True)
if library:
library_dict = library.to_dict(self.default_dict)
do_update = True
parent_identifier = None
if library.parent is not None:
parent_identifier = library.parent.identifier
if library.status_id == done_status.get('id') and not force:
do_update = False
season_params = {'season_identifier': identifier}
info = fireEvent('season.info', merge = True, identifier = parent_identifier, params = season_params)
# Don't need those here
try: del info['in_wanted']
except: pass
try: del info['in_library']
except: pass
if not info or len(info) == 0:
log.error('Could not update, no movie info to work with: %s', identifier)
return False
# Main info
if do_update:
library.plot = toUnicode(info.get('plot', ''))
library.tagline = toUnicode(info.get('tagline', ''))
library.year = info.get('year', 0)
library.status_id = done_status.get('id')
library.season_number = tryInt(info.get('seasonnumber', None))
library.info.update(info)
db.commit()
# Titles
[db.delete(title) for title in library.titles]
db.commit()
titles = info.get('titles', [])
log.debug('Adding titles: %s', titles)
counter = 0
for title in titles:
if not title:
continue
title = toUnicode(title)
t = LibraryTitle(
title = title,
simple_title = self.simplifyTitle(title),
# XXX: default was None; so added a quick hack since we don't really need titiles for seasons anyway
#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 = True,
)
library.titles.append(t)
counter += 1
db.commit()
# Files
images = info.get('images', [])
for image_type in ['poster']:
for image in images.get(image_type, []):
if not isinstance(image, (str, unicode)):
continue
file_path = fireEvent('file.download', url = image, single = True)
if file_path:
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj)
db.commit()
break
except:
log.debug('Failed to attach to library: %s', traceback.format_exc())
library_dict = library.to_dict(self.default_dict)
db.expire_all()
return library_dict
def updateReleaseDate(self, identifier):
'''XXX: Not sure what this is for yet in relation to a tvshow'''
pass
#db = get_session()
#library = db.query(SeasonLibrary).filter_by(identifier = identifier).first()
#if not library.info:
#library_dict = self.update(identifier, force = True)
#dates = library_dict.get('info', {}).get('release_date')
#else:
#dates = library.info.get('release_date')
#if dates and dates.get('expires', 0) < time.time() or not dates:
#dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
#library.info.update({'release_date': dates })
#db.commit()
#db.expire_all()
#return dates
#TODO: Add to base class
def simplifyTitle(self, title):
title = toUnicode(title)
nr_prefix = '' if title[0] in ascii_letters else '#'
title = simplifyString(title)
for prefix in ['the ']:
if prefix == title[:len(prefix)]:
title = title[len(prefix):]
break
return nr_prefix + title
@@ -1,6 +0,0 @@
from .main import ShowLibraryPlugin
def start():
return ShowLibraryPlugin()
config = []
@@ -1,229 +0,0 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.logger import CPLog
from couchpotato.core.settings.model import ShowLibrary, LibraryTitle, File
from couchpotato.core.media._base.library.base import LibraryBase
from qcond.helpers import simplify
from qcond import QueryCondenser
from string import ascii_letters
import time
import traceback
log = CPLog(__name__)
class ShowLibraryPlugin(LibraryBase):
default_dict = {'titles': {}, 'files':{}}
def __init__(self):
self.query_condenser = QueryCondenser()
addEvent('library.query', self.query)
addEvent('library.add.show', self.add)
addEvent('library.update.show', self.update)
addEvent('library.update.show_release_date', self.updateReleaseDate)
def query(self, library, first = True, condense = True, **kwargs):
if library is list or library.get('type') != 'show':
return
titles = [title['title'] for title in library['titles']]
if condense:
# Use QueryCondenser to build a list of optimal search titles
condensed_titles = self.query_condenser.distinct(titles)
if condensed_titles:
# Use condensed titles if we got a valid result
titles = condensed_titles
else:
# Fallback to simplifying titles
titles = [simplify(title) for title in titles]
if first:
return titles[0] if titles else None
return titles
def add(self, attrs = {}, update_after = True):
type = attrs.get('type', 'show')
primary_provider = attrs.get('primary_provider', 'thetvdb')
db = get_session()
l = db.query(ShowLibrary).filter_by(type = type, identifier = attrs.get('identifier')).first()
if not l:
status = fireEvent('status.get', 'needs_update', single = True)
l = ShowLibrary(
type = type,
primary_provider = primary_provider,
year = attrs.get('year'),
identifier = attrs.get('identifier'),
plot = toUnicode(attrs.get('plot')),
tagline = toUnicode(attrs.get('tagline')),
status_id = status.get('id'),
info = {},
parent = None,
)
title = LibraryTitle(
title = toUnicode(attrs.get('title')),
simple_title = self.simplifyTitle(attrs.get('title')),
)
l.titles.append(title)
db.add(l)
db.commit()
# Update library info
if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent
handle('library.update.show', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
library_dict = l.to_dict(self.default_dict)
db.expire_all()
return library_dict
def update(self, identifier, default_title = '', force = False):
if self.shuttingDown():
return
db = get_session()
library = db.query(ShowLibrary).filter_by(identifier = identifier).first()
done_status = fireEvent('status.get', 'done', single = True)
if library:
library_dict = library.to_dict(self.default_dict)
do_update = True
info = fireEvent('show.info', merge = True, identifier = identifier)
# Don't need those here
try: del info['in_wanted']
except: pass
try: del info['in_library']
except: pass
if not info or len(info) == 0:
log.error('Could not update, no show info to work with: %s', identifier)
return False
# Main info
if do_update:
library.plot = toUnicode(info.get('plot', ''))
library.tagline = toUnicode(info.get('tagline', ''))
library.year = info.get('year', 0)
library.status_id = done_status.get('id')
library.show_status = toUnicode(info.get('status', '').lower())
library.airs_time = info.get('airs_time', None)
# Bits
days_of_week_map = {
u'Monday': 1,
u'Tuesday': 2,
u'Wednesday': 4,
u'Thursday': 8,
u'Friday': 16,
u'Saturday': 32,
u'Sunday': 64,
u'Daily': 127,
}
try:
library.airs_dayofweek = days_of_week_map.get(info.get('airs_dayofweek'))
except:
library.airs_dayofweek = 0
try:
library.last_updated = int(info.get('lastupdated'))
except:
library.last_updated = int(time.time())
library.info.update(info)
db.commit()
# Titles
[db.delete(title) for title in library.titles]
db.commit()
titles = info.get('titles', [])
log.debug('Adding titles: %s', titles)
counter = 0
for title in titles:
if not title:
continue
title = toUnicode(title)
t = LibraryTitle(
title = 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)
)
library.titles.append(t)
counter += 1
db.commit()
# Files
images = info.get('images', [])
for image_type in ['poster']:
for image in images.get(image_type, []):
if not isinstance(image, (str, unicode)):
continue
file_path = fireEvent('file.download', url = image, single = True)
if file_path:
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj)
db.commit()
break
except:
log.debug('Failed to attach to library: %s', traceback.format_exc())
library_dict = library.to_dict(self.default_dict)
db.expire_all()
return library_dict
def updateReleaseDate(self, identifier):
'''XXX: Not sure what this is for yet in relation to a show'''
pass
#db = get_session()
#library = db.query(ShowLibrary).filter_by(identifier = identifier).first()
#if not library.info:
#library_dict = self.update(identifier, force = True)
#dates = library_dict.get('info', {}).get('release_date')
#else:
#dates = library.info.get('release_date')
#if dates and dates.get('expires', 0) < time.time() or not dates:
#dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
#library.info.update({'release_date': dates })
#db.commit()
#db.expire_all()
#return dates
#TODO: Add to base class
def simplifyTitle(self, title):
title = toUnicode(title)
nr_prefix = '' if title[0] in ascii_letters else '#'
title = simplifyString(title)
for prefix in ['the ']:
if prefix == title[:len(prefix)]:
title = title[len(prefix):]
break
return nr_prefix + title
@@ -1,6 +0,0 @@
from .main import ShowMatcher
def start():
return ShowMatcher()
config = []
-127
View File
@@ -1,127 +0,0 @@
from couchpotato import CPLog
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import dictIsSubset, tryInt, toIterable
from couchpotato.core.media._base.matcher.base import MatcherBase
from couchpotato.core.providers.base import MultiProvider
log = CPLog(__name__)
class ShowMatcher(MultiProvider):
def getTypes(self):
return [Season, Episode]
class Base(MatcherBase):
# TODO come back to this later, think this could be handled better, this is starting to get out of hand....
quality_map = {
'bluray_1080p': {'resolution': ['1080p'], 'source': ['bluray']},
'bluray_720p': {'resolution': ['720p'], 'source': ['bluray']},
'bdrip_1080p': {'resolution': ['1080p'], 'source': ['BDRip']},
'bdrip_720p': {'resolution': ['720p'], 'source': ['BDRip']},
'brrip_1080p': {'resolution': ['1080p'], 'source': ['BRRip']},
'brrip_720p': {'resolution': ['720p'], 'source': ['BRRip']},
'webdl_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]},
'webdl_720p': {'resolution': ['720p'], 'source': ['webdl', ['web', 'dl']]},
'webdl_480p': {'resolution': ['480p'], 'source': ['webdl', ['web', 'dl']]},
'hdtv_720p': {'resolution': ['720p'], 'source': ['hdtv']},
'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']},
}
def __init__(self):
super(Base, self).__init__()
addEvent('%s.matcher.correct_identifier' % self.type, self.correctIdentifier)
def correct(self, chain, release, media, quality):
log.info("Checking if '%s' is valid", release['name'])
log.info2('Release parsed as: %s', chain.info)
if not fireEvent('matcher.correct_quality', chain, quality, self.quality_map, single = True):
log.info('Wrong: %s, quality does not match', release['name'])
return False
if not fireEvent('%s.matcher.correct_identifier' % self.type, chain, media):
log.info('Wrong: %s, identifier does not match', release['name'])
return False
if not fireEvent('matcher.correct_title', chain, media):
log.info("Wrong: '%s', undetermined naming.", (' '.join(chain.info['show_name'])))
return False
return True
def correctIdentifier(self, chain, media):
raise NotImplementedError()
def getChainIdentifier(self, chain):
if 'identifier' not in chain.info:
return None
identifier = self.flattenInfo(chain.info['identifier'])
# Try cast values to integers
for key, value in identifier.items():
if isinstance(value, list):
if len(value) <= 1:
value = value[0]
else:
log.warning('Wrong: identifier contains multiple season or episode values, unsupported')
return None
identifier[key] = tryInt(value, value)
return identifier
class Episode(Base):
type = 'episode'
def correctIdentifier(self, chain, media):
identifier = self.getChainIdentifier(chain)
if not identifier:
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
return False
# TODO - Parse episode ranges from identifier to determine if they are multi-part episodes
if any([x in identifier for x in ['episode_from', 'episode_to']]):
log.info2('Wrong: releases with identifier ranges are not supported yet')
return False
required = fireEvent('library.identifier', media['library'], single = True)
# TODO - Support air by date episodes
# TODO - Support episode parts
if identifier != required:
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
return False
return True
class Season(Base):
type = 'season'
def correctIdentifier(self, chain, media):
identifier = self.getChainIdentifier(chain)
if not identifier:
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
return False
# TODO - Parse episode ranges from identifier to determine if they are season packs
if any([x in identifier for x in ['episode_from', 'episode_to']]):
log.info2('Wrong: releases with identifier ranges are not supported yet')
return False
required = fireEvent('library.identifier', media['library'], single = True)
if identifier != required:
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
return False
return True
@@ -1,7 +0,0 @@
from .main import ShowSearcher
import random
def start():
return ShowSearcher()
config = []
@@ -1,189 +0,0 @@
from couchpotato import Env, get_session
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import getTitle, toIterable
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.main import SearchSetupError
from couchpotato.core.media.show._base import ShowBase
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Media
from qcond import QueryCondenser
from qcond.helpers import simplify
log = CPLog(__name__)
class ShowSearcher(Plugin):
type = ['show', 'season', 'episode']
in_progress = False
def __init__(self):
super(ShowSearcher, self).__init__()
self.query_condenser = QueryCondenser()
for type in toIterable(self.type):
addEvent('%s.searcher.single' % type, self.single)
addEvent('searcher.correct_release', self.correctRelease)
def single(self, media, search_protocols = None, manual = False):
show, season, episode = self.getLibraries(media['library'])
db = get_session()
if media['type'] == 'show':
for library in season:
# TODO ideally we shouldn't need to fetch the media for each season library here
m = db.query(Media).filter_by(library_id = library['library_id']).first()
fireEvent('season.searcher.single', m.to_dict(ShowBase.search_dict))
return
# Find out search type
try:
if not search_protocols:
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
done_status, available_status, ignored_status, failed_status = fireEvent('status.get', ['done', 'available', 'ignored', 'failed'], single = True)
if not media['profile'] or media['status_id'] == done_status.get('id'):
log.debug('Episode doesn\'t have a profile or already done, assuming in manage tab.')
return
#pre_releases = fireEvent('quality.pre_releases', single = True)
found_releases = []
too_early_to_search = []
default_title = fireEvent('library.query', media['library'], condense = False, single=True)
if not default_title:
log.error('No proper info found for episode, removing it from library to cause it from having more issues.')
#fireEvent('episode.delete', episode['id'], single = True)
return
if not show or not season:
log.error('Unable to find show or season library in database, missing required data for searching')
return
fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['id'], data = True, message = 'Searching for "%s"' % default_title)
ret = False
has_better_quality = None
for quality_type in media['profile']['types']:
# TODO check air date?
#if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
# too_early_to_search.append(quality_type['quality']['identifier'])
# continue
has_better_quality = 0
# See if better quality is available
for release in media['releases']:
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]:
has_better_quality += 1
# Don't search for quality lower then already available.
if has_better_quality is 0:
log.info('Search for %s S%02d%s in %s', (
getTitle(show),
season['season_number'],
"E%02d" % episode['episode_number'] if episode and len(episode) == 1 else "",
quality_type['quality']['label'])
)
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
if len(results) == 0:
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
# Check if movie isn't deleted while searching
if not db.query(Media).filter_by(id = media.get('id')).first():
break
# Add them to this movie releases list
found_releases += fireEvent('release.create_from_search', results, media, quality_type, single = True)
# Try find a valid result and download it
if fireEvent('release.try_download_result', results, media, quality_type, manual, single = True):
ret = True
# Remove releases that aren't found anymore
for release in media.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:
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
fireEvent('media.restatus', media['id'])
break
# Break if CP wants to shut down
if self.shuttingDown() or ret:
break
if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
elif media['type'] == 'season' and not ret and has_better_quality is 0:
# If nothing was found, start searching for episodes individually
log.info('No season pack found, starting individual episode search')
for library in episode:
# TODO ideally we shouldn't need to fetch the media for each episode library here
m = db.query(Media).filter_by(library_id = library['library_id']).first()
fireEvent('episode.searcher.single', m.to_dict(ShowBase.search_dict))
fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['id'], data = True)
return ret
def correctRelease(self, release = None, media = None, quality = None, **kwargs):
if media.get('type') not in ['season', 'episode']: return
retention = Env.setting('retention', section = 'nzb')
if release.get('seeders') is None and 0 < retention < release.get('age', 0):
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (release['age'], retention, release['name']))
return False
# Check for required and ignored words
if not fireEvent('searcher.correct_words', release['name'], media, single = True):
return False
# TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations)
match = fireEvent('matcher.match', release, media, quality, single = True)
if match:
return match.weight
return False
def getLibraries(self, library):
if 'related_libraries' not in library:
log.warning("'related_libraries' missing from media library, unable to continue searching")
return None, None, None
libraries = library['related_libraries']
# Show always collapses as there can never be any multiples
show = libraries.get('show', [])
show = show[0] if len(show) else None
# Season collapses if the subject is a season or episode
season = libraries.get('season', [])
if library['type'] in ['season', 'episode']:
season = season[0] if len(season) else None
# Episode collapses if the subject is a episode
episode = libraries.get('episode', [])
if library['type'] == 'episode':
episode = episode[0] if len(episode) else None
return show, season, episode
+2 -2
View File
@@ -1,4 +1,4 @@
config = [{ config = {
'name': 'notification_providers', 'name': 'notification_providers',
'groups': [ 'groups': [
{ {
@@ -10,4 +10,4 @@ config = [{
'options': [], 'options': [],
}, },
], ],
}] }
+1 -1
View File
@@ -17,7 +17,7 @@ class Notification(Provider):
listen_to = [ listen_to = [
'renamer.after', 'movie.snatched', 'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated', 'updater.available', 'updater.updated',
'core.message.important', 'core.message',
] ]
dont_listen_to = [] dont_listen_to = []
@@ -16,14 +16,14 @@ class Boxcar(Notification):
try: try:
message = message.strip() message = message.strip()
data = { params = {
'email': self.conf('email'), 'email': self.conf('email'),
'notification[from_screen_name]': self.default_title, 'notification[from_screen_name]': self.default_title,
'notification[message]': toUnicode(message), 'notification[message]': toUnicode(message),
'notification[from_remote_service_id]': int(time.time()), 'notification[from_remote_service_id]': int(time.time()),
} }
self.urlopen(self.url, data = data) self.urlopen(self.url, params = params)
except: except:
log.error('Check your email and added services on boxcar.io') log.error('Check your email and added services on boxcar.io')
return False return False
+3 -15
View File
@@ -21,12 +21,6 @@ class CoreNotifier(Notification):
m_lock = None m_lock = None
listen_to = [
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
'core.message', 'core.message.important',
]
def __init__(self): def __init__(self):
super(CoreNotifier, self).__init__() super(CoreNotifier, self).__init__()
@@ -127,10 +121,7 @@ class CoreNotifier(Notification):
for message in messages: for message in messages:
if message.get('time') > last_check: if message.get('time') > last_check:
message['sticky'] = True # Always sticky core messages fireEvent('core.message', message = message.get('message'), data = message)
message_type = 'core.message.important' if message.get('important') else 'core.message'
fireEvent(message_type, message = message.get('message'), data = message)
if last_check < message.get('time'): if last_check < message.get('time'):
last_check = message.get('time') last_check = message.get('time')
@@ -207,16 +198,13 @@ class CoreNotifier(Notification):
def removeListener(self, callback): def removeListener(self, callback):
self.m_lock.acquire() self.m_lock.acquire()
new_listeners = []
for list_tuple in self.listeners: for list_tuple in self.listeners:
try: try:
listener, last_id = list_tuple listener, last_id = list_tuple
if listener != callback: if listener == callback:
new_listeners.append(list_tuple) self.listeners.remove(list_tuple)
except: except:
log.debug('Failed removing listener: %s', traceback.format_exc()) log.debug('Failed removing listener: %s', traceback.format_exc())
self.listeners = new_listeners
self.m_lock.release() self.m_lock.release()
def cleanMessages(self): def cleanMessages(self):
@@ -10,8 +10,8 @@ var NotificationBase = new Class({
// Listener // Listener
App.addEvent('unload', self.stopPoll.bind(self)); App.addEvent('unload', self.stopPoll.bind(self));
App.addEvent('reload', self.startInterval.bind(self, [true])); App.addEvent('reload', self.startInterval.bind(self, [true]));
App.on('notification', self.notify.bind(self)); App.addEvent('notification', self.notify.bind(self));
App.on('message', self.showMessage.bind(self)); App.addEvent('message', self.showMessage.bind(self));
// Add test buttons to settings page // Add test buttons to settings page
App.addEvent('load', self.addTestButtons.bind(self)); App.addEvent('load', self.addTestButtons.bind(self));
@@ -50,9 +50,9 @@ var NotificationBase = new Class({
, 'top'); , 'top');
self.notifications.include(result); self.notifications.include(result);
if((result.data.important !== undefined || result.data.sticky !== undefined) && !result.read){ if(result.data.important !== undefined && !result.read){
var sticky = true var sticky = true
App.trigger('message', [result.message, sticky, result]) App.fireEvent('message', [result.message, sticky, result])
} }
else if(!result.read){ else if(!result.read){
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length) self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
@@ -147,7 +147,7 @@ var NotificationBase = new Class({
// Process data // Process data
if(json){ if(json){
Array.each(json.result, function(result){ Array.each(json.result, function(result){
App.trigger(result.type, result); App.fireEvent(result.type, result);
if(result.message && result.read === undefined) if(result.message && result.read === undefined)
self.showMessage(result.message); self.showMessage(result.message);
}) })
@@ -28,23 +28,12 @@ config = [{
'name': 'smtp_server', 'name': 'smtp_server',
'label': 'SMTP server', 'label': 'SMTP server',
}, },
{ 'name': 'smtp_port',
'label': 'SMTP server port',
'default': '25',
'type': 'int',
},
{ {
'name': 'ssl', 'name': 'ssl',
'label': 'Enable SSL', 'label': 'Enable SSL',
'default': 0, 'default': 0,
'type': 'bool', 'type': 'bool',
}, },
{
'name': 'starttls',
'label': 'Enable StartTLS',
'default': 0,
'type': 'bool',
},
{ {
'name': 'smtp_user', 'name': 'smtp_user',
'label': 'SMTP user', 'label': 'SMTP user',
+1 -15
View File
@@ -2,9 +2,7 @@ from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString 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 couchpotato.environment import Env
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
import smtplib import smtplib
import traceback import traceback
@@ -23,30 +21,18 @@ class Email(Notification):
smtp_server = self.conf('smtp_server') smtp_server = self.conf('smtp_server')
smtp_user = self.conf('smtp_user') smtp_user = self.conf('smtp_user')
smtp_pass = self.conf('smtp_pass') smtp_pass = self.conf('smtp_pass')
smtp_port = self.conf('smtp_port')
starttls = self.conf('starttls')
# Make the basic message # Make the basic message
message = MIMEText(toUnicode(message), _charset = Env.get('encoding')) message = MIMEText(toUnicode(message))
message['Subject'] = self.default_title message['Subject'] = self.default_title
message['From'] = from_address message['From'] = from_address
message['To'] = to_address message['To'] = to_address
message['Date'] = formatdate(localtime = 1)
message['Message-ID'] = make_msgid()
try: try:
# Open the SMTP connection, via SSL if requested # Open the SMTP connection, via SSL if requested
log.debug("Connecting to host %s on port %s" % (smtp_server, smtp_port))
log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled")) 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) mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server)
if (starttls):
log.debug("Using StartTLS to initiate the connection with the SMTP server")
mailserver.starttls()
# Say hello to the server
mailserver.ehlo()
# Check too see if an login attempt should be attempted # Check too see if an login attempt should be attempted
if len(smtp_user) > 0: 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 "")) log.debug("Logging on to SMTP server using username \'%s\'%s", (smtp_user, " and a password" if len(smtp_pass) > 0 else ""))
@@ -1,15 +1,16 @@
from .main import Pushbullet from .main import Notifo
def start(): def start():
return Pushbullet() return Notifo()
config = [{ config = [{
'name': 'pushbullet', 'name': 'notifo',
'groups': [ 'groups': [
{ {
'tab': 'notifications', 'tab': 'notifications',
'list': 'notification_providers', 'list': 'notification_providers',
'name': 'pushbullet', 'name': 'notifo',
'description': 'Keep in mind that Notifo service will end soon.',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@@ -17,14 +18,10 @@ config = [{
'type': 'enabler', 'type': 'enabler',
}, },
{ {
'name': 'api_key', 'name': 'username',
'label': 'User API Key'
}, },
{ {
'name': 'devices', 'name': 'api_key',
'default': '',
'advanced': True,
'description': 'IDs of devices to send notifications to, empty = all devices'
}, },
{ {
'name': 'on_snatch', 'name': 'on_snatch',
@@ -0,0 +1,39 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import base64
import json
import traceback
log = CPLog(__name__)
class Notifo(Notification):
url = 'https://api.notifo.com/v1/send_notification'
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
try:
params = {
'label': self.default_title,
'msg': toUnicode(message),
}
headers = {
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('api_key')))[:-1]
}
handle = self.urlopen(self.url, params = params, headers = headers)
result = json.loads(handle)
if result['status'] != 'success' or result['response_message'] != 'OK':
raise Exception
except:
log.error('Notification failed: %s', traceback.format_exc())
return False
log.info('Notifo notification successful.')
return True
+3 -8
View File
@@ -17,15 +17,10 @@ config = [{
'type': 'enabler', 'type': 'enabler',
}, },
{ {
'name': 'media_server', 'name': 'host',
'label': 'Media Server',
'default': 'localhost', 'default': 'localhost',
'description': 'Hostname/IP, default localhost' 'description': 'Default should be on localhost',
}, 'advanced': True,
{
'name': 'clients',
'default': '',
'description': 'Comma separated list of client names\'s (computer names). Top right when you start Plex'
}, },
{ {
'name': 'on_snatch', 'name': 'on_snatch',
@@ -1,85 +0,0 @@
import json
from couchpotato import CPLog
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode
import requests
log = CPLog(__name__)
class PlexClientProtocol(object):
def __init__(self, plex):
self.plex = plex
addEvent('notify.plex.notifyClient', self.notify)
def notify(self, client, message):
raise NotImplementedError()
class PlexClientHTTP(PlexClientProtocol):
def request(self, command, client):
url = 'http://%s:%s/xbmcCmds/xbmcHttp/?%s' % (
client['address'],
client['port'],
tryUrlencode(command)
)
headers = {}
try:
self.plex.urlopen(url, headers = headers, timeout = 3, show_error = False)
except Exception, err:
log.error("Couldn't sent command to Plex: %s", err)
return False
return True
def notify(self, client, message):
if client.get('protocol') != 'xbmchttp':
return None
data = {
'command': 'ExecBuiltIn',
'parameter': 'Notification(CouchPotato, %s)' % message
}
return self.request(data, client)
class PlexClientJSON(PlexClientProtocol):
def request(self, method, params, client):
log.debug('sendJSON("%s", %s, %s)', (method, params, client))
url = 'http://%s:%s/jsonrpc' % (
client['address'],
client['port']
)
headers = {
'Content-Type': 'application/json'
}
request = {
'id': 1,
'jsonrpc': '2.0',
'method': method,
'params': params
}
try:
requests.post(url, headers = headers, timeout = 3, data = json.dumps(request))
except Exception, err:
log.error("Couldn't sent command to Plex: %s", err)
return False
return True
def notify(self, client, message):
if client.get('protocol') not in ['xbmcjson', 'plex']:
return None
params = {
'title': 'CouchPotato',
'message': message
}
return self.request('GUI.ShowNotification', params, client)
+72 -42
View File
@@ -1,64 +1,78 @@
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost, 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 .client import PlexClientHTTP, PlexClientJSON from urllib2 import URLError
from .server import PlexServer from urlparse import urlparse
from xml.dom import minidom
import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Plex(Notification): class Plex(Notification):
http_time_between_calls = 0
def __init__(self): def __init__(self):
super(Plex, self).__init__() super(Plex, self).__init__()
self.server = PlexServer(self)
self.client_protocols = {
'http': PlexClientHTTP(self),
'json': PlexClientJSON(self)
}
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, message = None, group = None):
def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
if not group: group = {}
return self.server.refresh() log.info('Sending notification to Plex')
hosts = self.getHosts(port = 32400)
def getClientNames(self): for host in hosts:
return [
x.strip().lower()
for x in self.conf('clients').split(',')
]
def notifyClients(self, message, client_names): source_type = ['movie']
success = True base_url = '%s/library/sections' % host
refresh_url = '%s/%%s/refresh' % base_url
for client_name in client_names: try:
sections_xml = self.urlopen(base_url)
xml_sections = minidom.parseString(sections_xml)
sections = xml_sections.getElementsByTagName('Directory')
client_success = False for s in sections:
client = self.server.clients.get(client_name) if s.getAttribute('type') in source_type:
url = refresh_url % s.getAttribute('key')
self.urlopen(url)
if client and client['found']: except:
client_success = fireEvent('notify.plex.notifyClient', client, message, single = True) log.error('Plex library update failed for %s, Media Server not running: %s', (host, traceback.format_exc(1)))
return False
if not client_success: return True
if self.server.staleClients() or not client:
log.info('Failed to send notification to client "%s". '
'Client list is stale, updating the client list and retrying.', client_name)
self.server.updateClients(self.getClientNames())
else:
log.warning('Failed to send notification to client %s, skipping this time', client_name)
success = False
return success def notify(self, message = '', data = None, listener = None):
if not data: data = {}
def notify(self, message = '', data = {}, listener = None): hosts = self.getHosts(port = 3000)
return self.notifyClients(message, self.getClientNames()) successful = 0
for host in hosts:
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
successful += 1
return successful == len(hosts)
def send(self, command, host):
url = '%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
headers = {}
try:
self.urlopen(url, headers = headers, show_error = False)
except URLError:
log.error("Couldn't sent command to Plex, probably just running Media Server")
return False
except:
log.error("Couldn't sent command to Plex: %s", traceback.format_exc())
return False
log.info('Plex notification to %s successful.', host)
return True
def test(self, **kwargs): def test(self, **kwargs):
@@ -66,12 +80,28 @@ class Plex(Notification):
log.info('Sending test to %s', test_type) log.info('Sending test to %s', test_type)
notify_success = self.notify( success = self.notify(
message = self.test_message, message = self.test_message,
data = {}, data = {},
listener = 'test' listener = 'test'
) )
success2 = self.addToLibrary()
refresh_success = self.addToLibrary() return {
'success': success or success2
}
return {'success': notify_success or refresh_success} def getHosts(self, port = None):
raw_hosts = splitString(self.conf('host'))
hosts = []
for h in raw_hosts:
h = cleanHost(h)
p = urlparse(h)
h = h.rstrip('/')
if port and not p.port:
h += ':%s' % port
hosts.append(h)
return hosts
@@ -1,114 +0,0 @@
from datetime import timedelta, datetime
from couchpotato.core.helpers.variable import cleanHost
from couchpotato import CPLog
from urlparse import urlparse
import traceback
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
log = CPLog(__name__)
class PlexServer(object):
def __init__(self, plex):
self.plex = plex
self.clients = {}
self.last_clients_update = None
def staleClients(self):
if not self.last_clients_update:
return True
return self.last_clients_update + timedelta(minutes=15) < datetime.now()
def request(self, path, data_type='xml'):
if not self.plex.conf('media_server'):
log.warning("Plex media server hostname is required")
return None
if path.startswith('/'):
path = path[1:]
data = self.plex.urlopen('%s/%s' % (
self.createHost(self.plex.conf('media_server'), port = 32400),
path
))
if data_type == 'xml':
return etree.fromstring(data)
else:
return data
def updateClients(self, client_names):
log.info('Searching for clients on Plex Media Server')
self.clients = {}
result = self.request('clients')
if not result:
return
found_clients = [
c for c in result.findall('Server')
if c.get('name') and c.get('name').lower() in client_names
]
# Store client details in cache
for client in found_clients:
name = client.get('name').lower()
self.clients[name] = {
'name': client.get('name'),
'found': True,
'address': client.get('address'),
'port': client.get('port'),
'protocol': client.get('protocol', 'xbmchttp')
}
client_names.remove(name)
# Store dummy info for missing clients
for client_name in client_names:
self.clients[client_name] = {
'found': False
}
if len(client_names) > 0:
log.debug('Unable to find clients: %s', ', '.join(client_names))
self.last_clients_update = datetime.now()
def refresh(self, section_types=None):
if not section_types:
section_types = ['movie']
sections = self.request('library/sections')
try:
for section in sections.findall('Directory'):
if section.get('type') not in section_types:
continue
self.request('library/sections/%s/refresh' % section.get('key'), 'text')
except:
log.error('Plex library update failed for %s, Media Server not running: %s',
(self.plex.conf('media_server'), traceback.format_exc(1)))
return False
return True
def createHost(self, host, port = None):
h = cleanHost(host)
p = urlparse(h)
h = h.rstrip('/')
if port and not p.port:
h += ':%s' % port
return h
+1 -1
View File
@@ -26,7 +26,7 @@ class Prowl(Notification):
} }
try: try:
self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False) self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
log.info('Prowl notifications sent.') log.info('Prowl notifications sent.')
return True return True
except: except:
@@ -29,7 +29,7 @@ class Pushalot(Notification):
} }
try: try:
self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False) self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
return True return True
except: except:
log.error('PushAlot failed: %s', traceback.format_exc()) log.error('PushAlot failed: %s', traceback.format_exc())
@@ -1,86 +0,0 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import base64
import json
log = CPLog(__name__)
class Pushbullet(Notification):
url = 'https://api.pushbullet.com/api/%s'
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
devices = self.getDevices()
if devices is None:
return False
# Get all the device IDs linked to this user
if not len(devices):
response = self.request('devices')
if not response:
return False
devices += [device.get('id') for device in response['devices']]
successful = 0
for device in devices:
response = self.request(
'pushes',
cache = False,
device_id = device,
type = 'note',
title = self.default_title,
body = toUnicode(message)
)
if response:
successful += 1
else:
log.error('Unable to push notification to Pushbullet device with ID %s' % device)
return successful == len(devices)
def getDevices(self):
devices = [d.strip() for d in self.conf('devices').split(',')]
# Remove empty items
devices = [d for d in devices if len(d)]
# Break on any ids that aren't integers
valid_devices = []
for device_id in devices:
d = tryInt(device_id, None)
if not d:
log.error('Device ID "%s" is not valid', device_id)
return None
valid_devices.append(d)
return valid_devices
def request(self, method, cache = True, **kwargs):
try:
base64string = base64.encodestring('%s:' % self.conf('api_key'))[:-1]
headers = {
"Authorization": "Basic %s" % base64string
}
if cache:
return self.getJsonData(self.url % method, headers = headers, data = kwargs)
else:
data = self.urlopen(self.url % method, headers = headers, data = kwargs)
return json.loads(data)
except Exception, ex:
log.error('Pushbullet request failed')
log.debug(ex)
return None
+1 -1
View File
@@ -35,7 +35,7 @@ class Trakt(Notification):
def call(self, method_url, post_data): def call(self, method_url, post_data):
try: try:
response = self.getJsonData(self.urls['base'] % method_url, data = post_data, cache_timeout = 1) response = self.getJsonData(self.urls['base'] % method_url, params = post_data, cache_timeout = 1)
if response: if response:
if response.get('status') == "success": if response.get('status') == "success":
log.info('Successfully called Trakt') log.info('Successfully called Trakt')
@@ -46,14 +46,6 @@ config = [{
'advanced': True, 'advanced': True,
'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.', 'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.',
}, },
{
'name': 'force_full_scan',
'label': 'Always do a full scan',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Do a full scan instead of only the new movie. Useful if the XBMC path is different from the path CPS uses.',
},
{ {
'name': 'on_snatch', 'name': 'on_snatch',
'default': 0, 'default': 0,
+18 -11
View File
@@ -7,7 +7,6 @@ import json
import socket import socket
import traceback import traceback
import urllib import urllib
import requests
log = CPLog(__name__) log = CPLog(__name__)
@@ -37,7 +36,7 @@ class XBMC(Notification):
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0): if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
param = {} param = {}
if not self.conf('force_full_scan') and (self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0])): if self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0]):
param = {'directory': data['destination_dir']} param = {'directory': data['destination_dir']}
calls.append(('VideoLibrary.Scan', param)) calls.append(('VideoLibrary.Scan', param))
@@ -168,18 +167,22 @@ class XBMC(Notification):
# manually fake expected response array # manually fake expected response array
return [{'result': 'Error'}] return [{'result': 'Error'}]
except requests.exceptions.Timeout: except URLError, e:
log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off') if isinstance(e.reason, socket.timeout):
return [{'result': 'Error'}] log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
return [{'result': 'Error'}]
else:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
return [{'result': 'Error'}]
except: except:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc()) log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
return [{'result': 'Error'}] return [{'result': 'Error'}]
def request(self, host, do_requests): def request(self, host, requests):
server = 'http://%s/jsonrpc' % host server = 'http://%s/jsonrpc' % host
data = [] data = []
for req in do_requests: for req in requests:
method, kwargs = req method, kwargs = req
data.append({ data.append({
'method': method, 'method': method,
@@ -199,13 +202,17 @@ class XBMC(Notification):
try: try:
log.debug('Sending request to %s: %s', (host, data)) log.debug('Sending request to %s: %s', (host, data))
response = self.getJsonData(server, headers = headers, data = data, timeout = 3, show_error = False) response = self.getJsonData(server, headers = headers, params = data, timeout = 3, show_error = False)
log.debug('Returned from request %s: %s', (host, response)) log.debug('Returned from request %s: %s', (host, response))
return response return response
except requests.exceptions.Timeout: except URLError, e:
log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off') if isinstance(e.reason, socket.timeout):
return [] log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
return []
else:
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return []
except: except:
log.error('Failed sending request to XBMC: %s', traceback.format_exc()) log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return [] return []
@@ -1,52 +0,0 @@
from .main import Xmpp
def start():
return Xmpp()
config = [{
'name': 'xmpp',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'xmpp',
'label': 'XMPP',
'description`': 'for Jabber, Hangouts (Google Talk), AIM...',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'username',
'description': 'User sending the message. For Hangouts, e-mail of a single-step authentication Google account.',
},
{
'name': 'password',
'type': 'Password',
},
{
'name': 'hostname',
'default': 'talk.google.com',
},
{
'name': 'to',
'description': 'Username (or e-mail for Hangouts) of the person to send the messages to.',
},
{
'name': 'port',
'type': 'int',
'default': 5222,
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]
@@ -1,43 +0,0 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from time import sleep
import traceback
import xmpp
log = CPLog(__name__)
class Xmpp(Notification):
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
try:
jid = xmpp.protocol.JID(self.conf('username'))
client = xmpp.Client(jid.getDomain(), debug = [])
# Connect
if not client.connect(server = (self.conf('hostname'), self.conf('port'))):
log.error('XMPP failed: Connection to server failed.')
return False
# Authenticate
if not client.auth(jid.getNode(), self.conf('password'), resource = jid.getResource()):
log.error('XMPP failed: Failed to authenticate.')
return False
# Send message
client.send(xmpp.protocol.Message(to = self.conf('to'), body = message, typ = 'chat'))
# Disconnect
# some older servers will not send the message if you disconnect immediately after sending
sleep(1)
client.disconnect()
log.info('XMPP notifications sent.')
return True
except:
log.error('XMPP failed: %s', traceback.format_exc())
return False
@@ -41,7 +41,7 @@ config = [{
'label': 'Required Genres', 'label': 'Required Genres',
'default': '', 'default': '',
'placeholder': 'Example: Action, Crime & Drama', 'placeholder': 'Example: Action, Crime & Drama',
'description': ('Ignore movies that don\'t contain at least one set of genres.', 'Sets are separated by "," and each word within a set must be separated with "&"') 'description': 'Ignore movies that don\'t contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"'
}, },
{ {
'name': 'ignored_genres', 'name': 'ignored_genres',
+2 -2
View File
@@ -43,7 +43,7 @@ class Automation(Plugin):
if self.shuttingDown(): if self.shuttingDown():
break break
movie_dict = fireEvent('media.get', movie_id, single = True) movie_dict = fireEvent('movie.get', movie_id, single = True)
fireEvent('movie.searcher.single', movie_dict) fireEvent('movie.searcher.single', movie_dict)
return True return True
+61 -40
View File
@@ -1,15 +1,19 @@
from StringIO import StringIO
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import ss, toSafeString, \ from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
toUnicode, sp toUnicode
from couchpotato.core.helpers.variable import getExt, md5, isLocalIP from couchpotato.core.helpers.variable import getExt, md5, isLocalIP
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
import requests from multipartpost import MultipartPostHandler
from tornado import template from tornado import template
from tornado.web import StaticFileHandler from tornado.web import StaticFileHandler
from urlparse import urlparse from urlparse import urlparse
import cookielib
import glob import glob
import gzip
import inspect import inspect
import math
import os.path import os.path
import re import re
import time import time
@@ -35,7 +39,6 @@ class Plugin(object):
http_time_between_calls = 0 http_time_between_calls = 0
http_failed_request = {} http_failed_request = {}
http_failed_disabled = {} http_failed_disabled = {}
http_opener = requests.Session()
def __new__(typ, *args, **kwargs): def __new__(typ, *args, **kwargs):
new_plugin = super(Plugin, typ).__new__(typ) new_plugin = super(Plugin, typ).__new__(typ)
@@ -103,9 +106,7 @@ class Plugin(object):
f.close() f.close()
os.chmod(path, Env.getPermission('file')) os.chmod(path, Env.getPermission('file'))
except Exception, e: except Exception, e:
log.error('Unable writing to file "%s": %s', (path, traceback.format_exc())) log.error('Unable writing to file "%s": %s', (path, e))
if os.path.isfile(path):
os.remove(path)
def makeDir(self, path): def makeDir(self, path):
path = ss(path) path = ss(path)
@@ -119,11 +120,11 @@ class Plugin(object):
return False return False
# http request # http request
def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True, return_raw = False): def urlopen(self, url, timeout = 30, params = None, headers = None, opener = None, multipart = False, show_error = True):
url = urllib2.quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]") url = ss(url)
if not headers: headers = {} if not headers: headers = {}
if not data: data = {} if not params: params = {}
# Fill in some headers # Fill in some headers
parsed_url = urlparse(url) parsed_url = urlparse(url)
@@ -136,8 +137,6 @@ class Plugin(object):
headers['Connection'] = headers.get('Connection', 'keep-alive') headers['Connection'] = headers.get('Connection', 'keep-alive')
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0') headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
r = self.http_opener
# 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):
@@ -153,18 +152,45 @@ class Plugin(object):
self.wait(host) self.wait(host)
try: try:
kwargs = { # Make sure opener has the correct headers
'headers': headers, if opener:
'data': data if len(data) > 0 else None, opener.add_headers = headers
'timeout': timeout,
'files': files,
}
method = 'post' if len(data) > 0 or files else 'get'
log.info('Opening url: %s %s, data: %s', (method, url, [x for x in data.iterkeys()] if isinstance(data, dict) else 'with data')) if multipart:
response = r.request(method, url, verify = False, **kwargs) log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
request = urllib2.Request(url, params, headers)
data = response.content if return_raw else response.text if opener:
opener.add_handler(MultipartPostHandler())
else:
cookies = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
response = opener.open(request, timeout = timeout)
else:
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
if isinstance(params, (str, unicode)) and len(params) > 0:
data = params
else:
data = tryUrlencode(params) if len(params) > 0 else None
request = urllib2.Request(url, data, headers)
if opener:
response = opener.open(request, timeout = timeout)
else:
response = urllib2.urlopen(request, timeout = timeout)
# unzip if needed
if response.info().get('Content-Encoding') == 'gzip':
buf = StringIO(response.read())
f = gzip.GzipFile(fileobj = buf)
data = f.read()
f.close()
else:
data = response.read()
response.close()
self.http_failed_request[host] = 0 self.http_failed_request[host] = 0
except IOError: except IOError:
@@ -192,19 +218,15 @@ class Plugin(object):
return data return data
def wait(self, host = ''): def wait(self, host = ''):
if self.http_time_between_calls == 0:
return
now = time.time() now = time.time()
last_use = self.http_last_use.get(host, 0) last_use = self.http_last_use.get(host, 0)
if last_use > 0:
wait = (last_use - now) + self.http_time_between_calls wait = math.ceil(last_use - now + self.http_time_between_calls)
if wait > 0: if wait > 0:
log.debug('Waiting for %s, %d seconds', (self.getName(), wait)) log.debug('Waiting for %s, %d seconds', (self.getName(), wait))
time.sleep(wait) time.sleep(last_use - now + self.http_time_between_calls)
def beforeCall(self, handler): def beforeCall(self, handler):
self.isRunning('%s.%s' % (self.getName(), handler.__name__)) self.isRunning('%s.%s' % (self.getName(), handler.__name__))
@@ -247,19 +269,18 @@ class Plugin(object):
try: try:
cache_timeout = 300 cache_timeout = 300
if kwargs.has_key('cache_timeout'): if kwargs.get('cache_timeout'):
cache_timeout = kwargs.get('cache_timeout') cache_timeout = kwargs.get('cache_timeout')
del kwargs['cache_timeout'] del kwargs['cache_timeout']
data = self.urlopen(url, **kwargs) data = self.urlopen(url, **kwargs)
if data and cache_timeout > 0: 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): if not kwargs.get('show_error', True):
raise raise
log.error('Failed getting cache: %s', (traceback.format_exc()))
return '' return ''
def setCache(self, cache_key, value, timeout = 300): def setCache(self, cache_key, value, timeout = 300):
@@ -268,19 +289,19 @@ class Plugin(object):
Env.get('cache').set(cache_key_md5, value, timeout) Env.get('cache').set(cache_key_md5, value, timeout)
return value return value
def createNzbName(self, data, media): def createNzbName(self, data, movie):
tag = self.cpTag(media) tag = self.cpTag(movie)
return '%s%s' % (toSafeString(toUnicode(data.get('name'))[:127 - len(tag)]), tag) return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
def createFileName(self, data, filedata, media): def createFileName(self, data, filedata, movie):
name = sp(os.path.join(self.createNzbName(data, media))) name = os.path.join(self.createNzbName(data, movie))
if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata: if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
return '%s.%s' % (name, 'rar') return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('protocol')) return '%s.%s' % (name, data.get('protocol'))
def cpTag(self, media): def cpTag(self, movie):
if Env.setting('enabled', 'renamer'): if Env.setting('enabled', 'renamer'):
return '.cp(' + media['library'].get('identifier') + ')' if media['library'].get('identifier') else '' return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
return '' return ''
+2 -2
View File
@@ -4,7 +4,7 @@ from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
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 Media, Category from couchpotato.core.settings.model import Movie, Category
log = CPLog(__name__) log = CPLog(__name__)
@@ -113,7 +113,7 @@ class CategoryPlugin(Plugin):
def removeFromMovie(self, category_id): def removeFromMovie(self, category_id):
db = get_session() db = get_session()
movies = db.query(Media).filter(Media.category_id == category_id).all() movies = db.query(Movie).filter(Movie.category_id == category_id).all()
if len(movies) > 0: if len(movies) > 0:
for movie in movies: for movie in movies:
@@ -1,6 +0,0 @@
from .main import Custom
def start():
return Custom()
config = []
-21
View File
@@ -1,21 +0,0 @@
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
import os
log = CPLog(__name__)
class Custom(Plugin):
def __init__(self):
addEvent('app.load', self.createStructure)
def createStructure(self):
custom_dir = os.path.join(Env.get('data_dir'), 'custom_plugins')
if not os.path.isdir(custom_dir):
self.makeDir(custom_dir)
self.createFile(os.path.join(custom_dir, '__init__.py'), '# Don\'t remove this file')
+9 -12
View File
@@ -4,10 +4,9 @@ from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import splitString, tryInt from couchpotato.core.helpers.variable import splitString, 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.core.settings.model import Media, Library, LibraryTitle, \ from couchpotato.core.settings.model import Movie, Library, LibraryTitle
Release
from sqlalchemy.orm import joinedload_all from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import asc, or_ from sqlalchemy.sql.expression import asc
import random as rndm import random as rndm
import time import time
@@ -49,14 +48,12 @@ class Dashboard(Plugin):
limit = tryInt(splt[0]) limit = tryInt(splt[0])
# Get all active movies # Get all active movies
active_status, ignored_status = fireEvent('status.get', ['active', 'ignored'], single = True) active_status = fireEvent('status.get', ['active'], single = True)
q = db.query(Media) \ q = db.query(Movie) \
.join(Library) \ .join(Library) \
.outerjoin(Media.releases) \ .filter(Movie.status_id == active_status.get('id')) \
.filter(Media.status_id == active_status.get('id')) \ .with_entities(Movie.id, Movie.profile_id, Library.info, Library.year) \
.with_entities(Media.id, Media.profile_id, Library.info, Library.year) \ .group_by(Movie.id)
.group_by(Media.id) \
.filter(or_(Release.id == None, Release.status_id == ignored_status.get('id')))
if not random: if not random:
q = q.join(LibraryTitle) \ q = q.join(LibraryTitle) \
@@ -101,11 +98,11 @@ class Dashboard(Plugin):
if len(movie_ids) > 0: if len(movie_ids) > 0:
# Get all movie information # Get all movie information
movies_raw = db.query(Media) \ movies_raw = db.query(Movie) \
.options(joinedload_all('library.titles')) \ .options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \ .options(joinedload_all('library.files')) \
.options(joinedload_all('files')) \ .options(joinedload_all('files')) \
.filter(Media.id.in_(movie_ids)) \ .filter(Movie.id.in_(movie_ids)) \
.all() .all()
# Create dict by movie id # Create dict by movie id
+1 -1
View File
@@ -93,7 +93,7 @@ class FileManager(Plugin):
return dest return dest
try: try:
filedata = self.urlopen(url, return_raw = True, **urlopen_kwargs) filedata = self.urlopen(url, **urlopen_kwargs)
except: except:
log.error('Failed downloading file %s: %s', (url, traceback.format_exc())) log.error('Failed downloading file %s: %s', (url, traceback.format_exc()))
return False return False
+11 -11
View File
@@ -1,6 +1,6 @@
from couchpotato.api import addApiView 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 sp from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import splitString, getTitle 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
@@ -79,7 +79,6 @@ class Manage(Plugin):
try: try:
directories = self.directories() directories = self.directories()
directories.sort()
added_identifiers = [] added_identifiers = []
# Add some progress # Add some progress
@@ -112,20 +111,22 @@ class Manage(Plugin):
if self.conf('cleanup') and full and not self.shuttingDown(): if self.conf('cleanup') and full and not self.shuttingDown():
# Get movies with done status # Get movies with done status
total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', single = True) total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
for done_movie in done_movies: for done_movie in done_movies:
if done_movie['library']['identifier'] not in added_identifiers: if done_movie['library']['identifier'] not in added_identifiers:
fireEvent('media.delete', media_id = done_movie['id'], delete_from = 'all') fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
else: else:
releases = fireEvent('release.for_movie', id = done_movie.get('id'), single = True) releases = fireEvent('release.for_movie', id = done_movie.get('id'), single = True)
for release in releases: for release in releases:
if len(release.get('files', [])) > 0: if len(release.get('files', [])) == 0:
fireEvent('release.delete', release['id'])
else:
for release_file in release.get('files', []): for release_file in release.get('files', []):
# Remove release not available anymore # Remove release not available anymore
if not os.path.isfile(sp(release_file['path'])): if not os.path.isfile(ss(release_file['path'])):
fireEvent('release.clean', release['id']) fireEvent('release.clean', release['id'])
break break
@@ -200,7 +201,7 @@ class Manage(Plugin):
self.in_progress[folder]['to_go'] -= 1 self.in_progress[folder]['to_go'] -= 1
total = self.in_progress[folder]['total'] total = self.in_progress[folder]['total']
movie_dict = fireEvent('media.get', identifier, single = True) 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'])) fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library']))
@@ -221,10 +222,9 @@ class Manage(Plugin):
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True) groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
if groups: for group in groups.itervalues():
for group in groups.itervalues(): if group['library'] and group['library'].get('identifier'):
if group['library'] and group['library'].get('identifier'): fireEvent('release.add', group = group)
fireEvent('release.add', group = group)
def getDiskSpace(self): def getDiskSpace(self):
+2 -2
View File
@@ -4,7 +4,7 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
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 Profile, ProfileType, Media from couchpotato.core.settings.model import Profile, ProfileType, Movie
from sqlalchemy.orm import joinedload_all from sqlalchemy.orm import joinedload_all
log = CPLog(__name__) log = CPLog(__name__)
@@ -38,7 +38,7 @@ class ProfilePlugin(Plugin):
active_status = fireEvent('status.get', 'active', single = True) active_status = fireEvent('status.get', 'active', single = True)
db = get_session() db = get_session()
movies = db.query(Media).filter(Media.status_id == active_status.get('id'), Media.profile == None).all() movies = db.query(Movie).filter(Movie.status_id == active_status.get('id'), Movie.profile == None).all()
if len(movies) > 0: if len(movies) > 0:
default_profile = self.default() default_profile = self.default()
+56 -162
View File
@@ -1,8 +1,8 @@
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 from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode, ss from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import mergeDicts, getExt 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
@@ -17,40 +17,19 @@ 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': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']}, {'identifier': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']}, {'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r')]}, {'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': [], 'allow': [], 'ext':[], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': ['webrip', ('web', 'rip')]}, {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg'], 'tags': ['webrip', ('web', 'rip')]},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':[]}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':[]}, {'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':[]}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]}, {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
# TODO come back to this later, think this could be handled better, this is starting to get out of hand....
# BluRay
{'identifier': 'bluray_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BluRay - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'bluray_720p', 'hd': True, 'size': (800, 5000), 'label': 'BluRay - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# BDRip
{'identifier': 'bdrip_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BDRip - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'bdrip_720p', 'hd': True, 'size': (800, 5000), 'label': 'BDRip - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# BRRip
{'identifier': 'brrip_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BRRip - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'brrip_720p', 'hd': True, 'size': (800, 5000), 'label': 'BRRip - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# WEB-DL
{'identifier': 'webdl_1080p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'webdl_720p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'webdl_480p', 'hd': True, 'size': (100, 5000), 'label': 'WEB-DL - 480p', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# HDTV
{'identifier': 'hdtv_720p', 'hd': True, 'size': (800, 5000), 'label': 'HDTV - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'hdtv_sd', 'hd': False, 'size': (100, 1000), 'label': 'HDTV - SD', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'mp4', 'avi']},
] ]
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr'] pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
cached_qualities = None
cached_order = None
def __init__(self): def __init__(self):
addEvent('quality.all', self.all) addEvent('quality.all', self.all)
addEvent('quality.single', self.single) addEvent('quality.single', self.single)
@@ -68,8 +47,6 @@ class QualityPlugin(Plugin):
addEvent('app.initialize', self.fill, priority = 10) addEvent('app.initialize', self.fill, priority = 10)
addEvent('app.test', self.doTest)
def preReleases(self): def preReleases(self):
return self.pre_releases return self.pre_releases
@@ -82,9 +59,6 @@ class QualityPlugin(Plugin):
def all(self): def all(self):
if self.cached_qualities:
return self.cached_qualities
db = get_session() db = get_session()
qualities = db.query(Quality).all() qualities = db.query(Quality).all()
@@ -94,7 +68,6 @@ 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)
self.cached_qualities = temp
return temp return temp
def single(self, identifier = ''): def single(self, identifier = ''):
@@ -123,8 +96,6 @@ class QualityPlugin(Plugin):
setattr(quality, kwargs.get('value_type'), kwargs.get('value')) setattr(quality, kwargs.get('value_type'), kwargs.get('value'))
db.commit() db.commit()
self.cached_qualities = None
return { return {
'success': True 'success': True
} }
@@ -185,150 +156,73 @@ class QualityPlugin(Plugin):
if not extra: extra = {} if not extra: extra = {}
# Create hash for cache # Create hash for cache
cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files]) cache_key = md5(str([f.replace('.' + getExt(f), '') for f in files]))
cached = self.getCache(cache_key) cached = self.getCache(cache_key)
if cached and len(extra) == 0: if cached and len(extra) == 0: return cached
return cached
qualities = self.all() qualities = self.all()
# Start with 0
score = {}
for quality in qualities:
score[quality.get('identifier')] = 0
for cur_file in files: for cur_file in files:
words = re.split('\W+', cur_file.lower()) words = re.split('\W+', cur_file.lower())
found = {}
for quality in qualities: for quality in qualities:
contains_score = self.containsTagScore(quality, words, cur_file) contains = self.containsTag(quality, words, cur_file)
self.calcScore(score, quality, contains_score) if contains:
found[quality['identifier']] = True
# Try again with loose testing
for quality in qualities:
loose_score = self.guessLooseScore(quality, files = files, extra = extra)
self.calcScore(score, quality, loose_score)
# Return nothing if all scores are 0
has_non_zero = 0
for s in score:
if score[s] > 0:
has_non_zero += 1
if not has_non_zero:
return None
heighest_quality = max(score, key = score.get)
if heighest_quality:
for quality in qualities: for quality in qualities:
if quality.get('identifier') == heighest_quality:
# Check identifier
if quality['identifier'] in words:
if len(found) == 0 or len(found) == 1 and found.get(quality['identifier']):
log.debug('Found via identifier "%s" in %s', (quality['identifier'], cur_file))
return self.setCache(cache_key, quality)
# Check alt and tags
contains = self.containsTag(quality, words, cur_file)
if contains:
return self.setCache(cache_key, quality) return self.setCache(cache_key, quality)
# Try again with loose testing
quality = self.guessLoose(cache_key, files = files, extra = extra)
if quality:
return self.setCache(cache_key, quality)
log.debug('Could not identify quality for: %s', files)
return None return None
def containsTagScore(self, quality, words, cur_file = ''): def containsTag(self, quality, words, cur_file = ''):
cur_file = ss(cur_file)
score = 0
points = {
'identifier': 10,
'label': 10,
'alternative': 9,
'tags': 9,
'ext': 3,
}
# Check alt and tags # Check alt and tags
for tag_type in ['identifier', 'alternative', 'tags', 'label']: for tag_type in ['alternative', 'tags']:
qualities = quality.get(tag_type, []) for alt in quality.get(tag_type, []):
qualities = [qualities] if isinstance(qualities, (str, unicode)) else qualities if isinstance(alt, tuple) and '.'.join(alt) in '.'.join(words):
for alt in qualities:
if (isinstance(alt, tuple)):
if len(set(words) & set(alt)) == len(alt):
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
score += points.get(tag_type)
if (isinstance(alt, (str, unicode)) and ss(alt.lower()) in cur_file.lower()):
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
score += points.get(tag_type) / 2 return True
if list(set(qualities) & set(words)): if list(set(quality.get(tag_type, [])) & set(words)):
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
score += points.get(tag_type) return True
# Check extention return
for ext in quality.get('ext', []):
if ext == words[-1]:
log.debug('Found %s extension in %s', (ext, cur_file))
score += points['ext']
return score def guessLoose(self, cache_key, files = None, extra = None):
def guessLooseScore(self, quality, files = None, extra = None):
score = 0
if extra: if extra:
for quality in self.all():
# Check width resolution, range 20 # Check width resolution, range 20
if quality.get('width') and (quality.get('width') - 20) <= extra.get('resolution_width', 0) <= (quality.get('width') + 20): if quality.get('width') and (quality.get('width') - 20) <= extra.get('resolution_width', 0) <= (quality.get('width') + 20):
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width'), extra.get('resolution_width', 0))) log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width'), extra.get('resolution_width', 0)))
score += 5 return self.setCache(cache_key, quality)
# Check height resolution, range 20 # Check height resolution, range 20
if quality.get('height') and (quality.get('height') - 20) <= extra.get('resolution_height', 0) <= (quality.get('height') + 20): if quality.get('height') and (quality.get('height') - 20) <= extra.get('resolution_height', 0) <= (quality.get('height') + 20):
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height'), extra.get('resolution_height', 0))) log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height'), extra.get('resolution_height', 0)))
score += 5 return self.setCache(cache_key, quality)
if quality.get('identifier') == 'dvdrip' and 480 <= extra.get('resolution_width', 0) <= 720:
log.debug('Add point for correct dvdrip resolutions')
score += 1
return score
def calcScore(self, score, quality, add_score):
score[quality['identifier']] += add_score
# Set order for allow calculation (and cache)
if not self.cached_order:
self.cached_order = {}
for q in self.qualities:
self.cached_order[q.get('identifier')] = self.qualities.index(q)
if add_score != 0:
for allow in quality.get('allow', []):
score[allow] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5
def doTest(self):
tests = {
'Movie Name (1999)-DVD-Rip.avi': 'dvdrip',
'Movie Name 1999 720p Bluray.mkv': '720p',
'Movie Name 1999 BR-Rip 720p.avi': 'brrip',
'Movie Name 1999 720p Web Rip.avi': 'scr',
'Movie Name 1999 Web DL.avi': 'brrip',
'Movie.Name.1999.1080p.WEBRip.H264-Group': 'scr',
'Movie.Name.1999.DVDRip-Group': 'dvdrip',
'Movie.Name.1999.DVD-Rip-Group': 'dvdrip',
'Movie.Name.1999.DVD-R-Group': 'dvdr',
'Movie.Name.Camelie.1999.720p.BluRay.x264-Group': '720p',
}
correct = 0
for name in tests:
success = self.guess([name]).get('identifier') == tests[name]
if not success:
log.error('%s failed check, thinks it\'s %s', (name, self.guess([name]).get('identifier')))
correct += success
if correct == len(tests):
log.info('Quality test successful')
return True
else:
log.error('Quality test failed: %s out of %s succeeded', (correct, len(tests)))
if 480 <= extra.get('resolution_width', 0) <= 720:
log.debug('Found as dvdrip')
return self.setCache(cache_key, self.single('dvdrip'))
return None
+46 -270
View File
@@ -1,20 +1,14 @@
from couchpotato import get_session, md5 from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import ss, toUnicode from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import 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.core.plugins.scanner.main import Scanner from couchpotato.core.plugins.scanner.main import Scanner
from couchpotato.core.settings.model import File, Release as Relea, Media, \ from couchpotato.core.settings.model import File, Release as Relea, Movie
ReleaseInfo
from couchpotato.environment import Env
from inspect import ismethod, isfunction
from sqlalchemy.exc import InterfaceError
from sqlalchemy.orm import joinedload_all from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import and_, or_ from sqlalchemy.sql.expression import and_, or_
import os import os
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -25,7 +19,7 @@ class Release(Plugin):
def __init__(self): def __init__(self):
addEvent('release.add', self.add) addEvent('release.add', self.add)
addApiView('release.manual_download', self.manualDownload, docs = { addApiView('release.download', self.download, docs = {
'desc': 'Send a release manually to the downloaders', 'desc': 'Send a release manually to the downloaders',
'params': { 'params': {
'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'}
@@ -50,45 +44,9 @@ class Release(Plugin):
} }
}) })
addEvent('release.download', self.download)
addEvent('release.try_download_result', self.tryDownloadResult)
addEvent('release.create_from_search', self.createFromSearch)
addEvent('release.for_movie', self.forMovie) addEvent('release.for_movie', self.forMovie)
addEvent('release.delete', self.delete) addEvent('release.delete', self.delete)
addEvent('release.clean', self.clean) addEvent('release.clean', self.clean)
addEvent('release.update_status', self.updateStatus)
# Clean releases that didn't have activity in the last week
addEvent('app.load', self.cleanDone)
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 4)
def cleanDone(self):
log.debug('Removing releases from dashboard')
now = time.time()
week = 262080
done_status, available_status, snatched_status, downloaded_status, ignored_status = \
fireEvent('status.get', ['done', 'available', 'snatched', 'downloaded', 'ignored'], single = True)
db = get_session()
# get movies last_edit more than a week ago
media = db.query(Media) \
.filter(Media.status_id == done_status.get('id'), Media.last_edit < (now - week)) \
.all()
for item in media:
for rel in item.releases:
# Remove all available releases
if rel.status_id in [available_status.get('id')]:
fireEvent('release.delete', id = rel.id, single = True)
# Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the move
elif rel.status_id in [snatched_status.get('id'), downloaded_status.get('id')]:
self.updateStatus(id = rel.id, status = ignored_status)
db.expire_all()
def add(self, group): def add(self, group):
@@ -100,14 +58,14 @@ class Release(Plugin):
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True) done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
# Add movie # Add movie
media = db.query(Media).filter_by(library_id = group['library'].get('id')).first() movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first()
if not media: if not movie:
media = Media( movie = Movie(
library_id = group['library'].get('id'), library_id = group['library'].get('id'),
profile_id = 0, profile_id = 0,
status_id = done_status.get('id') status_id = done_status.get('id')
) )
db.add(media) db.add(movie)
db.commit() db.commit()
# Add Release # Add Release
@@ -120,7 +78,7 @@ class Release(Plugin):
if not rel: if not rel:
rel = Relea( rel = Relea(
identifier = identifier, identifier = identifier,
media = media, movie = movie,
quality_id = group['meta_data']['quality'].get('id'), quality_id = group['meta_data']['quality'].get('id'),
status_id = done_status.get('id') status_id = done_status.get('id')
) )
@@ -142,10 +100,11 @@ class Release(Plugin):
except: except:
log.debug('Failed to attach "%s" to release: %s', (added_files, traceback.format_exc())) log.debug('Failed to attach "%s" to release: %s', (added_files, traceback.format_exc()))
fireEvent('media.restatus', media.id) fireEvent('movie.restatus', movie.id)
return True return True
def saveFile(self, filepath, type = 'unknown', include_media_info = False): def saveFile(self, filepath, type = 'unknown', include_media_info = False):
properties = {} properties = {}
@@ -200,218 +159,64 @@ class Release(Plugin):
rel = db.query(Relea).filter_by(id = id).first() rel = db.query(Relea).filter_by(id = id).first()
if rel: if rel:
ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True) ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True)
self.updateStatus(id, available_status if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status) rel.status_id = available_status.get('id') if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status.get('id')
db.commit()
return { return {
'success': True 'success': True
} }
def manualDownload(self, id = None, **kwargs): def download(self, id = None, **kwargs):
db = get_session() db = get_session()
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
rel = db.query(Relea).filter_by(id = id).first() rel = db.query(Relea).filter_by(id = id).first()
if not rel: if rel:
log.error('Couldn\'t find release with id: %s', id) item = {}
return { for info in rel.info:
'success': False item[info.identifier] = info.value
}
item = {} fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Snatching "%s"' % item['name'])
for info in rel.info:
item[info.identifier] = info.value
fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Snatching "%s"' % item['name']) # Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
# Get matching provider if not item.get('protocol'):
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) item['protocol'] = item['type']
item['type'] = 'movie'
# Backwards compatibility code if item.get('protocol') != 'torrent_magnet':
if not item.get('protocol'): item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
item['protocol'] = item['type']
item['type'] = 'movie'
success = self.download(data = item, media = rel.media.to_dict({ success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}}, 'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
'files': {} 'files': {}
}), manual = True) }), manual = True, single = True)
if item.get('protocol') != 'torrent_magnet':
item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
success = self.download(data = item, media = rel.movie.to_dict({ if success:
'profile': {'types': {'quality': {}}}, db.expunge_all()
'releases': {'status': {}, 'quality': {}}, rel = db.query(Relea).filter_by(id = id).first() # Get release again
'library': {'titles': {}, 'files':{}},
'files': {}
}), manual = True)
if success == True: if rel.status_id != done_status.get('id'):
db.expunge_all() rel.status_id = snatched_status.get('id')
rel = db.query(Relea).filter_by(id = id).first() # Get release again @RuudBurger why do we need to get it again?? db.commit()
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return {
'success': success
}
else:
log.error('Couldn\'t find release with id: %s', id)
fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return { return {
'success': success == True 'success': False
} }
def download(self, data, media, manual = False):
# Backwards compatibility code
if not data.get('protocol'):
data['protocol'] = data['type']
data['type'] = 'movie'
# Test to see if any downloaders are enabled for this type
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
if not downloader_enabled:
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', data.get('protocol'))
return False
# Download NZB or torrent file
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'))
log.info('Tried to download, but the "%s" provider gave an error', data.get('protocol'))
if filedata == 'try_next':
return filedata
# Send NZB or torrent file to downloader
download_result = fireEvent('download', data = data, media = media, manual = manual, filedata = filedata, single = True)
if not download_result:
log.info('Tried to download, but the "%s" downloader gave an error', data.get('protocol'))
return False
log.debug('Downloader result: %s', download_result)
snatched_status, done_status, downloaded_status, active_status = fireEvent('status.get', ['snatched', 'done', 'downloaded', 'active'], single = True)
try:
db = get_session()
rls = db.query(Relea).filter_by(identifier = md5(data['url'])).first()
if not rls:
log.error('No release found to store download information in')
return False
renamer_enabled = Env.setting('enabled', 'renamer')
# Save download-id info if returned
if isinstance(download_result, dict):
for key in download_result:
rls_info = ReleaseInfo(
identifier = 'download_%s' % key,
value = toUnicode(download_result.get(key))
)
rls.info.append(rls_info)
db.commit()
log_movie = '%s (%s) in %s' % (getTitle(media['library']), media['library']['year'], rls.quality.label)
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message)
fireEvent('%s.snatched' % data['type'], message = snatch_message, data = rls.to_dict())
# Mark release as snatched
if renamer_enabled:
self.updateStatus(rls.id, status = snatched_status)
# If renamer isn't used, mark media done if finished or release downloaded
else:
if media['status_id'] == active_status.get('id'):
finished = next((True for profile_type in media['profile']['types'] if \
profile_type['quality_id'] == rls.quality.id and profile_type['finish']), False)
if finished:
log.info('Renamer disabled, marking media as finished: %s', log_movie)
# Mark release done
self.updateStatus(rls.id, status = done_status)
# Mark media done
mdia = db.query(Media).filter_by(id = media['id']).first()
mdia.status_id = done_status.get('id')
mdia.last_edit = int(time.time())
db.commit()
return True
# Assume release downloaded
self.updateStatus(rls.id, status = downloaded_status)
except:
log.error('Failed storing download status: %s', traceback.format_exc())
return False
return True
def tryDownloadResult(self, results, media, quality_type, manual = False):
ignored_status, failed_status = fireEvent('status.get', ['ignored', 'failed'], single = True)
for rel in results:
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and rel.get('age') <= quality_type.get('wait_for', 0):
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), rel['name']))
continue
if rel['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
log.info('Ignored: %s', rel['name'])
continue
if rel['score'] <= 0:
log.info('Ignored, score to low: %s', rel['name'])
continue
downloaded = fireEvent('release.download', data = rel, media = media, manual = manual, single = True)
if downloaded is True:
return True
elif downloaded != 'try_next':
break
return False
def createFromSearch(self, search_results, media, quality_type):
available_status = fireEvent('status.get', ['available'], single = True)
db = get_session()
found_releases = []
for rel in search_results:
rel_identifier = md5(rel['url'])
found_releases.append(rel_identifier)
rls = db.query(Relea).filter_by(identifier = rel_identifier).first()
if not rls:
rls = Relea(
identifier = rel_identifier,
media_id = media.get('id'),
quality_id = quality_type.get('quality_id'),
status_id = available_status.get('id')
)
db.add(rls)
else:
[db.delete(old_info) for old_info in rls.info]
rls.last_edit = int(time.time())
db.commit()
for info in rel:
try:
if not isinstance(rel[info], (str, unicode, int, long, float)):
continue
rls_info = ReleaseInfo(
identifier = info,
value = toUnicode(rel[info])
)
rls.info.append(rls_info)
except InterfaceError:
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
db.commit()
rel['status_id'] = rls.status_id
return found_releases
def forMovie(self, id = None): def forMovie(self, id = None):
db = get_session() db = get_session()
@@ -419,7 +224,7 @@ class Release(Plugin):
releases_raw = db.query(Relea) \ releases_raw = db.query(Relea) \
.options(joinedload_all('info')) \ .options(joinedload_all('info')) \
.options(joinedload_all('files')) \ .options(joinedload_all('files')) \
.filter(Relea.media_id == id) \ .filter(Relea.movie_id == id) \
.all() .all()
releases = [r.to_dict({'info':{}, 'files':{}}) for r in releases_raw] releases = [r.to_dict({'info':{}, 'files':{}}) for r in releases_raw]
@@ -436,32 +241,3 @@ class Release(Plugin):
'success': True 'success': True
} }
def updateStatus(self, id, status = None):
if not status: return False
db = get_session()
rel = db.query(Relea).filter_by(id = id).first()
if rel and status and rel.status_id != status.get('id'):
item = {}
for info in rel.info:
item[info.identifier] = info.value
if rel.files:
for file_item in rel.files:
if file_item.type.identifier == 'movie':
release_name = os.path.basename(file_item.path)
break
else:
release_name = item['name']
#update status in Db
log.debug('Marking release %s as %s', (release_name, status.get("label")))
rel.status_id = status.get('id')
rel.last_edit = int(time.time())
db.commit()
#Update all movie info as there is no release update function
fireEvent('notify.frontend', type = 'release.update_status', data = rel.to_dict())
return True
+4 -5
View File
@@ -28,7 +28,6 @@ rename_options = {
'cd': 'CD number (cd1)', 'cd': 'CD number (cd1)',
'cd_nr': 'Just the cd nr. (1)', 'cd_nr': 'Just the cd nr. (1)',
'mpaa': 'MPAA Rating', 'mpaa': 'MPAA Rating',
'category': 'Category label',
}, },
} }
@@ -93,7 +92,7 @@ config = [{
'default': 1, 'default': 1,
'type': 'int', 'type': 'int',
'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, 'advanced': True,
@@ -122,13 +121,13 @@ config = [{
'advanced': True, 'advanced': True,
'name': 'separator', 'name': 'separator',
'label': 'File-Separator', 'label': 'File-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, 'advanced': True,
'name': 'foldersep', 'name': 'foldersep',
'label': 'Folder-Separator', 'label': 'Folder-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.',
}, },
{ {
'name': 'file_action', 'name': 'file_action',
@@ -136,7 +135,7 @@ config = [{
'default': 'link', 'default': 'link',
'type': 'dropdown', 'type': 'dropdown',
'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')], 'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
'description': ('<strong>Link</strong>, <strong>Copy</strong> or <strong>Move</strong> after download completed.', 'Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy. It is perfered to use link when downloading torrents as it will save you space, while still beeing able to seed.'), 'description': '<strong>Link</strong> or <strong>Copy</strong> after downloading completed (and allow for seeding), or <strong>Move</strong> after seeding completed. Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy.',
'advanced': True, 'advanced': True,
}, },
{ {

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