Compare commits

..
167 Commits
Author SHA1 Message Date
Ruud f79fcda27f Small one up 2013-11-17 21:22:24 +01:00
Ruud cdbcad2238 Merge branch 'refs/heads/develop' into desktop 2013-11-17 21:20:30 +01:00
Ruud 5d913e87c3 One up! 2013-11-17 20:20:18 +01:00
Ruud 16f02bda27 Merge branch 'refs/heads/develop' into desktop 2013-11-17 20:03:22 +01:00
Ruud 8d108b92bf One Up 2013-09-23 21:48:12 +02:00
Ruud 46783028b1 Merge branch 'refs/heads/develop' into desktop 2013-09-23 21:36:45 +02:00
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
365 changed files with 14553 additions and 39499 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 ;)**
-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
+3
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()
@@ -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');
} }
} }
}) })
+7 -14
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,27 +49,22 @@ 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):
return []
def _removeFailed(self, release_download): def _removeFailed(self, release_download):
if self.isDisabled(manual = True, data = {}): if self.isDisabled(manual = True, data = {}):
@@ -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
} }
@@ -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': [
{ {
@@ -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,7 +33,7 @@ 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) file_name = self.createFileName(data, filedata, movie)
full_path = os.path.join(directory, file_name) full_path = os.path.join(directory, file_name)
if self.conf('create_subdir'): if self.conf('create_subdir'):
@@ -52,10 +51,10 @@ class Blackhole(Downloader):
with open(full_path, 'wb') as f: with open(full_path, 'wb') as f:
f.write(filedata) f.write(filedata)
os.chmod(full_path, Env.getPermission('file')) os.chmod(full_path, Env.getPermission('file'))
return self.downloadReturnId('') return True
else: else:
log.info('File %s already exists.', full_path) log.info('File %s already exists.', full_path)
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())
+8 -11
View File
@@ -32,10 +32,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,7 +73,7 @@ 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, filedata, options)
if not remote_torrent: if not remote_torrent:
@@ -86,20 +83,20 @@ 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 self.connect(): if not self.connect():
return [] return False
release_downloads = ReleaseDownloadList(self) release_downloads = ReleaseDownloadList(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] torrent = queue[torrent_id]
@@ -208,11 +205,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:
@@ -25,13 +25,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',
+41 -44
View File
@@ -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,59 +97,56 @@ 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) release_downloads = ReleaseDownloadList(self)
for nzb in groups: for nzb in groups:
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
try: try:
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0] nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
except: except:
nzb_id = nzb['NZBID'] nzb_id = nzb['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({ timeleft = -1
'id': nzb_id, try:
'name': nzb['NZBFilename'], if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED', timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item except:
'timeleft': timeleft, 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 for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
if nzb['NZBID'] in ids: log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename']) release_downloads.append({
release_downloads.append({ 'id': nzb['NZBID'],
'id': nzb['NZBID'], 'name': nzb['NZBFilename'],
'name': nzb['NZBFilename'], 'original_status': nzb['Stage'],
'original_status': nzb['Stage'], 'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1, })
})
for nzb in history: for nzb in history:
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
try: try:
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0] nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
except: except:
nzb_id = nzb['NZBID'] nzb_id = nzb['NZBID']
release_downloads.append({
if nzb_id in ids: 'id': nzb_id,
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log'])) 'name': nzb['NZBFilename'],
release_downloads.append({ 'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
'id': nzb_id, 'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
'name': nzb['NZBFilename'], 'timeleft': str(timedelta(seconds = 0)),
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed', 'folder': sp(nzb['DestDir'])
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'], })
'timeleft': str(timedelta(seconds = 0)),
'folder': sp(nzb['DestDir'])
})
return release_downloads return release_downloads
+24 -26
View File
@@ -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,46 +23,44 @@ 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': (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 = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if nzb['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) release_downloads = ReleaseDownloadList(self)
for nzb in raw_statuses.get('nzbs', []): for nzb in raw_statuses.get('nzbs', []):
if nzb['id'] in ids:
# Check status # Check status
status = 'busy' status = 'busy'
if nzb['state'] == 20: if nzb['state'] == 20:
status = 'completed' status = 'completed'
elif nzb['state'] in [21, 22, 24]: elif nzb['state'] in [21, 22, 24]:
status = 'failed' status = 'failed'
release_downloads.append({ release_downloads.append({
'id': nzb['id'], 'id': nzb['id'],
'name': nzb['uiTitle'], 'name': nzb['uiTitle'],
'status': status, 'status': status,
'original_status': nzb['state'], 'original_status': nzb['state'],
'timeleft':-1, 'timeleft':-1,
'folder': sp(nzb['destinationPath']), 'folder': sp(nzb['destinationPath']),
}) })
return release_downloads return release_downloads
@@ -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())
@@ -58,6 +58,14 @@ config = [{
'advanced': True, 'advanced': True,
'description': 'Also remove the leftover files.', 'description': 'Also remove the leftover files.',
}, },
{
'name': 'append_label',
'label': 'Append Label',
'default': False,
'advanced': True,
'type': 'bool',
'description': 'Append label to download location. Requires you to set the download location above.',
},
{ {
'name': 'paused', 'name': 'paused',
'type': 'bool', 'type': 'bool',
+26 -28
View File
@@ -77,10 +77,7 @@ class rTorrent(Downloader):
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():
@@ -128,7 +125,9 @@ class rTorrent(Downloader):
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'): if self.conf('directory') and self.conf('append_label'):
torrent.set_directory(os.path.join(self.conf('directory'), self.conf('label')))
elif self.conf('directory'):
torrent.set_directory(self.conf('directory')) torrent.set_directory(self.conf('directory'))
# Set Ratio Group # Set Ratio Group
@@ -143,11 +142,11 @@ 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()
@@ -155,34 +154,33 @@ class rTorrent(Downloader):
release_downloads = ReleaseDownloadList(self) release_downloads = ReleaseDownloadList(self)
for torrent in torrents: for torrent in torrents:
if torrent.info_hash in ids: torrent_files = []
torrent_files = [] for file_item in torrent.get_files():
for file_item in torrent.get_files(): torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
status = 'busy' status = 'busy'
if torrent.complete: if torrent.complete:
if torrent.active: if torrent.active:
status = 'seeding' status = 'seeding'
else: else:
status = 'completed' status = 'completed'
release_downloads.append({ release_downloads.append({
'id': torrent.info_hash, 'id': torrent.info_hash,
'name': torrent.name, 'name': torrent.name,
'status': status, 'status': status,
'seed_ratio': torrent.ratio, 'seed_ratio': torrent.ratio,
'original_status': torrent.state, 'original_status': torrent.state,
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1, 'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
'folder': sp(torrent.directory), 'folder': sp(torrent.directory),
'files': '|'.join(torrent_files) 'files': '|'.join(torrent_files)
}) })
return release_downloads return release_downloads
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, release_download, pause = True):
if not self.connect(): if not self.connect():
+32 -33
View File
@@ -16,8 +16,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 +25,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 +36,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 +64,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 +75,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,42 +85,41 @@ 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) release_downloads = ReleaseDownloadList(self)
# Get busy releases # Get busy releases
for nzb in queue.get('slots', []): for nzb in queue.get('slots', []):
if nzb['nzo_id'] in ids: status = 'busy'
status = 'busy' if 'ENCRYPTED / ' in nzb['filename']:
if 'ENCRYPTED / ' in nzb['filename']: status = 'failed'
status = 'failed'
release_downloads.append({ release_downloads.append({
'id': nzb['nzo_id'], 'id': nzb['nzo_id'],
'name': nzb['filename'], 'name': nzb['filename'],
'status': status, 'status': status,
'original_status': nzb['status'], 'original_status': nzb['status'],
'timeleft': nzb['timeleft'] if not queue['paused'] else -1, 'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
}) })
# Get old releases # Get old releases
for nzb in history.get('slots', []): for nzb 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({ status = 'busy'
'id': nzb['nzo_id'], if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()):
'name': nzb['name'], status = 'failed'
'status': status, elif nzb['status'] == 'Completed':
'original_status': nzb['status'], status = 'completed'
'timeleft': str(timedelta(seconds = 0)),
'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']), 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 return release_downloads
@@ -11,10 +11,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
@@ -42,7 +42,7 @@ class Synology(Downloader):
except: except:
log.error('Exception while adding torrent: %s', traceback.format_exc()) log.error('Exception while adding torrent: %s', traceback.format_exc())
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':
@@ -31,9 +31,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,12 +81,12 @@ 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) release_downloads = ReleaseDownloadList(self)
@@ -99,35 +97,34 @@ class Transmission(Downloader):
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 torrent 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', (torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent['eta'], torrent['uploadRatio'], torrent['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 = [] torrent_files = []
for file_item in torrent['files']: for file_item in torrent['files']:
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name']))) torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name'])))
status = 'busy' status = 'busy'
if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'): if torrent.get('isStalled') and self.conf('stalled_as_failed'):
status = 'failed' status = 'failed'
elif torrent['status'] == 0 and torrent['percentDone'] == 1: elif torrent['status'] == 0 and torrent['percentDone'] == 1:
status = 'completed' status = 'completed'
elif torrent['status'] in [5, 6]: elif torrent['status'] in [5, 6]:
status = 'seeding' status = 'seeding'
release_downloads.append({ release_downloads.append({
'id': torrent['hashString'], 'id': torrent['hashString'],
'name': torrent['name'], 'name': torrent['name'],
'status': status, 'status': status,
'original_status': torrent['status'], 'original_status': torrent['status'],
'seed_ratio': torrent['uploadRatio'], 'seed_ratio': torrent['uploadRatio'],
'timeleft': str(timedelta(seconds = torrent['eta'])), 'timeleft': str(timedelta(seconds = torrent['eta'])),
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])), 'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
'files': '|'.join(torrent_files) 'files': '|'.join(torrent_files)
}) })
return release_downloads return release_downloads
+65 -66
View File
@@ -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
@@ -115,62 +104,72 @@ class uTorrent(Downloader):
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) release_downloads = ReleaseDownloadList(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 torrent in queue['torrents']:
if torrent[0] in ids:
#Get files of the torrent #Get files of the torrent
torrent_files = [] torrent_files = []
try: try:
torrent_files = json.loads(self.utorrent_api.get_files(torrent[0])) torrent_files = json.loads(self.utorrent_api.get_files(torrent[0]))
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]] torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]]
except: except:
log.debug('Failed getting files from torrent: %s', torrent[2]) log.debug('Failed getting files from torrent: %s', torrent[2])
status = 'busy' status_flags = {
if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000: "STARTED" : 1,
status = 'seeding' "CHECKING" : 2,
elif (torrent[1] & self.status_flags['ERROR']): "CHECK-START" : 4,
status = 'failed' "CHECKED" : 8,
elif torrent[4] == 1000: "ERROR" : 16,
status = 'completed' "PAUSED" : 32,
"QUEUED" : 64,
"LOADED" : 128
}
if not status == 'busy': status = 'busy'
self.removeReadOnly(torrent_files) if (torrent[1] & status_flags["STARTED"] or torrent[1] & status_flags["QUEUED"]) and torrent[4] == 1000:
status = 'seeding'
elif (torrent[1] & status_flags["ERROR"]):
status = 'failed'
elif torrent[4] == 1000:
status = 'completed'
release_downloads.append({ if not status == 'busy':
'id': torrent[0], self.removeReadOnly(torrent_files)
'name': torrent[2],
'status': status, release_downloads.append({
'seed_ratio': float(torrent[7]) / 1000, 'id': torrent[0],
'original_status': torrent[1], 'name': torrent[2],
'timeleft': str(timedelta(seconds = torrent[10])), 'status': status,
'folder': sp(torrent[26]), 'seed_ratio': float(torrent[7]) / 1000,
'files': '|'.join(torrent_files) 'original_status': torrent[1],
}) 'timeleft': str(timedelta(seconds = torrent[10])),
'folder': sp(torrent[26]),
'files': '|'.join(torrent_files)
})
return release_downloads return release_downloads
@@ -223,7 +222,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,52 +242,52 @@ 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))
@@ -320,5 +319,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)
+1 -22
View File
@@ -49,29 +49,8 @@ def ss(original, *args):
return u_original.encode('UTF-8') return u_original.encode('UTF-8')
def sp(path, *args): def sp(path, *args):
# Standardise encoding, normalise case, path and strip trailing '/' or '\' # Standardise encoding, normalise case, path and strip trailing '/' or '\'
if not path or len(path) == 0: return os.path.normcase(os.path.normpath(ss(path, *args))).rstrip(os.path.sep)
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)):
+2 -9
View File
@@ -2,7 +2,7 @@ from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import collections 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 +11,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
@@ -170,7 +167,7 @@ def natcmp(a, b):
return cmp(natsortKey(a), natsortKey(b)) return cmp(natsortKey(a), natsortKey(b))
def toIterable(value): def toIterable(value):
if type(value) in [list, tuple]: if isinstance(value, collections.Iterable):
return value return value
return [value] return [value]
@@ -219,7 +216,3 @@ def splitString(str, split_on = ',', clean = True):
def dictIsSubset(a, b): def dictIsSubset(a, b):
return all([k in b and b[k] == v for k, v in a.items()]) 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
+2 -10
View File
@@ -1,6 +1,5 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Media from couchpotato.core.settings.model import Media
@@ -18,13 +17,6 @@ class MediaBase(Plugin):
'category': {}, '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)
@@ -36,7 +28,7 @@ class MediaBase(Plugin):
def onComplete(): def onComplete():
db = get_session() db = get_session()
media = db.query(Media).filter_by(id = id).first() 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)) fireEventAsync('%s.searcher.single' % media.type, media.to_dict(self.default_dict), on_complete = self.createNotifyFront(id))
db.expire_all() db.expire_all()
return onComplete return onComplete
@@ -46,7 +38,7 @@ class MediaBase(Plugin):
def notifyFront(): def notifyFront():
db = get_session() db = get_session()
media = db.query(Media).filter_by(id = media_id).first() 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)) fireEvent('notify.frontend', type = '%s.update.%s' % (media.type, media.id), data = media.to_dict(self.default_dict))
db.expire_all() db.expire_all()
return notifyFront 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
+5 -414
View File
@@ -1,15 +1,10 @@
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.variable import splitString
from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase from couchpotato.core.media import MediaBase
from couchpotato.core.settings.model import Library, LibraryTitle, Release, \ from couchpotato.core.settings.model import Media
Media
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_, desc
from string import ascii_lowercase
log = CPLog(__name__) log = CPLog(__name__)
@@ -25,49 +20,7 @@ class MediaPlugin(MediaBase):
} }
}) })
addApiView('media.list', self.listView, docs = { addEvent('app.load', self.addSingleRefresh)
'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): def refresh(self, id = '', **kwargs):
db = get_session() db = get_session()
@@ -81,7 +34,7 @@ class MediaPlugin(MediaBase):
for title in media.library.titles: for title in media.library.titles:
if title.default: default_title = title.title if title.default: default_title = title.title
fireEvent('notify.frontend', type = '%s.busy' % media.type, data = {'id': x}) fireEvent('notify.frontend', type = '%s.busy.%s' % (media.type, x), data = True)
fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(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() db.expire_all()
@@ -90,369 +43,7 @@ class MediaPlugin(MediaBase):
'success': True, 'success': True,
} }
def addSingleRefreshView(self): def addSingleRefresh(self):
for media_type in fireEvent('media.types', merge = True): for media_type in fireEvent('media.types', merge = True):
addApiView('%s.refresh' % media_type, self.refresh) 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,17 +1,11 @@
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
from couchpotato.core.helpers.variable import md5, getTitle, splitString from couchpotato.core.helpers.variable import splitString
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.environment import Env
from inspect import ismethod, isfunction
import datetime import datetime
import re import re
import time
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -171,7 +165,7 @@ class Searcher(SearcherBase):
return False return False
def correctWords(self, rel_name, media): def correctWords(self, rel_name, media):
media_title = fireEvent('library.title', media['library'], single = True) media_title = fireEvent('searcher.get_search_title', media, single = True)
media_words = re.split('\W+', simplifyString(media_title)) media_words = re.split('\W+', simplifyString(media_title))
rel_name = simplifyString(rel_name) rel_name = simplifyString(rel_name)
+367 -9
View File
@@ -2,10 +2,15 @@ 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
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, Media, \
Release
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__)
@@ -21,12 +26,33 @@ class MovieBase(MovieTypeBase):
super(MovieBase, self).__init__() super(MovieBase, self).__init__()
self.initType() self.initType()
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.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 +61,258 @@ 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)
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(Media).filter(Media.library.has(identifier = imdb_id)).first()
else:
m = db.query(Media).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(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))
# 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 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(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_(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(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))
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 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 = {}
@@ -149,9 +421,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(Media).filter_by(id = movie_id).first()
if not m: if not m:
continue continue
@@ -174,12 +446,98 @@ 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(Media).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(Media).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
@@ -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();
@@ -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),
@@ -126,9 +126,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();
@@ -254,11 +252,9 @@ MA.Release = new Class({
} }
var update_handle = function(notification) { var update_handle = function(notification) {
if(notification.data.id != release.id) return;
var q = self.movie.quality.getElement('.q_id' + release.quality_id), var q = self.movie.quality.getElement('.q_id' + release.quality_id),
status = Status.get(release.status_id), status = Status.get(release.status_id),
new_status = Status.get(notification.data.status_id); new_status = Status.get(notification.data);
release.status_id = new_status.id release.status_id = new_status.id
release.el.set('class', 'item ' + new_status.identifier); release.el.set('class', 'item ' + new_status.identifier);
@@ -276,7 +272,7 @@ MA.Release = new Class({
} }
} }
App.on('release.update_status', update_handle); App.addEvent('release.update_status.' + release.id, update_handle);
}); });
@@ -431,7 +427,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 +446,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', {
@@ -821,7 +817,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
@@ -1036,7 +1036,7 @@
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 == '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)
}
});
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;
@@ -181,7 +181,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(
+13 -12
View File
@@ -30,6 +30,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
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) addEvent('searcher.correct_release', self.correctRelease)
addEvent('searcher.get_search_title', self.getSearchTitle)
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',
@@ -144,10 +145,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
@@ -191,7 +192,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,7 +202,7 @@ 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
@@ -209,7 +210,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
if media.get('type') != 'movie': return if media.get('type') != 'movie': return
media_title = fireEvent('library.title', media['library'], single = True) media_title = fireEvent('searcher.get_search_title', media, 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')
@@ -283,10 +284,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 +318,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 +333,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)
@@ -346,5 +343,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
log.error('Failed searching for next release: %s', traceback.format_exc()) log.error('Failed searching for next release: %s', traceback.format_exc())
return False return False
def getSearchTitle(self, media):
if media['type'] == 'movie':
return getTitle(media['library'])
class SearchSetupError(Exception): class SearchSetupError(Exception):
pass pass
@@ -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
+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
+1 -10
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')
@@ -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);
}) })
@@ -4,7 +4,6 @@ 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 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
@@ -31,8 +30,6 @@ class Email(Notification):
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
+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,39 +0,0 @@
from .main import Pushbullet
def start():
return Pushbullet()
config = [{
'name': 'pushbullet',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'pushbullet',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'api_key',
'label': 'User API Key'
},
{
'name': 'devices',
'default': '',
'advanced': True,
'description': 'IDs of devices to send notifications to, empty = all devices'
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]
@@ -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 []
@@ -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',
+1 -1
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
+58 -37
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, sp
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 = urllib2.quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
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(toUnicode(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 = sp(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 ''
+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
+8 -7
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']))
+15 -62
View File
@@ -2,7 +2,7 @@ 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, ss
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
@@ -19,32 +19,14 @@ class QualityPlugin(Plugin):
{'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', 'x264', 'h264']},
{'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'], 'tags': ['x264', 'h264']},
{'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': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r')]},
{'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', 'dvdrip', '720p', '1080p'], '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']
@@ -68,8 +50,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
@@ -185,10 +165,9 @@ 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()
@@ -249,6 +228,11 @@ class QualityPlugin(Plugin):
if len(set(words) & set(alt)) == len(alt): 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)) log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
score += points.get(tag_type) score += points.get(tag_type)
elif len(set(words) & set(alt)) > 0:
partial = list(set(words) & set(alt))[0]
if len(partial) > 2:
log.debug('Found %s via partial %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
score += points.get(tag_type) / 3
if (isinstance(alt, (str, unicode)) and ss(alt.lower()) in cur_file.lower()): 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))
@@ -301,34 +285,3 @@ class QualityPlugin(Plugin):
if add_score != 0: if add_score != 0:
for allow in quality.get('allow', []): for allow in quality.get('allow', []):
score[allow] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5 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)))
+89 -105
View File
@@ -100,14 +100,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(Media).filter_by(library_id = group['library'].get('id')).first()
if not media: if not movie:
media = Media( movie = Media(
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 +120,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,7 +142,7 @@ 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
@@ -211,136 +211,119 @@ class Release(Plugin):
db = get_session() db = get_session()
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.manual_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 = self.download(data = item, media = 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)
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 @RuudBurger why do we need to get it again??
'library': {'titles': {}, 'files':{}},
'files': {}
}), manual = True)
if success == True: fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name'])
db.expunge_all() return {
rel = db.query(Relea).filter_by(id = id).first() # Get release again @RuudBurger why do we need to get it again?? '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): def download(self, data, media, manual = False):
# Backwards compatibility code
if not data.get('protocol'): if not data.get('protocol'):
data['protocol'] = data['type'] data['protocol'] = data['type']
data['type'] = 'movie' data['type'] = 'movie'
# Test to see if any downloaders are enabled for this type # Test to see if any downloaders are enabled for this type
downloader_enabled = fireEvent('download.enabled', manual, data, single = True) 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 if downloader_enabled:
filedata = None snatched_status, done_status, active_status = fireEvent('status.get', ['snatched', 'done', 'active'], single = True)
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 release to temp
download_result = fireEvent('download', data = data, media = media, manual = manual, filedata = filedata, single = True) filedata = None
if not download_result: if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
log.info('Tried to download, but the "%s" downloader gave an error', data.get('protocol')) filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
return False if filedata == 'try_next':
log.debug('Downloader result: %s', download_result) return filedata
snatched_status, done_status, downloaded_status, active_status = fireEvent('status.get', ['snatched', 'done', 'downloaded', 'active'], single = True) download_result = fireEvent('download', data = data, movie = media, manual = manual, filedata = filedata, single = True)
log.debug('Downloader result: %s', download_result)
try: if download_result:
db = get_session() try:
rls = db.query(Relea).filter_by(identifier = md5(data['url'])).first() # Mark release as snatched
if not rls: db = get_session()
log.error('No release found to store download information in') rls = db.query(Relea).filter_by(identifier = md5(data['url'])).first()
return False if rls:
renamer_enabled = Env.setting('enabled', 'renamer')
renamer_enabled = Env.setting('enabled', 'renamer') # Save download-id info if returned
if isinstance(download_result, dict):
# Save download-id info if returned for key in download_result:
if isinstance(download_result, dict): rls_info = ReleaseInfo(
for key in download_result: identifier = 'download_%s' % key,
rls_info = ReleaseInfo( value = toUnicode(download_result.get(key))
identifier = 'download_%s' % key, )
value = toUnicode(download_result.get(key)) rls.info.append(rls_info)
)
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() db.commit()
return True 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())
# Assume release downloaded # If renamer isn't used, mark media done
self.updateStatus(rls.id, status = downloaded_status) if not renamer_enabled:
try:
if media['status_id'] == active_status.get('id'):
for profile_type in media['profile']['types']:
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking media as finished: %s', log_movie)
except: # Mark release done
log.error('Failed storing download status: %s', traceback.format_exc()) self.updateStatus(rls.id, status = done_status)
return False
return True # 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()
except:
log.error('Failed marking media finished, renamer disabled: %s', traceback.format_exc())
else:
self.updateStatus(rls.id, status = snatched_status)
except:
log.error('Failed marking media 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 tryDownloadResult(self, results, media, quality_type, manual = False): def tryDownloadResult(self, results, media, quality_type, manual = False):
ignored_status, failed_status = fireEvent('status.get', ['ignored', 'failed'], single = True) ignored_status, failed_status = fireEvent('status.get', ['ignored', 'failed'], single = True)
@@ -382,7 +365,8 @@ class Release(Plugin):
if not rls: if not rls:
rls = Relea( rls = Relea(
identifier = rel_identifier, identifier = rel_identifier,
media_id = media.get('id'), movie_id = media.get('id'),
#media_id = media.get('id'),
quality_id = quality_type.get('quality_id'), quality_id = quality_type.get('quality_id'),
status_id = available_status.get('id') status_id = available_status.get('id')
) )
@@ -419,7 +403,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]
@@ -462,6 +446,6 @@ class Release(Plugin):
db.commit() db.commit()
#Update all movie info as there is no release update function #Update all movie info as there is no release update function
fireEvent('notify.frontend', type = 'release.update_status', data = rel.to_dict()) fireEvent('notify.frontend', type = 'release.update_status.%s' % rel.id, data = status.get('id'))
return True return True
+4 -4
View File
@@ -93,7 +93,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 +122,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 +136,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,
}, },
{ {
+173 -234
View File
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss, sp from couchpotato.core.helpers.encoding import toUnicode, ss, sp
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
getImdb, link, symlink, tryInt, splitString, fnEscape, isSubFolder getImdb, link, symlink, tryInt, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, File, Profile, Release, \ from couchpotato.core.settings.model import Library, File, Profile, Release, \
@@ -30,11 +30,10 @@ class Renamer(Plugin):
'desc': 'For the renamer to check for new files to rename in a folder', 'desc': 'For the renamer to check for new files to rename in a folder',
'params': { 'params': {
'async': {'desc': 'Optional: Set to 1 if you dont want to fire the renamer.scan asynchronous.'}, 'async': {'desc': 'Optional: Set to 1 if you dont want to fire the renamer.scan asynchronous.'},
'media_folder': {'desc': 'Optional: The folder of the media to scan. Keep empty for default renamer folder.'}, 'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'},
'files': {'desc': 'Optional: Provide the release files if more releases are in the same media_folder, delimited with a \'|\'. Note that no dedicated release folder is expected for releases with one file.'}, 'files': {'desc': 'Optional: Provide the release files if more releases are in the same movie_folder, delimited with a \'|\'. Note that no dedicated release folder is expected for releases with one file.'},
'base_folder': {'desc': 'Optional: The folder to find releases in. Leave empty for default folder.'},
'downloader' : {'desc': 'Optional: The downloader the release has been downloaded with. \'download_id\' is required with this option.'}, 'downloader' : {'desc': 'Optional: The downloader the release has been downloaded with. \'download_id\' is required with this option.'},
'download_id': {'desc': 'Optional: The nzb/torrent ID of the release in media_folder. \'downloader\' is required with this option.'}, 'download_id': {'desc': 'Optional: The nzb/torrent ID of the release in movie_folder. \'downloader\' is required with this option.'},
'status': {'desc': 'Optional: The status of the release: \'completed\' (default) or \'seeding\''}, 'status': {'desc': 'Optional: The status of the release: \'completed\' (default) or \'seeding\''},
}, },
}) })
@@ -65,33 +64,25 @@ class Renamer(Plugin):
def scanView(self, **kwargs): def scanView(self, **kwargs):
async = tryInt(kwargs.get('async', 0)) async = tryInt(kwargs.get('async', 0))
base_folder = kwargs.get('base_folder') movie_folder = sp(kwargs.get('movie_folder'))
media_folder = sp(kwargs.get('media_folder'))
# Backwards compatibility, to be removed after a few versions :)
if not media_folder:
media_folder = sp(kwargs.get('movie_folder'))
downloader = kwargs.get('downloader') downloader = kwargs.get('downloader')
download_id = kwargs.get('download_id') download_id = kwargs.get('download_id')
files = '|'.join([sp(filename) for filename in splitString(kwargs.get('files'), '|')]) files = '|'.join([sp(filename) for filename in splitString(kwargs.get('files'), '|')])
status = kwargs.get('status', 'completed') status = kwargs.get('status', 'completed')
release_download = None release_download = {'folder': movie_folder} if movie_folder else None
if not base_folder and media_folder: if release_download:
release_download = {'folder': media_folder}
release_download.update({'id': download_id, 'downloader': downloader, 'status': status, 'files': files} if download_id else {}) release_download.update({'id': download_id, 'downloader': downloader, 'status': status, 'files': files} if download_id else {})
fire_handle = fireEvent if not async else fireEventAsync fire_handle = fireEvent if not async else fireEventAsync
fire_handle('renamer.scan', base_folder = base_folder, release_download = release_download) fire_handle('renamer.scan', release_download)
return { return {
'success': True 'success': True
} }
def scan(self, base_folder = None, release_download = None): def scan(self, release_download = None):
if not release_download: release_download = {}
if self.isDisabled(): if self.isDisabled():
return return
@@ -100,14 +91,11 @@ class Renamer(Plugin):
log.info('Renamer is already running, if you see this often, check the logs above for errors.') log.info('Renamer is already running, if you see this often, check the logs above for errors.')
return return
if not base_folder:
base_folder = self.conf('from')
from_folder = sp(self.conf('from')) from_folder = sp(self.conf('from'))
to_folder = sp(self.conf('to')) to_folder = sp(self.conf('to'))
# Get media folder to process # Get movie folder to process
media_folder = release_download.get('folder') movie_folder = release_download and release_download.get('folder')
# Get all folders that should not be processed # Get all folders that should not be processed
no_process = [to_folder] no_process = [to_folder]
@@ -120,73 +108,73 @@ class Renamer(Plugin):
pass pass
# Check to see if the no_process folders are inside the "from" folder. # Check to see if the no_process folders are inside the "from" folder.
if not os.path.isdir(base_folder) or not os.path.isdir(to_folder): if not os.path.isdir(from_folder) or not os.path.isdir(to_folder):
log.error('Both the "To" and "From" folder have to exist.') log.error('Both the "To" and "From" have to exist.')
return return
else: else:
for item in no_process: for item in no_process:
if isSubFolder(item, base_folder): if from_folder in item:
log.error('To protect your data, the media libraries can\'t be inside of or the same as the "from" folder.') log.error('To protect your data, the movie libraries can\'t be inside of or the same as the "from" folder.')
return return
# Check to see if the no_process folders are inside the provided media_folder # Check to see if the no_process folders are inside the provided movie_folder
if media_folder and not os.path.isdir(media_folder): if movie_folder and not os.path.isdir(movie_folder):
log.debug('The provided media folder %s does not exist. Trying to find it in the \'from\' folder.', media_folder) log.debug('The provided movie folder %s does not exist. Trying to find it in the \'from\' folder.', movie_folder)
# Update to the from folder # Update to the from folder
if len(splitString(release_download.get('files'), '|')) == 1: if len(release_download.get('files')) == 1:
new_media_folder = from_folder new_movie_folder = from_folder
else: else:
new_media_folder = os.path.join(from_folder, os.path.basename(media_folder)) new_movie_folder = os.path.join(from_folder, os.path.basename(movie_folder))
if not os.path.isdir(new_media_folder): if not os.path.isdir(new_movie_folder):
log.error('The provided media folder %s does not exist and could also not be found in the \'from\' folder.', media_folder) log.error('The provided movie folder %s does not exist and could also not be found in the \'from\' folder.', movie_folder)
return return
# Update the files # Update the files
new_files = [os.path.join(new_media_folder, os.path.relpath(filename, media_folder)) for filename in splitString(release_download.get('files'), '|')] new_files = [os.path.join(new_movie_folder, os.path.relpath(filename, movie_folder)) for filename in splitString(release_download.get('files'), '|')]
if new_files and not os.path.isfile(new_files[0]): if new_files and not os.path.isfile(new_files[0]):
log.error('The provided media folder %s does not exist and its files could also not be found in the \'from\' folder.', media_folder) log.error('The provided movie folder %s does not exist and its files could also not be found in the \'from\' folder.', movie_folder)
return return
# Update release_download info to the from folder # Update release_download info to the from folder
log.debug('Release %s found in the \'from\' folder.', media_folder) log.debug('Release %s found in the \'from\' folder.', movie_folder)
release_download['folder'] = new_media_folder release_download['folder'] = new_movie_folder
release_download['files'] = '|'.join(new_files) release_download['files'] = '|'.join(new_files)
media_folder = new_media_folder movie_folder = new_movie_folder
if media_folder: if movie_folder:
for item in no_process: for item in no_process:
if isSubFolder(item, media_folder): if movie_folder in item:
log.error('To protect your data, the media libraries can\'t be inside of or the same as the provided media folder.') log.error('To protect your data, the movie libraries can\'t be inside of or the same as the provided movie folder.')
return return
# Make sure a checkSnatched marked all downloads/seeds as such # Make sure a checkSnatched marked all downloads/seeds as such
if not release_download and self.conf('run_every') > 0: if not release_download and self.conf('run_every') > 0:
self.checkSnatched(fire_scan = False) fireEvent('renamer.check_snatched')
self.renaming_started = True self.renaming_started = True
# make sure the media folder name is included in the search # make sure the movie folder name is included in the search
folder = None folder = None
files = [] files = []
if media_folder: if movie_folder:
log.info('Scanning media folder %s...', media_folder) log.info('Scanning movie folder %s...', movie_folder)
folder = os.path.dirname(media_folder) folder = os.path.dirname(movie_folder)
if release_download.get('files', ''): if release_download.get('files', ''):
files = splitString(release_download['files'], '|') files = splitString(release_download['files'], '|')
# If there is only one file in the torrent, the downloader did not create a subfolder # If there is only one file in the torrent, the downloader did not create a subfolder
if len(files) == 1: if len(files) == 1:
folder = media_folder folder = movie_folder
else: else:
# Get all files from the specified folder # Get all files from the specified folder
try: try:
for root, folders, names in os.walk(media_folder): for root, folders, names in os.walk(movie_folder):
files.extend([sp(os.path.join(root, name)) for name in names]) files.extend([sp(os.path.join(root, name)) for name in names])
except: except:
log.error('Failed getting files from %s: %s', (media_folder, traceback.format_exc())) log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc()))
db = get_session() db = get_session()
@@ -196,10 +184,10 @@ class Renamer(Plugin):
# Unpack any archives # Unpack any archives
extr_files = None extr_files = None
if self.conf('unrar'): if self.conf('unrar'):
folder, media_folder, files, extr_files = self.extractFiles(folder = folder, media_folder = media_folder, files = files, folder, movie_folder, files, extr_files = self.extractFiles(folder = folder, movie_folder = movie_folder, files = files,
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(release_download)) cleanup = self.conf('cleanup') and not self.downloadIsTorrent(release_download))
groups = fireEvent('scanner.scan', folder = folder if folder else base_folder, groups = fireEvent('scanner.scan', folder = folder if folder else from_folder,
files = files, release_download = release_download, return_ignored = False, single = True) or [] files = files, release_download = release_download, return_ignored = False, single = True) or []
folder_name = self.conf('folder_name') folder_name = self.conf('folder_name')
@@ -240,7 +228,7 @@ class Renamer(Plugin):
# Overwrite destination when set in category # Overwrite destination when set in category
destination = to_folder destination = to_folder
category_label = '' category_label = ''
for movie in library_ent.media: for movie in library_ent.movies:
if movie.category and movie.category.label: if movie.category and movie.category.label:
category_label = movie.category.label category_label = movie.category.label
@@ -414,13 +402,13 @@ class Renamer(Plugin):
remove_leftovers = True remove_leftovers = True
# Add it to the wanted list before we continue # Add it to the wanted list before we continue
if len(library_ent.media) == 0: if len(library_ent.movies) == 0:
profile = db.query(Profile).filter_by(core = True, label = group['meta_data']['quality']['label']).first() profile = db.query(Profile).filter_by(core = True, label = group['meta_data']['quality']['label']).first()
fireEvent('movie.add', params = {'identifier': group['library']['identifier'], 'profile_id': profile.id}, search_after = False) fireEvent('movie.add', params = {'identifier': group['library']['identifier'], 'profile_id': profile.id}, search_after = False)
db.expire_all() db.expire_all()
library_ent = db.query(Library).filter_by(identifier = group['library']['identifier']).first() library_ent = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
for movie in library_ent.media: for movie in library_ent.movies:
# Mark movie "done" once it's found the quality with the finish check # Mark movie "done" once it's found the quality with the finish check
try: try:
@@ -508,10 +496,7 @@ class Renamer(Plugin):
os.remove(src) os.remove(src)
parent_dir = os.path.dirname(src) parent_dir = os.path.dirname(src)
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and \ if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not parent_dir in [destination, movie_folder] and not from_folder in parent_dir:
not isSubFolder(destination, parent_dir) and not isSubFolder(media_folder, parent_dir) and \
not isSubFolder(parent_dir, base_folder):
delete_folders.append(parent_dir) delete_folders.append(parent_dir)
except: except:
@@ -543,7 +528,7 @@ class Renamer(Plugin):
self.tagRelease(group = group, tag = 'failed_rename') self.tagRelease(group = group, tag = 'failed_rename')
# Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent # Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent
if self.movieInFromFolder(media_folder) and self.downloadIsTorrent(release_download): if self.movieInFromFolder(movie_folder) and self.downloadIsTorrent(release_download):
self.tagRelease(group = group, tag = 'renamed_already') self.tagRelease(group = group, tag = 'renamed_already')
# Remove matching releases # Remove matching releases
@@ -555,12 +540,12 @@ class Renamer(Plugin):
log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc())) log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(release_download): if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(release_download):
if media_folder: if movie_folder:
# Delete the movie folder # Delete the movie folder
group_folder = media_folder group_folder = movie_folder
else: else:
# Delete the first empty subfolder in the tree relative to the 'from' folder # Delete the first empty subfolder in the tree relative to the 'from' folder
group_folder = sp(os.path.join(base_folder, os.path.relpath(group['parentdir'], base_folder).split(os.path.sep)[0])) group_folder = sp(os.path.join(from_folder, os.path.relpath(group['parentdir'], from_folder).split(os.path.sep)[0]))
try: try:
log.info('Deleting folder: %s', group_folder) log.info('Deleting folder: %s', group_folder)
@@ -628,11 +613,6 @@ Remove it if you want it to be renamed (again, or at least let it try again)
tag_files.extend([os.path.join(root, name) for name in names]) tag_files.extend([os.path.join(root, name) for name in names])
for filename in tag_files: for filename in tag_files:
# Dont tag .ignore files
if os.path.splitext(filename)[1] == '.ignore':
continue
tag_filename = '%s.%s.ignore' % (os.path.splitext(filename)[0], tag) tag_filename = '%s.%s.ignore' % (os.path.splitext(filename)[0], tag)
if not os.path.isfile(tag_filename): if not os.path.isfile(tag_filename):
self.createFile(tag_filename, text) self.createFile(tag_filename, text)
@@ -663,7 +643,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
# Match all found ignore files with the tag_files and delete if found # Match all found ignore files with the tag_files and delete if found
for tag_file in tag_files: for tag_file in tag_files:
ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*'))) ignore_file = fnmatch.filter(ignore_files, '%s.%s.ignore' % (re.escape(os.path.splitext(tag_file)[0]), tag if tag else '*'))
for filename in ignore_file: for filename in ignore_file:
try: try:
os.remove(filename) os.remove(filename)
@@ -696,7 +676,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
# Match all found ignore files with the tag_files and return True found # Match all found ignore files with the tag_files and return True found
for tag_file in tag_files: for tag_file in tag_files:
ignore_file = fnmatch.filter(ignore_files, fnEscape('%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*'))) ignore_file = fnmatch.filter(ignore_files, '%s.%s.ignore' % (os.path.splitext(tag_file)[0], tag if tag else '*'))
if ignore_file: if ignore_file:
return True return True
@@ -809,7 +789,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
except: except:
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc())) loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
def checkSnatched(self, fire_scan = True): def checkSnatched(self):
if self.checking_snatched: if self.checking_snatched:
log.debug('Already checking snatched') log.debug('Already checking snatched')
@@ -825,168 +805,126 @@ Remove it if you want it to be renamed (again, or at least let it try again)
Release.status_id.in_([snatched_status.get('id'), seeding_status.get('id'), missing_status.get('id')]) Release.status_id.in_([snatched_status.get('id'), seeding_status.get('id'), missing_status.get('id')])
).all() ).all()
if not rels:
#No releases found that need status checking
self.checking_snatched = False
return True
# Collect all download information with the download IDs from the releases
download_ids = []
no_status_support = []
try:
for rel in rels:
rel_dict = rel.to_dict({'info': {}})
if rel_dict['info'].get('download_id') and rel_dict['info'].get('download_downloader'):
download_ids.append({'id': rel_dict['info']['download_id'], 'downloader': rel_dict['info']['download_downloader']})
ds = rel_dict['info'].get('download_status_support')
if ds == False or ds == 'False':
no_status_support.append(ss(rel_dict['info'].get('download_downloader')))
except:
log.error('Error getting download IDs from database')
self.checking_snatched = False
return False
release_downloads = fireEvent('download.status', download_ids, merge = True) if download_ids else []
if len(no_status_support) > 0:
log.debug('Download status functionality is not implemented for one of the active downloaders: %s', no_status_support)
if not release_downloads:
if fire_scan:
self.scan()
self.checking_snatched = False
return True
scan_releases = [] scan_releases = []
scan_required = False scan_required = False
log.debug('Checking status snatched releases...') if rels:
log.debug('Checking status snatched releases...')
try: release_downloads = fireEvent('download.status', merge = True)
for rel in rels: if not release_downloads:
rel_dict = rel.to_dict({'info': {}}) log.debug('Download status functionality is not implemented for active downloaders.')
movie_dict = fireEvent('media.get', media_id = rel.media_id, single = True) scan_required = True
else:
try:
for rel in rels:
rel_dict = rel.to_dict({'info': {}})
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
if not isinstance(rel_dict['info'], (dict)): if not isinstance(rel_dict['info'], (dict)):
log.error('Faulty release found without any info, ignoring.') log.error('Faulty release found without any info, ignoring.')
fireEvent('release.update_status', rel.id, status = ignored_status, single = True)
continue
# Check if download ID is available
if not rel_dict['info'].get('download_id') or not rel_dict['info'].get('download_downloader'):
log.debug('Download status functionality is not implemented for downloader (%s) of release %s.', (rel_dict['info'].get('download_downloader', 'unknown'), rel_dict['info']['name']))
scan_required = True
# Continue with next release
continue
# Find release in downloaders
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
for release_download in release_downloads:
found_release = False
if rel_dict['info'].get('download_id'):
if release_download['id'] == rel_dict['info']['download_id'] and release_download['downloader'] == rel_dict['info']['download_downloader']:
log.debug('Found release by id: %s', release_download['id'])
found_release = True
break
else:
if release_download['name'] == nzbname or rel_dict['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']:
log.debug('Found release by release name or imdb ID: %s', release_download['name'])
found_release = True
break
if not found_release:
log.info('%s not found in downloaders', nzbname)
#Check status if already missing and for how long, if > 1 week, set to ignored else to missing
if rel.status_id == missing_status.get('id'):
if rel.last_edit < int(time.time()) - 7 * 24 * 60 * 60:
fireEvent('release.update_status', rel.id, status = ignored_status, single = True) fireEvent('release.update_status', rel.id, status = ignored_status, single = True)
else: continue
# Set the release to missing
fireEvent('release.update_status', rel.id, status = missing_status, single = True)
# Continue with next release # check status
continue nzbname = self.createNzbName(rel_dict['info'], movie_dict)
# Log that we found the release found = False
timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft'] for release_download in release_downloads:
log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft)) found_release = False
if rel_dict['info'].get('download_id'):
# Check status of release if release_download['id'] == rel_dict['info']['download_id'] and release_download['downloader'] == rel_dict['info']['download_downloader']:
if release_download['status'] == 'busy': log.debug('Found release by id: %s', release_download['id'])
# Set the release to snatched if it was missing before found_release = True
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
# Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading
if self.movieInFromFolder(release_download['folder']):
self.tagRelease(release_download = release_download, tag = 'downloading')
elif release_download['status'] == 'seeding':
#If linking setting is enabled, process release
if self.conf('file_action') != 'move' and not rel.status_id == seeding_status.get('id') and self.statusInfoComplete(release_download):
log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio']))
# Remove the downloading tag
self.untagRelease(release_download = release_download, tag = 'downloading')
# Scan and set the torrent to paused if required
release_download.update({'pause': True, 'scan': True, 'process_complete': False})
scan_releases.append(release_download)
else:
#let it seed
log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio']))
# Set the release to seeding
fireEvent('release.update_status', rel.id, status = seeding_status, single = True)
elif release_download['status'] == 'failed':
# Set the release to failed
fireEvent('release.update_status', rel.id, status = failed_status, single = True)
fireEvent('download.remove_failed', release_download, single = True)
if self.conf('next_on_failed'):
fireEvent('movie.searcher.try_next_release', media_id = rel.media_id)
elif release_download['status'] == 'completed':
log.info('Download of %s completed!', release_download['name'])
#Make sure the downloader sent over a path to look in
if self.statusInfoComplete(release_download):
# If the release has been seeding, process now the seeding is done
if rel.status_id == seeding_status.get('id'):
if self.conf('file_action') != 'move':
# Set the release to done as the movie has already been renamed
fireEvent('release.update_status', rel.id, status = downloaded_status, single = True)
# Allow the downloader to clean-up
release_download.update({'pause': False, 'scan': False, 'process_complete': True})
scan_releases.append(release_download)
else: else:
# Scan and Allow the downloader to clean-up if release_download['name'] == nzbname or rel_dict['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']:
release_download.update({'pause': False, 'scan': True, 'process_complete': True}) found_release = True
scan_releases.append(release_download)
else: if found_release:
# Set the release to snatched if it was missing before timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft']
fireEvent('release.update_status', rel.id, status = snatched_status, single = True) log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft))
# Remove the downloading tag if release_download['status'] == 'busy':
self.untagRelease(release_download = release_download, tag = 'downloading') # Set the release to snatched if it was missing before
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
# Scan and Allow the downloader to clean-up # Tag folder if it is in the 'from' folder and it will not be processed because it is still downloading
release_download.update({'pause': False, 'scan': True, 'process_complete': True}) if self.movieInFromFolder(release_download['folder']):
scan_releases.append(release_download) self.tagRelease(release_download = release_download, tag = 'downloading')
else:
scan_required = True
except: elif release_download['status'] == 'seeding':
log.error('Failed checking for release in downloader: %s', traceback.format_exc()) #If linking setting is enabled, process release
if self.conf('file_action') != 'move' and not rel.status_id == seeding_status.get('id') and self.statusInfoComplete(release_download):
log.info('Download of %s completed! It is now being processed while leaving the original files alone for seeding. Current ratio: %s.', (release_download['name'], release_download['seed_ratio']))
# Remove the downloading tag
self.untagRelease(release_download = release_download, tag = 'downloading')
# Scan and set the torrent to paused if required
release_download.update({'pause': True, 'scan': True, 'process_complete': False})
scan_releases.append(release_download)
else:
#let it seed
log.debug('%s is seeding with ratio: %s', (release_download['name'], release_download['seed_ratio']))
# Set the release to seeding
fireEvent('release.update_status', rel.id, status = seeding_status, single = True)
elif release_download['status'] == 'failed':
# Set the release to failed
fireEvent('release.update_status', rel.id, status = failed_status, single = True)
fireEvent('download.remove_failed', release_download, single = True)
if self.conf('next_on_failed'):
fireEvent('movie.searcher.try_next_release', movie_id = rel.movie_id)
elif release_download['status'] == 'completed':
log.info('Download of %s completed!', release_download['name'])
if self.statusInfoComplete(release_download):
# If the release has been seeding, process now the seeding is done
if rel.status_id == seeding_status.get('id'):
if self.conf('file_action') != 'move':
# Set the release to done as the movie has already been renamed
fireEvent('release.update_status', rel.id, status = downloaded_status, single = True)
# Allow the downloader to clean-up
release_download.update({'pause': False, 'scan': False, 'process_complete': True})
scan_releases.append(release_download)
else:
# Scan and Allow the downloader to clean-up
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
scan_releases.append(release_download)
else:
# Set the release to snatched if it was missing before
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
# Remove the downloading tag
self.untagRelease(release_download = release_download, tag = 'downloading')
# Scan and Allow the downloader to clean-up
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
scan_releases.append(release_download)
else:
scan_required = True
found = True
break
if not found:
log.info('%s not found in downloaders', nzbname)
#Check status if already missing and for how long, if > 1 week, set to ignored else to missing
if rel.status_id == missing_status.get('id'):
if rel.last_edit < int(time.time()) - 7 * 24 * 60 * 60:
fireEvent('release.update_status', rel.id, status = ignored_status, single = True)
else:
# Set the release to missing
fireEvent('release.update_status', rel.id, status = missing_status, single = True)
except:
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
# The following can either be done here, or inside the scanner if we pass it scan_items in one go # The following can either be done here, or inside the scanner if we pass it scan_items in one go
for release_download in scan_releases: for release_download in scan_releases:
@@ -994,7 +932,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
if release_download['scan']: if release_download['scan']:
if release_download['pause'] and self.conf('file_action') == 'link': if release_download['pause'] and self.conf('file_action') == 'link':
fireEvent('download.pause', release_download = release_download, pause = True, single = True) fireEvent('download.pause', release_download = release_download, pause = True, single = True)
self.scan(release_download = release_download) fireEvent('renamer.scan', release_download = release_download)
if release_download['pause'] and self.conf('file_action') == 'link': if release_download['pause'] and self.conf('file_action') == 'link':
fireEvent('download.pause', release_download = release_download, pause = False, single = True) fireEvent('download.pause', release_download = release_download, pause = False, single = True)
if release_download['process_complete']: if release_download['process_complete']:
@@ -1005,10 +943,11 @@ Remove it if you want it to be renamed (again, or at least let it try again)
# Ask the downloader to process the item # Ask the downloader to process the item
fireEvent('download.process_complete', release_download = release_download, single = True) fireEvent('download.process_complete', release_download = release_download, single = True)
if fire_scan and (scan_required or len(no_status_support) > 0): if scan_required:
self.scan() fireEvent('renamer.scan')
self.checking_snatched = False self.checking_snatched = False
return True return True
def extendReleaseDownload(self, release_download): def extendReleaseDownload(self, release_download):
@@ -1055,10 +994,10 @@ Remove it if you want it to be renamed (again, or at least let it try again)
def statusInfoComplete(self, release_download): def statusInfoComplete(self, release_download):
return release_download['id'] and release_download['downloader'] and release_download['folder'] return release_download['id'] and release_download['downloader'] and release_download['folder']
def movieInFromFolder(self, media_folder): def movieInFromFolder(self, movie_folder):
return media_folder and isSubFolder(media_folder, sp(self.conf('from'))) or not media_folder return movie_folder and sp(self.conf('from')) in sp(movie_folder) or not movie_folder
def extractFiles(self, folder = None, media_folder = None, files = None, cleanup = False): def extractFiles(self, folder = None, movie_folder = None, files = None, cleanup = False):
if not files: files = [] if not files: files = []
# RegEx for finding rar files # RegEx for finding rar files
@@ -1073,7 +1012,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
folder = from_folder folder = from_folder
check_file_date = True check_file_date = True
if media_folder: if movie_folder:
check_file_date = False check_file_date = False
if not files: if not files:
@@ -1169,18 +1108,18 @@ Remove it if you want it to be renamed (again, or at least let it try again)
if cleanup: if cleanup:
# Remove all left over folders # Remove all left over folders
log.debug('Removing old movie folder %s...', media_folder) log.debug('Removing old movie folder %s...', movie_folder)
self.deleteEmptyFolder(media_folder) self.deleteEmptyFolder(movie_folder)
media_folder = os.path.join(from_folder, os.path.relpath(media_folder, folder)) movie_folder = os.path.join(from_folder, os.path.relpath(movie_folder, folder))
folder = from_folder folder = from_folder
if extr_files: if extr_files:
files.extend(extr_files) files.extend(extr_files)
# Cleanup files and folder if media_folder was not provided # Cleanup files and folder if movie_folder was not provided
if not media_folder: if not movie_folder:
files = [] files = []
folder = None folder = None
return folder, media_folder, files, extr_files return folder, movie_folder, files, extr_files
+2 -3
View File
@@ -80,8 +80,7 @@ class Scanner(Plugin):
'hdtv': ['hdtv'] 'hdtv': ['hdtv']
} }
clean = '[ _\,\.\(\)\[\]\-]?(extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ clean = '[ _\,\.\(\)\[\]\-](extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
'|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
multipart_regex = [ multipart_regex = [
'[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1 '[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
'[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1 '[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1
@@ -455,7 +454,7 @@ class Scanner(Plugin):
data['resolution_width'] = meta.get('resolution_width', 720) data['resolution_width'] = meta.get('resolution_width', 720)
data['resolution_height'] = meta.get('resolution_height', 480) data['resolution_height'] = meta.get('resolution_height', 480)
data['audio_channels'] = meta.get('audio_channels', 2.0) data['audio_channels'] = meta.get('audio_channels', 2.0)
data['aspect'] = round(float(meta.get('resolution_width', 720)) / meta.get('resolution_height', 480), 2) data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480)
except: except:
log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc())) log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
pass pass
@@ -20,7 +20,7 @@ config = [{
}, },
{ {
'name': 'languages', 'name': 'languages',
'description': ('Comma separated, 2 letter country code.', 'Example: en, nl. See the codes at <a href="http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes">on Wikipedia</a>'), 'description': 'Comma separated, 2 letter country code. Example: en, nl. See the codes at <a href="http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes">on Wikipedia</a>',
}, },
# { # {
# 'name': 'automatic', # 'name': 'automatic',
+1 -1
View File
@@ -27,7 +27,7 @@ class Subtitle(Plugin):
library = db.query(Library).all() library = db.query(Library).all()
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
for movie in library.media: for movie in library.movies:
for release in movie.releases: for release in movie.releases:
@@ -59,7 +59,7 @@ config = [{
{ {
'name': 'automation_charts_boxoffice', 'name': 'automation_charts_boxoffice',
'type': 'bool', 'type': 'bool',
'label': 'Box office TOP 10', 'label': 'Box offce TOP 10',
'description': 'IMDB Box office <a href="http://www.imdb.com/chart/">TOP 10</a> chart', 'description': 'IMDB Box office <a href="http://www.imdb.com/chart/">TOP 10</a> chart',
'default': True, 'default': True,
}, },
+33 -28
View File
@@ -1,14 +1,16 @@
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \ from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
possibleTitles, toIterable possibleTitles, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from urlparse import urlparse from urlparse import urlparse
import cookielib
import json import json
import re import re
import time import time
import traceback import traceback
import urllib2
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
@@ -93,7 +95,7 @@ class Provider(Plugin):
def getHTMLData(self, url, **kwargs): def getHTMLData(self, url, **kwargs):
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('data', {}))) cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
return self.getCache(cache_key, url, **kwargs) return self.getCache(cache_key, url, **kwargs)
@@ -109,14 +111,13 @@ class YarrProvider(Provider):
sizeMb = ['mb', 'mib'] sizeMb = ['mb', 'mib']
sizeKb = ['kb', 'kib'] sizeKb = ['kb', 'kib']
last_login_check = None login_opener = None
last_login_check = 0
def __init__(self): def __init__(self):
addEvent('provider.enabled_protocols', self.getEnabledProtocol) addEvent('provider.enabled_protocols', self.getEnabledProtocol)
addEvent('provider.belongs_to', self.belongsTo) addEvent('provider.belongs_to', self.belongsTo)
addEvent('provider.search.%s.%s' % (self.protocol, self.type), self.search)
for type in toIterable(self.type):
addEvent('provider.search.%s.%s' % (self.protocol, type), self.search)
def getEnabledProtocol(self): def getEnabledProtocol(self):
if self.isEnabled(): if self.isEnabled():
@@ -128,30 +129,35 @@ class YarrProvider(Provider):
# Check if we are still logged in every hour # Check if we are still logged in every hour
now = time.time() now = time.time()
if self.last_login_check and self.last_login_check < (now - 3600): if self.login_opener and self.last_login_check < (now - 3600):
try: try:
output = self.urlopen(self.urls['login_check']) output = self.urlopen(self.urls['login_check'], opener = self.login_opener)
if self.loginCheckSuccess(output): if self.loginCheckSuccess(output):
self.last_login_check = now self.last_login_check = now
return True return True
except: pass else:
self.last_login_check = None self.login_opener = None
except:
self.login_opener = None
if self.last_login_check: if self.login_opener:
return True return True
try: try:
output = self.urlopen(self.urls['login'], data = self.getLoginParams()) cookiejar = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
output = self.urlopen(self.urls['login'], params = self.getLoginParams(), opener = opener)
if self.loginSuccess(output): if self.loginSuccess(output):
self.last_login_check = now self.last_login_check = now
self.login_opener = opener
return True return True
error = 'unknown' error = 'unknown'
except: except:
error = traceback.format_exc() error = traceback.format_exc()
self.last_login_check = None self.login_opener = None
log.error('Failed to login %s: %s', (self.getName(), error)) log.error('Failed to login %s: %s', (self.getName(), error))
return False return False
@@ -165,12 +171,12 @@ class YarrProvider(Provider):
try: try:
if not self.login(): if not self.login():
log.error('Failed downloading from %s', self.getName()) log.error('Failed downloading from %s', self.getName())
return self.urlopen(url) return self.urlopen(url, opener = self.login_opener)
except: except:
log.error('Failed downloading from %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed downloading from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self): def getLoginParams(self):
return {} return ''
def download(self, url = '', nzb_id = ''): def download(self, url = '', nzb_id = ''):
try: try:
@@ -180,7 +186,7 @@ class YarrProvider(Provider):
return 'try_next' return 'try_next'
def search(self, media, quality): def search(self, movie, quality):
if self.isDisabled(): if self.isDisabled():
return [] return []
@@ -192,17 +198,15 @@ class YarrProvider(Provider):
# Create result container # Create result container
imdb_results = hasattr(self, '_search') imdb_results = hasattr(self, '_search')
results = ResultList(self, media, quality, imdb_results = imdb_results) results = ResultList(self, movie, quality, imdb_results = imdb_results)
# Do search based on imdb id # Do search based on imdb id
if imdb_results: if imdb_results:
self._search(media, quality, results) self._search(movie, quality, results)
# Search possible titles # Search possible titles
else: else:
media_title = fireEvent('library.query', media['library'], single = True) for title in possibleTitles(getTitle(movie['library'])):
self._searchOnTitle(title, movie, quality, results)
for title in possibleTitles(media_title):
self._searchOnTitle(title, media, quality, results)
return results return results
@@ -245,7 +249,8 @@ class YarrProvider(Provider):
def getCatId(self, identifier): def getCatId(self, identifier):
for ids, qualities in self.cat_ids: for cats in self.cat_ids:
ids, qualities = cats
if identifier in qualities: if identifier in qualities:
return ids return ids
@@ -259,14 +264,14 @@ class ResultList(list):
result_ids = None result_ids = None
provider = None provider = None
media = None movie = None
quality = None quality = None
def __init__(self, provider, media, quality, **kwargs): def __init__(self, provider, movie, quality, **kwargs):
self.result_ids = [] self.result_ids = []
self.provider = provider self.provider = provider
self.media = media self.movie = movie
self.quality = quality self.quality = quality
self.kwargs = kwargs self.kwargs = kwargs
@@ -280,13 +285,13 @@ class ResultList(list):
new_result = self.fillResult(result) new_result = self.fillResult(result)
is_correct = fireEvent('searcher.correct_release', new_result, self.media, self.quality, is_correct = fireEvent('searcher.correct_release', new_result, self.movie, self.quality,
imdb_results = self.kwargs.get('imdb_results', False), single = True) imdb_results = self.kwargs.get('imdb_results', False), single = True)
if is_correct and new_result['id'] not in self.result_ids: if is_correct and new_result['id'] not in self.result_ids:
is_correct_weight = float(is_correct) is_correct_weight = float(is_correct)
new_result['score'] += fireEvent('score.calculate', new_result, self.media, single = True) new_result['score'] += fireEvent('score.calculate', new_result, self.movie, single = True)
old_score = new_result['score'] old_score = new_result['score']
new_result['score'] = int(old_score * is_correct_weight) new_result['score'] = int(old_score * is_correct_weight)
@@ -1,7 +1,7 @@
from .main import InfoResultModifier from .main import MovieResultModifier
def start(): def start():
return InfoResultModifier() return MovieResultModifier()
config = [] config = []
@@ -3,7 +3,6 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import mergeDicts, randomString from couchpotato.core.helpers.variable import mergeDicts, randomString
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.providers.base import MultiProvider
from couchpotato.core.settings.model import Library from couchpotato.core.settings.model import Library
import copy import copy
import traceback import traceback
@@ -11,17 +10,7 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class InfoResultModifier(MultiProvider): class MovieResultModifier(Plugin):
def getTypes(self):
return [Movie, Show]
class ModifierBase(Plugin):
pass
class Movie(ModifierBase):
default_info = { default_info = {
'tmdb_id': 0, 'tmdb_id': 0,
@@ -32,17 +21,14 @@ class Movie(ModifierBase):
'poster': [], 'poster': [],
'backdrop': [], 'backdrop': [],
'poster_original': [], 'poster_original': [],
'backdrop_original': [], 'backdrop_original': []
'actors': {}
}, },
'runtime': 0, 'runtime': 0,
'plot': '', 'plot': '',
'tagline': '', 'tagline': '',
'imdb': '', 'imdb': '',
'genres': [], 'genres': [],
'mpaa': None, 'mpaa': None
'actors': [],
'actor_roles': {}
} }
def __init__(self): def __init__(self):
@@ -105,13 +91,13 @@ class Movie(ModifierBase):
# Statuses # Statuses
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
for movie in l.media: for movie in l.movies:
if movie.status_id == active_status['id']: if movie.status_id == active_status['id']:
temp['in_wanted'] = fireEvent('media.get', movie.id, single = True) temp['in_wanted'] = fireEvent('movie.get', movie.id, single = True)
for release in movie.releases: for release in movie.releases:
if release.status_id == done_status['id']: if release.status_id == done_status['id']:
temp['in_library'] = fireEvent('media.get', movie.id, single = True) temp['in_library'] = fireEvent('movie.get', movie.id, single = True)
except: except:
log.error('Tried getting more info on searched movies: %s', traceback.format_exc()) log.error('Tried getting more info on searched movies: %s', traceback.format_exc())
@@ -124,7 +110,3 @@ class Movie(ModifierBase):
if result and result.get('imdb'): if result and result.get('imdb'):
return mergeDicts(result, self.getLibraryTags(result['imdb'])) return mergeDicts(result, self.getLibraryTags(result['imdb']))
return result return result
class Show(ModifierBase):
pass
-12
View File
@@ -3,15 +3,3 @@ from couchpotato.core.providers.base import Provider
class MovieProvider(Provider): class MovieProvider(Provider):
type = 'movie' type = 'movie'
class ShowProvider(Provider):
type = 'show'
class SeasonProvider(Provider):
type = 'season'
class EpisodeProvider(Provider):
type = 'episode'
@@ -74,7 +74,7 @@ class CouchPotatoApi(MovieProvider):
return True return True
def getInfo(self, identifier = None, **kwargs): def getInfo(self, identifier = None):
if not identifier: if not identifier:
return return
@@ -97,7 +97,7 @@ class CouchPotatoApi(MovieProvider):
if not ignore: ignore = [] if not ignore: ignore = []
if not movies: movies = [] if not movies: movies = []
suggestions = self.getJsonData(self.urls['suggest'], data = { suggestions = self.getJsonData(self.urls['suggest'], params = {
'movies': ','.join(movies), 'movies': ','.join(movies),
'ignore': ','.join(ignore), 'ignore': ','.join(ignore),
}, headers = self.getRequestHeaders()) }, headers = self.getRequestHeaders())
@@ -46,7 +46,7 @@ class OMDBAPI(MovieProvider):
return [] return []
def getInfo(self, identifier = None, **kwargs): def getInfo(self, identifier = None):
if not identifier: if not identifier:
return {} return {}
@@ -82,10 +82,6 @@ class OMDBAPI(MovieProvider):
if tmp_movie.get(key).lower() == 'n/a': if tmp_movie.get(key).lower() == 'n/a':
del movie[key] del movie[key]
# Ignore series from omdbapi for now, should we use this in the future?
if movie.get('Type') == "series":
return
year = tryInt(movie.get('Year', '')) year = tryInt(movie.get('Year', ''))
movie_data = { movie_data = {
@@ -11,8 +11,8 @@ log = CPLog(__name__)
class TheMovieDb(MovieProvider): class TheMovieDb(MovieProvider):
def __init__(self): def __init__(self):
#addEvent('info.search', self.search, priority = 2) addEvent('info.search', self.search, priority = 2)
#addEvent('movie.search', self.search, priority = 2) addEvent('movie.search', self.search, priority = 2)
addEvent('movie.info', self.getInfo, priority = 2) addEvent('movie.info', self.getInfo, priority = 2)
addEvent('movie.info_by_tmdb', self.getInfo) addEvent('movie.info_by_tmdb', self.getInfo)
@@ -45,7 +45,7 @@ class TheMovieDb(MovieProvider):
nr = 0 nr = 0
for movie in raw: for movie in raw:
results.append(self.parseMovie(movie, extended = False)) results.append(self.parseMovie(movie, with_titles = False))
nr += 1 nr += 1
if nr == limit: if nr == limit:
@@ -61,7 +61,7 @@ class TheMovieDb(MovieProvider):
return results return results
def getInfo(self, identifier = None, extended = True): def getInfo(self, identifier = None):
if not identifier: if not identifier:
return {} return {}
@@ -73,14 +73,14 @@ class TheMovieDb(MovieProvider):
try: try:
log.debug('Getting info: %s', cache_key) log.debug('Getting info: %s', cache_key)
movie = tmdb3.Movie(identifier) movie = tmdb3.Movie(identifier)
result = self.parseMovie(movie, extended = extended) result = self.parseMovie(movie)
self.setCache(cache_key, result) self.setCache(cache_key, result)
except: except:
pass pass
return result return result
def parseMovie(self, movie, extended = True): def parseMovie(self, movie, with_titles = True):
cache_key = 'tmdb.cache.%s' % movie.id cache_key = 'tmdb.cache.%s' % movie.id
movie_data = self.getCache(cache_key) movie_data = self.getCache(cache_key)
@@ -92,14 +92,6 @@ class TheMovieDb(MovieProvider):
poster_original = self.getImage(movie, type = 'poster', size = 'original') poster_original = self.getImage(movie, type = 'poster', size = 'original')
backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original') backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
images = {
'poster': [poster] if poster else [],
#'backdrop': [backdrop] if backdrop else [],
'poster_original': [poster_original] if poster_original else [],
'backdrop_original': [backdrop_original] if backdrop_original else [],
'actors': {}
}
# Genres # Genres
try: try:
genres = [genre.name for genre in movie.genres] genres = [genre.name for genre in movie.genres]
@@ -111,23 +103,18 @@ class TheMovieDb(MovieProvider):
if not movie.releasedate or year == '1900' or year.lower() == 'none': if not movie.releasedate or year == '1900' or year.lower() == 'none':
year = None year = None
# Gather actors data
actors = {}
if extended:
for cast_item in movie.cast:
try:
actors[toUnicode(cast_item.name)] = toUnicode(cast_item.character)
images['actors'][toUnicode(cast_item.name)] = self.getImage(cast_item, type = 'profile', size = 'original')
except:
log.debug('Error getting cast info for %s: %s', (cast_item, traceback.format_exc()))
movie_data = { movie_data = {
'type': 'movie', 'type': 'movie',
'via_tmdb': True, 'via_tmdb': True,
'tmdb_id': movie.id, 'tmdb_id': movie.id,
'titles': [toUnicode(movie.title)], 'titles': [toUnicode(movie.title)],
'original_title': movie.originaltitle, 'original_title': movie.originaltitle,
'images': images, 'images': {
'poster': [poster] if poster else [],
#'backdrop': [backdrop] if backdrop else [],
'poster_original': [poster_original] if poster_original else [],
'backdrop_original': [backdrop_original] if backdrop_original else [],
},
'imdb': movie.imdb, 'imdb': movie.imdb,
'runtime': movie.runtime, 'runtime': movie.runtime,
'released': str(movie.releasedate), 'released': str(movie.releasedate),
@@ -135,13 +122,12 @@ class TheMovieDb(MovieProvider):
'plot': movie.overview, 'plot': movie.overview,
'genres': genres, 'genres': genres,
'collection': getattr(movie.collection, 'name', None), 'collection': getattr(movie.collection, 'name', None),
'actor_roles': actors
} }
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v) movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
# Add alternative names # Add alternative names
if extended: if with_titles:
movie_data['titles'].append(movie.originaltitle) movie_data['titles'].append(movie.originaltitle)
for alt in movie.alternate_titles: for alt in movie.alternate_titles:
alt_name = alt.title alt_name = alt.title
@@ -159,7 +145,7 @@ class TheMovieDb(MovieProvider):
try: try:
image_url = getattr(movie, type).geturl(size = 'original') image_url = getattr(movie, type).geturl(size = 'original')
except: except:
log.debug('Failed getting %s.%s for "%s"', (type, size, movie)) log.debug('Failed getting %s.%s for "%s"', (type, size, movie.title))
return image_url return image_url
@@ -1,24 +0,0 @@
from .main import TheTVDb
def start():
return TheTVDb()
config = [{
'name': 'thetvdb',
'groups': [
{
'tab': 'providers',
'name': 'tmdb',
'label': 'TheTVDB',
'hidden': True,
'description': 'Used for all calls to TheTVDB.',
'options': [
{
'name': 'api_key',
'default': '7966C02F860586D2',
'label': 'Api Key',
},
],
},
],
}]
@@ -1,468 +0,0 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.info.base import ShowProvider
from couchpotato.environment import Env
from tvdb_api import tvdb_api, tvdb_exceptions
from datetime import datetime
import traceback
import os
log = CPLog(__name__)
# TODO: Consider grabbing zips to put less strain on tvdb
# TODO: Unicode stuff (check)
# TODO: Notigy frontend on error (tvdb down at monent)
# TODO: Expose apikey in setting so it can be changed by user
class TheTVDb(ShowProvider):
def __init__(self):
addEvent('info.search', self.search, priority = 1)
addEvent('show.search', self.search, priority = 1)
addEvent('show.info', self.getShowInfo, priority = 1)
addEvent('season.info', self.getSeasonInfo, priority = 1)
addEvent('episode.info', self.getEpisodeInfo, priority = 1)
self.tvdb_api_parms = {
'apikey': self.conf('api_key'),
'banners': True,
'language': 'en',
'cache': os.path.join(Env.get('cache_dir'), 'thetvdb_api'),
}
self._setup()
def _setup(self):
self.tvdb = tvdb_api.Tvdb(**self.tvdb_api_parms)
self.valid_languages = self.tvdb.config['valid_languages']
def search(self, q, limit = 12, language = 'en'):
''' Find show by name
show = { 'id': 74713,
'language': 'en',
'lid': 7,
'seriesid': '74713',
'seriesname': u'Breaking Bad',}
'''
if self.isDisabled():
return False
if language != self.tvdb_api_parms['language'] and language in self.valid_languages:
self.tvdb_api_parms['language'] = language
self._setup()
search_string = simplifyString(q)
cache_key = 'thetvdb.cache.%s.%s' % (search_string, limit)
results = self.getCache(cache_key)
if not results:
log.debug('Searching for show: %s', q)
raw = None
try:
raw = self.tvdb.search(search_string)
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed searching TheTVDB for "%s": %s', (search_string, traceback.format_exc()))
return False
results = []
if raw:
try:
nr = 0
for show_info in raw:
show = self.tvdb[int(show_info['id'])]
results.append(self._parseShow(show))
nr += 1
if nr == limit:
break
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
self.setCache(cache_key, results)
return results
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed parsing TheTVDB for "%s": %s', (show, traceback.format_exc()))
return False
return results
def getShow(self, identifier = None):
show = None
try:
log.debug('Getting show: %s', identifier)
show = self.tvdb[int(identifier)]
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed to getShowInfo for show id "%s": %s', (identifier, traceback.format_exc()))
return None
return show
def getShowInfo(self, identifier = None):
if not identifier:
return None
cache_key = 'thetvdb.cache.%s' % identifier
log.debug('Getting showInfo: %s', cache_key)
result = self.getCache(cache_key) or {}
if result:
return result
show = self.getShow(identifier = identifier)
if show:
result = self._parseShow(show)
self.setCache(cache_key, result)
return result
def getSeasonInfo(self, identifier = None, params = {}):
"""Either return a list of all seasons or a single season by number.
identifier is the show 'id'
"""
if not identifier:
return False
season_identifier = params.get('season_identifier', None)
# season_identifier must contain the 'show id : season number' since there is no tvdb id
# for season and we need a reference to both the show id and season number
if season_identifier:
try: season_identifier = int(season_identifier.split(':')[1])
except: return False
cache_key = 'thetvdb.cache.%s.%s' % (identifier, season_identifier)
log.debug('Getting SeasonInfo: %s', cache_key)
result = self.getCache(cache_key) or {}
if result:
return result
try:
show = self.tvdb[int(identifier)]
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed parsing TheTVDB SeasonInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
return False
result = []
for number, season in show.items():
if season_identifier is not None and number == season_identifier:
result = self._parseSeason(show, (number, season))
self.setCache(cache_key, result)
return result
else:
result.append(self._parseSeason(show, (number, season)))
self.setCache(cache_key, result)
return result
def getEpisodeInfo(self, identifier = None, params = {}):
"""Either return a list of all episodes or a single episode.
If episode_identifer contains an episode number to search for
"""
season_identifier = params.get('season_identifier', None)
episode_identifier = params.get('episode_identifier', None)
if not identifier and season_identifier is None:
return False
# season_identifier must contain the 'show id : season number' since there is no tvdb id
# for season and we need a reference to both the show id and season number
if season_identifier:
try:
identifier, season_identifier = season_identifier.split(':')
season_identifier = int(season_identifier)
except: return None
cache_key = 'thetvdb.cache.%s.%s.%s' % (identifier, episode_identifier, season_identifier)
log.debug('Getting EpisodeInfo: %s', cache_key)
result = self.getCache(cache_key) or {}
if result:
return result
try:
show = self.tvdb[int(identifier)]
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed parsing TheTVDB EpisodeInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
return False
result = []
for number, season in show.items():
if season_identifier is not None and number != season_identifier:
continue
for episode in season.values():
if episode_identifier is not None and episode['id'] == toUnicode(episode_identifier):
result = self._parseEpisode(show, episode)
self.setCache(cache_key, result)
return result
else:
result.append(self._parseEpisode(show, episode))
self.setCache(cache_key, result)
return result
def _parseShow(self, show):
"""
'actors': u'|Bryan Cranston|Aaron Paul|Dean Norris|RJ Mitte|Betsy Brandt|Anna Gunn|Laura Fraser|Jesse Plemons|Christopher Cousins|Steven Michael Quezada|Jonathan Banks|Giancarlo Esposito|Bob Odenkirk|',
'added': None,
'addedby': None,
'airs_dayofweek': u'Sunday',
'airs_time': u'9:00 PM',
'banner': u'http://thetvdb.com/banners/graphical/81189-g13.jpg',
'contentrating': u'TV-MA',
'fanart': u'http://thetvdb.com/banners/fanart/original/81189-28.jpg',
'firstaired': u'2008-01-20',
'genre': u'|Crime|Drama|Suspense|',
'id': u'81189',
'imdb_id': u'tt0903747',
'language': u'en',
'lastupdated': u'1376620212',
'network': u'AMC',
'networkid': None,
'overview': u"Walter White, a struggling high school chemistry teacher is diagnosed with advanced lung cancer. He turns to a life of crime, producing and selling methamphetamine accompanied by a former student, Jesse Pinkman with the aim of securing his family's financial future before he dies.",
'poster': u'http://thetvdb.com/banners/posters/81189-22.jpg',
'rating': u'9.3',
'ratingcount': u'473',
'runtime': u'60',
'seriesid': u'74713',
'seriesname': u'Breaking Bad',
'status': u'Continuing',
'zap2it_id': u'SH01009396'
"""
#
# NOTE: show object only allows direct access via
# show['id'], not show.get('id')
#
# TODO: Make sure we have a valid show id, not '' or None
#if len (show['id']) is 0:
# return None
## Images
poster = show['poster'] or None
backdrop = show['fanart'] or None
genres = [] if show['genre'] is None else show['genre'].strip('|').split('|')
if show['firstaired'] is not None:
try: year = datetime.strptime(show['firstaired'], '%Y-%m-%d').year
except: year = None
else:
year = None
try:
id = int(show['id'])
except:
id = None
show_data = {
'id': id,
'type': 'show',
'primary_provider': 'thetvdb',
'titles': [show['seriesname'] or u'', ],
'original_title': show['seriesname'] or u'',
'images': {
'poster': [poster] if poster else [],
'backdrop': [backdrop] if backdrop else [],
'poster_original': [],
'backdrop_original': [],
},
'year': year,
'genres': genres,
'imdb': show['imdb_id'] or None,
'zap2it_id': show['zap2it_id'] or None,
'seriesid': show['seriesid'] or None,
'network': show['network'] or None,
'networkid': show['networkid'] or None,
'airs_dayofweek': show['airs_dayofweek'] or None,
'airs_time': show['airs_time'] or None,
'firstaired': show['firstaired'] or None,
'released': show['firstaired'] or None,
'runtime': show['runtime'] or None,
'contentrating': show['contentrating'] or None,
'rating': show['rating'] or None,
'ratingcount': show['ratingcount'] or None,
'actors': show['actors'] or None,
'lastupdated': show['lastupdated'] or None,
'status': show['status'] or None,
'language': show['language'] or None,
}
show_data = dict((k, v) for k, v in show_data.iteritems() if v)
# Add alternative titles
try:
raw = self.tvdb.search(show['seriesname'])
if raw:
for show_info in raw:
if show_info['id'] == show_data['id'] and show_info.get('aliasnames', None):
for alt_name in show_info['aliasnames'].split('|'):
show_data['titles'].append(toUnicode(alt_name))
except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed searching TheTVDB for "%s": %s', (show['seriesname'], traceback.format_exc()))
return show_data
def _parseSeason(self, show, season_tuple):
"""
contains no data
"""
number, season = season_tuple
title = toUnicode('%s - Season %s' % (show['seriesname'] or u'', str(number)))
poster = []
try:
for id, data in show.data['_banners']['season']['season'].items():
if data.get('season', None) == str(number) and data['bannertype'] == 'season' and data['bannertype2'] == 'season':
poster.append(data.get('_bannerpath'))
break # Only really need one
except:
pass
try:
id = (show['id'] + ':' + str(number))
except:
id = None
# XXX: work on title; added defualt_title to fix an error
season_data = {
'id': id,
'type': 'season',
'primary_provider': 'thetvdb',
'titles': [title, ],
'original_title': title,
'via_thetvdb': True,
'parent_identifier': show['id'] or None,
'seasonnumber': str(number),
'images': {
'poster': poster,
'backdrop': [],
'poster_original': [],
'backdrop_original': [],
},
'year': None,
'genres': None,
'imdb': None,
}
season_data = dict((k, v) for k, v in season_data.iteritems() if v)
return season_data
def _parseEpisode(self, show, episode):
"""
('episodenumber', u'1'),
('thumb_added', None),
('rating', u'7.7'),
('overview',
u'Experienced waitress Max Black meets her new co-worker, former rich-girl Caroline Channing, and puts her skills to the test at an old but re-emerging Brooklyn diner. Despite her initial distaste for Caroline, Max eventually softens and the two team up for a new business venture.'),
('dvd_episodenumber', None),
('dvd_discid', None),
('combined_episodenumber', u'1'),
('epimgflag', u'7'),
('id', u'4099506'),
('seasonid', u'465948'),
('thumb_height', u'225'),
('tms_export', u'1374789754'),
('seasonnumber', u'1'),
('writer', u'|Michael Patrick King|Whitney Cummings|'),
('lastupdated', u'1371420338'),
('filename', u'http://thetvdb.com/banners/episodes/248741/4099506.jpg'),
('absolute_number', u'1'),
('ratingcount', u'102'),
('combined_season', u'1'),
('thumb_width', u'400'),
('imdb_id', u'tt1980319'),
('director', u'James Burrows'),
('dvd_chapter', None),
('dvd_season', None),
('gueststars',
u'|Brooke Lyons|Noah Mills|Shoshana Bush|Cale Hartmann|Adam Korson|Alex Enriquez|Matt Cook|Bill Parks|Eugene Shaw|Sergey Brusilovsky|Greg Lewis|Cocoa Brown|Nick Jameson|'),
('seriesid', u'248741'),
('language', u'en'),
('productioncode', u'296793'),
('firstaired', u'2011-09-19'),
('episodename', u'Pilot')]
"""
poster = episode.get('filename', [])
backdrop = []
genres = []
plot = "%s - %sx%s - %s" % (show['seriesname'] or u'',
episode.get('seasonnumber', u'?'),
episode.get('episodenumber', u'?'),
episode.get('overview', u''))
if episode.get('firstaired', None) is not None:
try: year = datetime.strptime(episode['firstaired'], '%Y-%m-%d').year
except: year = None
else:
year = None
try:
id = int(episode['id'])
except:
id = None
episode_data = {
'id': id,
'type': 'episode',
'primary_provider': 'thetvdb',
'via_thetvdb': True,
'thetvdb_id': id,
'titles': [episode.get('episodename', u''), ],
'original_title': episode.get('episodename', u'') ,
'images': {
'poster': [poster] if poster else [],
'backdrop': [backdrop] if backdrop else [],
'poster_original': [],
'backdrop_original': [],
},
'imdb': episode.get('imdb_id', None),
'runtime': None,
'released': episode.get('firstaired', None),
'year': year,
'plot': plot,
'genres': genres,
'parent_identifier': show['id'] or None,
'seasonnumber': episode.get('seasonnumber', None),
'episodenumber': episode.get('episodenumber', None),
'combined_episodenumber': episode.get('combined_episodenumber', None),
'absolute_number': episode.get('absolute_number', None),
'combined_season': episode.get('combined_season', None),
'productioncode': episode.get('productioncode', None),
'seriesid': episode.get('seriesid', None),
'seasonid': episode.get('seasonid', None),
'firstaired': episode.get('firstaired', None),
'thumb_added': episode.get('thumb_added', None),
'thumb_height': episode.get('thumb_height', None),
'thumb_width': episode.get('thumb_width', None),
'rating': episode.get('rating', None),
'ratingcount': episode.get('ratingcount', None),
'epimgflag': episode.get('epimgflag', None),
'dvd_episodenumber': episode.get('dvd_episodenumber', None),
'dvd_discid': episode.get('dvd_discid', None),
'dvd_chapter': episode.get('dvd_chapter', None),
'dvd_season': episode.get('dvd_season', None),
'tms_export': episode.get('tms_export', None),
'writer': episode.get('writer', None),
'director': episode.get('director', None),
'gueststars': episode.get('gueststars', None),
'lastupdated': episode.get('lastupdated', None),
'language': episode.get('language', None),
}
episode_data = dict((k, v) for k, v in episode_data.iteritems() if v)
return episode_data
#def getImage(self, show, type = 'poster', size = 'cover'):
#""""""
## XXX: Need to implement size
#image_url = ''
#for res, res_data in show['_banners'].get(type, {}).items():
#for bid, banner_info in res_data.items():
#image_url = banner_info.get('_bannerpath', '')
#break
#return image_url
def isDisabled(self):
if self.conf('api_key') == '':
log.error('No API key provided.')
True
else:
False
@@ -1,24 +0,0 @@
from .main import Xem
def start():
return Xem()
config = [{
'name': 'xem',
'groups': [
{
'tab': 'providers',
'name': 'xem',
'label': 'TheXem',
'hidden': True,
'description': 'Used for all calls to TheXem.',
'options': [
{
'name': 'enabled',
'default': True,
'label': 'Enabled',
},
],
},
],
}]
-184
View File
@@ -1,184 +0,0 @@
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.info.base import ShowProvider
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
import traceback
log = CPLog(__name__)
class Xem(ShowProvider):
'''
Mapping Information
===================
Single
------
You will need the id / identifier of the show e.g. tvdb-id for American Dad! is 73141
the origin is the name of the site/entity the episode, season (and/or absolute) numbers are based on
http://thexem.de/map/single?id=&origin=&episode=&season=&absolute=
episode, season and absolute are all optional but it wont work if you don't provide either episode and season OR absolute in
addition you can provide destination as the name of the wished destination, if not provided it will output all available
When a destination has two or more addresses another entry will be added as _ ... for now the second address gets the index "2"
(the first index is omitted) and so on
http://thexem.de/map/single?id=7529&origin=anidb&season=1&episode=2&destination=trakt
{
"result":"success",
"data":{
"trakt": {"season":1,"episode":3,"absolute":3},
"trakt_2":{"season":1,"episode":4,"absolute":4}
},
"message":"single mapping for 7529 on anidb."
}
All
---
Basically same as "single" just a little easier
The origin address is added into the output too!!
http://thexem.de/map/all?id=7529&origin=anidb
All Names
---------
Get all names xem has to offer
non optional params: origin(an entity string like 'tvdb')
optional params: season, language
- season: a season number or a list like: 1,3,5 or a compare operator like ne,gt,ge,lt,le,eq and a season number. default would
return all
- language: a language string like 'us' or 'jp' default is all
- defaultNames: 1(yes) or 0(no) should the default names be added to the list ? default is 0(no)
http://thexem.de/map/allNames?origin=tvdb&season=le1
{
"result": "success",
"data": {
"248812": ["Dont Trust the Bitch in Apartment 23", "Don't Trust the Bitch in Apartment 23"],
"257571": ["Nazo no Kanojo X"],
"257875": ["Lupin III - Mine Fujiko to Iu Onna", "Lupin III Fujiko to Iu Onna", "Lupin the Third - Mine Fujiko to Iu Onna"]
},
"message": ""
}
'''
def __init__(self):
addEvent('show.info', self.getShowInfo, priority = 5)
addEvent('episode.info', self.getEpisodeInfo, priority = 5)
self.config = {}
self.config['base_url'] = "http://thexem.de"
self.config['url_single'] = u"%(base_url)s/map/single?" % self.config
self.config['url_all'] = u"%(base_url)s/map/all?" % self.config
self.config['url_names'] = u"%(base_url)s/map/names?" % self.config
self.config['url_all_names'] = u"%(base_url)s/map/allNames?" % self.config
# TODO: Also get show aliases (store as titles)
def getShowInfo(self, identifier = None):
if self.isDisabled():
return {}
cache_key = 'xem.cache.%s' % identifier
log.debug('Getting showInfo: %s', cache_key)
result = self.getCache(cache_key) or {}
if result:
return result
# Create season/episode and absolute mappings
url = self.config['url_all'] + "id=%s&origin=tvdb" % tryUrlencode(identifier)
response = self.getJsonData(url)
if response:
if response.get('result') == 'success':
data = response.get('data', None)
result = self._parse(data)
# Create name alias mappings
url = self.config['url_names'] + "id=%s&origin=tvdb" % tryUrlencode(identifier)
response = self.getJsonData(url)
if response:
if response.get('result') == 'success':
data = response.get('data', None)
result.update({'map_names': data})
self.setCache(cache_key, result)
return result
def getEpisodeInfo(self, identifier = None, params = {}):
episode = params.get('episode', None)
if episode is None:
return False
season_identifier = params.get('season_identifier', None)
if season_identifier is None:
return False
episode_identifier = params.get('episode_identifier', None)
absolute = params.get('absolute', None)
# season_identifier must contain the 'show id : season number' since there is no tvdb id
# for season and we need a reference to both the show id and season number
if season_identifier:
try:
identifier, season_identifier = season_identifier.split(':')
season = int(season_identifier)
except: return False
result = self.getShowInfo(identifier)
map = {}
if result:
map_episode = result.get('map_episode', {}).get(season, {}).get(episode, {})
if map_episode:
map.update({'map_episode': map_episode})
if absolute:
map_absolute = result.get('map_absolute', {}).get(absolute, {})
if map_absolute:
map.update({'map_absolute': map_absolute})
map_names = result.get('map_names', {}).get(toUnicode(season), {})
if map_names:
map.update({'map_names': map_names})
return map
def _parse(self, data, master = 'tvdb'):
'''parses xem map and returns a custom formatted dict map
To retreive map for scene:
if 'scene' in map['map_episode'][1][1]:
print map['map_episode'][1][1]['scene']['season']
'''
if not isinstance(data, list):
return {}
map = {'map_episode': {}, 'map_absolute': {}}
for maps in data:
origin = maps.pop(master, None)
if origin is None:
continue # No master origin to map to
map.get('map_episode').setdefault(origin['season'], {}).setdefault(origin['episode'], maps.copy())
map.get('map_absolute').setdefault(origin['absolute'], maps.copy())
return map
def isDisabled(self):
if __name__ == '__main__':
return False
if self.conf('enabled'):
return False
else:
return True
#XXX: REMOVE, just for degugging
def main():
"""Simple example of using xem
"""
xem_instance = Xem()
print xem_instance.getShowInfo(identifier=73141) # (American Dad)
if __name__ == '__main__':
main()
@@ -65,7 +65,7 @@ class XBMC(MetaDataBase):
name = type name = type
try: try:
if movie_info.get(type): if data['library'].get(type):
el = SubElement(nfoxml, name) el = SubElement(nfoxml, name)
el.text = toUnicode(movie_info.get(type, '')) el.text = toUnicode(movie_info.get(type, ''))
except: except:
@@ -89,18 +89,10 @@ class XBMC(MetaDataBase):
genres.text = toUnicode(genre) genres.text = toUnicode(genre)
# Actors # Actors
for actor_name in movie_info.get('actor_roles', {}): for actor in movie_info.get('actors', []):
role_name = movie_info['actor_roles'][actor_name] actors = SubElement(nfoxml, 'actor')
name = SubElement(actors, 'name')
actor = SubElement(nfoxml, 'actor') name.text = toUnicode(actor)
name = SubElement(actor, 'name')
name.text = toUnicode(actor_name)
if role_name:
role = SubElement(actor, 'role')
role.text = toUnicode(role_name)
if movie_info['images']['actors'].get(actor_name):
thumb = SubElement(actor, 'thumb')
thumb.text = toUnicode(movie_info['images']['actors'].get(actor_name))
# Directors # Directors
for director_name in movie_info.get('directors', []): for director_name in movie_info.get('directors', []):
@@ -120,51 +112,6 @@ class XBMC(MetaDataBase):
sorttitle = SubElement(nfoxml, 'sorttitle') sorttitle = SubElement(nfoxml, 'sorttitle')
sorttitle.text = '%s %s' % (toUnicode(collection_name), movie_info.get('year')) sorttitle.text = '%s %s' % (toUnicode(collection_name), movie_info.get('year'))
# Images
for image_url in movie_info['images']['poster_original']:
image = SubElement(nfoxml, 'thumb')
image.text = toUnicode(image_url)
fanart = SubElement(nfoxml, 'fanart')
for image_url in movie_info['images']['backdrop_original']:
image = SubElement(fanart, 'thumb')
image.text = toUnicode(image_url)
# Add trailer if found
trailer_found = False
if data.get('renamed_files'):
for filename in data.get('renamed_files'):
if 'trailer' in filename:
trailer = SubElement(nfoxml, 'trailer')
trailer.text = toUnicode(filename)
trailer_found = True
if not trailer_found and data['files'].get('trailer'):
trailer = SubElement(nfoxml, 'trailer')
trailer.text = toUnicode(data['files']['trailer'][0])
# Add file metadata
fileinfo = SubElement(nfoxml, 'fileinfo')
streamdetails = SubElement(fileinfo, 'streamdetails')
# Video data
if data['meta_data'].get('video'):
video = SubElement(streamdetails, 'video')
codec = SubElement(video, 'codec')
codec.text = toUnicode(data['meta_data']['video'])
aspect = SubElement(video, 'aspect')
aspect.text = str(data['meta_data']['aspect'])
width = SubElement(video, 'width')
width.text = str(data['meta_data']['resolution_width'])
height = SubElement(video, 'height')
height.text = str(data['meta_data']['resolution_height'])
# Audio data
if data['meta_data'].get('audio'):
audio = SubElement(streamdetails, 'audio')
codec = SubElement(audio, 'codec')
codec.text = toUnicode(data['meta_data'].get('audio'))
channels = SubElement(audio, 'channels')
channels.text = toUnicode(data['meta_data'].get('audio_channels'))
# Clean up the xml and return it # Clean up the xml and return it
nfoxml = xml.dom.minidom.parseString(tostring(nfoxml)) nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
xml_string = nfoxml.toprettyxml(indent = ' ') xml_string = nfoxml.toprettyxml(indent = ' ')
@@ -2,9 +2,6 @@ from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.providers.base import MultiProvider
from couchpotato.core.providers.info.base import MovieProvider, SeasonProvider, EpisodeProvider
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
import re import re
@@ -12,12 +9,8 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class BinSearch(MultiProvider):
def getTypes(self): class BinSearch(NZBProvider):
return [Movie, Season, Episode]
class Base(NZBProvider):
urls = { urls = {
'download': 'https://www.binsearch.info/fcgi/nzb.fcgi?q=%s', 'download': 'https://www.binsearch.info/fcgi/nzb.fcgi?q=%s',
@@ -27,9 +20,21 @@ class Base(NZBProvider):
http_time_between_calls = 4 # Seconds http_time_between_calls = 4 # Seconds
def _search(self, media, quality, results): def _search(self, movie, quality, results):
data = self.getHTMLData(self.urls['search'] % self.buildUrl(media, quality)) arguments = tryUrlencode({
'q': movie['library']['identifier'],
'm': 'n',
'max': 400,
'adv_age': Env.setting('retention', 'nzb'),
'adv_sort': 'date',
'adv_col': 'on',
'adv_nfo': 'on',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
})
data = self.getHTMLData(self.urls['search'] % arguments)
if data: if data:
try: try:
@@ -85,62 +90,15 @@ class Base(NZBProvider):
def download(self, url = '', nzb_id = ''): def download(self, url = '', nzb_id = ''):
data = { params = {
'action': 'nzb', 'action': 'nzb',
nzb_id: 'on' nzb_id: 'on'
} }
try: try:
return self.urlopen(url, data = data, show_error = False) return self.urlopen(url, params = params, show_error = False)
except: except:
log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc()))
return 'try_next' return 'try_next'
class Movie(MovieProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': media['library']['identifier'], # TODO should this use library.title?
'm': 'n',
'max': 400,
'adv_age': Env.setting('retention', 'nzb'),
'adv_sort': 'date',
'adv_col': 'on',
'adv_nfo': 'on',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
})
return query
class Season(SeasonProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('library.query', media['library'], single = True),
'm': 'n',
'max': 400,
'adv_age': Env.setting('retention', 'nzb'),
'adv_sort': 'date',
'adv_col': 'on',
'adv_nfo': 'on',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('library.query', media['library'], single = True),
'm': 'n',
'max': 400,
'adv_age': Env.setting('retention', 'nzb'),
'adv_sort': 'date',
'adv_col': 'on',
'adv_nfo': 'on',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
})
return query
+19 -73
View File
@@ -1,10 +1,8 @@
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import MultiProvider, ResultList from couchpotato.core.providers.base import ResultList
from couchpotato.core.providers.info.base import MovieProvider, SeasonProvider, EpisodeProvider
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
from dateutil.parser import parse from dateutil.parser import parse
@@ -12,44 +10,43 @@ from urllib2 import HTTPError
from urlparse import urlparse from urlparse import urlparse
import time import time
import traceback import traceback
import urllib2
log = CPLog(__name__) log = CPLog(__name__)
class Newznab(MultiProvider):
def getTypes(self): class Newznab(NZBProvider, RSS):
return [Movie, Season, Episode]
class Base(NZBProvider, RSS):
urls = { urls = {
'download': 'get&id=%s',
'detail': 'details&id=%s', 'detail': 'details&id=%s',
'download': 't=get&id=%s' 'search': 'movie',
} }
limits_reached = {} limits_reached = {}
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds
def search(self, media, quality): def search(self, movie, quality):
hosts = self.getHosts() hosts = self.getHosts()
results = ResultList(self, media, quality, imdb_results = True) results = ResultList(self, movie, quality, imdb_results = True)
for host in hosts: for host in hosts:
if self.isDisabled(host): if self.isDisabled(host):
continue continue
self._searchOnHost(host, media, quality, results) self._searchOnHost(host, movie, quality, results)
return results return results
def _searchOnHost(self, host, media, quality, results): def _searchOnHost(self, host, movie, quality, results):
query = self.buildUrl(media, host['api_key']) arguments = tryUrlencode({
'imdbid': movie['library']['identifier'].replace('tt', ''),
url = '%s&%s' % (self.getUrl(host['host']), query) 'apikey': host['api_key'],
'extended': 1
})
url = '%s&%s' % (self.getUrl(host['host'], self.urls['search']), arguments)
nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
@@ -90,7 +87,7 @@ class Base(NZBProvider, RSS):
'name_extra': name_extra, 'name_extra': name_extra,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024, 'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host), 'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)), 'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)),
'content': self.getTextElement(nzb, 'description'), 'content': self.getTextElement(nzb, 'description'),
'score': host['extra_score'], 'score': host['extra_score'],
@@ -130,11 +127,11 @@ class Base(NZBProvider, RSS):
if result: if result:
return result return result
def getUrl(self, host): def getUrl(self, host, type):
if '?page=newznabapi' in host: if '?page=newznabapi' in host:
return cleanHost(host)[:-1] + '&' return cleanHost(host)[:-1] + '&t=' + type
return cleanHost(host) + 'api?' return cleanHost(host) + 'api?t=' + type
def isDisabled(self, host = None): def isDisabled(self, host = None):
return not self.isEnabled(host) return not self.isEnabled(host)
@@ -162,16 +159,7 @@ class Base(NZBProvider, RSS):
return 'try_next' return 'try_next'
try: try:
# Get final redirected url data = self.urlopen(url, show_error = False)
log.debug('Checking %s for redirects.', url)
req = urllib2.Request(url)
req.add_header('User-Agent', self.user_agent)
res = urllib2.urlopen(req)
finalurl = res.geturl()
if finalurl != url:
log.debug('Redirect url used: %s', finalurl)
data = self.urlopen(finalurl, show_error = False)
self.limits_reached[host] = False self.limits_reached[host] = False
return data return data
except HTTPError, e: except HTTPError, e:
@@ -186,45 +174,3 @@ class Base(NZBProvider, RSS):
log.error('Failed download from %s: %s', (host, traceback.format_exc())) log.error('Failed download from %s: %s', (host, traceback.format_exc()))
return 'try_next' return 'try_next'
class Movie(MovieProvider, Base):
def buildUrl(self, media, api_key):
query = tryUrlencode({
't': 'movie',
'imdbid': media['library']['identifier'].replace('tt', ''),
'apikey': api_key,
'extended': 1
})
return query
class Season(SeasonProvider, Base):
def buildUrl(self, media, api_key):
search_title = fireEvent('library.query', media['library'], include_identifier = False, single = True)
identifier = fireEvent('library.identifier', media['library'], single = True)
query = tryUrlencode({
't': 'tvsearch',
'q': search_title,
'season': identifier['season'],
'apikey': api_key,
'extended': 1
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media, api_key):
search_title = fireEvent('library.query', media['library'], include_identifier = False, single = True)
identifier = fireEvent('library.identifier', media['library'], single = True)
query = tryUrlencode({
't': 'tvsearch',
'q': search_title,
'season': identifier['season'],
'ep': identifier['episode'],
'apikey': api_key,
'extended': 1
})
return query
+14 -50
View File
@@ -3,22 +3,14 @@ from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.providers.base import MultiProvider
from couchpotato.core.providers.info.base import MovieProvider, SeasonProvider, EpisodeProvider
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse from dateutil.parser import parse
import time import time
log = CPLog(__name__) log = CPLog(__name__)
class NZBClub(MultiProvider):
def getTypes(self): class NZBClub(NZBProvider, RSS):
return [Movie, Season, Episode]
class Base(NZBProvider, RSS):
urls = { urls = {
'search': 'http://www.nzbclub.com/nzbfeed.aspx?%s', 'search': 'http://www.nzbclub.com/nzbfeed.aspx?%s',
@@ -26,9 +18,20 @@ class Base(NZBProvider, RSS):
http_time_between_calls = 4 #seconds http_time_between_calls = 4 #seconds
def _search(self, media, quality, results): def _searchOnTitle(self, title, movie, quality, results):
nzbs = self.getRSSData(self.urls['search'] % self.buildUrl(media)) q = '"%s %s"' % (title, movie['library']['year'])
params = tryUrlencode({
'q': q,
'ig': 1,
'rpp': 200,
'st': 5,
'sp': 1,
'ns': 1,
})
nzbs = self.getRSSData(self.urls['search'] % params)
for nzb in nzbs: for nzb in nzbs:
@@ -75,42 +78,3 @@ class Base(NZBProvider, RSS):
return False return False
return True return True
class Movie(MovieProvider, Base):
def buildUrl(self, media):
query = tryUrlencode({
'q': '"%s"' % fireEvent('library.query', media['library'], single = True),
'ig': 1,
'rpp': 200,
'st': 5,
'sp': 1,
'ns': 1,
})
return query
class Season(SeasonProvider, Base):
def buildUrl(self, media):
query = tryUrlencode({
'q': fireEvent('library.query', media['library'], single = True),
'ig': 1,
'rpp': 200,
'st': 5,
'sp': 1,
'ns': 1,
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media):
query = tryUrlencode({
'q': fireEvent('library.query', media['library'], single = True),
'ig': 1,
'rpp': 200,
'st': 5,
'sp': 1,
'ns': 1,
})
return query
+17 -91
View File
@@ -3,9 +3,6 @@ from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.providers.base import MultiProvider
from couchpotato.core.providers.info.base import MovieProvider, SeasonProvider, EpisodeProvider
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
from dateutil.parser import parse from dateutil.parser import parse
@@ -14,13 +11,8 @@ import time
log = CPLog(__name__) log = CPLog(__name__)
class NzbIndex(MultiProvider):
def getTypes(self): class NzbIndex(NZBProvider, RSS):
return [Movie, Season, Episode]
class Base(NZBProvider, RSS):
urls = { urls = {
'download': 'https://www.nzbindex.com/download/', 'download': 'https://www.nzbindex.com/download/',
@@ -29,44 +21,28 @@ class Base(NZBProvider, RSS):
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds
def _search(self, media, quality, results): def _searchOnTitle(self, title, movie, quality, results):
nzbs = self.getRSSData(self.urls['search'] % self.buildUrl(media, quality)) q = '"%s %s" | "%s (%s)"' % (title, movie['library']['year'], title, movie['library']['year'])
arguments = tryUrlencode({
'q': q,
'age': Env.setting('retention', 'nzb'),
'sort': 'agedesc',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
'rating': 1,
'max': 250,
'more': 1,
'complete': 1,
})
nzbs = self.getRSSData(self.urls['search'] % arguments)
for nzb in nzbs: for nzb in nzbs:
enclosure = self.getElement(nzb, 'enclosure').attrib enclosure = self.getElement(nzb, 'enclosure').attrib
nzbindex_id = int(self.getTextElement(nzb, "link").split('/')[4]) nzbindex_id = int(self.getTextElement(nzb, "link").split('/')[4])
title = self.getTextElement(nzb, "title")
match = fireEvent('matcher.parse', title, parser='usenet', single = True)
if not match.chains:
log.info('Unable to parse release with title "%s"', title)
continue
# TODO should we consider other lower-weight chains here?
info = fireEvent('matcher.flatten_info', match.chains[0].info, single = True)
release_name = fireEvent('matcher.construct_from_raw', info.get('release_name'), single = True)
file_name = info.get('detail', {}).get('file_name')
file_name = file_name[0] if file_name else None
title = release_name or file_name
# Strip extension from parsed title (if one exists)
ext_pos = title.rfind('.')
# Assume extension if smaller than 4 characters
# TODO this should probably be done a better way
if len(title[ext_pos + 1:]) <= 4:
title = title[:ext_pos]
if not title:
log.info('Unable to find release name from match')
continue
try: try:
description = self.getTextElement(nzb, "description") description = self.getTextElement(nzb, "description")
except: except:
@@ -81,7 +57,7 @@ class Base(NZBProvider, RSS):
results.append({ results.append({
'id': nzbindex_id, 'id': nzbindex_id,
'name': title, 'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024, 'size': tryInt(enclosure['length']) / 1024 / 1024,
'url': enclosure['url'], 'url': enclosure['url'],
@@ -101,53 +77,3 @@ class Base(NZBProvider, RSS):
except: except:
pass pass
class Movie(MovieProvider, Base):
def buildUrl(self, media, quality):
title = fireEvent('library.query', media['library'], include_year = False, single = True)
year = media['library']['year']
query = tryUrlencode({
'q': '"%s %s" | "%s (%s)"' % (title, year, title, year),
'age': Env.setting('retention', 'nzb'),
'sort': 'agedesc',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
'rating': 1,
'max': 250,
'more': 1,
'complete': 1,
})
return query
class Season(SeasonProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('library.query', media['library'], single = True),
'age': Env.setting('retention', 'nzb'),
'sort': 'agedesc',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
'rating': 1,
'max': 250,
'more': 1,
'complete': 1,
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media, quality):
query = tryUrlencode({
'q': fireEvent('library.query', media['library'], single = True),
'age': Env.setting('retention', 'nzb'),
'sort': 'agedesc',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
'rating': 1,
'max': 250,
'more': 1,
'complete': 1,
})
return query
@@ -2,20 +2,12 @@ from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.providers.base import MultiProvider
from couchpotato.core.providers.info.base import MovieProvider, SeasonProvider, EpisodeProvider
from couchpotato.core.providers.torrent.base import TorrentProvider from couchpotato.core.providers.torrent.base import TorrentProvider
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
class BiTHDTV(MultiProvider): class BiTHDTV(TorrentProvider):
def getTypes(self):
return [Movie, Season, Episode]
class Base(TorrentProvider):
urls = { urls = {
'test' : 'http://www.bit-hdtv.com/', 'test' : 'http://www.bit-hdtv.com/',
@@ -26,15 +18,20 @@ class Base(TorrentProvider):
} }
# Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken # Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken
cat_id_movies = 7
http_time_between_calls = 1 #seconds http_time_between_calls = 1 #seconds
def _search(self, media, quality, results): def _searchOnTitle(self, title, movie, quality, results):
query = self.buildUrl(media) arguments = tryUrlencode({
'search': '%s %s' % (title.replace(':', ''), movie['library']['year']),
'cat': self.cat_id_movies
})
url = "%s&%s" % (self.urls['search'], query) url = "%s&%s" % (self.urls['search'], arguments)
data = self.getHTMLData(url) data = self.getHTMLData(url, opener = self.login_opener)
if data: if data:
# Remove BiT-HDTV's output garbage so outdated BS4 versions successfully parse the HTML # Remove BiT-HDTV's output garbage so outdated BS4 versions successfully parse the HTML
@@ -71,10 +68,10 @@ class Base(TorrentProvider):
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self): def getLoginParams(self):
return { return tryUrlencode({
'username': self.conf('username'), 'username': self.conf('username'),
'password': self.conf('password'), 'password': self.conf('password'),
} })
def getMoreInfo(self, item): def getMoreInfo(self, item):
full_description = self.getCache('bithdtv.%s' % item['id'], item['detail_url'], cache_timeout = 25920000) full_description = self.getCache('bithdtv.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
@@ -89,31 +86,3 @@ class Base(TorrentProvider):
return 'logout.php' in output.lower() return 'logout.php' in output.lower()
loginCheckSuccess = loginSuccess loginCheckSuccess = loginSuccess
# Only searches BiT-HDTV's main category, subcategory and resolution search filters appear to be broken
class Movie(MovieProvider, Base):
def buildUrl(self, media):
query = tryUrlencode({
'search': fireEvent('library.query', media['library'], single = True),
'cat': 7 # Movie cat
})
return query
class Season(SeasonProvider, Base):
def buildUrl(self, media):
query = tryUrlencode({
'search': fireEvent('library.query', media['library'], single = True),
'cat': 12 # Season cat
})
return query
class Episode(EpisodeProvider, Base):
def buildUrl(self, media):
query = tryUrlencode({
'search': fireEvent('library.query', media['library'], single = True),
'cat': 10 # Episode cat
})
return query
@@ -2,46 +2,39 @@ from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.providers.base import MultiProvider
from couchpotato.core.providers.info.base import EpisodeProvider, SeasonProvider, MovieProvider
from couchpotato.core.providers.torrent.base import TorrentProvider from couchpotato.core.providers.torrent.base import TorrentProvider
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Bitsoup(MultiProvider): class Bitsoup(TorrentProvider):
def getTypes(self):
return [Movie, Season, Episode]
class Base(TorrentProvider):
urls = { urls = {
'test': 'https://www.bitsoup.me/', 'test': 'https://www.bitsoup.me/',
'login' : 'https://www.bitsoup.me/takelogin.php', 'login' : 'https://www.bitsoup.me/takelogin.php',
'login_check': 'https://www.bitsoup.me/my.php', 'login_check': 'https://www.bitsoup.me/my.php',
'search': 'https://www.bitsoup.me/browse.php?%s', 'search': 'https://www.bitsoup.me/browse.php?',
'baseurl': 'https://www.bitsoup.me/%s', 'baseurl': 'https://www.bitsoup.me/%s',
} }
http_time_between_calls = 1 #seconds http_time_between_calls = 1 #seconds
def _search(self, media, quality, results): def _searchOnTitle(self, title, movie, quality, results):
url = self.urls['search'] % self.buildUrl(media, quality) q = '"%s" %s' % (simplifyString(title), movie['library']['year'])
data = self.getHTMLData(url) arguments = tryUrlencode({
'search': q,
})
url = "%s&%s" % (self.urls['search'], arguments)
data = self.getHTMLData(url, opener = self.login_opener)
if data: if data:
html = BeautifulSoup(data, "html.parser") html = BeautifulSoup(data)
try: try:
result_table = html.find('table', attrs = {'class': 'koptekst'}) result_table = html.find('table', attrs = {'class': 'koptekst'})
if not result_table or 'nothing found!' in data.lower():
return
entries = result_table.find_all('tr') entries = result_table.find_all('tr')
for result in entries[1:]: for result in entries[1:]:
@@ -89,57 +82,3 @@ class Base(TorrentProvider):
loginCheckSuccess = loginSuccess loginCheckSuccess = loginSuccess
# Bitsoup Categories
# Movies
# Movies/3D - 17 (unused)
# Movies/DVD-R - 20
# Movies/Packs - 27 (unused)
# Movies/XviD - 19
# The site doesn't have HD Movie caterogies, they bundle HD under x264
# x264 - 41
# TV
# TV-HDx264 - 42
# TV-Packs - 45
# TV-SDx264 - 49
# TV-XVID - 7 (unused)
class Movie(MovieProvider, Base):
cat_ids = [
([41], ['720p', '1080p']),
([20], ['dvdr']),
([19], ['brrip', 'dvdrip']),
]
cat_backup_id = 0
def buildUrl(self, media, quality):
query = tryUrlencode({
'search': '"%s" %s' % (
fireEvent('library.query', media['library'], include_year = False, single = True),
media['library']['year']
),
'cat': self.getCatId(quality['identifier'])[0],
})
return query
class Season(SeasonProvider, Base):
# For season bundles, bitsoup currently only has one category
def buildUrl(self, media, quality):
query = tryUrlencode({
'search': fireEvent('library.query', media['library'], single = True),
'cat': 45 # TV-Packs Category
})
return query
class Episode(EpisodeProvider, Base):
cat_ids = [
([42], ['hdtv_720p', 'webdl_720p', 'webdl_1080p', 'bdrip_1080p', 'bdrip_720p', 'brrip_1080p', 'brrip_720p']),
([49], ['hdtv_sd', 'webdl_480p'])
]
cat_backup_id = 0
def buildUrl(self, media, quality):
query = tryUrlencode({
'search': fireEvent('library.query', media['library'], single = True),
'cat': self.getCatId(quality['identifier'])[0],
})
return query
@@ -1,4 +1,5 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider from couchpotato.core.providers.torrent.base import TorrentProvider
@@ -22,7 +23,7 @@ class HDBits(TorrentProvider):
def _search(self, movie, quality, results): def _search(self, movie, quality, results):
data = self.getJsonData(self.urls['search'] % movie['library']['identifier']) data = self.getJsonData(self.urls['search'] % movie['library']['identifier'], opener = self.login_opener)
if data: if data:
try: try:
@@ -41,17 +42,15 @@ class HDBits(TorrentProvider):
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self): def getLoginParams(self):
data = self.getHTMLData('https://hdbits.org/login', cache_timeout = 0) data = self.getHTMLData('https://hdbits.org/login')
bs = BeautifulSoup(data) bs = BeautifulSoup(data)
secret = bs.find('input', attrs = {'name': 'lol'})['value'] secret = bs.find('input', attrs = {'name': 'lol'})['value']
return { return tryUrlencode({
'uname': self.conf('username'), 'uname': self.conf('username'),
'password': self.conf('password'), 'password': self.conf('password'),
'returnto': '/',
'lol': secret 'lol': secret
} })
def loginSuccess(self, output): def loginSuccess(self, output):
return '/logout.php' in output.lower() return '/logout.php' in output.lower()
@@ -42,7 +42,7 @@ class ILoveTorrents(TorrentProvider):
search_url = self.urls['search'] % (movieTitle, page, cats[0]) search_url = self.urls['search'] % (movieTitle, page, cats[0])
page += 1 page += 1
data = self.getHTMLData(search_url) data = self.getHTMLData(search_url, opener = self.login_opener)
if data: if data:
try: try:
soup = BeautifulSoup(data) soup = BeautifulSoup(data)
@@ -96,11 +96,11 @@ class ILoveTorrents(TorrentProvider):
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self): def getLoginParams(self):
return { return tryUrlencode({
'username': self.conf('username'), 'username': self.conf('username'),
'password': self.conf('password'), 'password': self.conf('password'),
'submit': 'Welcome to ILT', 'submit': 'Welcome to ILT',
} })
def getMoreInfo(self, item): def getMoreInfo(self, item):
cache_key = 'ilt.%s' % item['id'] cache_key = 'ilt.%s' % item['id']
@@ -109,7 +109,7 @@ class ILoveTorrents(TorrentProvider):
if not description: if not description:
try: try:
full_description = self.getHTMLData(item['detail_url']) full_description = self.getHTMLData(item['detail_url'], opener = self.login_opener)
html = BeautifulSoup(full_description) html = BeautifulSoup(full_description)
nfo_pre = html.find('td', attrs = {'class':'main'}).findAll('table')[1] nfo_pre = html.find('td', attrs = {'class':'main'}).findAll('table')[1]
description = toUnicode(nfo_pre.text) if nfo_pre else '' description = toUnicode(nfo_pre.text) if nfo_pre else ''

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