Compare commits

..

165 Commits

Author SHA1 Message Date
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 14577 additions and 39527 deletions

231
Desktop.py Normal file
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:])

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.
* 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 are you're settings for the specific problem?
* What providers are you using? (While you're logs include these, scanning through hundred of lines of log isn't our hobby)
* 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 is the movie + quality you are searching for.
* What are you settings for the specific problem.
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby).
* Give me a short step by step of how to reproduce.
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
* I will mark issues with the "can't reproduce" tag. Don't go asking "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!!
* 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 ;)
## Pull Request
* 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 ;)**
**If I don't get enough info, the chance of the issue getting closed is a lot bigger ;)**

View File

@@ -55,10 +55,6 @@ class Core(Plugin):
if not Env.get('desktop'):
self.signalHandler()
# Set default urlopen timeout
import socket
socket.setdefaulttimeout(30)
def md5Password(self, value):
return md5(value) if value else ''

View File

@@ -34,8 +34,6 @@ class ClientScript(Plugin):
'scripts/library/question.js',
'scripts/library/scrollspy.js',
'scripts/library/spin.js',
'scripts/library/Array.stableSort.js',
'scripts/library/async.js',
'scripts/couchpotato.js',
'scripts/api.js',
'scripts/library/history.js',

View File

@@ -31,13 +31,13 @@ class Scheduler(Plugin):
pass
def doShutdown(self):
super(Scheduler, self).doShutdown()
self.stop()
return super(Scheduler, self).doShutdown()
def stop(self):
if self.started:
log.debug('Stopping scheduler')
self.sched.shutdown(wait = False)
self.sched.shutdown()
log.debug('Scheduler stopped')
self.started = False

View File

@@ -183,6 +183,9 @@ class GitUpdater(BaseUpdater):
def doUpdate(self):
try:
log.debug('Stashing local changes')
self.repo.saveStash()
log.info('Updating to latest version')
self.repo.pull()

View File

@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
self.doUpdate();
else {
App.unBlockPage();
App.on('message', 'No updates available');
App.fireEvent('message', 'No updates available');
}
}
})

View File

@@ -13,7 +13,6 @@ class Downloader(Provider):
protocol = []
http_time_between_calls = 0
status_support = True
torrent_sources = [
'http://torrage.com/torrent/%s.torrent',
@@ -50,27 +49,22 @@ class Downloader(Provider):
return []
def _download(self, data = None, media = None, manual = False, filedata = None):
if not media: media = {}
def _download(self, data = None, movie = None, manual = False, filedata = None):
if not movie: movie = {}
if not data: data = {}
if self.isDisabled(manual, data):
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 = {}):
return
ids = [download_id['id'] for download_id in download_ids if download_id['downloader'] == self.getName()]
return self.getAllDownloadStatus()
if ids:
return self.getAllDownloadStatus(ids)
else:
return
def getAllDownloadStatus(self, ids):
return []
def getAllDownloadStatus(self):
return
def _removeFailed(self, release_download):
if self.isDisabled(manual = True, data = {}):
@@ -134,7 +128,6 @@ class Downloader(Provider):
def downloadReturnId(self, download_id):
return {
'downloader': self.getName(),
'status_support': self.status_support,
'id': download_id
}

View File

@@ -13,7 +13,7 @@ config = [{
'list': 'download_providers',
'name': 'blackhole',
'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,
'options': [
{

View File

@@ -11,10 +11,9 @@ log = CPLog(__name__)
class Blackhole(Downloader):
protocol = ['nzb', 'torrent', 'torrent_magnet']
status_support = False
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
directory = self.conf('directory')
@@ -34,7 +33,7 @@ class Blackhole(Downloader):
log.error('No nzb/torrent available: %s', data.get('url'))
return False
file_name = self.createFileName(data, filedata, media)
file_name = self.createFileName(data, filedata, movie)
full_path = os.path.join(directory, file_name)
if self.conf('create_subdir'):
@@ -52,10 +51,10 @@ class Blackhole(Downloader):
with open(full_path, 'wb') as f:
f.write(filedata)
os.chmod(full_path, Env.getPermission('file'))
return self.downloadReturnId('')
return True
else:
log.info('File %s already exists.', full_path)
return self.downloadReturnId('')
return True
except:
log.error('Failed to download to blackhole %s', traceback.format_exc())

View File

@@ -32,10 +32,7 @@ class Deluge(Downloader):
return self.drpc
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
def download(self, data, movie, filedata = None):
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
if not self.connect():
@@ -76,7 +73,7 @@ class Deluge(Downloader):
if data.get('protocol') == 'torrent_magnet':
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
else:
filename = self.createFileName(data, filedata, media)
filename = self.createFileName(data, filedata, movie)
remote_torrent = self.drpc.add_torrent_file(filename, filedata, options)
if not remote_torrent:
@@ -86,25 +83,25 @@ class Deluge(Downloader):
log.info('Torrent sent to Deluge successfully.')
return self.downloadReturnId(remote_torrent)
def getAllDownloadStatus(self, ids):
def getAllDownloadStatus(self):
log.debug('Checking Deluge download status.')
if not self.connect():
return []
return False
release_downloads = ReleaseDownloadList(self)
queue = self.drpc.get_alltorrents(ids)
queue = self.drpc.get_alltorrents()
if not queue:
log.debug('Nothing in queue or error')
return []
return False
for torrent_id in queue:
torrent = queue[torrent_id]
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
# Deluge has no easy way to work out if a torrent is stalled or failing.
#status = 'failed'
status = 'busy'
@@ -120,11 +117,11 @@ class Deluge(Downloader):
download_dir = sp(torrent['save_path'])
if torrent['move_on_completed']:
download_dir = torrent['move_completed_path']
torrent_files = []
for file_item in torrent['files']:
torrent_files.append(sp(os.path.join(download_dir, file_item['path'])))
release_downloads.append({
'id': torrent['hash'],
'name': torrent['name'],
@@ -208,11 +205,11 @@ class DelugeRPC(object):
return torrent_id
def get_alltorrents(self, ids):
def get_alltorrents(self):
ret = False
try:
self.connect()
ret = self.client.core.get_torrents_status({'id': ids}, {}).get()
ret = self.client.core.get_torrents_status({}, {}).get()
except Exception, err:
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
finally:

View File

@@ -25,13 +25,6 @@ config = [{
'default': 'localhost:6789',
'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',
'default': 'nzbget',

View File

@@ -17,10 +17,10 @@ class NZBGet(Downloader):
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):
if not media: media = {}
def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
if not filedata:
@@ -29,8 +29,8 @@ class NZBGet(Downloader):
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')}
nzb_name = ss('%s.nzb' % self.createNzbName(data, media))
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
rpc = xmlrpclib.ServerProxy(url)
try:
@@ -67,11 +67,11 @@ class NZBGet(Downloader):
log.error('NZBGet could not add %s to the queue.', nzb_name)
return False
def getAllDownloadStatus(self, ids):
def getAllDownloadStatus(self):
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)
try:
@@ -81,13 +81,13 @@ class NZBGet(Downloader):
log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return []
return False
except xmlrpclib.ProtocolError, e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
log.error('Protocol Error: %s', e)
return []
return False
# Get NZBGet data
try:
@@ -97,59 +97,56 @@ class NZBGet(Downloader):
history = rpc.history()
except:
log.error('Failed getting data: %s', traceback.format_exc(1))
return []
return False
release_downloads = ReleaseDownloadList(self)
for nzb in groups:
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
try:
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
except:
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({
'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,
})
timeleft = -1
try:
if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
except:
pass
release_downloads.append({
'id': nzb_id,
'name': nzb['NZBFilename'],
'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED',
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
'timeleft': timeleft,
})
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
if nzb['NZBID'] in ids:
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
release_downloads.append({
'id': nzb['NZBID'],
'name': nzb['NZBFilename'],
'original_status': nzb['Stage'],
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
})
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
release_downloads.append({
'id': nzb['NZBID'],
'name': nzb['NZBFilename'],
'original_status': nzb['Stage'],
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
})
for nzb in history:
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
try:
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
except:
nzb_id = nzb['NZBID']
if nzb_id in ids:
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
release_downloads.append({
'id': nzb_id,
'name': nzb['NZBFilename'],
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
'timeleft': str(timedelta(seconds = 0)),
'folder': sp(nzb['DestDir'])
})
release_downloads.append({
'id': nzb_id,
'name': nzb['NZBFilename'],
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
'timeleft': str(timedelta(seconds = 0)),
'folder': sp(nzb['DestDir'])
})
return release_downloads

View File

@@ -8,11 +8,9 @@ from uuid import uuid4
import hashlib
import httplib
import json
import os
import socket
import ssl
import sys
import time
import traceback
import urllib2
@@ -25,46 +23,44 @@ class NZBVortex(Downloader):
api_level = None
session_id = None
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
# Send the nzb
try:
nzb_filename = self.createFileName(data, filedata, media)
self.call('nzb/add', files = {'file': (nzb_filename, filedata)})
nzb_filename = self.createFileName(data, filedata, movie)
self.call('nzb/add', params = {'file': (nzb_filename, filedata)}, multipart = True)
time.sleep(10)
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)
except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False
def getAllDownloadStatus(self, ids):
def getAllDownloadStatus(self):
raw_statuses = self.call('nzb')
release_downloads = ReleaseDownloadList(self)
for nzb in raw_statuses.get('nzbs', []):
if nzb['id'] in ids:
# Check status
status = 'busy'
if nzb['state'] == 20:
status = 'completed'
elif nzb['state'] in [21, 22, 24]:
status = 'failed'
release_downloads.append({
'id': nzb['id'],
'name': nzb['uiTitle'],
'status': status,
'original_status': nzb['state'],
'timeleft':-1,
'folder': sp(nzb['destinationPath']),
})
# Check status
status = 'busy'
if nzb['state'] == 20:
status = 'completed'
elif nzb['state'] in [21, 22, 24]:
status = 'failed'
release_downloads.append({
'id': nzb['id'],
'name': nzb['uiTitle'],
'status': status,
'original_status': nzb['state'],
'timeleft':-1,
'folder': sp(nzb['destinationPath']),
})
return release_downloads
@@ -117,9 +113,10 @@ class NZBVortex(Downloader):
params = tryUrlencode(parameters)
url = cleanHost(self.conf('host')) + 'api/' + call
url_opener = urllib2.build_opener(HTTPSHandler())
try:
data = self.urlopen('%s?%s' % (url, params), *args, **kwargs)
data = self.urlopen('%s?%s' % (url, params), opener = url_opener, *args, **kwargs)
if data:
return json.loads(data)
@@ -141,9 +138,10 @@ class NZBVortex(Downloader):
if not self.api_level:
url = cleanHost(self.conf('host')) + 'api/app/apilevel'
url_opener = urllib2.build_opener(HTTPSHandler())
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'))
except URLError, e:
if hasattr(e, 'code') and e.code == 403:

View File

@@ -11,10 +11,9 @@ class Pneumatic(Downloader):
protocol = ['nzb']
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):
if not media: media = {}
def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
directory = self.conf('directory')
@@ -26,7 +25,7 @@ class Pneumatic(Downloader):
log.error('No nzb available!')
return False
fullPath = os.path.join(directory, self.createFileName(data, filedata, media))
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
try:
if not os.path.isfile(fullPath):
@@ -34,7 +33,7 @@ class Pneumatic(Downloader):
with open(fullPath, 'wb') as f:
f.write(filedata)
nzb_name = self.createNzbName(data, media)
nzb_name = self.createNzbName(data, movie)
strm_path = os.path.join(directory, nzb_name)
strm_file = open(strm_path + '.strm', 'wb')
@@ -42,11 +41,11 @@ class Pneumatic(Downloader):
strm_file.write(strmContent)
strm_file.close()
return self.downloadReturnId('')
return True
else:
log.info('File %s already exists.', fullPath)
return self.downloadReturnId('')
return True
except:
log.error('Failed to download .strm: %s', traceback.format_exc())

View File

@@ -58,6 +58,14 @@ config = [{
'advanced': True,
'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',
'type': 'bool',

View File

@@ -77,10 +77,7 @@ class rTorrent(Downloader):
return True
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
def download(self, data, movie, filedata = None):
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
if not self.connect():
@@ -128,7 +125,9 @@ class rTorrent(Downloader):
if 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'))
# Set Ratio Group
@@ -143,11 +142,11 @@ class rTorrent(Downloader):
log.error('Failed to send torrent to rTorrent: %s', err)
return False
def getAllDownloadStatus(self, ids):
def getAllDownloadStatus(self):
log.debug('Checking rTorrent download status.')
if not self.connect():
return []
return False
try:
torrents = self.rt.get_torrents()
@@ -155,34 +154,33 @@ class rTorrent(Downloader):
release_downloads = ReleaseDownloadList(self)
for torrent in torrents:
if torrent.info_hash in ids:
torrent_files = []
for file_item in torrent.get_files():
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
status = 'busy'
if torrent.complete:
if torrent.active:
status = 'seeding'
else:
status = 'completed'
release_downloads.append({
'id': torrent.info_hash,
'name': torrent.name,
'status': status,
'seed_ratio': torrent.ratio,
'original_status': torrent.state,
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
'folder': sp(torrent.directory),
'files': '|'.join(torrent_files)
})
torrent_files = []
for file_item in torrent.get_files():
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
status = 'busy'
if torrent.complete:
if torrent.active:
status = 'seeding'
else:
status = 'completed'
release_downloads.append({
'id': torrent.info_hash,
'name': torrent.name,
'status': status,
'seed_ratio': torrent.ratio,
'original_status': torrent.state,
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
'folder': sp(torrent.directory),
'files': '|'.join(torrent_files)
})
return release_downloads
except Exception, err:
log.error('Failed to get status from rTorrent: %s', err)
return []
return False
def pause(self, release_download, pause = True):
if not self.connect():

View File

@@ -16,8 +16,8 @@ class Sabnzbd(Downloader):
protocol = ['nzb']
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
log.info('Sending "%s" to SABnzbd.', data.get('name'))
@@ -25,7 +25,7 @@ class Sabnzbd(Downloader):
req_params = {
'cat': self.conf('category'),
'mode': 'addurl',
'nzbname': self.createNzbName(data, media),
'nzbname': self.createNzbName(data, movie),
'priority': self.conf('priority'),
}
@@ -36,14 +36,14 @@ class Sabnzbd(Downloader):
return False
# 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'
else:
req_params['name'] = data.get('url')
try:
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:
sab_data = self.call(req_params)
except URLError:
@@ -64,7 +64,7 @@ class Sabnzbd(Downloader):
log.error('Error getting data from SABNZBd: %s', sab_data)
return False
def getAllDownloadStatus(self, ids):
def getAllDownloadStatus(self):
log.debug('Checking SABnzbd download status.')
@@ -75,7 +75,7 @@ class Sabnzbd(Downloader):
})
except:
log.error('Failed getting queue: %s', traceback.format_exc(1))
return []
return False
# Go through history items
try:
@@ -85,42 +85,41 @@ class Sabnzbd(Downloader):
})
except:
log.error('Failed getting history json: %s', traceback.format_exc(1))
return []
return False
release_downloads = ReleaseDownloadList(self)
# Get busy releases
for nzb in queue.get('slots', []):
if nzb['nzo_id'] in ids:
status = 'busy'
if 'ENCRYPTED / ' in nzb['filename']:
status = 'failed'
release_downloads.append({
'id': nzb['nzo_id'],
'name': nzb['filename'],
'status': status,
'original_status': nzb['status'],
'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
})
status = 'busy'
if 'ENCRYPTED / ' in nzb['filename']:
status = 'failed'
release_downloads.append({
'id': nzb['nzo_id'],
'name': nzb['filename'],
'status': status,
'original_status': nzb['status'],
'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
})
# Get old releases
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({
'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']),
})
status = 'busy'
if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()):
status = 'failed'
elif nzb['status'] == 'Completed':
status = 'completed'
release_downloads.append({
'id': nzb['nzo_id'],
'name': nzb['name'],
'status': status,
'original_status': nzb['status'],
'timeleft': str(timedelta(seconds = 0)),
'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']),
})
return release_downloads

View File

@@ -11,10 +11,10 @@ log = CPLog(__name__)
class Synology(Downloader):
protocol = ['nzb', 'torrent', 'torrent_magnet']
status_support = False
log = CPLog(__name__)
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {}
response = False
@@ -42,7 +42,7 @@ class Synology(Downloader):
except:
log.error('Exception while adding torrent: %s', traceback.format_exc())
finally:
return self.downloadReturnId('') if response else False
return response
def getEnabledProtocol(self):
if self.conf('use_for') == 'both':

View File

@@ -31,9 +31,7 @@ class Transmission(Downloader):
return self.trpc
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
def download(self, data, movie, filedata = None):
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.')
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
def getAllDownloadStatus(self, ids):
def getAllDownloadStatus(self):
log.debug('Checking Transmission download status.')
if not self.connect():
return []
return False
release_downloads = ReleaseDownloadList(self)
@@ -99,35 +97,34 @@ class Transmission(Downloader):
queue = self.trpc.get_alltorrents(return_params)
if not (queue and queue.get('torrents')):
log.debug('Nothing in queue or error')
return []
return False
for torrent in queue['torrents']:
if torrent['hashString'] in ids:
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.get('isStalled', 'N/A'), torrent['eta'], torrent['uploadRatio'], torrent['isFinished']))
torrent_files = []
for file_item in torrent['files']:
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name'])))
status = 'busy'
if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'):
status = 'failed'
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
status = 'completed'
elif torrent['status'] in [5, 6]:
status = 'seeding'
release_downloads.append({
'id': torrent['hashString'],
'name': torrent['name'],
'status': status,
'original_status': torrent['status'],
'seed_ratio': torrent['uploadRatio'],
'timeleft': str(timedelta(seconds = torrent['eta'])),
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
'files': '|'.join(torrent_files)
})
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%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_files = []
for file_item in torrent['files']:
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name'])))
status = 'busy'
if torrent.get('isStalled') and self.conf('stalled_as_failed'):
status = 'failed'
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
status = 'completed'
elif torrent['status'] in [5, 6]:
status = 'seeding'
release_downloads.append({
'id': torrent['hashString'],
'name': torrent['name'],
'status': status,
'original_status': torrent['status'],
'seed_ratio': torrent['uploadRatio'],
'timeleft': str(timedelta(seconds = torrent['eta'])),
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
'files': '|'.join(torrent_files)
})
return release_downloads

View File

@@ -24,16 +24,6 @@ class uTorrent(Downloader):
protocol = ['torrent', 'torrent_magnet']
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):
# Load host from config and split out port.
@@ -46,11 +36,11 @@ class uTorrent(Downloader):
return self.utorrent_api
def download(self, data = None, media = None, filedata = None):
if not media: media = {}
def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
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():
return False
@@ -85,10 +75,9 @@ class uTorrent(Downloader):
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
else:
info = bdecode(filedata)['info']
info = bdecode(filedata)["info"]
torrent_hash = sha1(benc(info)).hexdigest().upper()
torrent_filename = self.createFileName(data, filedata, media)
torrent_filename = self.createFileName(data, filedata, movie)
if data.get('seed_ratio'):
torrent_params['seed_override'] = 1
@@ -115,62 +104,72 @@ class uTorrent(Downloader):
return self.downloadReturnId(torrent_hash)
def getAllDownloadStatus(self, ids):
def getAllDownloadStatus(self):
log.debug('Checking uTorrent download status.')
if not self.connect():
return []
return False
release_downloads = ReleaseDownloadList(self)
data = self.utorrent_api.get_status()
if not data:
log.error('Error getting data from uTorrent')
return []
return False
queue = json.loads(data)
if queue.get('error'):
log.error('Error getting data from uTorrent: %s', queue.get('error'))
return []
return False
if not queue.get('torrents'):
log.debug('Nothing in queue')
return []
return False
# Get torrents
for torrent in queue['torrents']:
if torrent[0] in ids:
#Get files of the torrent
torrent_files = []
try:
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]]
except:
log.debug('Failed getting files from torrent: %s', torrent[2])
status = 'busy'
if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000:
status = 'seeding'
elif (torrent[1] & self.status_flags['ERROR']):
status = 'failed'
elif torrent[4] == 1000:
status = 'completed'
if not status == 'busy':
self.removeReadOnly(torrent_files)
release_downloads.append({
'id': torrent[0],
'name': torrent[2],
'status': status,
'seed_ratio': float(torrent[7]) / 1000,
'original_status': torrent[1],
'timeleft': str(timedelta(seconds = torrent[10])),
'folder': sp(torrent[26]),
'files': '|'.join(torrent_files)
})
#Get files of the torrent
torrent_files = []
try:
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]]
except:
log.debug('Failed getting files from torrent: %s', torrent[2])
status_flags = {
"STARTED" : 1,
"CHECKING" : 2,
"CHECK-START" : 4,
"CHECKED" : 8,
"ERROR" : 16,
"PAUSED" : 32,
"QUEUED" : 64,
"LOADED" : 128
}
status = 'busy'
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'
if not status == 'busy':
self.removeReadOnly(torrent_files)
release_downloads.append({
'id': torrent[0],
'name': torrent[2],
'status': status,
'seed_ratio': float(torrent[7]) / 1000,
'original_status': torrent[1],
'timeleft': str(timedelta(seconds = torrent[10])),
'folder': sp(torrent[26]),
'files': '|'.join(torrent_files)
})
return release_downloads
@@ -223,7 +222,7 @@ class uTorrentAPI(object):
if time.time() > self.last_time + 1800:
self.last_time = time.time()
self.token = self.get_token()
request = urllib2.Request(self.url + '?token=' + self.token + '&' + action, data)
request = urllib2.Request(self.url + "?token=" + self.token + "&" + action, data)
try:
open_request = self.opener.open(request)
response = open_request.read()
@@ -243,52 +242,52 @@ class uTorrentAPI(object):
return False
def get_token(self):
request = self.opener.open(self.url + 'token.html')
token = re.findall('<div.*?>(.*?)</', request.read())[0]
request = self.opener.open(self.url + "token.html")
token = re.findall("<div.*?>(.*?)</", request.read())[0]
return token
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:
action += '&path=%s' % urllib.quote(filename)
action += "&path=%s" % urllib.quote(filename)
return self._request(action)
def add_torrent_file(self, filename, filedata, add_folder = False):
action = 'action=add-file'
action = "action=add-file"
if add_folder:
action += '&path=%s' % urllib.quote(filename)
return self._request(action, {'torrent_file': (ss(filename), filedata)})
action += "&path=%s" % urllib.quote(filename)
return self._request(action, {"torrent_file": (ss(filename), filedata)})
def set_torrent(self, hash, params):
action = 'action=setprops&hash=%s' % hash
action = "action=setprops&hash=%s" % hash
for k, v in params.iteritems():
action += '&s=%s&v=%s' % (k, v)
action += "&s=%s&v=%s" % (k, v)
return self._request(action)
def pause_torrent(self, hash, pause = True):
if pause:
action = 'action=pause&hash=%s' % hash
action = "action=pause&hash=%s" % hash
else:
action = 'action=unpause&hash=%s' % hash
action = "action=unpause&hash=%s" % hash
return self._request(action)
def stop_torrent(self, hash):
action = 'action=stop&hash=%s' % hash
action = "action=stop&hash=%s" % hash
return self._request(action)
def remove_torrent(self, hash, remove_data = False):
if remove_data:
action = 'action=removedata&hash=%s' % hash
action = "action=removedata&hash=%s" % hash
else:
action = 'action=remove&hash=%s' % hash
action = "action=remove&hash=%s" % hash
return self._request(action)
def get_status(self):
action = 'list=1'
action = "list=1"
return self._request(action)
def get_settings(self):
action = 'action=getsettings'
action = "action=getsettings"
settings_dict = {}
try:
utorrent_settings = json.loads(self._request(action))
@@ -320,5 +319,5 @@ class uTorrentAPI(object):
return self._request(action)
def get_files(self, hash):
action = 'action=getfiles&hash=%s' % hash
action = "action=getfiles&hash=%s" % hash
return self._request(action)

View File

@@ -49,29 +49,8 @@ def ss(original, *args):
return u_original.encode('UTF-8')
def sp(path, *args):
# Standardise encoding, normalise case, path and strip trailing '/' or '\'
if not path or len(path) == 0:
return path
# convert windows path (from remote box) to *nix path
if os.path.sep == '/' and '\\' in path:
path = '/' + path.replace(':', '').replace('\\', '/')
path = os.path.normcase(os.path.normpath(ss(path, *args)))
# Remove any trailing path separators
if path != os.path.sep:
path = path.rstrip(os.path.sep)
# Add a trailing separator in case it is a root folder on windows (crashes guessit)
if len(path) == 2 and path[1] == ':':
path = path + os.path.sep
# Replace *NIX ambiguous '//' at the beginning of a path with '/' (crashes guessit)
path = re.sub('^//', '/', path)
return path
return os.path.normcase(os.path.normpath(ss(path, *args))).rstrip(os.path.sep)
def ek(original, *args):
if isinstance(original, (str, unicode)):

View File

@@ -2,7 +2,7 @@ from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
from couchpotato.core.logger import CPLog
import collections
import hashlib
import os
import os.path
import platform
import random
import re
@@ -11,9 +11,6 @@ import sys
log = CPLog(__name__)
def fnEscape(pattern):
return pattern.replace('[','[[').replace(']','[]]').replace('[[','[[]')
def link(src, dst):
if os.name == 'nt':
import ctypes
@@ -170,7 +167,7 @@ def natcmp(a, b):
return cmp(natsortKey(a), natsortKey(b))
def toIterable(value):
if type(value) in [list, tuple]:
if isinstance(value, collections.Iterable):
return value
return [value]
@@ -219,7 +216,3 @@ def splitString(str, split_on = ',', clean = True):
def dictIsSubset(a, b):
return all([k in b and b[k] == v for k, v in a.items()])
def isSubFolder(sub_folder, base_folder):
# Returns True is sub_folder is the same as or in base_folder
return base_folder.rstrip(os.path.sep) + os.path.sep in sub_folder.rstrip(os.path.sep) + os.path.sep

View File

@@ -1,6 +1,5 @@
from couchpotato import get_session
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.settings.model import Media
@@ -18,13 +17,6 @@ class MediaBase(Plugin):
'category': {},
}
search_dict = mergeDicts({
'library': {
'related_libraries': {},
'root_library': {}
},
}, default_dict)
def initType(self):
addEvent('media.types', self.getType)
@@ -36,7 +28,7 @@ class MediaBase(Plugin):
def onComplete():
db = get_session()
media = db.query(Media).filter_by(id = id).first()
fireEventAsync('%s.searcher.single' % media.type, media.to_dict(self.search_dict), on_complete = self.createNotifyFront(id))
fireEventAsync('%s.searcher.single' % media.type, media.to_dict(self.default_dict), on_complete = self.createNotifyFront(id))
db.expire_all()
return onComplete
@@ -46,7 +38,7 @@ class MediaBase(Plugin):
def notifyFront():
db = get_session()
media = db.query(Media).filter_by(id = media_id).first()
fireEvent('notify.frontend', type = '%s.update' % media.type, data = media.to_dict(self.default_dict))
fireEvent('notify.frontend', type = '%s.update.%s' % (media.type, media.id), data = media.to_dict(self.default_dict))
db.expire_all()
return notifyFront

View File

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

View File

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

View File

@@ -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
)

View File

@@ -1,6 +0,0 @@
from .main import Matcher
def start():
return Matcher()
config = []

View File

@@ -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()])

View File

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

View File

@@ -1,15 +1,10 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase
from couchpotato.core.settings.model import Library, LibraryTitle, Release, \
Media
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_, desc
from string import ascii_lowercase
from couchpotato.core.settings.model import Media
log = CPLog(__name__)
@@ -25,49 +20,7 @@ class MediaPlugin(MediaBase):
}
})
addApiView('media.list', self.listView, docs = {
'desc': 'List media',
'params': {
'type': {'type': 'string', 'desc': 'Media type to filter on.'},
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
'search': {'desc': 'Search movie title'},
},
'return': {'type': 'object', 'example': """{
'success': True,
'empty': bool, any movies returned or not,
'media': array, media found,
}"""}
})
addApiView('media.get', self.getView, docs = {
'desc': 'Get media by id',
'params': {
'id': {'desc': 'The id of the media'},
}
})
addApiView('media.delete', self.deleteView, docs = {
'desc': 'Delete a media from the wanted list',
'params': {
'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'},
'delete_from': {'desc': 'Delete media from this page', 'type': 'string: all (default), wanted, manage'},
}
})
addApiView('media.available_chars', self.charView)
addEvent('app.load', self.addSingleRefreshView)
addEvent('app.load', self.addSingleListView)
addEvent('app.load', self.addSingleCharView)
addEvent('app.load', self.addSingleDeleteView)
addEvent('media.get', self.get)
addEvent('media.list', self.list)
addEvent('media.delete', self.delete)
addEvent('media.restatus', self.restatus)
addEvent('app.load', self.addSingleRefresh)
def refresh(self, id = '', **kwargs):
db = get_session()
@@ -81,7 +34,7 @@ class MediaPlugin(MediaBase):
for title in media.library.titles:
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))
db.expire_all()
@@ -90,369 +43,7 @@ class MediaPlugin(MediaBase):
'success': True,
}
def addSingleRefreshView(self):
def addSingleRefresh(self):
for media_type in fireEvent('media.types', merge = True):
addApiView('%s.refresh' % media_type, self.refresh)
def get(self, media_id):
db = get_session()
imdb_id = getImdb(str(media_id))
if imdb_id:
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
else:
m = db.query(Media).filter_by(id = media_id).first()
results = None
if m:
results = m.to_dict(self.default_dict)
db.expire_all()
return results
def getView(self, id = None, **kwargs):
media = self.get(id) if id else None
return {
'success': media is not None,
'media': media,
}
def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
db = get_session()
# Make a list from string
if status and not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
if types and not isinstance(types, (list, tuple)):
types = [types]
# query movie ids
q = db.query(Media) \
.with_entities(Media.id) \
.group_by(Media.id)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Media.status_id.in_(statuses))
# Filter on release status
if release_status and len(release_status) > 0:
q = q.join(Media.releases)
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Release.status_id.in_(statuses))
# Filter on type
if types and len(types) > 0:
try: q = q.filter(Media.type.in_(types))
except: pass
# Only join when searching / ordering
if starts_with or search or order != 'release_order':
q = q.join(Media.library, Library.titles) \
.filter(LibraryTitle.default == True)
# Add search filters
filter_or = []
if starts_with:
starts_with = toUnicode(starts_with.lower())
if starts_with in ascii_lowercase:
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
else:
ignore = []
for letter in ascii_lowercase:
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
filter_or.append(not_(or_(*ignore)))
if search:
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
if len(filter_or) > 0:
q = q.filter(or_(*filter_or))
total_count = q.count()
if total_count == 0:
return 0, []
if order == 'release_order':
q = q.order_by(desc(Release.last_edit))
else:
q = q.order_by(asc(LibraryTitle.simple_title))
if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
# Get all media_ids in sorted order
media_ids = [m.id for m in q.all()]
# List release statuses
releases = db.query(Release) \
.filter(Release.media_id.in_(media_ids)) \
.all()
release_statuses = dict((m, set()) for m in media_ids)
releases_count = dict((m, 0) for m in media_ids)
for release in releases:
release_statuses[release.media_id].add('%d,%d' % (release.status_id, release.quality_id))
releases_count[release.media_id] += 1
# Get main movie data
q2 = db.query(Media) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files'))
q2 = q2.filter(Media.id.in_(media_ids))
results = q2.all()
# Create dict by movie id
movie_dict = {}
for movie in results:
movie_dict[movie.id] = movie
# List movies based on media_ids order
movies = []
for media_id in media_ids:
releases = []
for r in release_statuses.get(media_id):
x = splitString(r)
releases.append({'status_id': x[0], 'quality_id': x[1]})
# Merge releases with movie dict
movies.append(mergeDicts(movie_dict[media_id].to_dict({
'library': {'titles': {}, 'files':{}},
'files': {},
}), {
'releases': releases,
'releases_count': releases_count.get(media_id),
}))
db.expire_all()
return total_count, movies
def listView(self, **kwargs):
types = splitString(kwargs.get('types'))
status = splitString(kwargs.get('status'))
release_status = splitString(kwargs.get('release_status'))
limit_offset = kwargs.get('limit_offset')
starts_with = kwargs.get('starts_with')
search = kwargs.get('search')
order = kwargs.get('order')
total_movies, movies = self.list(
types = types,
status = status,
release_status = release_status,
limit_offset = limit_offset,
starts_with = starts_with,
search = search,
order = order
)
return {
'success': True,
'empty': len(movies) == 0,
'total': total_movies,
'movies': movies,
}
def addSingleListView(self):
for media_type in fireEvent('media.types', merge = True):
def tempList(*args, **kwargs):
return self.listView(types = media_type, *args, **kwargs)
addApiView('%s.list' % media_type, tempList)
def availableChars(self, types = None, status = None, release_status = None):
types = types or []
status = status or []
release_status = release_status or []
db = get_session()
# Make a list from string
if not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
if types and not isinstance(types, (list, tuple)):
types = [types]
q = db.query(Media)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Media.status_id.in_(statuses))
# Filter on release status
if release_status and len(release_status) > 0:
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.join(Media.releases) \
.filter(Release.status_id.in_(statuses))
# Filter on type
if types and len(types) > 0:
try: q = q.filter(Media.type.in_(types))
except: pass
q = q.join(Library, LibraryTitle) \
.with_entities(LibraryTitle.simple_title) \
.filter(LibraryTitle.default == True)
titles = q.all()
chars = set()
for title in titles:
try:
char = title[0][0]
char = char if char in ascii_lowercase else '#'
chars.add(str(char))
except:
log.error('Failed getting title for %s', title.libraries_id)
if len(chars) == 25:
break
db.expire_all()
return ''.join(sorted(chars))
def charView(self, **kwargs):
type = splitString(kwargs.get('type', 'movie'))
status = splitString(kwargs.get('status', None))
release_status = splitString(kwargs.get('release_status', None))
chars = self.availableChars(type, status, release_status)
return {
'success': True,
'empty': len(chars) == 0,
'chars': chars,
}
def addSingleCharView(self):
for media_type in fireEvent('media.types', merge = True):
def tempChar(*args, **kwargs):
return self.charView(types = media_type, *args, **kwargs)
addApiView('%s.available_chars' % media_type, tempChar)
def delete(self, media_id, delete_from = None):
db = get_session()
media = db.query(Media).filter_by(id = media_id).first()
if media:
deleted = False
if delete_from == 'all':
db.delete(media)
db.commit()
deleted = True
else:
done_status = fireEvent('status.get', 'done', single = True)
total_releases = len(media.releases)
total_deleted = 0
new_movie_status = None
for release in media.releases:
if delete_from in ['wanted', 'snatched', 'late']:
if release.status_id != done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'done'
elif delete_from == 'manage':
if release.status_id == done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'active'
db.commit()
if total_releases == total_deleted:
db.delete(media)
db.commit()
deleted = True
elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True)
media.profile_id = None
media.status_id = new_status.get('id')
db.commit()
else:
fireEvent('media.restatus', media.id, single = True)
if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict())
db.expire_all()
return True
def deleteView(self, id = '', **kwargs):
ids = splitString(id)
for media_id in ids:
self.delete(media_id, delete_from = kwargs.get('delete_from', 'all'))
return {
'success': True,
}
def addSingleDeleteView(self):
for media_type in fireEvent('media.types', merge = True):
def tempDelete(*args, **kwargs):
return self.deleteView(types = media_type, *args, **kwargs)
addApiView('%s.delete' % media_type, tempDelete)
def restatus(self, media_id):
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
db = get_session()
m = db.query(Media).filter_by(id = media_id).first()
if not m or len(m.library.titles) == 0:
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
return False
log.debug('Changing status for %s', m.library.titles[0].title)
if not m.profile:
m.status_id = done_status.get('id')
else:
move_to_wanted = True
for t in m.profile.types:
for release in m.releases:
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
move_to_wanted = False
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
db.commit()
return True

View File

@@ -1,17 +1,11 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.variable import md5, getTitle, splitString
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
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 re
import time
import traceback
log = CPLog(__name__)
@@ -171,7 +165,7 @@ class Searcher(SearcherBase):
return False
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))
rel_name = simplifyString(rel_name)

View File

@@ -2,10 +2,15 @@ from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString, tryInt
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
mergeDicts
from couchpotato.core.logger import CPLog
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
log = CPLog(__name__)
@@ -21,12 +26,33 @@ class MovieBase(MovieTypeBase):
super(MovieBase, self).__init__()
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 = {
'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.'},
'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.'},
}
})
@@ -35,12 +61,258 @@ class MovieBase(MovieTypeBase):
'params': {
'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.'},
'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.'},
}
})
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.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):
if not params: params = {}
@@ -149,9 +421,9 @@ class MovieBase(MovieTypeBase):
available_status = fireEvent('status.get', 'available', single = True)
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:
continue
@@ -174,12 +446,98 @@ class MovieBase(MovieTypeBase):
db.commit()
fireEvent('media.restatus', m.id)
fireEvent('movie.restatus', m.id)
movie_dict = m.to_dict(self.search_dict)
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id))
movie_dict = m.to_dict(self.default_dict)
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
db.expire_all()
return {
'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

View File

@@ -52,8 +52,8 @@ var MovieList = new Class({
self.getMovies();
App.on('movie.added', self.movieAdded.bind(self))
App.on('movie.deleted', self.movieDeleted.bind(self))
App.addEvent('movie.added', self.movieAdded.bind(self))
App.addEvent('movie.deleted', self.movieDeleted.bind(self))
},
movieDeleted: function(notification){
@@ -65,7 +65,6 @@ var MovieList = new Class({
movie.destroy();
delete self.movies_added[notification.data.id];
self.setCounter(self.counter_count-1);
self.total_movies--;
}
})
}
@@ -76,7 +75,6 @@ var MovieList = new Class({
movieAdded: function(notification){
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){
window.scroll(0,0);
self.createMovie(notification.data, 'top');
@@ -281,7 +279,7 @@ var MovieList = new Class({
// Get available chars and highlight
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
Api.request('media.available_chars', {
Api.request('movie.available_chars', {
'data': Object.merge({
'status': self.options.status
}, self.filter),
@@ -372,7 +370,7 @@ var MovieList = new Class({
'click': function(e){
(e).preventDefault();
this.set('text', 'Deleting..')
Api.request('media.delete', {
Api.request('movie.delete', {
'data': {
'id': ids.join(','),
'delete_from': self.options.identifier
@@ -392,7 +390,6 @@ var MovieList = new Class({
self.movies.erase(movie);
movie.destroy();
self.setCounter(self.counter_count-1);
self.total_movies--;
});
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({
'type': 'movie',
'status': self.options.status,
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
}, self.filter),

View File

@@ -126,9 +126,7 @@ MA.Release = new Class({
else
self.showHelper();
App.on('movie.searcher.ended', function(notification){
if(self.movie.data.id != notification.data.id) return;
App.addEvent('movie.searcher.ended.'+self.movie.data.id, function(notification){
self.releases = null;
if(self.options_container){
self.options_container.destroy();
@@ -252,14 +250,12 @@ MA.Release = new Class({
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
var update_handle = function(notification) {
if(notification.data.id != release.id) return;
var q = self.movie.quality.getElement('.q_id' + release.quality_id),
var q = self.movie.quality.getElement('.q_id' + release.quality_id),
status = Status.get(release.status_id),
new_status = Status.get(notification.data.status_id);
new_status = Status.get(notification.data);
release.status_id = new_status.id
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);
});
@@ -289,7 +285,7 @@ MA.Release = new Class({
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
var nr = self.next_release,
lr = self.last_release;
@@ -431,7 +427,7 @@ MA.Release = new Class({
markMovieDone: function(){
var self = this;
Api.request('media.delete', {
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id'),
'delete_from': 'wanted'
@@ -450,7 +446,7 @@ MA.Release = new Class({
},
tryNextRelease: function(){
tryNextRelease: function(movie_id){
var self = this;
Api.request('movie.searcher.try_next', {
@@ -821,7 +817,7 @@ MA.Delete = new Class({
self.callChain();
},
function(){
Api.request('media.delete', {
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id'),
'delete_from': self.movie.list.options.identifier

View File

@@ -1036,7 +1036,7 @@
text-overflow: ellipsis;
overflow: hidden;
width: 85%;
direction: ltr;
direction: rtl;
vertical-align: middle;
}

View File

@@ -23,49 +23,23 @@ var Movie = new Class({
addEvents: function(){
var self = this;
self.global_events = {}
// Do refresh with new data
self.global_events['movie.update'] = function(notification){
if(self.data.id != notification.data.id) return;
App.addEvent('movie.update.'+self.data.id, function(notification){
self.busy(false);
self.removeView();
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){
self.global_events[listener] = function(notification){
if(notification.data && self.data.id == notification.data.id)
App.addEvent(listener+'.'+self.data.id, function(notification){
if(notification.data)
self.busy(true)
}
App.on(listener, self.global_events[listener]);
});
})
// Remove spinner
self.global_events['movie.searcher.ended'] = function(notification){
if(notification.data && self.data.id == notification.data.id)
App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){
if(notification.data)
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(){
@@ -78,9 +52,10 @@ var Movie = new Class({
self.list.checkIfEmpty();
// Remove events
Object.each(self.global_events, function(handle, listener){
App.off(listener, handle);
});
App.removeEvents('movie.update.'+self.data.id);
['movie.busy', 'movie.searcher.started'].each(function(listener){
App.removeEvents(listener+'.'+self.data.id);
})
},
busy: function(set_busy, timeout){
@@ -204,7 +179,21 @@ var Movie = new Class({
});
// 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){
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){
var self = this;

View File

@@ -181,7 +181,7 @@ Block.Search.MovieItem = new Class({
if(categories.length == 0)
self.category_select.hide();
else {
self.category_select.movie();
self.category_select.show();
categories.each(function(category){
new Element('option', {
'value': category.data.id,

View File

@@ -2,7 +2,7 @@ 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.media._base.library.base import LibraryBase
from couchpotato.core.media._base.library import LibraryBase
from couchpotato.core.settings.model import Library, LibraryTitle, File
from string import ascii_letters
import time
@@ -16,46 +16,27 @@ class MovieLibraryPlugin(LibraryBase):
default_dict = {'titles': {}, 'files':{}}
def __init__(self):
addEvent('library.query', self.query)
addEvent('library.add.movie', self.add)
addEvent('library.update.movie', self.update)
addEvent('library.update.movie.release_date', self.updateReleaseDate)
def query(self, library, first = True, include_year = True, **kwargs):
if library.get('type') != 'movie':
return
def add(self, attrs = None, update_after = True):
if not attrs: attrs = {}
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')
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:
status = fireEvent('status.get', 'needs_update', single = True)
l = Library(
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
info = {}
)
title = LibraryTitle(

View File

@@ -30,6 +30,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
addEvent('movie.searcher.try_next_release', self.tryNextRelease)
addEvent('movie.searcher.could_be_released', self.couldBeReleased)
addEvent('searcher.correct_release', self.correctRelease)
addEvent('searcher.get_search_title', self.getSearchTitle)
addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
'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'])
if not default_title:
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
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
@@ -191,7 +192,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
else:
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 if CP wants to shut down
@@ -201,7 +202,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
if len(too_early_to_search) > 0:
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
@@ -209,7 +210,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
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)
retention = Env.setting('retention', section = 'nzb')
@@ -283,10 +284,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
return True
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
if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
return True
@@ -321,14 +318,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
'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)
try:
db = get_session()
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')])) \
.all()
@@ -336,7 +333,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
rel.status_id = ignored_status.get('id')
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']))
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())
return False
def getSearchTitle(self, media):
if media['type'] == 'movie':
return getTitle(media['library'])
class SearchSetupError(Exception):
pass

View File

@@ -1,6 +0,0 @@
from .main import ShowBase
def start():
return ShowBase()
config = []

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

View File

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

View File

@@ -1,6 +0,0 @@
from .main import EpisodeLibraryPlugin
def start():
return EpisodeLibraryPlugin()
config = []

View File

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

View File

@@ -1,6 +0,0 @@
from .main import SeasonLibraryPlugin
def start():
return SeasonLibraryPlugin()
config = []

View File

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

View File

@@ -1,6 +0,0 @@
from .main import ShowLibraryPlugin
def start():
return ShowLibraryPlugin()
config = []

View File

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

View File

@@ -1,6 +0,0 @@
from .main import ShowMatcher
def start():
return ShowMatcher()
config = []

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

View File

@@ -1,7 +0,0 @@
from .main import ShowSearcher
import random
def start():
return ShowSearcher()
config = []

View File

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

View File

@@ -17,7 +17,7 @@ class Notification(Provider):
listen_to = [
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
'core.message.important',
'core.message',
]
dont_listen_to = []

View File

@@ -16,14 +16,14 @@ class Boxcar(Notification):
try:
message = message.strip()
data = {
params = {
'email': self.conf('email'),
'notification[from_screen_name]': self.default_title,
'notification[message]': toUnicode(message),
'notification[from_remote_service_id]': int(time.time()),
}
self.urlopen(self.url, data = data)
self.urlopen(self.url, params = params)
except:
log.error('Check your email and added services on boxcar.io')
return False

View File

@@ -21,12 +21,6 @@ class CoreNotifier(Notification):
m_lock = None
listen_to = [
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
'core.message', 'core.message.important',
]
def __init__(self):
super(CoreNotifier, self).__init__()
@@ -127,10 +121,7 @@ class CoreNotifier(Notification):
for message in messages:
if message.get('time') > last_check:
message['sticky'] = True # Always sticky core messages
message_type = 'core.message.important' if message.get('important') else 'core.message'
fireEvent(message_type, message = message.get('message'), data = message)
fireEvent('core.message', message = message.get('message'), data = message)
if last_check < message.get('time'):
last_check = message.get('time')

View File

@@ -10,8 +10,8 @@ var NotificationBase = new Class({
// Listener
App.addEvent('unload', self.stopPoll.bind(self));
App.addEvent('reload', self.startInterval.bind(self, [true]));
App.on('notification', self.notify.bind(self));
App.on('message', self.showMessage.bind(self));
App.addEvent('notification', self.notify.bind(self));
App.addEvent('message', self.showMessage.bind(self));
// Add test buttons to settings page
App.addEvent('load', self.addTestButtons.bind(self));
@@ -50,9 +50,9 @@ var NotificationBase = new Class({
, 'top');
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
App.trigger('message', [result.message, sticky, result])
App.fireEvent('message', [result.message, sticky, result])
}
else if(!result.read){
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
@@ -147,7 +147,7 @@ var NotificationBase = new Class({
// Process data
if(json){
Array.each(json.result, function(result){
App.trigger(result.type, result);
App.fireEvent(result.type, result);
if(result.message && result.read === undefined)
self.showMessage(result.message);
})

View File

@@ -4,7 +4,6 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.environment import Env
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
import smtplib
import traceback
@@ -31,8 +30,6 @@ class Email(Notification):
message['Subject'] = self.default_title
message['From'] = from_address
message['To'] = to_address
message['Date'] = formatdate(localtime = 1)
message['Message-ID'] = make_msgid()
try:
# Open the SMTP connection, via SSL if requested

View File

@@ -26,7 +26,7 @@ class Prowl(Notification):
}
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.')
return True
except:

View File

@@ -29,7 +29,7 @@ class Pushalot(Notification):
}
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
except:
log.error('PushAlot failed: %s', traceback.format_exc())

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ class Trakt(Notification):
def call(self, method_url, post_data):
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.get('status') == "success":
log.info('Successfully called Trakt')

View File

@@ -46,14 +46,6 @@ config = [{
'advanced': True,
'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',
'default': 0,

View File

@@ -7,7 +7,6 @@ import json
import socket
import traceback
import urllib
import requests
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):
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']}
calls.append(('VideoLibrary.Scan', param))
@@ -168,18 +167,22 @@ class XBMC(Notification):
# manually fake expected response array
return [{'result': 'Error'}]
except requests.exceptions.Timeout:
log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off')
return [{'result': 'Error'}]
except URLError, e:
if isinstance(e.reason, socket.timeout):
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:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
return [{'result': 'Error'}]
def request(self, host, do_requests):
def request(self, host, requests):
server = 'http://%s/jsonrpc' % host
data = []
for req in do_requests:
for req in requests:
method, kwargs = req
data.append({
'method': method,
@@ -199,13 +202,17 @@ class XBMC(Notification):
try:
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))
return response
except requests.exceptions.Timeout:
log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off')
return []
except URLError, e:
if isinstance(e.reason, socket.timeout):
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:
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return []

View File

@@ -41,7 +41,7 @@ config = [{
'label': 'Required Genres',
'default': '',
'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',

View File

@@ -43,7 +43,7 @@ class Automation(Plugin):
if self.shuttingDown():
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)
return True
return True

View File

@@ -1,15 +1,19 @@
from StringIO import StringIO
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
from couchpotato.core.helpers.variable import getExt, md5, isLocalIP
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
import requests
from multipartpost import MultipartPostHandler
from tornado import template
from tornado.web import StaticFileHandler
from urlparse import urlparse
import cookielib
import glob
import gzip
import inspect
import math
import os.path
import re
import time
@@ -35,7 +39,6 @@ class Plugin(object):
http_time_between_calls = 0
http_failed_request = {}
http_failed_disabled = {}
http_opener = requests.Session()
def __new__(typ, *args, **kwargs):
new_plugin = super(Plugin, typ).__new__(typ)
@@ -103,9 +106,7 @@ class Plugin(object):
f.close()
os.chmod(path, Env.getPermission('file'))
except Exception, e:
log.error('Unable writing to file "%s": %s', (path, traceback.format_exc()))
if os.path.isfile(path):
os.remove(path)
log.error('Unable writing to file "%s": %s', (path, e))
def makeDir(self, path):
path = ss(path)
@@ -119,11 +120,11 @@ class Plugin(object):
return False
# 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 = "%/:=&?~#+!$,;'@()*[]")
if not headers: headers = {}
if not data: data = {}
if not params: params = {}
# Fill in some headers
parsed_url = urlparse(url)
@@ -136,8 +137,6 @@ class Plugin(object):
headers['Connection'] = headers.get('Connection', 'keep-alive')
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
r = self.http_opener
# Don't try for failed requests
if self.http_failed_disabled.get(host, 0) > 0:
if self.http_failed_disabled[host] > (time.time() - 900):
@@ -153,18 +152,45 @@ class Plugin(object):
self.wait(host)
try:
kwargs = {
'headers': headers,
'data': data if len(data) > 0 else None,
'timeout': timeout,
'files': files,
}
method = 'post' if len(data) > 0 or files else 'get'
# Make sure opener has the correct headers
if opener:
opener.add_headers = headers
log.info('Opening url: %s %s, data: %s', (method, url, [x for x in data.iterkeys()] if isinstance(data, dict) else 'with data'))
response = r.request(method, url, verify = False, **kwargs)
if multipart:
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
except IOError:
@@ -192,19 +218,15 @@ class Plugin(object):
return data
def wait(self, host = ''):
if self.http_time_between_calls == 0:
return
now = time.time()
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:
log.debug('Waiting for %s, %d seconds', (self.getName(), wait))
time.sleep(wait)
if wait > 0:
log.debug('Waiting for %s, %d seconds', (self.getName(), wait))
time.sleep(last_use - now + self.http_time_between_calls)
def beforeCall(self, handler):
self.isRunning('%s.%s' % (self.getName(), handler.__name__))
@@ -247,19 +269,18 @@ class Plugin(object):
try:
cache_timeout = 300
if kwargs.has_key('cache_timeout'):
if kwargs.get('cache_timeout'):
cache_timeout = kwargs.get('cache_timeout')
del kwargs['cache_timeout']
data = self.urlopen(url, **kwargs)
if data and cache_timeout > 0:
if data:
self.setCache(cache_key, data, timeout = cache_timeout)
return data
except:
if not kwargs.get('show_error', True):
raise
log.error('Failed getting cache: %s', (traceback.format_exc()))
return ''
def setCache(self, cache_key, value, timeout = 300):
@@ -268,19 +289,19 @@ class Plugin(object):
Env.get('cache').set(cache_key_md5, value, timeout)
return value
def createNzbName(self, data, media):
tag = self.cpTag(media)
def createNzbName(self, data, movie):
tag = self.cpTag(movie)
return '%s%s' % (toSafeString(toUnicode(data.get('name'))[:127 - len(tag)]), tag)
def createFileName(self, data, filedata, media):
name = sp(os.path.join(self.createNzbName(data, media)))
def createFileName(self, data, filedata, movie):
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:
return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('protocol'))
def cpTag(self, media):
def cpTag(self, movie):
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 ''

View File

@@ -93,7 +93,7 @@ class FileManager(Plugin):
return dest
try:
filedata = self.urlopen(url, return_raw = True, **urlopen_kwargs)
filedata = self.urlopen(url, **urlopen_kwargs)
except:
log.error('Failed downloading file %s: %s', (url, traceback.format_exc()))
return False

View File

@@ -1,6 +1,6 @@
from couchpotato.api import addApiView
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.logger import CPLog
from couchpotato.core.plugins.base import Plugin
@@ -79,7 +79,6 @@ class Manage(Plugin):
try:
directories = self.directories()
directories.sort()
added_identifiers = []
# Add some progress
@@ -112,20 +111,22 @@ class Manage(Plugin):
if self.conf('cleanup') and full and not self.shuttingDown():
# 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:
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:
releases = fireEvent('release.for_movie', id = done_movie.get('id'), single = True)
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', []):
# 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'])
break
@@ -200,7 +201,7 @@ class Manage(Plugin):
self.in_progress[folder]['to_go'] -= 1
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']))

View File

@@ -2,7 +2,7 @@ from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
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.plugins.base import Plugin
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': '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': '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': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':[], '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': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':[]},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':[]},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':[]},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]},
# 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']},
{'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':['avi', 'mpg', 'mpeg'], 'tags': ['webrip', ('web', 'rip')]},
{'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':['avi', 'mpg', 'mpeg']},
{'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':['avi', 'mpg', 'mpeg']}
]
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
@@ -68,8 +50,6 @@ class QualityPlugin(Plugin):
addEvent('app.initialize', self.fill, priority = 10)
addEvent('app.test', self.doTest)
def preReleases(self):
return self.pre_releases
@@ -185,10 +165,9 @@ class QualityPlugin(Plugin):
if not extra: extra = {}
# 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)
if cached and len(extra) == 0:
return cached
if cached and len(extra) == 0: return cached
qualities = self.all()
@@ -249,6 +228,11 @@ class QualityPlugin(Plugin):
if len(set(words) & set(alt)) == len(alt):
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
score += points.get(tag_type)
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()):
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:
for allow in quality.get('allow', []):
score[allow] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5
def doTest(self):
tests = {
'Movie Name (1999)-DVD-Rip.avi': 'dvdrip',
'Movie Name 1999 720p Bluray.mkv': '720p',
'Movie Name 1999 BR-Rip 720p.avi': 'brrip',
'Movie Name 1999 720p Web Rip.avi': 'scr',
'Movie Name 1999 Web DL.avi': 'brrip',
'Movie.Name.1999.1080p.WEBRip.H264-Group': 'scr',
'Movie.Name.1999.DVDRip-Group': 'dvdrip',
'Movie.Name.1999.DVD-Rip-Group': 'dvdrip',
'Movie.Name.1999.DVD-R-Group': 'dvdr',
'Movie.Name.Camelie.1999.720p.BluRay.x264-Group': '720p',
}
correct = 0
for name in tests:
success = self.guess([name]).get('identifier') == tests[name]
if not success:
log.error('%s failed check, thinks it\'s %s', (name, self.guess([name]).get('identifier')))
correct += success
if correct == len(tests):
log.info('Quality test successful')
return True
else:
log.error('Quality test failed: %s out of %s succeeded', (correct, len(tests)))

View File

@@ -100,14 +100,14 @@ class Release(Plugin):
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
# Add movie
media = db.query(Media).filter_by(library_id = group['library'].get('id')).first()
if not media:
media = Media(
movie = db.query(Media).filter_by(library_id = group['library'].get('id')).first()
if not movie:
movie = Media(
library_id = group['library'].get('id'),
profile_id = 0,
status_id = done_status.get('id')
)
db.add(media)
db.add(movie)
db.commit()
# Add Release
@@ -120,7 +120,7 @@ class Release(Plugin):
if not rel:
rel = Relea(
identifier = identifier,
media = media,
movie = movie,
quality_id = group['meta_data']['quality'].get('id'),
status_id = done_status.get('id')
)
@@ -142,7 +142,7 @@ class Release(Plugin):
except:
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
@@ -211,136 +211,119 @@ class Release(Plugin):
db = get_session()
rel = db.query(Relea).filter_by(id = id).first()
if not rel:
log.error('Couldn\'t find release with id: %s', id)
return {
'success': False
}
if rel:
item = {}
for info in rel.info:
item[info.identifier] = info.value
item = {}
for info in rel.info:
item[info.identifier] = info.value
fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Snatching "%s"' % item['name'])
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
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
if not item.get('protocol'):
item['protocol'] = item['type']
item['type'] = 'movie'
# Backwards compatibility code
if not item.get('protocol'):
item['protocol'] = item['type']
item['type'] = 'movie'
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.media.to_dict({
success = self.download(data = item, media = rel.movie.to_dict({
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
}), 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({
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
}), manual = True)
if success:
db.expunge_all()
rel = db.query(Relea).filter_by(id = id).first() # Get release again @RuudBurger why do we need to get it again??
if success == True:
db.expunge_all()
rel = db.query(Relea).filter_by(id = id).first() # Get release again @RuudBurger why do we need to get it again??
fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return {
'success': success
}
else:
log.error('Couldn\'t find release with id: %s', id)
fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return {
'success': success == True
'success': False
}
def download(self, data, media, manual = False):
# Backwards compatibility code
if not data.get('protocol'):
data['protocol'] = data['type']
data['type'] = 'movie'
# Test to see if any downloaders are enabled for this type
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
if not downloader_enabled:
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', data.get('protocol'))
return False
# Download NZB or torrent file
filedata = None
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
log.info('Tried to download, but the "%s" provider gave an error', data.get('protocol'))
if filedata == 'try_next':
return filedata
if downloader_enabled:
snatched_status, done_status, active_status = fireEvent('status.get', ['snatched', 'done', 'active'], single = True)
# Send NZB or torrent file to downloader
download_result = fireEvent('download', data = data, media = media, manual = manual, filedata = filedata, single = True)
if not download_result:
log.info('Tried to download, but the "%s" downloader gave an error', data.get('protocol'))
return False
log.debug('Downloader result: %s', download_result)
# Download release to temp
filedata = None
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
if filedata == 'try_next':
return filedata
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:
db = get_session()
rls = db.query(Relea).filter_by(identifier = md5(data['url'])).first()
if not rls:
log.error('No release found to store download information in')
return False
if download_result:
try:
# Mark release as snatched
db = get_session()
rls = db.query(Relea).filter_by(identifier = md5(data['url'])).first()
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):
for key in download_result:
rls_info = ReleaseInfo(
identifier = 'download_%s' % key,
value = toUnicode(download_result.get(key))
)
rls.info.append(rls_info)
db.commit()
log_movie = '%s (%s) in %s' % (getTitle(media['library']), media['library']['year'], rls.quality.label)
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message)
fireEvent('%s.snatched' % data['type'], message = snatch_message, data = rls.to_dict())
# Mark release as snatched
if renamer_enabled:
self.updateStatus(rls.id, status = snatched_status)
# If renamer isn't used, mark media done if finished or release downloaded
else:
if media['status_id'] == active_status.get('id'):
finished = next((True for profile_type in media['profile']['types'] if \
profile_type['quality_id'] == rls.quality.id and profile_type['finish']), False)
if finished:
log.info('Renamer disabled, marking media as finished: %s', log_movie)
# Mark release done
self.updateStatus(rls.id, status = done_status)
# Mark media done
mdia = db.query(Media).filter_by(id = media['id']).first()
mdia.status_id = done_status.get('id')
mdia.last_edit = int(time.time())
# Save download-id info if returned
if isinstance(download_result, dict):
for key in download_result:
rls_info = ReleaseInfo(
identifier = 'download_%s' % key,
value = toUnicode(download_result.get(key))
)
rls.info.append(rls_info)
db.commit()
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
self.updateStatus(rls.id, status = downloaded_status)
# If renamer isn't used, mark media done
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:
log.error('Failed storing download status: %s', traceback.format_exc())
return False
# Mark release done
self.updateStatus(rls.id, status = done_status)
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):
ignored_status, failed_status = fireEvent('status.get', ['ignored', 'failed'], single = True)
@@ -382,7 +365,8 @@ class Release(Plugin):
if not rls:
rls = Relea(
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'),
status_id = available_status.get('id')
)
@@ -419,7 +403,7 @@ class Release(Plugin):
releases_raw = db.query(Relea) \
.options(joinedload_all('info')) \
.options(joinedload_all('files')) \
.filter(Relea.media_id == id) \
.filter(Relea.movie_id == id) \
.all()
releases = [r.to_dict({'info':{}, 'files':{}}) for r in releases_raw]
@@ -462,6 +446,6 @@ class Release(Plugin):
db.commit()
#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

View File

@@ -93,7 +93,7 @@ config = [{
'default': 1,
'type': 'int',
'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,
@@ -122,13 +122,13 @@ config = [{
'advanced': True,
'name': '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,
'name': 'foldersep',
'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',
@@ -136,7 +136,7 @@ config = [{
'default': 'link',
'type': 'dropdown',
'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,
},
{

View File

@@ -3,7 +3,7 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss, sp
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.plugins.base import Plugin
from couchpotato.core.settings.model import Library, File, Profile, Release, \
@@ -30,11 +30,9 @@ class Renamer(Plugin):
'desc': 'For the renamer to check for new files to rename in a folder',
'params': {
'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.'},
'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.'},
'base_folder': {'desc': 'Optional: The folder to find releases in. Leave empty for default folder.'},
'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'},
'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\''},
},
})
@@ -65,33 +63,24 @@ class Renamer(Plugin):
def scanView(self, **kwargs):
async = tryInt(kwargs.get('async', 0))
base_folder = kwargs.get('base_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'))
movie_folder = sp(kwargs.get('movie_folder'))
downloader = kwargs.get('downloader')
download_id = kwargs.get('download_id')
files = '|'.join([sp(filename) for filename in splitString(kwargs.get('files'), '|')])
status = kwargs.get('status', 'completed')
release_download = None
if not base_folder and media_folder:
release_download = {'folder': media_folder}
release_download.update({'id': download_id, 'downloader': downloader, 'status': status, 'files': files} if download_id else {})
release_download = {'folder': movie_folder} if movie_folder else None
if release_download:
release_download.update({'id': download_id, 'downloader': downloader, 'status': status} if download_id else {})
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 {
'success': True
}
def scan(self, base_folder = None, release_download = None):
if not release_download: release_download = {}
def scan(self, release_download = None):
if self.isDisabled():
return
@@ -100,14 +89,11 @@ class Renamer(Plugin):
log.info('Renamer is already running, if you see this often, check the logs above for errors.')
return
if not base_folder:
base_folder = self.conf('from')
from_folder = sp(self.conf('from'))
to_folder = sp(self.conf('to'))
# Get media folder to process
media_folder = release_download.get('folder')
# Get movie folder to process
movie_folder = release_download and release_download.get('folder')
# Get all folders that should not be processed
no_process = [to_folder]
@@ -115,78 +101,78 @@ class Renamer(Plugin):
no_process.extend([item['destination'] for item in cat_list])
try:
if Env.setting('library', section = 'manage').strip():
no_process.extend([sp(manage_folder) for manage_folder in splitString(Env.setting('library', section = 'manage'), '::')])
no_process.extend(splitString(Env.setting('library', section = 'manage'), '::'))
except:
pass
# 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):
log.error('Both the "To" and "From" folder have to exist.')
if not os.path.isdir(from_folder) or not os.path.isdir(to_folder):
log.error('Both the "To" and "From" have to exist.')
return
else:
for item in no_process:
if isSubFolder(item, base_folder):
log.error('To protect your data, the media libraries can\'t be inside of or the same as the "from" folder.')
if from_folder in item:
log.error('To protect your data, the movie libraries can\'t be inside of or the same as the "from" folder.')
return
# Check to see if the no_process folders are inside the provided media_folder
if media_folder and not os.path.isdir(media_folder):
log.debug('The provided media folder %s does not exist. Trying to find it in the \'from\' folder.', media_folder)
# Check to see if the no_process folders are inside the provided movie_folder
if movie_folder and not os.path.isdir(movie_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
if len(splitString(release_download.get('files'), '|')) == 1:
new_media_folder = from_folder
if len(release_download.get('files')) == 1:
new_movie_folder = from_folder
else:
new_media_folder = os.path.join(from_folder, os.path.basename(media_folder))
new_movie_folder = sp(os.path.join(from_folder, os.path.basename(movie_folder)))
if not os.path.isdir(new_media_folder):
log.error('The provided media folder %s does not exist and could also not be found in the \'from\' folder.', media_folder)
if not os.path.isdir(new_movie_folder):
log.error('The provided movie folder %s does not exist and could also not be found in the \'from\' folder.', movie_folder)
return
# 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]):
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
# Update release_download info to the from folder
log.debug('Release %s found in the \'from\' folder.', media_folder)
release_download['folder'] = new_media_folder
log.debug('Release %s found in the \'from\' folder.', movie_folder)
release_download['folder'] = new_movie_folder
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:
if isSubFolder(item, media_folder):
log.error('To protect your data, the media libraries can\'t be inside of or the same as the provided media folder.')
if movie_folder in item:
log.error('To protect your data, the movie libraries can\'t be inside of or the same as the provided movie folder.')
return
# Make sure a checkSnatched marked all downloads/seeds as such
if not release_download and self.conf('run_every') > 0:
self.checkSnatched(fire_scan = False)
fireEvent('renamer.check_snatched')
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
files = []
if media_folder:
log.info('Scanning media folder %s...', media_folder)
folder = os.path.dirname(media_folder)
if movie_folder:
log.info('Scanning movie folder %s...', movie_folder)
folder = os.path.dirname(movie_folder)
if release_download.get('files', ''):
files = splitString(release_download['files'], '|')
# If there is only one file in the torrent, the downloader did not create a subfolder
if len(files) == 1:
folder = media_folder
folder = movie_folder
else:
# Get all files from the specified folder
try:
for root, folders, names in os.walk(media_folder):
files.extend([sp(os.path.join(root, name)) for name in names])
for root, folders, names in os.walk(movie_folder):
files.extend([os.path.join(root, name) for name in names])
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()
@@ -196,10 +182,10 @@ class Renamer(Plugin):
# Unpack any archives
extr_files = None
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))
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 []
folder_name = self.conf('folder_name')
@@ -240,7 +226,7 @@ class Renamer(Plugin):
# Overwrite destination when set in category
destination = to_folder
category_label = ''
for movie in library_ent.media:
for movie in library_ent.movies:
if movie.category and movie.category.label:
category_label = movie.category.label
@@ -414,13 +400,13 @@ class Renamer(Plugin):
remove_leftovers = True
# 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()
fireEvent('movie.add', params = {'identifier': group['library']['identifier'], 'profile_id': profile.id}, search_after = False)
db.expire_all()
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
try:
@@ -508,10 +494,7 @@ class Renamer(Plugin):
os.remove(src)
parent_dir = os.path.dirname(src)
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and \
not isSubFolder(destination, parent_dir) and not isSubFolder(media_folder, parent_dir) and \
not isSubFolder(parent_dir, base_folder):
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:
delete_folders.append(parent_dir)
except:
@@ -543,7 +526,7 @@ class Renamer(Plugin):
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
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')
# Remove matching releases
@@ -555,12 +538,12 @@ class Renamer(Plugin):
log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(release_download):
if media_folder:
if movie_folder:
# Delete the movie folder
group_folder = media_folder
group_folder = movie_folder
else:
# 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:
log.info('Deleting folder: %s', group_folder)
@@ -578,7 +561,7 @@ class Renamer(Plugin):
# Break if CP wants to shut down
if self.shuttingDown():
break
self.renaming_started = False
def getRenameExtras(self, extra_type = '', replacements = None, folder_name = '', file_name = '', destination = '', group = None, current_file = '', remove_multiple = False):
@@ -620,7 +603,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
elif isinstance(release_download, dict):
# Tag download_files if they are known
if release_download['files']:
tag_files = splitString(release_download['files'], '|')
tag_files = release_download['files'].split('|')
# Tag all files in release folder
else:
@@ -628,11 +611,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])
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)
if not os.path.isfile(tag_filename):
self.createFile(tag_filename, text)
@@ -649,21 +627,21 @@ Remove it if you want it to be renamed (again, or at least let it try again)
# Untag download_files if they are known
if release_download['files']:
tag_files = splitString(release_download['files'], '|')
tag_files = release_download['files'].split('|')
# Untag all files in release folder
else:
for root, folders, names in os.walk(release_download['folder']):
tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore'])
tag_files.extend([os.path.join(root, name) for name in names if not os.path.splitext(name)[1] == '.ignore'])
# Find all .ignore files in folder
ignore_files = []
for root, dirnames, filenames in os.walk(folder):
ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag))
ignore_files.extend(fnmatch.filter([os.path.join(root, filename) for filename in filenames], '*%s.ignore' % tag))
# Match all found ignore files with the tag_files and delete if found
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:
try:
os.remove(filename)
@@ -683,20 +661,20 @@ Remove it if you want it to be renamed (again, or at least let it try again)
# Find tag on download_files if they are known
if release_download['files']:
tag_files = splitString(release_download['files'], '|')
tag_files = release_download['files'].split('|')
# Find tag on all files in release folder
else:
for root, folders, names in os.walk(release_download['folder']):
tag_files.extend([sp(os.path.join(root, name)) for name in names if not os.path.splitext(name)[1] == '.ignore'])
tag_files.extend([os.path.join(root, name) for name in names if not os.path.splitext(name)[1] == '.ignore'])
# Find all .ignore files in folder
for root, dirnames, filenames in os.walk(folder):
ignore_files.extend(fnmatch.filter([sp(os.path.join(root, filename)) for filename in filenames], '*%s.ignore' % tag))
ignore_files.extend(fnmatch.filter([os.path.join(root, filename) for filename in filenames], '*%s.ignore' % tag))
# Match all found ignore files with the tag_files and return True found
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:
return True
@@ -791,7 +769,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
return string
def deleteEmptyFolder(self, folder, show_error = True):
folder = sp(folder)
folder = ss(folder)
loge = log.error if show_error else log.debug
for root, dirs, files in os.walk(folder):
@@ -809,7 +787,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
except:
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:
log.debug('Already checking snatched')
@@ -825,168 +803,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')])
).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_required = False
log.debug('Checking status snatched releases...')
if rels:
log.debug('Checking status snatched releases...')
try:
for rel in rels:
rel_dict = rel.to_dict({'info': {}})
movie_dict = fireEvent('media.get', media_id = rel.media_id, single = True)
release_downloads = fireEvent('download.status', merge = True)
if not release_downloads:
log.debug('Download status functionality is not implemented for active downloaders.')
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)):
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:
if not isinstance(rel_dict['info'], (dict)):
log.error('Faulty release found without any info, ignoring.')
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)
continue
# Continue with next release
continue
# check status
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
# Log that we found the release
timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft']
log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft))
# Check status of release
if release_download['status'] == 'busy':
# Set the release to snatched if it was missing before
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)
found = False
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
else:
# Scan and Allow the downloader to clean-up
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
scan_releases.append(release_download)
if release_download['name'] == nzbname or rel_dict['info']['name'] in release_download['name'] or getImdb(release_download['name']) == movie_dict['library']['identifier']:
found_release = True
else:
# Set the release to snatched if it was missing before
fireEvent('release.update_status', rel.id, status = snatched_status, single = True)
if found_release:
timeleft = 'N/A' if release_download['timeleft'] == -1 else release_download['timeleft']
log.debug('Found %s: %s, time to go: %s', (release_download['name'], release_download['status'].upper(), timeleft))
# Remove the downloading tag
self.untagRelease(release_download = release_download, tag = 'downloading')
if release_download['status'] == 'busy':
# 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
release_download.update({'pause': False, 'scan': True, 'process_complete': True})
scan_releases.append(release_download)
else:
scan_required = 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')
except:
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
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', 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
for release_download in scan_releases:
@@ -994,7 +930,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['pause'] and self.conf('file_action') == 'link':
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':
fireEvent('download.pause', release_download = release_download, pause = False, single = True)
if release_download['process_complete']:
@@ -1005,10 +941,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
fireEvent('download.process_complete', release_download = release_download, single = True)
if fire_scan and (scan_required or len(no_status_support) > 0):
self.scan()
if scan_required:
fireEvent('renamer.scan')
self.checking_snatched = False
return True
def extendReleaseDownload(self, release_download):
@@ -1055,10 +992,10 @@ Remove it if you want it to be renamed (again, or at least let it try again)
def statusInfoComplete(self, release_download):
return release_download['id'] and release_download['downloader'] and release_download['folder']
def movieInFromFolder(self, media_folder):
return media_folder and isSubFolder(media_folder, sp(self.conf('from'))) or not media_folder
def movieInFromFolder(self, movie_folder):
return movie_folder and sp(self.conf('from')) in 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 = []
# RegEx for finding rar files
@@ -1066,19 +1003,17 @@ Remove it if you want it to be renamed (again, or at least let it try again)
restfile_regex = '(^%s\.(?:part(?!0*1\.rar$)\d+\.rar$|[rstuvw]\d+$))'
extr_files = []
from_folder = sp(self.conf('from'))
# Check input variables
if not folder:
folder = from_folder
folder = sp(self.conf('from'))
check_file_date = True
if media_folder:
if movie_folder:
check_file_date = False
if not files:
for root, folders, names in os.walk(folder):
files.extend([sp(os.path.join(root, name)) for name in names])
files.extend([os.path.join(root, name) for name in names])
# Find all archive files
archives = [re.search(archive_regex, name).groupdict() for name in files if re.search(archive_regex, name)]
@@ -1124,13 +1059,13 @@ Remove it if you want it to be renamed (again, or at least let it try again)
log.info('Archive %s found. Extracting...', os.path.basename(archive['file']))
try:
rar_handle = RarFile(archive['file'])
extr_path = os.path.join(from_folder, os.path.relpath(os.path.dirname(archive['file']), folder))
extr_path = os.path.join(sp(self.conf('from')), os.path.relpath(os.path.dirname(archive['file']), folder))
self.makeDir(extr_path)
for packedinfo in rar_handle.infolist():
if not packedinfo.isdir and not os.path.isfile(sp(os.path.join(extr_path, os.path.basename(packedinfo.filename)))):
if not packedinfo.isdir and not os.path.isfile(os.path.join(extr_path, os.path.basename(packedinfo.filename))):
log.debug('Extracting %s...', packedinfo.filename)
rar_handle.extract(condition = [packedinfo.index], path = extr_path, withSubpath = False, overwrite = False)
extr_files.append(sp(os.path.join(extr_path, os.path.basename(packedinfo.filename))))
extr_files.append(os.path.join(extr_path, os.path.basename(packedinfo.filename)))
del rar_handle
except Exception, e:
log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc()))
@@ -1147,9 +1082,9 @@ Remove it if you want it to be renamed (again, or at least let it try again)
files.remove(filename)
# Move the rest of the files and folders if any files are extracted to the from folder (only if folder was provided)
if extr_files and folder != from_folder:
if extr_files and folder != sp(self.conf('from')):
for leftoverfile in list(files):
move_to = os.path.join(from_folder, os.path.relpath(leftoverfile, folder))
move_to = os.path.join(sp(self.conf('from')), os.path.relpath(leftoverfile, folder))
try:
self.makeDir(os.path.dirname(move_to))
@@ -1169,18 +1104,18 @@ Remove it if you want it to be renamed (again, or at least let it try again)
if cleanup:
# Remove all left over folders
log.debug('Removing old movie folder %s...', media_folder)
self.deleteEmptyFolder(media_folder)
log.debug('Removing old movie folder %s...', movie_folder)
self.deleteEmptyFolder(movie_folder)
media_folder = os.path.join(from_folder, os.path.relpath(media_folder, folder))
folder = from_folder
movie_folder = os.path.join(sp(self.conf('from')), os.path.relpath(movie_folder, folder))
folder = sp(self.conf('from'))
if extr_files:
files.extend(extr_files)
# Cleanup files and folder if media_folder was not provided
if not media_folder:
# Cleanup files and folder if movie_folder was not provided
if not movie_folder:
files = []
folder = None
return folder, media_folder, files, extr_files
return folder, movie_folder, files, extr_files

View File

@@ -80,8 +80,7 @@ class Scanner(Plugin):
'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' \
'|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]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
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]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
multipart_regex = [
'[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
'[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1
@@ -125,7 +124,7 @@ class Scanner(Plugin):
try:
files = []
for root, dirs, walk_files in os.walk(folder):
files.extend([sp(os.path.join(root, filename)) for filename in walk_files])
files.extend([os.path.join(root, filename) for filename in walk_files])
# Break if CP wants to shut down
if self.shuttingDown():
@@ -135,7 +134,7 @@ class Scanner(Plugin):
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
else:
check_file_date = False
files = [sp(x) for x in files]
files = [ss(x) for x in files]
for file_path in files:
@@ -455,7 +454,7 @@ class Scanner(Plugin):
data['resolution_width'] = meta.get('resolution_width', 720)
data['resolution_height'] = meta.get('resolution_height', 480)
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:
log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
pass

View File

@@ -20,7 +20,7 @@ config = [{
},
{
'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',

View File

@@ -1,6 +1,6 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, sp
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
@@ -27,7 +27,7 @@ class Subtitle(Plugin):
library = db.query(Library).all()
done_status = fireEvent('status.get', 'done', single = True)
for movie in library.media:
for movie in library.movies:
for release in movie.releases:
@@ -58,9 +58,9 @@ class Subtitle(Plugin):
for d_sub in downloaded:
log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files))
group['files']['subtitle'].append(sp(d_sub.path))
group['before_rename'].append(sp(d_sub.path))
group['subtitle_language'][sp(d_sub.path)] = [d_sub.language.alpha2]
group['files']['subtitle'].append(d_sub.path)
group['before_rename'].append(d_sub.path)
group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2]
return True

View File

@@ -59,7 +59,7 @@ config = [{
{
'name': 'automation_charts_boxoffice',
'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',
'default': True,
},

View File

@@ -1,14 +1,16 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
possibleTitles, toIterable
possibleTitles, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from urlparse import urlparse
import cookielib
import json
import re
import time
import traceback
import urllib2
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
@@ -93,7 +95,7 @@ class Provider(Plugin):
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)
@@ -109,14 +111,13 @@ class YarrProvider(Provider):
sizeMb = ['mb', 'mib']
sizeKb = ['kb', 'kib']
last_login_check = None
login_opener = None
last_login_check = 0
def __init__(self):
addEvent('provider.enabled_protocols', self.getEnabledProtocol)
addEvent('provider.belongs_to', self.belongsTo)
for type in toIterable(self.type):
addEvent('provider.search.%s.%s' % (self.protocol, type), self.search)
addEvent('provider.search.%s.%s' % (self.protocol, self.type), self.search)
def getEnabledProtocol(self):
if self.isEnabled():
@@ -128,30 +129,35 @@ class YarrProvider(Provider):
# Check if we are still logged in every hour
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:
output = self.urlopen(self.urls['login_check'])
output = self.urlopen(self.urls['login_check'], opener = self.login_opener)
if self.loginCheckSuccess(output):
self.last_login_check = now
return True
except: pass
self.last_login_check = None
else:
self.login_opener = None
except:
self.login_opener = None
if self.last_login_check:
if self.login_opener:
return True
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):
self.last_login_check = now
self.login_opener = opener
return True
error = 'unknown'
except:
error = traceback.format_exc()
self.last_login_check = None
self.login_opener = None
log.error('Failed to login %s: %s', (self.getName(), error))
return False
@@ -165,12 +171,12 @@ class YarrProvider(Provider):
try:
if not self.login():
log.error('Failed downloading from %s', self.getName())
return self.urlopen(url)
return self.urlopen(url, opener = self.login_opener)
except:
log.error('Failed downloading from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return {}
return ''
def download(self, url = '', nzb_id = ''):
try:
@@ -180,7 +186,7 @@ class YarrProvider(Provider):
return 'try_next'
def search(self, media, quality):
def search(self, movie, quality):
if self.isDisabled():
return []
@@ -192,17 +198,15 @@ class YarrProvider(Provider):
# Create result container
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
if imdb_results:
self._search(media, quality, results)
self._search(movie, quality, results)
# Search possible titles
else:
media_title = fireEvent('library.query', media['library'], single = True)
for title in possibleTitles(media_title):
self._searchOnTitle(title, media, quality, results)
for title in possibleTitles(getTitle(movie['library'])):
self._searchOnTitle(title, movie, quality, results)
return results
@@ -245,7 +249,8 @@ class YarrProvider(Provider):
def getCatId(self, identifier):
for ids, qualities in self.cat_ids:
for cats in self.cat_ids:
ids, qualities = cats
if identifier in qualities:
return ids
@@ -259,14 +264,14 @@ class ResultList(list):
result_ids = None
provider = None
media = None
movie = None
quality = None
def __init__(self, provider, media, quality, **kwargs):
def __init__(self, provider, movie, quality, **kwargs):
self.result_ids = []
self.provider = provider
self.media = media
self.movie = movie
self.quality = quality
self.kwargs = kwargs
@@ -280,13 +285,13 @@ class ResultList(list):
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)
if is_correct and new_result['id'] not in self.result_ids:
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']
new_result['score'] = int(old_score * is_correct_weight)

View File

@@ -1,7 +1,7 @@
from .main import InfoResultModifier
from .main import MovieResultModifier
def start():
return InfoResultModifier()
return MovieResultModifier()
config = []

View File

@@ -3,7 +3,6 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import mergeDicts, randomString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.providers.base import MultiProvider
from couchpotato.core.settings.model import Library
import copy
import traceback
@@ -11,17 +10,7 @@ import traceback
log = CPLog(__name__)
class InfoResultModifier(MultiProvider):
def getTypes(self):
return [Movie, Show]
class ModifierBase(Plugin):
pass
class Movie(ModifierBase):
class MovieResultModifier(Plugin):
default_info = {
'tmdb_id': 0,
@@ -32,17 +21,14 @@ class Movie(ModifierBase):
'poster': [],
'backdrop': [],
'poster_original': [],
'backdrop_original': [],
'actors': {}
'backdrop_original': []
},
'runtime': 0,
'plot': '',
'tagline': '',
'imdb': '',
'genres': [],
'mpaa': None,
'actors': [],
'actor_roles': {}
'mpaa': None
}
def __init__(self):
@@ -105,13 +91,13 @@ class Movie(ModifierBase):
# Statuses
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']:
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:
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:
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'):
return mergeDicts(result, self.getLibraryTags(result['imdb']))
return result
class Show(ModifierBase):
pass

View File

@@ -3,15 +3,3 @@ from couchpotato.core.providers.base import Provider
class MovieProvider(Provider):
type = 'movie'
class ShowProvider(Provider):
type = 'show'
class SeasonProvider(Provider):
type = 'season'
class EpisodeProvider(Provider):
type = 'episode'

View File

@@ -74,7 +74,7 @@ class CouchPotatoApi(MovieProvider):
return True
def getInfo(self, identifier = None, **kwargs):
def getInfo(self, identifier = None):
if not identifier:
return
@@ -97,7 +97,7 @@ class CouchPotatoApi(MovieProvider):
if not ignore: ignore = []
if not movies: movies = []
suggestions = self.getJsonData(self.urls['suggest'], data = {
suggestions = self.getJsonData(self.urls['suggest'], params = {
'movies': ','.join(movies),
'ignore': ','.join(ignore),
}, headers = self.getRequestHeaders())

View File

@@ -46,7 +46,7 @@ class OMDBAPI(MovieProvider):
return []
def getInfo(self, identifier = None, **kwargs):
def getInfo(self, identifier = None):
if not identifier:
return {}
@@ -82,10 +82,6 @@ class OMDBAPI(MovieProvider):
if tmp_movie.get(key).lower() == 'n/a':
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', ''))
movie_data = {

View File

@@ -11,8 +11,8 @@ log = CPLog(__name__)
class TheMovieDb(MovieProvider):
def __init__(self):
#addEvent('info.search', self.search, priority = 2)
#addEvent('movie.search', self.search, priority = 2)
addEvent('info.search', self.search, priority = 2)
addEvent('movie.search', self.search, priority = 2)
addEvent('movie.info', self.getInfo, priority = 2)
addEvent('movie.info_by_tmdb', self.getInfo)
@@ -45,7 +45,7 @@ class TheMovieDb(MovieProvider):
nr = 0
for movie in raw:
results.append(self.parseMovie(movie, extended = False))
results.append(self.parseMovie(movie, with_titles = False))
nr += 1
if nr == limit:
@@ -61,7 +61,7 @@ class TheMovieDb(MovieProvider):
return results
def getInfo(self, identifier = None, extended = True):
def getInfo(self, identifier = None):
if not identifier:
return {}
@@ -73,14 +73,14 @@ class TheMovieDb(MovieProvider):
try:
log.debug('Getting info: %s', cache_key)
movie = tmdb3.Movie(identifier)
result = self.parseMovie(movie, extended = extended)
result = self.parseMovie(movie)
self.setCache(cache_key, result)
except:
pass
return result
def parseMovie(self, movie, extended = True):
def parseMovie(self, movie, with_titles = True):
cache_key = 'tmdb.cache.%s' % movie.id
movie_data = self.getCache(cache_key)
@@ -92,14 +92,6 @@ class TheMovieDb(MovieProvider):
poster_original = self.getImage(movie, type = 'poster', 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
try:
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':
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 = {
'type': 'movie',
'via_tmdb': True,
'tmdb_id': movie.id,
'titles': [toUnicode(movie.title)],
'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,
'runtime': movie.runtime,
'released': str(movie.releasedate),
@@ -135,13 +122,12 @@ class TheMovieDb(MovieProvider):
'plot': movie.overview,
'genres': genres,
'collection': getattr(movie.collection, 'name', None),
'actor_roles': actors
}
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
# Add alternative names
if extended:
if with_titles:
movie_data['titles'].append(movie.originaltitle)
for alt in movie.alternate_titles:
alt_name = alt.title
@@ -159,7 +145,7 @@ class TheMovieDb(MovieProvider):
try:
image_url = getattr(movie, type).geturl(size = 'original')
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

View File

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

View File

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

View File

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

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()

View File

@@ -65,7 +65,7 @@ class XBMC(MetaDataBase):
name = type
try:
if movie_info.get(type):
if data['library'].get(type):
el = SubElement(nfoxml, name)
el.text = toUnicode(movie_info.get(type, ''))
except:
@@ -89,18 +89,10 @@ class XBMC(MetaDataBase):
genres.text = toUnicode(genre)
# Actors
for actor_name in movie_info.get('actor_roles', {}):
role_name = movie_info['actor_roles'][actor_name]
actor = SubElement(nfoxml, 'actor')
name = SubElement(actor, 'name')
name.text = toUnicode(actor_name)
if role_name:
role = SubElement(actor, 'role')
role.text = toUnicode(role_name)
if movie_info['images']['actors'].get(actor_name):
thumb = SubElement(actor, 'thumb')
thumb.text = toUnicode(movie_info['images']['actors'].get(actor_name))
for actor in movie_info.get('actors', []):
actors = SubElement(nfoxml, 'actor')
name = SubElement(actors, 'name')
name.text = toUnicode(actor)
# Directors
for director_name in movie_info.get('directors', []):
@@ -120,51 +112,6 @@ class XBMC(MetaDataBase):
sorttitle = SubElement(nfoxml, 'sorttitle')
sorttitle.text = '%s %s' % (toUnicode(collection_name), movie_info.get('year'))
# Images
for image_url in movie_info['images']['poster_original']:
image = SubElement(nfoxml, 'thumb')
image.text = toUnicode(image_url)
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
nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
xml_string = nfoxml.toprettyxml(indent = ' ')

View File

@@ -2,9 +2,6 @@ from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
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.environment import Env
import re
@@ -12,12 +9,8 @@ import traceback
log = CPLog(__name__)
class BinSearch(MultiProvider):
def getTypes(self):
return [Movie, Season, Episode]
class Base(NZBProvider):
class BinSearch(NZBProvider):
urls = {
'download': 'https://www.binsearch.info/fcgi/nzb.fcgi?q=%s',
@@ -27,9 +20,21 @@ class Base(NZBProvider):
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:
try:
@@ -85,62 +90,15 @@ class Base(NZBProvider):
def download(self, url = '', nzb_id = ''):
data = {
params = {
'action': 'nzb',
nzb_id: 'on'
}
try:
return self.urlopen(url, data = data, show_error = False)
return self.urlopen(url, params = params, show_error = False)
except:
log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc()))
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

View File

@@ -1,10 +1,8 @@
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
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.logger import CPLog
from couchpotato.core.providers.base import MultiProvider, ResultList
from couchpotato.core.providers.info.base import MovieProvider, SeasonProvider, EpisodeProvider
from couchpotato.core.providers.base import ResultList
from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env
from dateutil.parser import parse
@@ -12,44 +10,43 @@ from urllib2 import HTTPError
from urlparse import urlparse
import time
import traceback
import urllib2
log = CPLog(__name__)
class Newznab(MultiProvider):
def getTypes(self):
return [Movie, Season, Episode]
class Base(NZBProvider, RSS):
class Newznab(NZBProvider, RSS):
urls = {
'download': 'get&id=%s',
'detail': 'details&id=%s',
'download': 't=get&id=%s'
'search': 'movie',
}
limits_reached = {}
http_time_between_calls = 1 # Seconds
def search(self, media, quality):
def search(self, movie, quality):
hosts = self.getHosts()
results = ResultList(self, media, quality, imdb_results = True)
results = ResultList(self, movie, quality, imdb_results = True)
for host in hosts:
if self.isDisabled(host):
continue
self._searchOnHost(host, media, quality, results)
self._searchOnHost(host, movie, quality, results)
return results
def _searchOnHost(self, host, media, quality, results):
def _searchOnHost(self, host, movie, quality, results):
query = self.buildUrl(media, host['api_key'])
url = '%s&%s' % (self.getUrl(host['host']), query)
arguments = tryUrlencode({
'imdbid': movie['library']['identifier'].replace('tt', ''),
'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()})
@@ -90,7 +87,7 @@ class Base(NZBProvider, RSS):
'name_extra': name_extra,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'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)),
'content': self.getTextElement(nzb, 'description'),
'score': host['extra_score'],
@@ -130,11 +127,11 @@ class Base(NZBProvider, RSS):
if result:
return result
def getUrl(self, host):
def getUrl(self, host, type):
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):
return not self.isEnabled(host)
@@ -162,16 +159,7 @@ class Base(NZBProvider, RSS):
return 'try_next'
try:
# Get final redirected url
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)
data = self.urlopen(url, show_error = False)
self.limits_reached[host] = False
return data
except HTTPError, e:
@@ -186,45 +174,3 @@ class Base(NZBProvider, RSS):
log.error('Failed download from %s: %s', (host, traceback.format_exc()))
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

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.variable import tryInt
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 dateutil.parser import parse
import time
log = CPLog(__name__)
class NZBClub(MultiProvider):
def getTypes(self):
return [Movie, Season, Episode]
class Base(NZBProvider, RSS):
class NZBClub(NZBProvider, RSS):
urls = {
'search': 'http://www.nzbclub.com/nzbfeed.aspx?%s',
@@ -26,9 +18,20 @@ class Base(NZBProvider, RSS):
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:
@@ -75,42 +78,3 @@ class Base(NZBProvider, RSS):
return False
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

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.variable import tryInt
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.environment import Env
from dateutil.parser import parse
@@ -14,13 +11,8 @@ import time
log = CPLog(__name__)
class NzbIndex(MultiProvider):
def getTypes(self):
return [Movie, Season, Episode]
class Base(NZBProvider, RSS):
class NzbIndex(NZBProvider, RSS):
urls = {
'download': 'https://www.nzbindex.com/download/',
@@ -29,44 +21,28 @@ class Base(NZBProvider, RSS):
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:
enclosure = self.getElement(nzb, 'enclosure').attrib
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:
description = self.getTextElement(nzb, "description")
except:
@@ -81,7 +57,7 @@ class Base(NZBProvider, RSS):
results.append({
'id': nzbindex_id,
'name': title,
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024,
'url': enclosure['url'],
@@ -101,53 +77,3 @@ class Base(NZBProvider, RSS):
except:
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

View File

@@ -2,20 +2,12 @@ from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.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
import traceback
log = CPLog(__name__)
class BiTHDTV(MultiProvider):
def getTypes(self):
return [Movie, Season, Episode]
class Base(TorrentProvider):
class BiTHDTV(TorrentProvider):
urls = {
'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
cat_id_movies = 7
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:
# 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()))
def getLoginParams(self):
return {
return tryUrlencode({
'username': self.conf('username'),
'password': self.conf('password'),
}
})
def getMoreInfo(self, item):
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()
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

View File

@@ -2,46 +2,39 @@ from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode
from couchpotato.core.helpers.variable import tryInt
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
import traceback
log = CPLog(__name__)
class Bitsoup(MultiProvider):
def getTypes(self):
return [Movie, Season, Episode]
class Base(TorrentProvider):
class Bitsoup(TorrentProvider):
urls = {
'test': 'https://www.bitsoup.me/',
'login' : 'https://www.bitsoup.me/takelogin.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',
}
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)
data = self.getHTMLData(url)
q = '"%s" %s' % (simplifyString(title), movie['library']['year'])
arguments = tryUrlencode({
'search': q,
})
url = "%s&%s" % (self.urls['search'], arguments)
data = self.getHTMLData(url, opener = self.login_opener)
if data:
html = BeautifulSoup(data, "html.parser")
html = BeautifulSoup(data)
try:
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')
for result in entries[1:]:
@@ -89,57 +82,3 @@ class Base(TorrentProvider):
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

View File

@@ -1,4 +1,5 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
@@ -22,7 +23,7 @@ class HDBits(TorrentProvider):
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:
try:
@@ -41,17 +42,15 @@ class HDBits(TorrentProvider):
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
data = self.getHTMLData('https://hdbits.org/login', cache_timeout = 0)
data = self.getHTMLData('https://hdbits.org/login')
bs = BeautifulSoup(data)
secret = bs.find('input', attrs = {'name': 'lol'})['value']
return {
return tryUrlencode({
'uname': self.conf('username'),
'password': self.conf('password'),
'returnto': '/',
'lol': secret
}
})
def loginSuccess(self, output):
return '/logout.php' in output.lower()

View File

@@ -42,7 +42,7 @@ class ILoveTorrents(TorrentProvider):
search_url = self.urls['search'] % (movieTitle, page, cats[0])
page += 1
data = self.getHTMLData(search_url)
data = self.getHTMLData(search_url, opener = self.login_opener)
if data:
try:
soup = BeautifulSoup(data)
@@ -96,11 +96,11 @@ class ILoveTorrents(TorrentProvider):
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return {
return tryUrlencode({
'username': self.conf('username'),
'password': self.conf('password'),
'submit': 'Welcome to ILT',
}
})
def getMoreInfo(self, item):
cache_key = 'ilt.%s' % item['id']
@@ -109,7 +109,7 @@ class ILoveTorrents(TorrentProvider):
if not description:
try:
full_description = self.getHTMLData(item['detail_url'])
full_description = self.getHTMLData(item['detail_url'], opener = self.login_opener)
html = BeautifulSoup(full_description)
nfo_pre = html.find('td', attrs = {'class':'main'}).findAll('table')[1]
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