Compare commits
356 Commits
build/2.0.
...
build/2.0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2852407ea | ||
|
|
88e738c6cd | ||
|
|
eaae8bdb0b | ||
|
|
95d146fea2 | ||
|
|
dc20b68a37 | ||
|
|
b3ba4db00b | ||
|
|
a4c1480a1a | ||
|
|
91e0452320 | ||
|
|
ad80ea7885 | ||
|
|
1c20cda389 | ||
|
|
631759d833 | ||
|
|
ca02c66f26 | ||
|
|
3ac095d359 | ||
|
|
e1bc223de0 | ||
|
|
e065ead9b3 | ||
|
|
f9471f9b9b | ||
|
|
2612b50d06 | ||
|
|
d9ce2906a0 | ||
|
|
b76397f98e | ||
|
|
fcad9e0be5 | ||
|
|
2934347865 | ||
|
|
315f1b0207 | ||
|
|
965bd79a86 | ||
|
|
c18563e34b | ||
|
|
161e0de8d5 | ||
|
|
40aeca0740 | ||
|
|
63dd7fa7c0 | ||
|
|
509b49caf1 | ||
|
|
38c51cf79c | ||
|
|
0b693bba4e | ||
|
|
1258f34c78 | ||
|
|
510c0d5f56 | ||
|
|
cdb630e580 | ||
|
|
65fbd38105 | ||
|
|
1570132a55 | ||
|
|
7b5b748d23 | ||
|
|
041601c4a5 | ||
|
|
f692fd0202 | ||
|
|
e7b4de56f2 | ||
|
|
4a616a0c04 | ||
|
|
0814675d2a | ||
|
|
13df35462b | ||
|
|
899868f51e | ||
|
|
ee466aebce | ||
|
|
687ef2662e | ||
|
|
5aa29acbd3 | ||
|
|
1c2b3d063b | ||
|
|
551a000893 | ||
|
|
0d82d425cc | ||
|
|
0e1cea1034 | ||
|
|
2b75153148 | ||
|
|
c170615fb3 | ||
|
|
f6e84b6a35 | ||
|
|
6144f09a1f | ||
|
|
de142e8050 | ||
|
|
d0c1a119fd | ||
|
|
8fd80d3185 | ||
|
|
ae28c82858 | ||
|
|
1766764c7d | ||
|
|
129f8d72bd | ||
|
|
7314b5ecae | ||
|
|
7b0806355f | ||
|
|
49cf72e058 | ||
|
|
a11cad619d | ||
|
|
c1d35e8a57 | ||
|
|
fede348fbd | ||
|
|
f3c60e8fa6 | ||
|
|
00e53439ed | ||
|
|
368fced0c4 | ||
|
|
666771fb0f | ||
|
|
9e3f978677 | ||
|
|
f467d1c4f7 | ||
|
|
d8fc9d937e | ||
|
|
821f68909d | ||
|
|
2b8dfed475 | ||
|
|
0a749ce913 | ||
|
|
e6db505cf7 | ||
|
|
9e8d6aaaa1 | ||
|
|
e814b551b4 | ||
|
|
080da48223 | ||
|
|
897330e646 | ||
|
|
c4c7b5b1a9 | ||
|
|
b90861bc63 | ||
|
|
6d1297a85f | ||
|
|
dfd2c33657 | ||
|
|
f5af551325 | ||
|
|
7aad27c3d2 | ||
|
|
60ff3b08d4 | ||
|
|
7a5588d5de | ||
|
|
56b6fbbe7f | ||
|
|
46c408befb | ||
|
|
6f808fc25a | ||
|
|
4cba44fbb1 | ||
|
|
91c45bad71 | ||
|
|
a30caefc04 | ||
|
|
eb20fda878 | ||
|
|
4a5aa02e6c | ||
|
|
25b37ad915 | ||
|
|
bbcceb982a | ||
|
|
c41f5eb84d | ||
|
|
89dc9e90b2 | ||
|
|
39b1dedf12 | ||
|
|
be28820fb2 | ||
|
|
0654c8cf07 | ||
|
|
2f5cb81029 | ||
|
|
067d6e8514 | ||
|
|
42e19e1e2b | ||
|
|
6b846b91b4 | ||
|
|
5838a41813 | ||
|
|
3cd5513c0c | ||
|
|
bfdc8d1053 | ||
|
|
30ec8216e1 | ||
|
|
12c3fc6ce3 | ||
|
|
7b3a1409d5 | ||
|
|
924bed06cb | ||
|
|
8b0aa7a6b3 | ||
|
|
367c385fff | ||
|
|
840efb1571 | ||
|
|
9ba19d27a6 | ||
|
|
1d603e1ec2 | ||
|
|
7818b43045 | ||
|
|
3936100000 | ||
|
|
1a846b04ee | ||
|
|
384a355a53 | ||
|
|
58ad5c3938 | ||
|
|
6ee68d1418 | ||
|
|
6e45c14ac5 | ||
|
|
e786c9c79a | ||
|
|
518ac16814 | ||
|
|
1d07eafa83 | ||
|
|
1600b6d0ea | ||
|
|
6de3a7246e | ||
|
|
cbd29df52a | ||
|
|
92998bafc8 | ||
|
|
1022753213 | ||
|
|
b85942989d | ||
|
|
f2f43a2231 | ||
|
|
2979a8edec | ||
|
|
0e90739786 | ||
|
|
185a530b59 | ||
|
|
4f6b31d14a | ||
|
|
f1dde5c925 | ||
|
|
64afa3701a | ||
|
|
cb0b6614c6 | ||
|
|
177063d39c | ||
|
|
be595aba91 | ||
|
|
66d9d853af | ||
|
|
95a68af795 | ||
|
|
c1937ea71f | ||
|
|
a7bd8c822a | ||
|
|
0eff4f0096 | ||
|
|
4d7fa08805 | ||
|
|
5fd4312ff8 | ||
|
|
a600430be4 | ||
|
|
f77b598899 | ||
|
|
ac045539d1 | ||
|
|
5b0fa9054b | ||
|
|
3c2a00b17b | ||
|
|
47ddf31f76 | ||
|
|
57ae06e139 | ||
|
|
7f4373e000 | ||
|
|
63609bb52c | ||
|
|
f0af184262 | ||
|
|
72cc3576d3 | ||
|
|
3fe7d2ea15 | ||
|
|
8eed54f1f7 | ||
|
|
c7ee8a0635 | ||
|
|
33a6a7d3a0 | ||
|
|
2851781a72 | ||
|
|
45b9919f67 | ||
|
|
207e846ae6 | ||
|
|
a83c276aa2 | ||
|
|
4cdb99a383 | ||
|
|
8fe60a893c | ||
|
|
0c44c48628 | ||
|
|
6a18e546ca | ||
|
|
4cedccb178 | ||
|
|
eab9a735a9 | ||
|
|
1df05cf344 | ||
|
|
843ff0eabc | ||
|
|
5a23be2224 | ||
|
|
45c8817c62 | ||
|
|
7f87b255f9 | ||
|
|
665c84c6de | ||
|
|
b91a077c91 | ||
|
|
59b924efe7 | ||
|
|
730718a396 | ||
|
|
3c0edc0d6a | ||
|
|
7c234ab7e9 | ||
|
|
b82319cb54 | ||
|
|
6685495400 | ||
|
|
b216589e88 | ||
|
|
744aa153f6 | ||
|
|
67612fce98 | ||
|
|
72ba1a173c | ||
|
|
989e217775 | ||
|
|
b0d556c8eb | ||
|
|
1a54d8fad9 | ||
|
|
f9ace29cab | ||
|
|
a97570027d | ||
|
|
de36faa0a7 | ||
|
|
19641bd897 | ||
|
|
2c64641a1b | ||
|
|
5ac1118db3 | ||
|
|
717b88b5fe | ||
|
|
158a7fc311 | ||
|
|
2c46279617 | ||
|
|
b843d5f13b | ||
|
|
4aff3f0495 | ||
|
|
4406f133b9 | ||
|
|
572dfd529e | ||
|
|
2cb6ddfe9a | ||
|
|
250236bd25 | ||
|
|
7f24563bba | ||
|
|
5d6a9ad2d0 | ||
|
|
0115bf254e | ||
|
|
607b5ea766 | ||
|
|
88579cd71a | ||
|
|
6c57316ce6 | ||
|
|
6702683da3 | ||
|
|
b9c2b42725 | ||
|
|
e54928720a | ||
|
|
f8f22cdef7 | ||
|
|
1ed58586a1 | ||
|
|
e694276a8d | ||
|
|
a8369b4e93 | ||
|
|
73b7bcc6ce | ||
|
|
f08ccd4fd8 | ||
|
|
312562a9f5 | ||
|
|
fab8e66fe1 | ||
|
|
4db1b57c70 | ||
|
|
b06dbd3069 | ||
|
|
f84aa8c638 | ||
|
|
8e07dfc730 | ||
|
|
a49a00a25f | ||
|
|
673843fb66 | ||
|
|
811f35b028 | ||
|
|
ec6e2c240f | ||
|
|
9e260a89af | ||
|
|
d233e4d22e | ||
|
|
23893dbcb9 | ||
|
|
3187a0f820 | ||
|
|
f86b9299c4 | ||
|
|
d27d0abeb0 | ||
|
|
506871b506 | ||
|
|
6115917660 | ||
|
|
21df8819d3 | ||
|
|
7c59348138 | ||
|
|
ab53f44157 | ||
|
|
b35f325d94 | ||
|
|
fb3f3e11f6 | ||
|
|
178c8942c3 | ||
|
|
393c14de54 | ||
|
|
51e747049d | ||
|
|
0582f7d694 | ||
|
|
fa7cac7538 | ||
|
|
bff17c0b95 | ||
|
|
d172828ac5 | ||
|
|
9500ac73fc | ||
|
|
e2cf7e4421 | ||
|
|
9a314cfbc4 | ||
|
|
5941d0bf77 | ||
|
|
d326c1c25c | ||
|
|
7e6234298d | ||
|
|
d4da206f93 | ||
|
|
985a168724 | ||
|
|
173c6194ed | ||
|
|
bcd23ad10c | ||
|
|
898e6f487d | ||
|
|
96472a9a8f | ||
|
|
27252561e2 | ||
|
|
6618c3927c | ||
|
|
c9e732651f | ||
|
|
7849e7170d | ||
|
|
087894eb4e | ||
|
|
4b58b40226 | ||
|
|
3ecc826629 | ||
|
|
25f1b8c7a7 | ||
|
|
e71da1f14d | ||
|
|
938b14ba18 | ||
|
|
d6522d8f38 | ||
|
|
78eab890e7 | ||
|
|
1a56191f83 | ||
|
|
41c0f34d95 | ||
|
|
37bf205d7a | ||
|
|
32fe3796e4 | ||
|
|
359d1aaafa | ||
|
|
fb5d336351 | ||
|
|
eb30dff986 | ||
|
|
9312336962 | ||
|
|
aa1fa3eb9a | ||
|
|
0e2f8a612c | ||
|
|
c087a6b49b | ||
|
|
ade4338ea6 | ||
|
|
55b20324c0 | ||
|
|
465e7b2abc | ||
|
|
578fb45785 | ||
|
|
c0fb28301d | ||
|
|
96995bbbe5 | ||
|
|
4cfdafebbc | ||
|
|
f9c2503f81 | ||
|
|
b97acb8ef5 | ||
|
|
5b4cdf05b1 | ||
|
|
d68d2dfdb6 | ||
|
|
39b269a454 | ||
|
|
ac081d3e10 | ||
|
|
5d4efb60cf | ||
|
|
6f25a6bdfd | ||
|
|
23427e95f7 | ||
|
|
cc408b980c | ||
|
|
90a09e573b | ||
|
|
e1d7440b9d | ||
|
|
59590b3ac9 | ||
|
|
ff759dacf3 | ||
|
|
a328e44130 | ||
|
|
7924cac5f9 | ||
|
|
1cef3b0c93 | ||
|
|
3cd59edc8b | ||
|
|
0d624af01d | ||
|
|
a09132570c | ||
|
|
ee3fc38432 | ||
|
|
dbf0192c8e | ||
|
|
6962cfc3f5 | ||
|
|
e096ec3b5b | ||
|
|
b30a74ae0c | ||
|
|
978eeb16c9 | ||
|
|
e5c9d91657 | ||
|
|
fa81c3a07a | ||
|
|
9cdd520d41 | ||
|
|
55d7898771 | ||
|
|
b8256bef97 | ||
|
|
5be9dc0b4a | ||
|
|
7d0be0cefb | ||
|
|
f7ce1edb13 | ||
|
|
5ad9280b60 | ||
|
|
2b353f1b20 | ||
|
|
75ab90b87b | ||
|
|
0219296120 | ||
|
|
20032b3a31 | ||
|
|
ea9e9a8c90 | ||
|
|
f7b0ee145b | ||
|
|
cc866738ee | ||
|
|
eadccf6e33 | ||
|
|
b70b66e567 | ||
|
|
5b6792dc20 | ||
|
|
f498e7343a | ||
|
|
6962f441e6 | ||
|
|
1def62b1b1 | ||
|
|
a4a4a6a185 | ||
|
|
d4c9469c1a | ||
|
|
3e2d4c5d7b | ||
|
|
d03f711d69 | ||
|
|
44dd8d9b96 | ||
|
|
549a3be0d8 | ||
|
|
1bb2edf8ec | ||
|
|
84c6f36315 |
231
Desktop.py
Normal file
231
Desktop.py
Normal 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/'
|
||||
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:])
|
||||
@@ -11,16 +11,12 @@ api_nonblock = {}
|
||||
|
||||
class NonBlockHandler(RequestHandler):
|
||||
|
||||
def __init__(self, application, request, **kwargs):
|
||||
cls = NonBlockHandler
|
||||
cls.stoppers = []
|
||||
super(NonBlockHandler, self).__init__(application, request, **kwargs)
|
||||
stoppers = []
|
||||
|
||||
@asynchronous
|
||||
def get(self, route):
|
||||
cls = NonBlockHandler
|
||||
start, stop = api_nonblock[route]
|
||||
cls.stoppers.append(stop)
|
||||
self.stoppers.append(stop)
|
||||
|
||||
start(self.onNewMessage, last_id = self.get_argument("last_id", None))
|
||||
|
||||
@@ -30,12 +26,11 @@ class NonBlockHandler(RequestHandler):
|
||||
self.finish(response)
|
||||
|
||||
def on_connection_close(self):
|
||||
cls = NonBlockHandler
|
||||
|
||||
for stop in cls.stoppers:
|
||||
for stop in self.stoppers:
|
||||
stop(self.onNewMessage)
|
||||
|
||||
cls.stoppers = []
|
||||
self.stoppers = []
|
||||
|
||||
|
||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||
|
||||
@@ -70,7 +70,7 @@ config = [{
|
||||
'name': 'development',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'description': 'Disables some checks/downloads for faster reloading.',
|
||||
'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
|
||||
},
|
||||
{
|
||||
'name': 'data_dir',
|
||||
|
||||
@@ -79,7 +79,7 @@ class Core(Plugin):
|
||||
|
||||
def shutdown():
|
||||
self.initShutdown()
|
||||
IOLoop.instance().add_callback(shutdown)
|
||||
IOLoop.current().add_callback(shutdown)
|
||||
|
||||
return 'shutdown'
|
||||
|
||||
@@ -89,7 +89,7 @@ class Core(Plugin):
|
||||
|
||||
def restart():
|
||||
self.initShutdown(restart = True)
|
||||
IOLoop.instance().add_callback(restart)
|
||||
IOLoop.current().add_callback(restart)
|
||||
|
||||
return 'restarting'
|
||||
|
||||
@@ -128,7 +128,7 @@ class Core(Plugin):
|
||||
log.debug('Save to shutdown/restart')
|
||||
|
||||
try:
|
||||
IOLoop.instance().stop()
|
||||
IOLoop.current().stop()
|
||||
except RuntimeError:
|
||||
pass
|
||||
except:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -6,6 +7,8 @@ from couchpotato.environment import Env
|
||||
from minify.cssmin import cssmin
|
||||
from minify.jsmin import jsmin
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -23,7 +26,6 @@ class ClientScript(Plugin):
|
||||
'script': [
|
||||
'scripts/library/mootools.js',
|
||||
'scripts/library/mootools_more.js',
|
||||
'scripts/library/prefix_free.js',
|
||||
'scripts/library/uniform.js',
|
||||
'scripts/library/form_replacement/form_check.js',
|
||||
'scripts/library/form_replacement/form_radio.js',
|
||||
@@ -69,7 +71,8 @@ class ClientScript(Plugin):
|
||||
addEvent('clientscript.get_styles', self.getStyles)
|
||||
addEvent('clientscript.get_scripts', self.getScripts)
|
||||
|
||||
addEvent('app.load', self.minify)
|
||||
if not Env.get('dev'):
|
||||
addEvent('app.load', self.minify)
|
||||
|
||||
self.addCore()
|
||||
|
||||
@@ -108,8 +111,11 @@ class ClientScript(Plugin):
|
||||
if file_type == 'script':
|
||||
data = jsmin(f)
|
||||
else:
|
||||
data = self.prefix(f)
|
||||
data = cssmin(f)
|
||||
data = data.replace('../images/', '../static/images/')
|
||||
data = data.replace('../fonts/', '../static/fonts/')
|
||||
data = data.replace('../../static/', '../static/') # Replace inside plugins
|
||||
|
||||
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
|
||||
|
||||
@@ -167,3 +173,28 @@ class ClientScript(Plugin):
|
||||
if not self.paths[type].get(location):
|
||||
self.paths[type][location] = []
|
||||
self.paths[type][location].append(file_path)
|
||||
|
||||
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
|
||||
prefix_tags = ['ms', 'moz', 'webkit']
|
||||
def prefix(self, data):
|
||||
|
||||
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
|
||||
|
||||
new_data = ''
|
||||
colon_split = trimmed_data.split(';')
|
||||
for splt in colon_split:
|
||||
curl_split = splt.strip().split('{')
|
||||
for curly in curl_split:
|
||||
curly = curly.strip()
|
||||
for prop in self.prefix_properties:
|
||||
if curly[:len(prop) + 1] == prop + ':':
|
||||
for tag in self.prefix_tags:
|
||||
new_data += ' -%s-%s; ' % (tag, curly)
|
||||
|
||||
new_data += curly + (' { ' if len(curl_split) > 1 else ' ')
|
||||
|
||||
new_data += '; '
|
||||
|
||||
new_data = new_data.replace('{ ;', '; ').replace('} ;', '} ')
|
||||
|
||||
return new_data
|
||||
|
||||
@@ -16,51 +16,19 @@ class Scheduler(Plugin):
|
||||
|
||||
addEvent('schedule.cron', self.cron)
|
||||
addEvent('schedule.interval', self.interval)
|
||||
addEvent('schedule.start', self.start)
|
||||
addEvent('schedule.restart', self.start)
|
||||
|
||||
addEvent('app.load', self.start)
|
||||
addEvent('schedule.remove', self.remove)
|
||||
|
||||
self.sched = Sched(misfire_grace_time = 60)
|
||||
|
||||
def remove(self, identifier):
|
||||
for type in ['interval', 'cron']:
|
||||
try:
|
||||
self.sched.unschedule_job(getattr(self, type)[identifier]['job'])
|
||||
log.debug('%s unscheduled %s', (type.capitalize(), identifier))
|
||||
except:
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
|
||||
# Stop all running
|
||||
self.stop()
|
||||
|
||||
# Crons
|
||||
for identifier in self.crons:
|
||||
try:
|
||||
self.remove(identifier)
|
||||
cron = self.crons[identifier]
|
||||
job = self.sched.add_cron_job(cron['handle'], day = cron['day'], hour = cron['hour'], minute = cron['minute'])
|
||||
cron['job'] = job
|
||||
except ValueError, e:
|
||||
log.error('Failed adding cronjob: %s', e)
|
||||
|
||||
# Intervals
|
||||
for identifier in self.intervals:
|
||||
try:
|
||||
self.remove(identifier)
|
||||
interval = self.intervals[identifier]
|
||||
job = self.sched.add_interval_job(interval['handle'], hours = interval['hours'], minutes = interval['minutes'], seconds = interval['seconds'])
|
||||
interval['job'] = job
|
||||
except ValueError, e:
|
||||
log.error('Failed adding interval cronjob: %s', e)
|
||||
|
||||
# Start it
|
||||
log.debug('Starting scheduler')
|
||||
self.sched.start()
|
||||
self.started = True
|
||||
log.debug('Scheduler started')
|
||||
|
||||
def remove(self, identifier):
|
||||
for cron_type in ['intervals', 'crons']:
|
||||
try:
|
||||
self.sched.unschedule_job(getattr(self, cron_type)[identifier]['job'])
|
||||
log.debug('%s unscheduled %s', (cron_type.capitalize(), identifier))
|
||||
except:
|
||||
pass
|
||||
|
||||
def doShutdown(self):
|
||||
super(Scheduler, self).doShutdown()
|
||||
@@ -82,6 +50,7 @@ class Scheduler(Plugin):
|
||||
'day': day,
|
||||
'hour': hour,
|
||||
'minute': minute,
|
||||
'job': self.sched.add_cron_job(handle, day = day, hour = hour, minute = minute)
|
||||
}
|
||||
|
||||
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
|
||||
@@ -93,4 +62,5 @@ class Scheduler(Plugin):
|
||||
'hours': hours,
|
||||
'minutes': minutes,
|
||||
'seconds': seconds,
|
||||
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import tarfile
|
||||
import time
|
||||
import traceback
|
||||
import version
|
||||
import zipfile
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -32,8 +33,7 @@ class Updater(Plugin):
|
||||
else:
|
||||
self.updater = SourceUpdater()
|
||||
|
||||
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
|
||||
addEvent('app.load', self.autoUpdate)
|
||||
addEvent('app.load', self.setCrons)
|
||||
addEvent('updater.info', self.info)
|
||||
|
||||
addApiView('updater.info', self.getInfo, docs = {
|
||||
@@ -52,8 +52,17 @@ class Updater(Plugin):
|
||||
'return': {'type': 'see updater.info'}
|
||||
})
|
||||
|
||||
addEvent('setting.save.updater.enabled.after', self.setCrons)
|
||||
|
||||
def setCrons(self):
|
||||
|
||||
fireEvent('schedule.remove', 'updater.check', single = True)
|
||||
if self.isEnabled():
|
||||
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
|
||||
self.autoUpdate() # Check after enabling
|
||||
|
||||
def autoUpdate(self):
|
||||
if self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||
if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||
if self.updater.doUpdate():
|
||||
|
||||
# Notify before restarting
|
||||
@@ -71,8 +80,8 @@ class Updater(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def check(self):
|
||||
if self.isDisabled():
|
||||
def check(self, force = False):
|
||||
if not force and self.isDisabled():
|
||||
return
|
||||
|
||||
if self.updater.check():
|
||||
@@ -91,7 +100,7 @@ class Updater(Plugin):
|
||||
|
||||
def checkView(self):
|
||||
return jsonified({
|
||||
'update_available': self.check(),
|
||||
'update_available': self.check(force = True),
|
||||
'info': self.updater.info()
|
||||
})
|
||||
|
||||
@@ -255,11 +264,11 @@ class SourceUpdater(BaseUpdater):
|
||||
def doUpdate(self):
|
||||
|
||||
try:
|
||||
url = 'https://github.com/%s/%s/tarball/%s' % (self.repo_user, self.repo_name, self.branch)
|
||||
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash') + '.tar.gz')
|
||||
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
|
||||
download_data = fireEvent('cp.source_url', repo = self.repo_user, repo_name = self.repo_name, branch = self.branch, single = True)
|
||||
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash')) + '.' + download_data.get('type')
|
||||
|
||||
destination = fireEvent('file.download', url = url, dest = destination, single = True)
|
||||
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
|
||||
destination = fireEvent('file.download', url = download_data.get('url'), dest = destination, single = True)
|
||||
|
||||
# Cleanup leftover from last time
|
||||
if os.path.isdir(extracted_path):
|
||||
@@ -267,9 +276,14 @@ class SourceUpdater(BaseUpdater):
|
||||
self.makeDir(extracted_path)
|
||||
|
||||
# Extract
|
||||
tar = tarfile.open(destination)
|
||||
tar.extractall(path = extracted_path)
|
||||
tar.close()
|
||||
if download_data.get('type') == 'zip':
|
||||
zip = zipfile.ZipFile(destination)
|
||||
zip.extractall(extracted_path)
|
||||
else:
|
||||
tar = tarfile.open(destination)
|
||||
tar.extractall(path = extracted_path)
|
||||
tar.close()
|
||||
|
||||
os.remove(destination)
|
||||
|
||||
if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):
|
||||
|
||||
@@ -5,7 +5,7 @@ var UpdaterBase = new Class({
|
||||
initialize: function(){
|
||||
var self = this;
|
||||
|
||||
App.addEvent('load', self.info.bind(self, 1000))
|
||||
App.addEvent('load', self.info.bind(self, 2000))
|
||||
App.addEvent('unload', function(){
|
||||
if(self.timer)
|
||||
clearTimeout(self.timer);
|
||||
@@ -84,7 +84,7 @@ var UpdaterBase = new Class({
|
||||
'click': self.doUpdate.bind(self)
|
||||
}
|
||||
})
|
||||
).inject($(document.body).getElement('.header'))
|
||||
).inject(document.body)
|
||||
},
|
||||
|
||||
doUpdate: function(){
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from base64 import b32decode, b16encode
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import Provider
|
||||
import random
|
||||
@@ -103,6 +104,12 @@ class Downloader(Provider):
|
||||
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
|
||||
return False
|
||||
|
||||
def downloadReturnId(self, download_id):
|
||||
return {
|
||||
'downloader': self.getName(),
|
||||
'id': download_id
|
||||
}
|
||||
|
||||
def isDisabled(self, manual, data):
|
||||
return not self.isEnabled(manual, data)
|
||||
|
||||
@@ -116,3 +123,35 @@ class Downloader(Provider):
|
||||
return super(Downloader, self).isEnabled() and \
|
||||
((d_manual and manual) or (d_manual is False)) and \
|
||||
(not data or self.isCorrectType(data.get('type')))
|
||||
|
||||
|
||||
class StatusList(list):
|
||||
|
||||
provider = None
|
||||
|
||||
def __init__(self, provider, **kwargs):
|
||||
|
||||
self.provider = provider
|
||||
self.kwargs = kwargs
|
||||
|
||||
super(StatusList, self).__init__()
|
||||
|
||||
def extend(self, results):
|
||||
for r in results:
|
||||
self.append(r)
|
||||
|
||||
def append(self, result):
|
||||
new_result = self.fillResult(result)
|
||||
super(StatusList, self).append(new_result)
|
||||
|
||||
def fillResult(self, result):
|
||||
|
||||
defaults = {
|
||||
'id': 0,
|
||||
'status': 'busy',
|
||||
'downloader': self.provider.getName(),
|
||||
'folder': '',
|
||||
}
|
||||
|
||||
return mergeDicts(defaults, result)
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@ config = [{
|
||||
'default': 'localhost:6789',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': 'nzbget',
|
||||
'advanced': True,
|
||||
'description': 'Set a different username to connect. Default: nzbget',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
@@ -48,6 +54,12 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from base64 import standard_b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.helpers.variable import tryInt, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import traceback
|
||||
import xmlrpclib
|
||||
@@ -14,7 +16,7 @@ class NZBGet(Downloader):
|
||||
|
||||
type = ['nzb']
|
||||
|
||||
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
|
||||
url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc'
|
||||
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
|
||||
@@ -24,7 +26,7 @@ class NZBGet(Downloader):
|
||||
|
||||
log.info('Sending "%s" to NZBGet.', data.get('name'))
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
|
||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
@@ -50,7 +52,124 @@ class NZBGet(Downloader):
|
||||
|
||||
if xml_response:
|
||||
log.info('NZB sent successfully to NZBGet')
|
||||
return True
|
||||
nzb_id = md5(data['url']) # about as unique as they come ;)
|
||||
couchpotato_id = "couchpotato=" + nzb_id
|
||||
groups = rpc.listgroups()
|
||||
file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name]
|
||||
confirmed = rpc.editqueue("GroupSetParameter", 0, couchpotato_id, file_id)
|
||||
if confirmed:
|
||||
log.debug('couchpotato parameter set in nzbget download')
|
||||
return self.downloadReturnId(nzb_id)
|
||||
else:
|
||||
log.error('NZBGet could not add %s to the queue.', nzb_name)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking NZBGet download status.')
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to check status'):
|
||||
log.info('Successfully connected to NZBGet')
|
||||
else:
|
||||
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 False
|
||||
except xmlrpclib.ProtocolError, e:
|
||||
if e.errcode == 401:
|
||||
log.error('Password is incorrect.')
|
||||
else:
|
||||
log.error('Protocol Error: %s', e)
|
||||
return False
|
||||
|
||||
# Get NZBGet data
|
||||
try:
|
||||
status = rpc.status()
|
||||
groups = rpc.listgroups()
|
||||
queue = rpc.postqueue(0)
|
||||
history = rpc.history()
|
||||
except:
|
||||
log.error('Failed getting data: %s', traceback.format_exc(1))
|
||||
return False
|
||||
|
||||
statuses = StatusList(self)
|
||||
|
||||
for item in groups:
|
||||
log.debug('Found %s in NZBGet download queue', item['NZBFilename'])
|
||||
try:
|
||||
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
except:
|
||||
nzb_id = item['NZBID']
|
||||
statuses.append({
|
||||
'id': nzb_id,
|
||||
'name': item['NZBFilename'],
|
||||
'original_status': 'DOWNLOADING' if item['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': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1,
|
||||
})
|
||||
|
||||
for item in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||
log.debug('Found %s in NZBGet postprocessing queue', item['NZBFilename'])
|
||||
statuses.append({
|
||||
'id': item['NZBID'],
|
||||
'name': item['NZBFilename'],
|
||||
'original_status': item['Stage'],
|
||||
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
|
||||
})
|
||||
|
||||
for item in history:
|
||||
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log']))
|
||||
try:
|
||||
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
except:
|
||||
nzb_id = item['NZBID']
|
||||
statuses.append({
|
||||
'id': nzb_id,
|
||||
'name': item['NZBFilename'],
|
||||
'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed',
|
||||
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': item['DestDir']
|
||||
})
|
||||
|
||||
return statuses
|
||||
|
||||
def removeFailed(self, item):
|
||||
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
|
||||
log.info('Successfully connected to NZBGet')
|
||||
else:
|
||||
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 False
|
||||
except xmlrpclib.ProtocolError, e:
|
||||
if e.errcode == 401:
|
||||
log.error('Password is incorrect.')
|
||||
else:
|
||||
log.error('Protocol Error: %s', e)
|
||||
return False
|
||||
|
||||
try:
|
||||
history = rpc.history()
|
||||
for hist in history:
|
||||
if hist['Parameters'] and hist['Parameters']['couchpotato'] and hist['Parameters']['couchpotato'] == item['id']:
|
||||
nzb_id = hist['ID']
|
||||
path = hist['DestDir']
|
||||
if rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
|
||||
shutil.rmtree(path, True)
|
||||
except:
|
||||
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from base64 import b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
@@ -29,7 +29,9 @@ class NZBVortex(Downloader):
|
||||
nzb_filename = self.createFileName(data, filedata, movie)
|
||||
self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
|
||||
|
||||
return True
|
||||
raw_statuses = self.call('nzb')
|
||||
nzb_id = [item['id'] for item in raw_statuses.get('nzbs', []) if item['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
|
||||
@@ -38,7 +40,7 @@ class NZBVortex(Downloader):
|
||||
|
||||
raw_statuses = self.call('nzb')
|
||||
|
||||
statuses = []
|
||||
statuses = StatusList(self)
|
||||
for item in raw_statuses.get('nzbs', []):
|
||||
|
||||
# Check status
|
||||
@@ -53,7 +55,8 @@ class NZBVortex(Downloader):
|
||||
'name': item['uiTitle'],
|
||||
'status': status,
|
||||
'original_status': item['state'],
|
||||
'timeleft':-1,
|
||||
'timeleft': -1,
|
||||
'folder': item['destinationPath'],
|
||||
})
|
||||
|
||||
return statuses
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss
|
||||
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from datetime import timedelta
|
||||
from urllib2 import URLError
|
||||
import json
|
||||
import traceback
|
||||
@@ -17,8 +18,7 @@ class Sabnzbd(Downloader):
|
||||
|
||||
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
||||
|
||||
params = {
|
||||
'apikey': self.conf('api_key'),
|
||||
req_params = {
|
||||
'cat': self.conf('category'),
|
||||
'mode': 'addurl',
|
||||
'nzbname': self.createNzbName(data, movie),
|
||||
@@ -31,17 +31,15 @@ class Sabnzbd(Downloader):
|
||||
|
||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||
nzb_filename = self.createFileName(data, filedata, movie)
|
||||
params['mode'] = 'addfile'
|
||||
req_params['mode'] = 'addfile'
|
||||
else:
|
||||
params['name'] = data.get('url')
|
||||
|
||||
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
|
||||
req_params['name'] = data.get('url')
|
||||
|
||||
try:
|
||||
if params.get('mode') is 'addfile':
|
||||
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
|
||||
if req_params.get('mode') is 'addfile':
|
||||
sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
|
||||
else:
|
||||
sab = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
|
||||
sab_data = self.call(req_params)
|
||||
except URLError:
|
||||
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
|
||||
return False
|
||||
@@ -49,17 +47,15 @@ class Sabnzbd(Downloader):
|
||||
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
|
||||
return False
|
||||
|
||||
result = sab.strip()
|
||||
if not result:
|
||||
log.error('SABnzbd didn\'t return anything.')
|
||||
return False
|
||||
|
||||
log.debug('Result text from SAB: %s', result[:40])
|
||||
if result[:2] == 'ok':
|
||||
log.debug('Result from SAB: %s', sab_data)
|
||||
if sab_data.get('status') and not sab_data.get('error'):
|
||||
log.info('NZB sent to SAB successfully.')
|
||||
return True
|
||||
if filedata:
|
||||
return self.downloadReturnId(sab_data.get('nzo_ids')[0])
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
log.error(result[:40])
|
||||
log.error('Error getting data from SABNZBd: %s', sab_data)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
@@ -85,14 +81,13 @@ class Sabnzbd(Downloader):
|
||||
log.error('Failed getting history json: %s', traceback.format_exc(1))
|
||||
return False
|
||||
|
||||
statuses = []
|
||||
statuses = StatusList(self)
|
||||
|
||||
# Get busy releases
|
||||
for item in queue.get('slots', []):
|
||||
statuses.append({
|
||||
'id': item['nzo_id'],
|
||||
'name': item['filename'],
|
||||
'status': 'busy',
|
||||
'original_status': item['status'],
|
||||
'timeleft': item['timeleft'] if not queue['paused'] else -1,
|
||||
})
|
||||
@@ -111,7 +106,8 @@ class Sabnzbd(Downloader):
|
||||
'name': item['name'],
|
||||
'status': status,
|
||||
'original_status': item['status'],
|
||||
'timeleft': 0,
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': item['storage'],
|
||||
})
|
||||
|
||||
return statuses
|
||||
@@ -133,21 +129,21 @@ class Sabnzbd(Downloader):
|
||||
|
||||
return True
|
||||
|
||||
def call(self, params, use_json = True):
|
||||
def call(self, request_params, use_json = True, **kwargs):
|
||||
|
||||
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
|
||||
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
|
||||
'apikey': self.conf('api_key'),
|
||||
'output': 'json'
|
||||
}))
|
||||
|
||||
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
|
||||
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs)
|
||||
if use_json:
|
||||
d = json.loads(data)
|
||||
if d.get('error'):
|
||||
log.error('Error getting data from SABNZBd: %s', d.get('error'))
|
||||
return {}
|
||||
|
||||
return d[params['mode']]
|
||||
return d.get(request_params['mode']) or d
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
@@ -41,15 +41,22 @@ config = [{
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Where should Transmission saved the downloaded files?',
|
||||
'description': 'Download to this directory. Keep empty for default Transmission download directory.',
|
||||
},
|
||||
{
|
||||
'name': 'ratio',
|
||||
'default': 10,
|
||||
'type': 'int',
|
||||
'type': 'float',
|
||||
'advanced': True,
|
||||
'description': 'Stop transfer when reaching ratio',
|
||||
},
|
||||
{
|
||||
'name': 'ratiomode',
|
||||
'default': 0,
|
||||
'type': 'int',
|
||||
'advanced': True,
|
||||
'description': '0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from base64 import b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import isInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from datetime import timedelta
|
||||
import httplib
|
||||
import json
|
||||
import os.path
|
||||
import re
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -18,7 +21,7 @@ class Transmission(Downloader):
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
|
||||
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
|
||||
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
|
||||
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
@@ -27,22 +30,19 @@ class Transmission(Downloader):
|
||||
return False
|
||||
|
||||
# Set parameters for Transmission
|
||||
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
|
||||
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
||||
|
||||
# Create the empty folder to download too
|
||||
self.makeDir(folder_path)
|
||||
|
||||
params = {
|
||||
'paused': self.conf('paused', default = 0),
|
||||
'download-dir': folder_path
|
||||
}
|
||||
|
||||
if len(self.conf('directory', default = '')) > 0:
|
||||
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
|
||||
params['download-dir'] = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
||||
|
||||
torrent_params = {}
|
||||
if self.conf('ratio'):
|
||||
torrent_params = {
|
||||
'seedRatioLimit': self.conf('ratio'),
|
||||
'seedRatioMode': self.conf('ratio')
|
||||
'seedRatioMode': self.conf('ratiomode')
|
||||
}
|
||||
|
||||
if not filedata and data.get('type') == 'torrent':
|
||||
@@ -58,15 +58,99 @@ class Transmission(Downloader):
|
||||
else:
|
||||
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
||||
|
||||
if not remote_torrent:
|
||||
return False
|
||||
|
||||
# Change settings of added torrents
|
||||
if torrent_params:
|
||||
elif torrent_params:
|
||||
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
||||
|
||||
return True
|
||||
except Exception, err:
|
||||
log.error('Failed to change settings for transfer: %s', err)
|
||||
log.info('Torrent sent to Transmission successfully.')
|
||||
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
|
||||
except:
|
||||
log.error('Failed to change settings for transfer: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking Transmission download status.')
|
||||
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
# Go through Queue
|
||||
try:
|
||||
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
return_params = {
|
||||
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio']
|
||||
}
|
||||
queue = trpc.get_alltorrents(return_params)
|
||||
except Exception, err:
|
||||
log.error('Failed getting queue: %s', err)
|
||||
return False
|
||||
|
||||
if not queue:
|
||||
return []
|
||||
|
||||
statuses = StatusList(self)
|
||||
|
||||
# Get torrents status
|
||||
# CouchPotato Status
|
||||
#status = 'busy'
|
||||
#status = 'failed'
|
||||
#status = 'completed'
|
||||
# Transmission Status
|
||||
#status = 0 => "Torrent is stopped"
|
||||
#status = 1 => "Queued to check files"
|
||||
#status = 2 => "Checking files"
|
||||
#status = 3 => "Queued to download"
|
||||
#status = 4 => "Downloading"
|
||||
#status = 4 => "Queued to seed"
|
||||
#status = 6 => "Seeding"
|
||||
#To do :
|
||||
# add checking file
|
||||
# manage no peer in a range time => fail
|
||||
|
||||
for item in queue['torrents']:
|
||||
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished']))
|
||||
|
||||
if not os.path.isdir(Env.setting('from', 'renamer')):
|
||||
log.error('Renamer "from" folder doesn\'t to exist.')
|
||||
return
|
||||
|
||||
if (item['percentDone'] * 100) >= 100 and (item['status'] == 6 or item['status'] == 0) and item['uploadRatio'] > self.conf('ratio'):
|
||||
try:
|
||||
trpc.stop_torrent(item['hashString'], {})
|
||||
statuses.append({
|
||||
'id': item['hashString'],
|
||||
'name': item['name'],
|
||||
'status': 'completed',
|
||||
'original_status': item['status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': os.path.join(item['downloadDir'], item['name']),
|
||||
})
|
||||
except Exception, err:
|
||||
log.error('Failed to stop and remove torrent "%s" with error: %s', (item['name'], err))
|
||||
statuses.append({
|
||||
'id': item['hashString'],
|
||||
'name': item['name'],
|
||||
'status': 'failed',
|
||||
'original_status': item['status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
})
|
||||
else:
|
||||
statuses.append({
|
||||
'id': item['hashString'],
|
||||
'name': item['name'],
|
||||
'status': 'busy',
|
||||
'original_status': item['status'],
|
||||
'timeleft': str(timedelta(seconds = item['eta'])), # Is ETA in seconds??
|
||||
})
|
||||
|
||||
return statuses
|
||||
|
||||
class TransmissionRPC(object):
|
||||
|
||||
@@ -97,6 +181,7 @@ class TransmissionRPC(object):
|
||||
try:
|
||||
open_request = urllib2.urlopen(request)
|
||||
response = json.loads(open_request.read())
|
||||
log.debug('request: %s', json.dumps(ojson))
|
||||
log.debug('response: %s', json.dumps(response))
|
||||
if response['result'] == 'success':
|
||||
log.debug('Transmission action successfull')
|
||||
@@ -146,3 +231,18 @@ class TransmissionRPC(object):
|
||||
arguments['ids'] = torrent_id
|
||||
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
|
||||
return self._request(post_data)
|
||||
|
||||
def get_alltorrents(self, arguments):
|
||||
post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag}
|
||||
return self._request(post_data)
|
||||
|
||||
def stop_torrent(self, torrent_id, arguments):
|
||||
arguments['ids'] = torrent_id
|
||||
post_data = {'arguments': arguments, 'method': 'torrent-stop', 'tag': self.tag}
|
||||
return self._request(post_data)
|
||||
|
||||
def remove_torrent(self, torrent_id, remove_local_data, arguments):
|
||||
arguments['ids'] = torrent_id
|
||||
arguments['delete-local-data'] = remove_local_data
|
||||
post_data = {'arguments': arguments, 'method': 'torrent-remove', 'tag': self.tag}
|
||||
return self._request(post_data)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from base64 import b16encode, b32decode
|
||||
from bencode import bencode, bdecode
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import isInt, ss
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from multipartpost import MultipartPostHandler
|
||||
import cookielib
|
||||
@@ -66,7 +67,7 @@ class uTorrent(Downloader):
|
||||
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
||||
if self.conf('paused', default = 0):
|
||||
self.utorrent_api.pause_torrent(torrent_hash)
|
||||
return True
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
except Exception, err:
|
||||
log.error('Failed to send torrent to uTorrent: %s', err)
|
||||
return False
|
||||
@@ -103,7 +104,7 @@ class uTorrent(Downloader):
|
||||
log.debug('Nothing in queue')
|
||||
return False
|
||||
|
||||
statuses = []
|
||||
statuses = StatusList(self)
|
||||
|
||||
# Get torrents
|
||||
for item in queue.get('torrents', []):
|
||||
@@ -118,7 +119,8 @@ class uTorrent(Downloader):
|
||||
'name': item[2],
|
||||
'status': status,
|
||||
'original_status': item[1],
|
||||
'timeleft': item[10],
|
||||
'timeleft': str(timedelta(seconds = item[10])),
|
||||
'folder': item[26],
|
||||
})
|
||||
|
||||
return statuses
|
||||
@@ -195,3 +197,25 @@ class uTorrentAPI(object):
|
||||
def get_status(self):
|
||||
action = "list=1"
|
||||
return self._request(action)
|
||||
|
||||
def get_settings(self):
|
||||
action = "action=getsettings"
|
||||
settings_dict = {}
|
||||
try:
|
||||
utorrent_settings = json.loads(self._request(action))
|
||||
|
||||
# Create settings dict
|
||||
for item in utorrent_settings['settings']:
|
||||
if item[1] == 0: # int
|
||||
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
|
||||
elif item[1] == 1: # bool
|
||||
settings_dict[item[0]] = True if item[2] == 'true' else False
|
||||
elif item[1] == 2: # string
|
||||
settings_dict[item[0]] = item[2]
|
||||
|
||||
#log.debug('uTorrent settings: %s', settings_dict)
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed to get settings from uTorrent: %s', err)
|
||||
|
||||
return settings_dict
|
||||
|
||||
@@ -16,10 +16,8 @@ def runHandler(name, handler, *args, **kwargs):
|
||||
|
||||
def addEvent(name, handler, priority = 100):
|
||||
|
||||
if events.get(name):
|
||||
e = events[name]
|
||||
else:
|
||||
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
|
||||
if not events.get(name):
|
||||
events[name] = []
|
||||
|
||||
def createHandle(*args, **kwargs):
|
||||
|
||||
@@ -35,7 +33,10 @@ def addEvent(name, handler, priority = 100):
|
||||
|
||||
return h
|
||||
|
||||
e.handle(createHandle, priority = priority)
|
||||
events[name].append({
|
||||
'handler': createHandle,
|
||||
'priority': priority,
|
||||
})
|
||||
|
||||
def removeEvent(name, handler):
|
||||
e = events[name]
|
||||
@@ -43,6 +44,12 @@ def removeEvent(name, handler):
|
||||
|
||||
def fireEvent(name, *args, **kwargs):
|
||||
if not events.get(name): return
|
||||
|
||||
e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock())
|
||||
|
||||
for event in events[name]:
|
||||
e.handle(event['handler'], priority = event['priority'])
|
||||
|
||||
#log.debug('Firing event %s', name)
|
||||
try:
|
||||
|
||||
@@ -52,6 +59,7 @@ def fireEvent(name, *args, **kwargs):
|
||||
'single': False, # Return single handler
|
||||
'merge': False, # Merge items
|
||||
'in_order': False, # Fire them in specific order, waits for the other to finish
|
||||
'async': False
|
||||
}
|
||||
|
||||
# Do options
|
||||
@@ -62,13 +70,6 @@ def fireEvent(name, *args, **kwargs):
|
||||
options[x] = val
|
||||
except: pass
|
||||
|
||||
e = events[name]
|
||||
|
||||
# Lock this event
|
||||
e.lock.acquire()
|
||||
|
||||
e.asynchronous = False
|
||||
|
||||
# Make sure only 1 event is fired at a time when order is wanted
|
||||
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
|
||||
kwargs['event_return_on_result'] = options['single']
|
||||
@@ -76,9 +77,6 @@ def fireEvent(name, *args, **kwargs):
|
||||
# Fire
|
||||
result = e(*args, **kwargs)
|
||||
|
||||
# Release lock for this event
|
||||
e.lock.release()
|
||||
|
||||
if options['single'] and not options['merge']:
|
||||
results = None
|
||||
|
||||
@@ -104,13 +102,14 @@ def fireEvent(name, *args, **kwargs):
|
||||
|
||||
# Merge
|
||||
if options['merge'] and len(results) > 0:
|
||||
results.reverse() # Priority 1 is higher then 100
|
||||
|
||||
# Dict
|
||||
if isinstance(results[0], dict):
|
||||
results.reverse()
|
||||
|
||||
merged = {}
|
||||
for result in results:
|
||||
merged = mergeDicts(merged, result)
|
||||
merged = mergeDicts(merged, result, prepend_list = True)
|
||||
|
||||
results = merged
|
||||
# Lists
|
||||
@@ -141,9 +140,9 @@ def fireEvent(name, *args, **kwargs):
|
||||
|
||||
def fireEventAsync(*args, **kwargs):
|
||||
try:
|
||||
my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
||||
my_thread.setDaemon(True)
|
||||
my_thread.start()
|
||||
t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
return True
|
||||
except Exception, e:
|
||||
log.error('%s: %s', (args[0], e))
|
||||
|
||||
@@ -10,6 +10,20 @@ import sys
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
def link(src, dst):
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
if ctypes.windll.kernel32.CreateHardLinkW(unicode(dst), unicode(src), 0) == 0: raise ctypes.WinError()
|
||||
else:
|
||||
os.link(src, dst)
|
||||
|
||||
def symlink(src, dst):
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
if ctypes.windll.kernel32.CreateSymbolicLinkW(unicode(dst), unicode(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
|
||||
else:
|
||||
os.symlink(src, dst)
|
||||
|
||||
def getUserDir():
|
||||
try:
|
||||
import pwd
|
||||
@@ -53,7 +67,7 @@ def getDataDir():
|
||||
def isDict(object):
|
||||
return isinstance(object, dict)
|
||||
|
||||
def mergeDicts(a, b):
|
||||
def mergeDicts(a, b, prepend_list = False):
|
||||
assert isDict(a), isDict(b)
|
||||
dst = a.copy()
|
||||
|
||||
@@ -67,7 +81,7 @@ def mergeDicts(a, b):
|
||||
if isDict(current_src[key]) and isDict(current_dst[key]):
|
||||
stack.append((current_dst[key], current_src[key]))
|
||||
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
|
||||
current_dst[key].extend(current_src[key])
|
||||
current_dst[key] = current_src[key] + current_dst[key] if prepend_list else current_dst[key] + current_src[key]
|
||||
current_dst[key] = removeListDuplicates(current_dst[key])
|
||||
else:
|
||||
current_dst[key] = current_src[key]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import re
|
||||
import traceback
|
||||
|
||||
class CPLog(object):
|
||||
|
||||
@@ -50,8 +49,8 @@ class CPLog(object):
|
||||
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
|
||||
else:
|
||||
msg = msg % ss(replace_tuple)
|
||||
except:
|
||||
self.logger.error(u'Failed encoding stuff to log: %s' % traceback.format_exc())
|
||||
except Exception, e:
|
||||
self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e))
|
||||
|
||||
if not Env.get('dev'):
|
||||
|
||||
|
||||
@@ -2,13 +2,15 @@ from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.providers.base import Provider
|
||||
from couchpotato.environment import Env
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Notification(Plugin):
|
||||
class Notification(Provider):
|
||||
|
||||
type = 'notification'
|
||||
|
||||
default_title = Env.get('appname')
|
||||
test_message = 'ZOMG Lazors Pewpewpew!'
|
||||
@@ -16,11 +18,12 @@ class Notification(Plugin):
|
||||
listen_to = [
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
'core.message',
|
||||
]
|
||||
dont_listen_to = []
|
||||
|
||||
def __init__(self):
|
||||
addEvent('notify.%s' % self.getName().lower(), self.notify)
|
||||
addEvent('notify.%s' % self.getName().lower(), self._notify)
|
||||
|
||||
addApiView(self.testNotifyName(), self.test)
|
||||
|
||||
@@ -33,10 +36,17 @@ class Notification(Plugin):
|
||||
def notify(message = None, group = {}, data = None):
|
||||
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
|
||||
return
|
||||
return self.notify(message = message, data = data if data else group, listener = listener)
|
||||
return self._notify(message = message, data = data if data else group, listener = listener)
|
||||
|
||||
return notify
|
||||
|
||||
def getNotificationImage(self, size = 'small'):
|
||||
return 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/notify.couch.%s.png' % size
|
||||
|
||||
def _notify(self, *args, **kwargs):
|
||||
if self.isEnabled():
|
||||
return self.notify(*args, **kwargs)
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
pass
|
||||
|
||||
@@ -46,7 +56,7 @@ class Notification(Plugin):
|
||||
|
||||
log.info('Sending test to %s', test_type)
|
||||
|
||||
success = self.notify(
|
||||
success = self._notify(
|
||||
message = self.test_message,
|
||||
data = {},
|
||||
listener = 'test'
|
||||
|
||||
@@ -11,7 +11,6 @@ class Boxcar(Notification):
|
||||
url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
try:
|
||||
message = message.strip()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView, addNonBlockApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from couchpotato.core.settings.model import Notification as Notif
|
||||
from couchpotato.environment import Env
|
||||
from sqlalchemy.sql.expression import or_
|
||||
import threading
|
||||
import time
|
||||
@@ -21,11 +22,6 @@ class CoreNotifier(Notification):
|
||||
messages = []
|
||||
listeners = []
|
||||
|
||||
listen_to = [
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(CoreNotifier, self).__init__()
|
||||
|
||||
@@ -54,7 +50,10 @@ class CoreNotifier(Notification):
|
||||
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
|
||||
addApiView('notification.listener', self.listener)
|
||||
|
||||
fireEvent('schedule.interval', 'core.check_messages', self.checkMessages, hours = 12, single = True)
|
||||
|
||||
addEvent('app.load', self.clean)
|
||||
addEvent('app.load', self.checkMessages)
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -112,6 +111,22 @@ class CoreNotifier(Notification):
|
||||
'notifications': notifications
|
||||
})
|
||||
|
||||
def checkMessages(self):
|
||||
|
||||
prop_name = 'messages.last_check'
|
||||
last_check = tryInt(Env.prop(prop_name, default = 0))
|
||||
|
||||
messages = fireEvent('cp.messages', last_check = last_check, single = True)
|
||||
|
||||
for message in messages:
|
||||
if message.get('time') > last_check:
|
||||
fireEvent('core.message', message = message.get('message'), data = message)
|
||||
|
||||
if last_check < message.get('time'):
|
||||
last_check = message.get('time')
|
||||
|
||||
Env.prop(prop_name, value = last_check)
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -21,21 +21,18 @@ var NotificationBase = new Class({
|
||||
App.addEvent('load', function(){
|
||||
|
||||
App.block.notification = new Block.Menu(self, {
|
||||
'button_class': 'icon2.eye-open',
|
||||
'class': 'notification_menu',
|
||||
'onOpen': self.markAsRead.bind(self)
|
||||
})
|
||||
$(App.block.notification).inject(App.getBlock('search'), 'after');
|
||||
self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide();
|
||||
|
||||
/* App.getBlock('notification').addLink(new Element('a.more', {
|
||||
'href': App.createUrl('notifications'),
|
||||
'text': 'Show older notifications'
|
||||
})); */
|
||||
});
|
||||
|
||||
window.addEvent('load', function(){
|
||||
self.startInterval.delay(Browser.safari ? 100 : 0, self)
|
||||
});
|
||||
self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 100, self);
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
@@ -47,14 +44,19 @@ var NotificationBase = new Class({
|
||||
|
||||
result.el = App.getBlock('notification').addLink(
|
||||
new Element('span.'+(result.read ? 'read' : '' )).adopt(
|
||||
new Element('span.message', {'text': result.message}),
|
||||
new Element('span.message', {'html': result.message}),
|
||||
new Element('span.added', {'text': added.timeDiffInWords(), 'title': added})
|
||||
)
|
||||
, 'top');
|
||||
self.notifications.include(result);
|
||||
|
||||
if(!result.read)
|
||||
if(result.data.important !== undefined && !result.read){
|
||||
var sticky = true
|
||||
App.fireEvent('message', [result.message, sticky, result])
|
||||
}
|
||||
else if(!result.read){
|
||||
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -64,20 +66,26 @@ var NotificationBase = new Class({
|
||||
self.badge[value ? 'show' : 'hide']()
|
||||
},
|
||||
|
||||
markAsRead: function(){
|
||||
var self = this;
|
||||
markAsRead: function(force_ids){
|
||||
var self = this,
|
||||
ids = force_ids;
|
||||
|
||||
var rn = self.notifications.filter(function(n){
|
||||
return !n.read
|
||||
})
|
||||
if(!force_ids) {
|
||||
var rn = self.notifications.filter(function(n){
|
||||
return !n.read && n.data.important === undefined
|
||||
})
|
||||
|
||||
var ids = []
|
||||
rn.each(function(n){
|
||||
ids.include(n.id)
|
||||
})
|
||||
var ids = []
|
||||
rn.each(function(n){
|
||||
ids.include(n.id)
|
||||
})
|
||||
}
|
||||
|
||||
if(ids.length > 0)
|
||||
Api.request('notification.markread', {
|
||||
'data': {
|
||||
'ids': ids.join(',')
|
||||
},
|
||||
'onSuccess': function(){
|
||||
self.setBadge('')
|
||||
}
|
||||
@@ -93,11 +101,20 @@ var NotificationBase = new Class({
|
||||
return;
|
||||
}
|
||||
|
||||
Api.request('notification.listener', {
|
||||
self.request = Api.request('notification.listener', {
|
||||
'data': {'init':true},
|
||||
'onSuccess': self.processData.bind(self)
|
||||
}).send()
|
||||
|
||||
setInterval(function(){
|
||||
|
||||
if(self.request && self.request.isRunning()){
|
||||
self.request.cancel();
|
||||
self.startPoll()
|
||||
}
|
||||
|
||||
}, 120000);
|
||||
|
||||
},
|
||||
|
||||
startPoll: function(){
|
||||
@@ -143,26 +160,41 @@ var NotificationBase = new Class({
|
||||
self.startPoll()
|
||||
},
|
||||
|
||||
showMessage: function(message){
|
||||
showMessage: function(message, sticky, data){
|
||||
var self = this;
|
||||
|
||||
if(!self.message_container)
|
||||
self.message_container = new Element('div.messages').inject(document.body);
|
||||
|
||||
var new_message = new Element('div.message', {
|
||||
'text': message
|
||||
}).inject(self.message_container);
|
||||
var new_message = new Element('div', {
|
||||
'class': 'message' + (sticky ? ' sticky' : ''),
|
||||
'html': message
|
||||
}).inject(self.message_container, 'top');
|
||||
|
||||
setTimeout(function(){
|
||||
new_message.addClass('show')
|
||||
}, 10);
|
||||
|
||||
setTimeout(function(){
|
||||
var hide_message = function(){
|
||||
new_message.addClass('hide')
|
||||
setTimeout(function(){
|
||||
new_message.destroy();
|
||||
}, 1000);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
if(sticky)
|
||||
new_message.grab(
|
||||
new Element('a.close.icon2', {
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.markAsRead([data.id]);
|
||||
hide_message();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
else
|
||||
setTimeout(hide_message, 4000);
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ log = CPLog(__name__)
|
||||
class Email(Notification):
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
# Extract all the settings from settings
|
||||
from_address = self.conf('from')
|
||||
@@ -50,6 +49,5 @@ class Email(Notification):
|
||||
return True
|
||||
except:
|
||||
log.error('E-mail failed: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
@@ -44,7 +44,6 @@ class Growl(Notification):
|
||||
log.error('Failed register of growl: %s', traceback.format_exc())
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
self.register()
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ class Notifo(Notification):
|
||||
url = 'https://api.notifo.com/v1/send_notification'
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
try:
|
||||
params = {
|
||||
|
||||
@@ -9,7 +9,6 @@ log = CPLog(__name__)
|
||||
class NotifyMyAndroid(Notification):
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
nma = pynma.PyNMA()
|
||||
keys = splitString(self.conf('api_key'))
|
||||
|
||||
@@ -9,7 +9,6 @@ log = CPLog(__name__)
|
||||
class NotifyMyWP(Notification):
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
keys = splitString(self.conf('api_key'))
|
||||
p = PyNMWP(keys, self.conf('dev_key'))
|
||||
|
||||
@@ -22,6 +22,13 @@ config = [{
|
||||
'description': 'Default should be on localhost',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -46,7 +46,6 @@ class Plex(Notification):
|
||||
return True
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")]
|
||||
successful = 0
|
||||
|
||||
@@ -13,7 +13,6 @@ class Prowl(Notification):
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
data = {
|
||||
'apikey': self.conf('api_key'),
|
||||
|
||||
@@ -12,16 +12,15 @@ class Pushalot(Notification):
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
data = {
|
||||
'AuthorizationToken': self.conf('auth_token'),
|
||||
'Title': self.default_title,
|
||||
'Body': toUnicode(message),
|
||||
'LinkTitle': toUnicode("CouchPotato"),
|
||||
'link': toUnicode("https://couchpota.to/"),
|
||||
'IsImportant': self.conf('important'),
|
||||
'IsSilent': self.conf('silent'),
|
||||
'Image': toUnicode(self.getNotificationImage('medium') + '?1'),
|
||||
'Source': toUnicode(self.default_title)
|
||||
}
|
||||
|
||||
headers = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.variable import getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from httplib import HTTPSConnection
|
||||
@@ -11,21 +12,26 @@ class Pushover(Notification):
|
||||
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
http_handler = HTTPSConnection("api.pushover.net:443")
|
||||
|
||||
data = {
|
||||
api_data = {
|
||||
'user': self.conf('user_key'),
|
||||
'token': self.app_token,
|
||||
'message': toUnicode(message),
|
||||
'priority': self.conf('priority')
|
||||
'priority': self.conf('priority'),
|
||||
}
|
||||
|
||||
if data and data.get('library'):
|
||||
api_data.update({
|
||||
'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']),
|
||||
'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])),
|
||||
})
|
||||
|
||||
http_handler.request('POST',
|
||||
"/1/messages.json",
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'},
|
||||
body = tryUrlencode(data)
|
||||
body = tryUrlencode(api_data)
|
||||
)
|
||||
|
||||
response = http_handler.getresponse()
|
||||
|
||||
@@ -12,7 +12,6 @@ class Toasty(Notification):
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
data = {
|
||||
'title': self.default_title,
|
||||
|
||||
30
couchpotato/core/notifications/trakt/__init__.py
Normal file
30
couchpotato/core/notifications/trakt/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from .main import Trakt
|
||||
|
||||
def start():
|
||||
return Trakt()
|
||||
|
||||
config = [{
|
||||
'name': 'trakt',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'trakt',
|
||||
'label': 'Trakt',
|
||||
'description': 'add movies to your collection once downloaded. Fill in your username and password in the <a href="../automation/">Automation Trakt settings</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'notification_enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'remove_watchlist_enabled',
|
||||
'label': 'Remove from watchlist',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
46
couchpotato/core/notifications/trakt/main.py
Normal file
46
couchpotato/core/notifications/trakt/main.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class Trakt(Notification):
|
||||
|
||||
urls = {
|
||||
'base': 'http://api.trakt.tv/%s',
|
||||
'library': 'movie/library/%s',
|
||||
'unwatchlist': 'movie/unwatchlist/%s',
|
||||
}
|
||||
|
||||
listen_to = ['movie.downloaded']
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
|
||||
post_data = {
|
||||
'username': self.conf('automation_username'),
|
||||
'password' : self.conf('automation_password'),
|
||||
'movies': [{
|
||||
'imdb_id': data['library']['identifier'],
|
||||
'title': data['library']['titles'][0]['title'],
|
||||
'year': data['library']['year']
|
||||
}] if data else []
|
||||
}
|
||||
|
||||
result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data)
|
||||
if self.conf('remove_watchlist_enabled'):
|
||||
result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data)
|
||||
|
||||
return result
|
||||
|
||||
def call(self, method_url, post_data):
|
||||
|
||||
try:
|
||||
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')
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
log.error('Failed to call trakt, check your login.')
|
||||
return False
|
||||
@@ -32,7 +32,6 @@ class Twitter(Notification):
|
||||
addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials)
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret'))
|
||||
|
||||
|
||||
@@ -31,6 +31,13 @@ config = [{
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -13,10 +13,9 @@ class XBMC(Notification):
|
||||
|
||||
listen_to = ['renamer.after']
|
||||
use_json_notifications = {}
|
||||
couch_logo_url = 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/xbmc-notify.png'
|
||||
http_time_between_calls = 0
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
hosts = splitString(self.conf('host'))
|
||||
|
||||
@@ -28,7 +27,7 @@ class XBMC(Notification):
|
||||
|
||||
if self.use_json_notifications.get(host):
|
||||
response = self.request(host, [
|
||||
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.couch_logo_url}),
|
||||
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
|
||||
('VideoLibrary.Scan', {}),
|
||||
])
|
||||
else:
|
||||
@@ -90,7 +89,7 @@ class XBMC(Notification):
|
||||
self.use_json_notifications[host] = True
|
||||
|
||||
# send the text message
|
||||
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image':self.couch_logo_url})])
|
||||
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})])
|
||||
for result in resp:
|
||||
if (result.get('result') and result['result'] == 'OK'):
|
||||
log.debug('Message delivered successfully!')
|
||||
@@ -113,7 +112,7 @@ class XBMC(Notification):
|
||||
server = 'http://%s/xbmcCmds/' % host
|
||||
|
||||
# Notification(title, message [, timeout , image])
|
||||
cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.couch_logo_url))
|
||||
cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.getNotificationImage('medium')))
|
||||
server += cmd
|
||||
|
||||
# I have no idea what to set to, just tried text/plain and seems to be working :)
|
||||
|
||||
@@ -36,6 +36,20 @@ config = [{
|
||||
'unit': 'hours',
|
||||
'description': 'hours',
|
||||
},
|
||||
{
|
||||
'name': 'required_genres',
|
||||
'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 "&"'
|
||||
},
|
||||
{
|
||||
'name': 'ignored_genres',
|
||||
'label': 'Ignored Genres',
|
||||
'default': '',
|
||||
'placeholder': 'Example: Horror, Comedy & Drama & Romance',
|
||||
'description': 'Ignore movies that contain at least one set of genres. Sets work the same as above.'
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -10,11 +10,16 @@ class Automation(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
|
||||
addEvent('app.load', self.setCrons)
|
||||
|
||||
if not Env.get('dev'):
|
||||
addEvent('app.load', self.addMovies)
|
||||
|
||||
addEvent('setting.save.automation.hour.after', self.setCrons)
|
||||
|
||||
def setCrons(self):
|
||||
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
|
||||
|
||||
def addMovies(self):
|
||||
|
||||
movies = fireEvent('automation.get_movies', merge = True)
|
||||
|
||||
@@ -28,6 +28,7 @@ class Plugin(object):
|
||||
|
||||
_needs_shutdown = False
|
||||
|
||||
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20130519 Firefox/24.0'
|
||||
http_last_use = {}
|
||||
http_time_between_calls = 0
|
||||
http_failed_request = {}
|
||||
@@ -104,12 +105,13 @@ class Plugin(object):
|
||||
if not params: params = {}
|
||||
|
||||
# Fill in some headers
|
||||
headers['Referer'] = headers.get('Referer', urlparse(url).hostname)
|
||||
headers['Host'] = headers.get('Host', urlparse(url).hostname)
|
||||
headers['User-Agent'] = headers.get('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.2) Gecko/20100101 Firefox/10.0.2')
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
parsed_url = urlparse(url)
|
||||
host = parsed_url.hostname
|
||||
|
||||
host = urlparse(url).hostname
|
||||
headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
|
||||
headers['Host'] = headers.get('Host', host)
|
||||
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
|
||||
# Don't try for failed requests
|
||||
if self.http_failed_disabled.get(host, 0) > 0:
|
||||
@@ -138,7 +140,7 @@ class Plugin(object):
|
||||
|
||||
response = opener.open(request, timeout = timeout)
|
||||
else:
|
||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()]))
|
||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
data = tryUrlencode(params) if len(params) > 0 else None
|
||||
request = urllib2.Request(url, data, headers)
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class Dashboard(Plugin):
|
||||
profile_pre[profile.get('id')] = contains
|
||||
|
||||
# Get all active movies
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
active_status, snatched_status, downloaded_status, available_status = fireEvent('status.get', ['active', 'snatched', 'downloaded', 'available'], single = True)
|
||||
subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery()
|
||||
|
||||
q = db.query(Movie).join((subq, subq.c.id == Movie.id)) \
|
||||
@@ -108,6 +108,14 @@ class Dashboard(Plugin):
|
||||
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
|
||||
coming_soon = True
|
||||
|
||||
# Skip if movie is snatched/downloaded/available
|
||||
skip = False
|
||||
for release in movie.releases:
|
||||
if release.status_id in [snatched_status.get('id'), downloaded_status.get('id'), available_status.get('id')]:
|
||||
skip = True
|
||||
break
|
||||
if skip:
|
||||
continue
|
||||
|
||||
if coming_soon:
|
||||
temp = movie.to_dict({
|
||||
@@ -125,6 +133,7 @@ class Dashboard(Plugin):
|
||||
if len(movies) >= limit:
|
||||
break
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
|
||||
@@ -9,6 +9,8 @@ from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.scanner.main import Scanner
|
||||
from couchpotato.core.settings.model import FileType, File
|
||||
from couchpotato.environment import Env
|
||||
from flask.helpers import send_file
|
||||
from werkzeug.exceptions import NotFound
|
||||
import os.path
|
||||
import time
|
||||
import traceback
|
||||
@@ -71,7 +73,7 @@ class FileManager(Plugin):
|
||||
db = get_session()
|
||||
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
|
||||
for filename in walk_files:
|
||||
if root == python_cache or 'minified' in filename: continue
|
||||
if root == python_cache or 'minified' in filename or 'version' in filename or 'temp_updater' in root: continue
|
||||
file_path = os.path.join(root, filename)
|
||||
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
|
||||
if not f:
|
||||
@@ -81,11 +83,13 @@ class FileManager(Plugin):
|
||||
|
||||
def showCacheFile(self, filename = ''):
|
||||
|
||||
cache_dir = Env.get('cache_dir')
|
||||
filename = os.path.basename(filename)
|
||||
file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename))
|
||||
|
||||
from flask.helpers import send_from_directory
|
||||
return send_from_directory(cache_dir, filename)
|
||||
if not os.path.isfile(file_path):
|
||||
log.error('File "%s" not found', file_path)
|
||||
raise NotFound()
|
||||
|
||||
return send_file(file_path, conditional = True)
|
||||
|
||||
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
var File = new Class({
|
||||
|
||||
initialize: function(file){
|
||||
initialize: function(type, file){
|
||||
var self = this;
|
||||
|
||||
if(!file){
|
||||
self.empty = true;
|
||||
self.el = new Element('div');
|
||||
self.el = new Element('div.empty_file.'+type);
|
||||
return
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ var File = new Class({
|
||||
var file_name = self.data.path.replace(/^.*[\\\/]/, '');
|
||||
|
||||
self.el = new Element('div', {
|
||||
'class': 'type_image ' + self.type.identifier
|
||||
'class': 'type_image ' + self.type.identifier,
|
||||
'styles': {
|
||||
'background-image': 'url('+Api.createUrl('file.cache') + file_name+')'
|
||||
}
|
||||
}).adopt(
|
||||
new Element('img', {
|
||||
'src': Api.createUrl('file.cache') + file_name
|
||||
@@ -45,7 +48,7 @@ var FileSelect = new Class({
|
||||
});
|
||||
|
||||
if(single)
|
||||
return new File(results.pop());
|
||||
return new File(type, results.pop());
|
||||
|
||||
return results;
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ class LibraryPlugin(Plugin):
|
||||
addEvent('library.update', self.update)
|
||||
addEvent('library.update_release_date', self.updateReleaseDate)
|
||||
|
||||
|
||||
def add(self, attrs = {}, update_after = True):
|
||||
|
||||
db = get_session()
|
||||
@@ -53,6 +52,7 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
library_dict = l.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def update(self, identifier, default_title = '', force = False):
|
||||
@@ -132,6 +132,7 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def updateReleaseDate(self, identifier):
|
||||
@@ -150,6 +151,7 @@ class LibraryPlugin(Plugin):
|
||||
library.info = mergeDicts(library.info, {'release_date': dates })
|
||||
db.commit()
|
||||
|
||||
db.expire_all()
|
||||
return dates
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
.page.log .nav {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
padding: 0 0 30px;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
position: fixed;
|
||||
width: 960px;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #4E5969;
|
||||
}
|
||||
|
||||
.page.log .nav li {
|
||||
display: inline;
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
@@ -24,7 +25,17 @@
|
||||
.page.log .nav li.active {
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
font-size: 30px;
|
||||
background: rgba(255,255,255,.1);
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.page.log .nav {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page.log .nav li {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.page.log .loading {
|
||||
|
||||
@@ -6,19 +6,6 @@ Page.Log = new Class({
|
||||
title: 'Show recent logs.',
|
||||
has_tab: false,
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
self.parent(options)
|
||||
|
||||
|
||||
App.getBlock('more').addLink(new Element('a', {
|
||||
'href': App.createUrl(self.name),
|
||||
'text': self.name.capitalize(),
|
||||
'title': self.title
|
||||
}))
|
||||
|
||||
},
|
||||
|
||||
indexAction: function(){
|
||||
var self = this;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'manage',
|
||||
'label': 'movie library manager',
|
||||
'label': 'Movie Library Manager',
|
||||
'description': 'Add your existing movie folders.',
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -185,6 +185,8 @@ class Manage(Plugin):
|
||||
# Add it to release and update the info
|
||||
fireEvent('release.add', group = group)
|
||||
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
|
||||
else:
|
||||
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
|
||||
|
||||
return addToLibrary
|
||||
|
||||
|
||||
@@ -108,9 +108,8 @@ class MoviePlugin(Plugin):
|
||||
now = time.time()
|
||||
week = 262080
|
||||
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
done_status, available_status, snatched_status = \
|
||||
fireEvent('status.get', ['done', 'available', 'snatched'], single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -119,12 +118,13 @@ class MoviePlugin(Plugin):
|
||||
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
|
||||
.all()
|
||||
|
||||
#
|
||||
for movie in movies:
|
||||
for rel in movie.releases:
|
||||
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
|
||||
fireEvent('release.delete', id = rel.id, single = True)
|
||||
|
||||
db.expire_all()
|
||||
|
||||
def getView(self):
|
||||
|
||||
movie_id = getParam('id')
|
||||
@@ -150,6 +150,7 @@ class MoviePlugin(Plugin):
|
||||
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):
|
||||
@@ -175,8 +176,6 @@ class MoviePlugin(Plugin):
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
|
||||
|
||||
total_count = q.count()
|
||||
|
||||
filter_or = []
|
||||
if starts_with:
|
||||
starts_with = toUnicode(starts_with.lower())
|
||||
@@ -194,6 +193,8 @@ class MoviePlugin(Plugin):
|
||||
if filter_or:
|
||||
q = q.filter(or_(*filter_or))
|
||||
|
||||
total_count = q.count()
|
||||
|
||||
if order == 'release_order':
|
||||
q = q.order_by(desc(Release.last_edit))
|
||||
else:
|
||||
@@ -217,15 +218,14 @@ class MoviePlugin(Plugin):
|
||||
results = q2.all()
|
||||
movies = []
|
||||
for movie in results:
|
||||
temp = movie.to_dict({
|
||||
movies.append(movie.to_dict({
|
||||
'profile': {'types': {}},
|
||||
'releases': {'files':{}, 'info': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
})
|
||||
movies.append(temp)
|
||||
}))
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return (total_count, movies)
|
||||
|
||||
def availableChars(self, status = None, release_status = None):
|
||||
@@ -260,7 +260,7 @@ class MoviePlugin(Plugin):
|
||||
if char not in chars:
|
||||
chars += str(char)
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return ''.join(sorted(chars, key = str.lower))
|
||||
|
||||
def listView(self):
|
||||
@@ -316,11 +316,10 @@ class MoviePlugin(Plugin):
|
||||
for title in movie.library.titles:
|
||||
if title.default: default_title = title.title
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True, message = 'Updating "%s"' % default_title)
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
|
||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
|
||||
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
})
|
||||
@@ -367,10 +366,8 @@ class MoviePlugin(Plugin):
|
||||
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
|
||||
|
||||
# Status
|
||||
status_active = fireEvent('status.add', 'active', single = True)
|
||||
snatched_status = fireEvent('status.add', 'snatched', single = True)
|
||||
ignored_status = fireEvent('status.add', 'ignored', single = True)
|
||||
downloaded_status = fireEvent('status.add', 'downloaded', single = True)
|
||||
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)
|
||||
|
||||
@@ -397,7 +394,7 @@ class MoviePlugin(Plugin):
|
||||
|
||||
# Clean snatched history
|
||||
for release in m.releases:
|
||||
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id')]:
|
||||
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:
|
||||
@@ -431,7 +428,7 @@ class MoviePlugin(Plugin):
|
||||
if added:
|
||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return movie_dict
|
||||
|
||||
|
||||
@@ -481,7 +478,7 @@ class MoviePlugin(Plugin):
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
})
|
||||
@@ -543,13 +540,12 @@ class MoviePlugin(Plugin):
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return True
|
||||
|
||||
def restatus(self, movie_id):
|
||||
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -572,7 +568,6 @@ class MoviePlugin(Plugin):
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
db.commit()
|
||||
#db.close()
|
||||
|
||||
return True
|
||||
|
||||
@@ -582,6 +577,7 @@ class MoviePlugin(Plugin):
|
||||
db = get_session()
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
|
||||
db.expire_all()
|
||||
|
||||
return onComplete
|
||||
|
||||
@@ -592,5 +588,6 @@ class MoviePlugin(Plugin):
|
||||
db = get_session()
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
|
||||
db.expire_all()
|
||||
|
||||
return notifyFront
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
var MovieList = new Class({
|
||||
|
||||
Implements: [Options],
|
||||
Implements: [Events, Options],
|
||||
|
||||
options: {
|
||||
navigation: true,
|
||||
limit: 50,
|
||||
load_more: true,
|
||||
loader: true,
|
||||
menu: [],
|
||||
add_new: false
|
||||
add_new: false,
|
||||
force_view: false
|
||||
},
|
||||
|
||||
movies: [],
|
||||
movies_added: {},
|
||||
total_movies: 0,
|
||||
letters: {},
|
||||
filter: null,
|
||||
|
||||
@@ -21,7 +24,7 @@ var MovieList = new Class({
|
||||
|
||||
self.offset = 0;
|
||||
self.filter = self.options.filter || {
|
||||
'startswith': null,
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
}
|
||||
|
||||
@@ -42,7 +45,10 @@ var MovieList = new Class({
|
||||
}) : null
|
||||
);
|
||||
|
||||
self.changeView(self.getSavedView() || self.options.view || 'details');
|
||||
if($(window).getSize().x <= 480 && !self.options.force_view)
|
||||
self.changeView('list');
|
||||
else
|
||||
self.changeView(self.getSavedView() || self.options.view || 'details');
|
||||
|
||||
self.getMovies();
|
||||
|
||||
@@ -57,7 +63,8 @@ var MovieList = new Class({
|
||||
self.movies.each(function(movie){
|
||||
if(movie.get('id') == notification.data.id){
|
||||
movie.destroy();
|
||||
delete self.movies_added[notification.data.id]
|
||||
delete self.movies_added[notification.data.id];
|
||||
self.setCounter(self.counter_count-1);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -71,6 +78,7 @@ var MovieList = new Class({
|
||||
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');
|
||||
self.setCounter(self.counter_count+1);
|
||||
|
||||
self.checkIfEmpty();
|
||||
}
|
||||
@@ -110,7 +118,7 @@ var MovieList = new Class({
|
||||
self.createMovie(movie);
|
||||
});
|
||||
|
||||
self.total_movies = total;
|
||||
self.total_movies += total;
|
||||
self.setCounter(total);
|
||||
|
||||
},
|
||||
@@ -120,7 +128,40 @@ var MovieList = new Class({
|
||||
|
||||
if(!self.navigation_counter) return;
|
||||
|
||||
self.navigation_counter.set('text', (count || 0));
|
||||
self.counter_count = count;
|
||||
self.navigation_counter.set('text', (count || 0) + ' movies');
|
||||
|
||||
if (self.empty_message) {
|
||||
self.empty_message.destroy();
|
||||
self.empty_message = null;
|
||||
}
|
||||
|
||||
if(self.total_movies && count == 0 && !self.empty_message){
|
||||
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
|
||||
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
|
||||
|
||||
self.empty_message = new Element('.message', {
|
||||
'html': 'No movies found ' + message + '.<br/>'
|
||||
}).grab(
|
||||
new Element('a', {
|
||||
'text': 'Reset filter',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.filter = {
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
};
|
||||
self.navigation_search_input.set('value', '');
|
||||
self.reset();
|
||||
self.activateLetter();
|
||||
self.getMovies(true);
|
||||
self.last_search_value = '';
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.movie_list);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -144,29 +185,9 @@ var MovieList = new Class({
|
||||
var self = this;
|
||||
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
self.current_view = self.getSavedView();
|
||||
self.el.addClass(self.current_view+'_list')
|
||||
self.el.addClass('with_navigation')
|
||||
|
||||
self.navigation = new Element('div.alph_nav').adopt(
|
||||
self.navigation_actions = new Element('ul.inlay.actions.reversed'),
|
||||
self.navigation_counter = new Element('span.counter[title=Total]'),
|
||||
self.navigation_alpha = new Element('ul.numbers', {
|
||||
'events': {
|
||||
'click:relay(li)': function(e, el){
|
||||
self.movie_list.empty()
|
||||
self.activateLetter(el.get('data-letter'))
|
||||
self.getMovies()
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.navigation_search_input = new Element('input.inlay', {
|
||||
'placeholder': 'Search',
|
||||
'events': {
|
||||
'keyup': self.search.bind(self),
|
||||
'change': self.search.bind(self)
|
||||
}
|
||||
}),
|
||||
self.navigation_menu = new Block.Menu(self),
|
||||
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
|
||||
new Element('span.select').adopt(
|
||||
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
|
||||
@@ -204,6 +225,31 @@ var MovieList = new Class({
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
new Element('div.menus').adopt(
|
||||
self.navigation_counter = new Element('span.counter[title=Total]'),
|
||||
self.filter_menu = new Block.Menu(self, {
|
||||
'class': 'filter'
|
||||
}),
|
||||
self.navigation_actions = new Element('ul.actions', {
|
||||
'events': {
|
||||
'click:relay(li)': function(e, el){
|
||||
var a = 'active';
|
||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||
self.changeView(el.get('data-view'));
|
||||
this.addClass(a);
|
||||
|
||||
el.inject(el.getParent(), 'top');
|
||||
el.getSiblings().hide()
|
||||
setTimeout(function(){
|
||||
el.getSiblings().setStyle('display', null);
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.navigation_menu = new Block.Menu(self, {
|
||||
'class': 'extra'
|
||||
})
|
||||
)
|
||||
).inject(self.el, 'top');
|
||||
|
||||
@@ -216,20 +262,39 @@ var MovieList = new Class({
|
||||
}).inject(self.mass_edit_quality)
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
self.navigation_search_input = new Element('input', {
|
||||
'title': 'Search through ' + self.options.identifier,
|
||||
'placeholder': 'Search through ' + self.options.identifier,
|
||||
'events': {
|
||||
'keyup': self.search.bind(self),
|
||||
'change': self.search.bind(self)
|
||||
}
|
||||
})
|
||||
).addClass('search');
|
||||
|
||||
self.filter_menu.addEvent('open', function(){
|
||||
self.navigation_search_input.focus();
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
self.navigation_alpha = new Element('ul.numbers', {
|
||||
'events': {
|
||||
'click:relay(li.available)': function(e, el){
|
||||
self.activateLetter(el.get('data-letter'))
|
||||
self.getMovies(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Actions
|
||||
['mass_edit', 'details', 'list'].each(function(view){
|
||||
self.navigation_actions.adopt(
|
||||
new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var a = 'active';
|
||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||
self.changeView(this.get('data-view'));
|
||||
this.addClass(a);
|
||||
}
|
||||
}
|
||||
}).adopt(new Element('span'))
|
||||
)
|
||||
var current = self.current_view == view;
|
||||
new Element('li', {
|
||||
'class': 'icon2 ' + view + (current ? ' active ' : ''),
|
||||
'data-view': view
|
||||
}).inject(self.navigation_actions, current ? 'top' : 'bottom');
|
||||
});
|
||||
|
||||
// All
|
||||
@@ -247,18 +312,19 @@ var MovieList = new Class({
|
||||
});
|
||||
|
||||
// Get available chars and highlight
|
||||
Api.request('movie.available_chars', {
|
||||
'data': Object.merge({
|
||||
'status': self.options.status
|
||||
}, self.filter),
|
||||
'onComplete': function(json){
|
||||
if(self.navigation.isDisplayed() || self.navigation.isVisible())
|
||||
Api.request('movie.available_chars', {
|
||||
'data': Object.merge({
|
||||
'status': self.options.status
|
||||
}, self.filter),
|
||||
'onSuccess': function(json){
|
||||
|
||||
json.chars.split('').each(function(c){
|
||||
self.letters[c.capitalize()].addClass('available')
|
||||
})
|
||||
json.chars.split('').each(function(c){
|
||||
self.letters[c.capitalize()].addClass('available')
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add menu or hide
|
||||
if (self.options.menu.length > 0)
|
||||
@@ -266,17 +332,7 @@ var MovieList = new Class({
|
||||
self.navigation_menu.addLink(menu_item);
|
||||
})
|
||||
else
|
||||
self.navigation_menu.hide()
|
||||
|
||||
self.nav_scrollspy = new ScrollSpy({
|
||||
min: 10,
|
||||
onEnter: function(){
|
||||
self.navigation.addClass('float')
|
||||
},
|
||||
onLeave: function(){
|
||||
self.navigation.removeClass('float')
|
||||
}
|
||||
});
|
||||
self.navigation_menu.hide();
|
||||
|
||||
},
|
||||
|
||||
@@ -324,14 +380,14 @@ var MovieList = new Class({
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected()){
|
||||
$(movie).destroy()
|
||||
erase_movies.include(movie)
|
||||
erase_movies.include(movie);
|
||||
}
|
||||
});
|
||||
|
||||
erase_movies.each(function(movie){
|
||||
self.movies.erase(movie);
|
||||
|
||||
movie.destroy()
|
||||
movie.destroy();
|
||||
self.setCounter(self.counter_count-1);
|
||||
});
|
||||
|
||||
self.calculateSelected();
|
||||
@@ -429,12 +485,12 @@ var MovieList = new Class({
|
||||
.addClass(new_view+'_list')
|
||||
|
||||
self.current_view = new_view;
|
||||
Cookie.write(self.options.identifier+'_view', new_view, {duration: 1000});
|
||||
Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000});
|
||||
},
|
||||
|
||||
getSavedView: function(){
|
||||
var self = this;
|
||||
return Cookie.read(self.options.identifier+'_view') || 'details';
|
||||
return Cookie.read(self.options.identifier+'_view2');
|
||||
},
|
||||
|
||||
search: function(){
|
||||
@@ -450,8 +506,7 @@ var MovieList = new Class({
|
||||
self.activateLetter();
|
||||
self.filter.search = search_value;
|
||||
|
||||
self.movie_list.empty();
|
||||
self.getMovies();
|
||||
self.getMovies(true);
|
||||
|
||||
self.last_search_value = search_value;
|
||||
|
||||
@@ -463,11 +518,10 @@ var MovieList = new Class({
|
||||
var self = this;
|
||||
|
||||
self.reset();
|
||||
self.movie_list.empty();
|
||||
self.getMovies();
|
||||
self.getMovies(true);
|
||||
},
|
||||
|
||||
getMovies: function(){
|
||||
getMovies: function(reset){
|
||||
var self = this;
|
||||
|
||||
if(self.scrollspy){
|
||||
@@ -475,12 +529,42 @@ var MovieList = new Class({
|
||||
self.load_more.set('text', 'loading...');
|
||||
}
|
||||
|
||||
if(self.movies.length == 0 && self.options.loader){
|
||||
|
||||
self.loader_first = new Element('div.loading').adopt(
|
||||
new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
|
||||
).inject(self.el, 'top');
|
||||
|
||||
createSpinner(self.loader_first, {
|
||||
radius: 4,
|
||||
length: 4,
|
||||
width: 1
|
||||
});
|
||||
|
||||
self.el.setStyle('min-height', 93);
|
||||
|
||||
}
|
||||
|
||||
Api.request(self.options.api_call || 'movie.list', {
|
||||
'data': Object.merge({
|
||||
'status': self.options.status,
|
||||
'limit_offset': self.options.limit + ',' + self.offset
|
||||
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
|
||||
}, self.filter),
|
||||
'onComplete': function(json){
|
||||
'onSuccess': function(json){
|
||||
|
||||
if(reset)
|
||||
self.movie_list.empty();
|
||||
|
||||
if(self.loader_first){
|
||||
var lf = self.loader_first;
|
||||
self.loader_first.addClass('hide')
|
||||
self.loader_first = null;
|
||||
setTimeout(function(){
|
||||
lf.destroy();
|
||||
}, 20000);
|
||||
self.el.setStyle('min-height', null);
|
||||
}
|
||||
|
||||
self.store(json.movies);
|
||||
self.addMovies(json.movies, json.total);
|
||||
if(self.scrollspy) {
|
||||
@@ -488,7 +572,8 @@ var MovieList = new Class({
|
||||
self.scrollspy.start();
|
||||
}
|
||||
|
||||
self.checkIfEmpty()
|
||||
self.checkIfEmpty();
|
||||
self.fireEvent('loaded');
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -515,10 +600,10 @@ var MovieList = new Class({
|
||||
self.title[is_empty ? 'hide' : 'show']()
|
||||
|
||||
if(self.description)
|
||||
self.description[is_empty ? 'hide' : 'show']()
|
||||
self.description.setStyle('display', [is_empty ? 'none' : ''])
|
||||
|
||||
if(is_empty && self.options.on_empty_element){
|
||||
self.el.grab(self.options.on_empty_element);
|
||||
self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after');
|
||||
|
||||
if(self.navigation)
|
||||
self.navigation.hide();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var MovieAction = new Class({
|
||||
|
||||
class_name: 'action icon',
|
||||
class_name: 'action icon2',
|
||||
|
||||
initialize: function(movie){
|
||||
var self = this;
|
||||
@@ -82,48 +82,27 @@ MA.Release = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.releases.icon.download', {
|
||||
self.el = new Element('a.releases.download', {
|
||||
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
|
||||
'events': {
|
||||
'click': self.show.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
if(self.movie.data.releases.length == 0){
|
||||
if(self.movie.data.releases.length == 0)
|
||||
self.el.hide()
|
||||
}
|
||||
else {
|
||||
|
||||
var buttons_done = false;
|
||||
|
||||
self.movie.data.releases.sortBy('-info.score').each(function(release){
|
||||
if(buttons_done) return;
|
||||
|
||||
var status = Status.get(release.status_id);
|
||||
|
||||
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
|
||||
self.hide_on_click = false;
|
||||
self.show();
|
||||
buttons_done = true;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
else
|
||||
self.showHelper();
|
||||
|
||||
},
|
||||
|
||||
show: function(e){
|
||||
createReleases: function(){
|
||||
var self = this;
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
if(!self.options_container){
|
||||
self.options_container = new Element('div.options').adopt(
|
||||
self.release_container = new Element('div.releases.table').adopt(
|
||||
self.trynext_container = new Element('div.buttons.try_container')
|
||||
)
|
||||
).inject(self.movie, 'top');
|
||||
self.options_container = new Element('div.options').grab(
|
||||
self.release_container = new Element('div.releases.table')
|
||||
);
|
||||
|
||||
// Header
|
||||
new Element('div.item.head').adopt(
|
||||
@@ -168,11 +147,11 @@ MA.Release = new Class({
|
||||
new Element('span.age', {'text': self.get(release, 'age')}),
|
||||
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||
new Element('span.provider', { 'text': provider, 'title': provider }),
|
||||
release.info['detail_url'] ? new Element('a.info.icon', {
|
||||
release.info['detail_url'] ? new Element('a.info.icon2', {
|
||||
'href': release.info['detail_url'],
|
||||
'target': '_blank'
|
||||
}) : null,
|
||||
new Element('a.download.icon', {
|
||||
}) : new Element('a'),
|
||||
new Element('a.download.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
@@ -181,7 +160,7 @@ MA.Release = new Class({
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('a.delete.icon', {
|
||||
new Element('a.delete.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
@@ -209,7 +188,9 @@ MA.Release = new Class({
|
||||
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
|
||||
}
|
||||
|
||||
if(self.next_release || self.last_release){
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
|
||||
|
||||
self.trynext_container.adopt(
|
||||
new Element('span.or', {
|
||||
@@ -238,9 +219,72 @@ MA.Release = new Class({
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
show: function(e){
|
||||
var self = this;
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.createReleases();
|
||||
self.options_container.inject(self.movie, 'top');
|
||||
self.movie.slide('in', self.options_container);
|
||||
},
|
||||
|
||||
showHelper: function(e){
|
||||
var self = this;
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.createReleases();
|
||||
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
|
||||
|
||||
self.trynext_container.adopt(
|
||||
self.next_release ? [new Element('a.icon2.readd', {
|
||||
'text': self.last_release ? 'Download another release' : 'Download the best release',
|
||||
'events': {
|
||||
'click': self.tryNextRelease.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('a.icon2.download', {
|
||||
'text': 'pick one yourself',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.movie.quality.fireEvent('click');
|
||||
}
|
||||
}
|
||||
})] : null,
|
||||
new Element('a.icon2.completed', {
|
||||
'text': 'mark this movie done',
|
||||
'events': {
|
||||
'click': function(){
|
||||
Api.request('movie.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': 'wanted'
|
||||
},
|
||||
'onComplete': function(){
|
||||
var movie = $(self.movie);
|
||||
movie.set('tween', {
|
||||
'duration': 300,
|
||||
'onComplete': function(){
|
||||
self.movie.destroy()
|
||||
}
|
||||
});
|
||||
movie.tween('height', 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
get: function(release, type){
|
||||
return release.info[type] || 'n/a'
|
||||
},
|
||||
@@ -249,16 +293,17 @@ MA.Release = new Class({
|
||||
var self = this;
|
||||
|
||||
var release_el = self.release_container.getElement('#release_'+release.id),
|
||||
icon = release_el.getElement('.download.icon');
|
||||
icon = release_el.getElement('.download.icon2');
|
||||
|
||||
icon.addClass('spinner');
|
||||
self.movie.busy(true);
|
||||
|
||||
Api.request('release.download', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
icon.removeClass('spinner')
|
||||
self.movie.busy(false);
|
||||
|
||||
if(json.success)
|
||||
icon.addClass('completed');
|
||||
else
|
||||
@@ -281,6 +326,8 @@ MA.Release = new Class({
|
||||
tryNextRelease: function(movie_id){
|
||||
var self = this;
|
||||
|
||||
self.createReleases();
|
||||
|
||||
if(self.last_release)
|
||||
self.ignore(self.last_release);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ var Movie = new Class({
|
||||
self.view = options.view || 'details';
|
||||
self.list = list;
|
||||
|
||||
self.el = new Element('div.movie.inlay');
|
||||
self.el = new Element('div.movie');
|
||||
|
||||
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||
self.parent(self, options);
|
||||
@@ -22,7 +22,11 @@ var Movie = new Class({
|
||||
addEvents: function(){
|
||||
var self = this;
|
||||
|
||||
App.addEvent('movie.update.'+self.data.id, self.update.bind(self));
|
||||
App.addEvent('movie.update.'+self.data.id, function(notification){
|
||||
self.busy(false);
|
||||
self.removeView();
|
||||
self.update.delay(2000, self, notification);
|
||||
});
|
||||
|
||||
['movie.busy', 'searcher.started'].each(function(listener){
|
||||
App.addEvent(listener+'.'+self.data.id, function(notification){
|
||||
@@ -57,17 +61,19 @@ var Movie = new Class({
|
||||
var self = this;
|
||||
|
||||
if(!set_busy){
|
||||
if(self.spinner){
|
||||
self.mask.fade('out');
|
||||
setTimeout(function(){
|
||||
if(self.mask)
|
||||
self.mask.destroy();
|
||||
if(self.spinner)
|
||||
self.spinner.el.destroy();
|
||||
self.spinner = null;
|
||||
self.mask = null;
|
||||
}, 400);
|
||||
}
|
||||
setTimeout(function(){
|
||||
if(self.spinner){
|
||||
self.mask.fade('out');
|
||||
setTimeout(function(){
|
||||
if(self.mask)
|
||||
self.mask.destroy();
|
||||
if(self.spinner)
|
||||
self.spinner.el.destroy();
|
||||
self.spinner = null;
|
||||
self.mask = null;
|
||||
}, 400);
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
else if(!self.spinner) {
|
||||
self.createMask();
|
||||
@@ -102,6 +108,7 @@ var Movie = new Class({
|
||||
|
||||
self.data = notification.data;
|
||||
self.el.empty();
|
||||
self.removeView();
|
||||
|
||||
self.profile = Quality.getProfile(self.data.profile_id) || {};
|
||||
self.create();
|
||||
@@ -126,15 +133,14 @@ var Movie = new Class({
|
||||
self.thumbnail = File.Select.single('poster', self.data.library.files),
|
||||
self.data_container = new Element('div.data.inlay.light').adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
self.title = new Element('div.title', {
|
||||
'text': self.getTitle() || 'n/a'
|
||||
}),
|
||||
self.year = new Element('div.year', {
|
||||
'text': self.data.library.year || 'n/a'
|
||||
}),
|
||||
self.rating = new Element('div.rating.icon', {
|
||||
'text': self.data.library.rating
|
||||
}),
|
||||
new Element('div.title').adopt(
|
||||
self.title = new Element('span', {
|
||||
'text': self.getTitle() || 'n/a'
|
||||
}),
|
||||
self.year = new Element('div.year', {
|
||||
'text': self.data.library.year || 'n/a'
|
||||
})
|
||||
),
|
||||
self.description = new Element('div.description', {
|
||||
'text': self.data.library.plot
|
||||
}),
|
||||
@@ -142,8 +148,8 @@ var Movie = new Class({
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.el.getElement('.actions .releases');
|
||||
if(releases)
|
||||
releases.fireEvent('click', [e])
|
||||
if(releases.isVisible())
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -192,9 +198,6 @@ var Movie = new Class({
|
||||
self.actions.adopt(action)
|
||||
});
|
||||
|
||||
if(!self.data.library.rating)
|
||||
self.rating.hide();
|
||||
|
||||
},
|
||||
|
||||
addQuality: function(quality_id){
|
||||
@@ -237,10 +240,10 @@ var Movie = new Class({
|
||||
|
||||
if(direction == 'in'){
|
||||
self.temp_view = self.view;
|
||||
self.changeView('details')
|
||||
self.changeView('details');
|
||||
|
||||
self.el.addEvent('outerClick', function(){
|
||||
self.removeView()
|
||||
self.removeView();
|
||||
self.slide('out')
|
||||
})
|
||||
el.show();
|
||||
|
||||
@@ -1,113 +1,152 @@
|
||||
.search_form {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 25%;
|
||||
position: absolute;
|
||||
right: 105px;
|
||||
top: 0;
|
||||
text-align: right;
|
||||
height: 100%;
|
||||
border-bottom: 4px solid transparent;
|
||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
border: 1px solid transparent;
|
||||
border-width: 0 0 4px;
|
||||
}
|
||||
|
||||
.search_form input {
|
||||
padding: 4px 20px 4px 4px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
.search_form:hover {
|
||||
border-color: #047792;
|
||||
}
|
||||
.search_form input:focus {
|
||||
padding-right: 83px;
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.search_form {
|
||||
right: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.search_form.focused,
|
||||
.search_form.shown {
|
||||
border-color: #04bce6;
|
||||
}
|
||||
|
||||
.search_form .input {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 45px;
|
||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||
}
|
||||
|
||||
.search_form.focused .input,
|
||||
.search_form.shown .input {
|
||||
width: 380px;
|
||||
background: #4e5969;
|
||||
}
|
||||
|
||||
.search_form .input .enter {
|
||||
background: #369545 url('../images/sprite.png') right -188px no-repeat;
|
||||
padding: 0 20px 0 4px;
|
||||
border-radius: 2px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
margin-left: -78px;
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
visibility: hidden;
|
||||
}
|
||||
.search_form.focused .input .enter {
|
||||
visibility: visible;
|
||||
.search_form .input input {
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: #FFF;
|
||||
font-size: 25px;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
padding: 0 40px 0 10px;
|
||||
transition: all .4s ease-in-out .2s;
|
||||
}
|
||||
.search_form.focused.filled .input .enter {
|
||||
opacity: 1;
|
||||
.search_form.focused .input input,
|
||||
.search_form.shown .input input {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.search_form .input input {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.search_form.focused .input,
|
||||
.search_form.shown .input {
|
||||
width: 277px;
|
||||
}
|
||||
}
|
||||
|
||||
.search_form .input a {
|
||||
width: 17px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
margin: -2px 0 0 2px;
|
||||
top: 4px;
|
||||
right: 5px;
|
||||
background: url('../images/sprite.png') left -37px no-repeat;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
vertical-align: middle;
|
||||
|
||||
.search_form .input a {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 44px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
line-height: 66px;
|
||||
font-size: 15px;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.search_form .input a:after {
|
||||
content: "\e03e";
|
||||
}
|
||||
|
||||
.search_form.filled .input a {
|
||||
opacity: 1;
|
||||
.search_form.shown.filled .input a:after {
|
||||
content: "\e04e";
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.search_form .input a {
|
||||
line-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.search_form .results_container {
|
||||
text-align: left;
|
||||
position: absolute;
|
||||
background: #5c697b;
|
||||
margin: 6px 0 0 -230px;
|
||||
margin: 4px 0 0;
|
||||
width: 470px;
|
||||
min-height: 140px;
|
||||
border-radius: 3px;
|
||||
min-height: 50px;
|
||||
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
|
||||
display: none;
|
||||
}
|
||||
@media all and (max-width: 480px) {
|
||||
.search_form .results_container {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
.search_form.focused.filled .results_container,
|
||||
.search_form.shown.filled .results_container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search_form .results_container:before {
|
||||
content: ' ';
|
||||
height: 0;
|
||||
position: relative;
|
||||
width: 0;
|
||||
border: 10px solid transparent;
|
||||
border-bottom-color: #5c697b;
|
||||
display: block;
|
||||
top: -20px;
|
||||
left: 346px;
|
||||
}
|
||||
|
||||
.search_form .results {
|
||||
max-height: 570px;
|
||||
overflow-x: hidden;
|
||||
padding: 10px 0;
|
||||
margin-top: -18px;
|
||||
}
|
||||
|
||||
.movie_result {
|
||||
overflow: hidden;
|
||||
height: 140px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.movie_result .options {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
left: 30px;
|
||||
right: 0;
|
||||
padding: 13px;
|
||||
border: 1px solid transparent;
|
||||
border-width: 1px 0;
|
||||
border-radius: 0;
|
||||
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
|
||||
}
|
||||
.movie_result .options > .in_library_wanted {
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
.movie_result .options > div {
|
||||
padding: 0 15px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@@ -122,6 +161,13 @@
|
||||
}
|
||||
.movie_result .options select[name=title] { width: 180px; }
|
||||
.movie_result .options select[name=profile] { width: 90px; }
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
|
||||
.movie_result .options select[name=title] { width: 90px; }
|
||||
.movie_result .options select[name=profile] { width: 60px; }
|
||||
|
||||
}
|
||||
|
||||
.movie_result .options .button {
|
||||
vertical-align: middle;
|
||||
@@ -130,25 +176,21 @@
|
||||
|
||||
.movie_result .options .message {
|
||||
height: 100%;
|
||||
line-height: 140px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.movie_result .data {
|
||||
padding: 0 15px;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
left: 30px;
|
||||
right: 0;
|
||||
background: #5c697b;
|
||||
cursor: pointer;
|
||||
|
||||
border-bottom: 1px solid #333;
|
||||
border-top: 1px solid rgba(255,255,255, 0.15);
|
||||
transition: all .6s cubic-bezier(0.9,0,0.1,1);
|
||||
border-top: 1px solid rgba(255,255,255, 0.08);
|
||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||
}
|
||||
.movie_result .data.open {
|
||||
left: 100%;
|
||||
@@ -158,53 +200,46 @@
|
||||
|
||||
.movie_result .in_wanted, .movie_result .in_library {
|
||||
position: absolute;
|
||||
margin-top: 105px;
|
||||
bottom: 2px;
|
||||
left: 14px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.movie_result .thumbnail {
|
||||
width: 17%;
|
||||
display: inline-block;
|
||||
margin: 15px 3% 15px 0;
|
||||
width: 34px;
|
||||
min-height: 100%;
|
||||
display: block;
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 3px rgba(0,0,0,0.35);
|
||||
}
|
||||
|
||||
.movie_result .info {
|
||||
width: 80%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding: 15px 0;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.movie_result .info .tagline {
|
||||
max-height: 70px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.movie_result .add +.info {
|
||||
margin-left: 20%;
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
left: 15px;
|
||||
right: 60px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.movie_result .info h2 {
|
||||
font-weight: normal;
|
||||
font-size: 20px;
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
line-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.movie_result .info h2 span {
|
||||
padding: 0 5px;
|
||||
position: absolute;
|
||||
right: -60px;
|
||||
}
|
||||
|
||||
.movie_result .info h2 span:before { content: "("; }
|
||||
.movie_result .info h2 span:after { content: ")"; }
|
||||
|
||||
.search_form .mask,
|
||||
.movie_result .mask {
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@@ -7,33 +7,30 @@ Block.Search = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var focus_timer = 0;
|
||||
self.el = new Element('div.search_form').adopt(
|
||||
new Element('div.input').adopt(
|
||||
self.input = new Element('input.inlay', {
|
||||
self.input = new Element('input', {
|
||||
'placeholder': 'Search & add a new movie',
|
||||
'events': {
|
||||
'keyup': self.keyup.bind(self),
|
||||
'focus': function(){
|
||||
if(focus_timer) clearTimeout(focus_timer);
|
||||
self.el.addClass('focused')
|
||||
if(this.get('value'))
|
||||
self.hideResults(false)
|
||||
},
|
||||
'blur': function(){
|
||||
(function(){
|
||||
focus_timer = (function(){
|
||||
self.el.removeClass('focused')
|
||||
}).delay(2000);
|
||||
}).delay(100);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('span.enter', {
|
||||
new Element('a.icon2', {
|
||||
'events': {
|
||||
'click': self.keyup.bind(self)
|
||||
},
|
||||
'text':'Enter'
|
||||
}),
|
||||
new Element('a', {
|
||||
'events': {
|
||||
'click': self.clear.bind(self)
|
||||
'click': self.clear.bind(self),
|
||||
'touchend': self.clear.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
@@ -59,13 +56,21 @@ Block.Search = new Class({
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
self.last_q = '';
|
||||
self.input.set('value', '');
|
||||
self.input.focus()
|
||||
if(self.last_q === ''){
|
||||
self.input.blur()
|
||||
self.last_q = null;
|
||||
}
|
||||
else {
|
||||
|
||||
self.movies = []
|
||||
self.results.empty()
|
||||
self.el.removeClass('filled')
|
||||
self.last_q = '';
|
||||
self.input.set('value', '');
|
||||
self.input.focus()
|
||||
|
||||
self.movies = []
|
||||
self.results.empty()
|
||||
self.el.removeClass('filled')
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
hideResults: function(bool){
|
||||
@@ -92,8 +97,13 @@ Block.Search = new Class({
|
||||
|
||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
||||
|
||||
if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click'))
|
||||
self.autocomplete()
|
||||
if(self.q() != self.last_q){
|
||||
if(self.api_request && self.api_request.isRunning())
|
||||
self.api_request.cancel();
|
||||
|
||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
||||
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -109,12 +119,9 @@ Block.Search = new Class({
|
||||
},
|
||||
|
||||
list: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.api_request && self.api_request.running) return
|
||||
|
||||
var q = self.q();
|
||||
var cache = self.cache[q];
|
||||
var self = this,
|
||||
q = self.q(),
|
||||
cache = self.cache[q];
|
||||
|
||||
self.hideResults(false);
|
||||
|
||||
@@ -157,9 +164,6 @@ Block.Search = new Class({
|
||||
|
||||
});
|
||||
|
||||
if(q != self.q())
|
||||
self.list()
|
||||
|
||||
// Calculate result heights
|
||||
var w = window.getSize(),
|
||||
rc = self.result_container.getCoordinates();
|
||||
@@ -197,6 +201,11 @@ Block.Search.Item = new Class({
|
||||
self.el = new Element('div.movie_result', {
|
||||
'id': info.imdb
|
||||
}).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', {
|
||||
'tween': {
|
||||
@@ -207,11 +216,6 @@ Block.Search.Item = new Class({
|
||||
'click': self.showOptions.bind(self)
|
||||
}
|
||||
}).adopt(
|
||||
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||
'src': info.images.poster[0],
|
||||
'height': null,
|
||||
'width': null
|
||||
}) : null,
|
||||
new Element('div.info').adopt(
|
||||
self.title = new Element('h2', {
|
||||
'text': info.titles[0]
|
||||
@@ -219,28 +223,11 @@ Block.Search.Item = new Class({
|
||||
self.year = info.year ? new Element('span.year', {
|
||||
'text': info.year
|
||||
}) : null
|
||||
),
|
||||
self.tagline = new Element('span.tagline', {
|
||||
'text': info.tagline ? info.tagline : info.plot,
|
||||
'title': info.tagline ? info.tagline : info.plot
|
||||
}),
|
||||
self.director = self.info.director ? new Element('span.director', {
|
||||
'text': 'Director:' + info.director
|
||||
}) : null,
|
||||
self.starring = info.actors ? new Element('span.actors', {
|
||||
'text': 'Starring:'
|
||||
}) : null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if(info.actors){
|
||||
Object.each(info.actors, function(actor){
|
||||
new Element('span', {
|
||||
'text': actor
|
||||
}).inject(self.starring)
|
||||
})
|
||||
}
|
||||
|
||||
info.titles.each(function(title){
|
||||
self.alternativeTitle({
|
||||
@@ -319,12 +306,9 @@ Block.Search.Item = new Class({
|
||||
}
|
||||
|
||||
self.options_el.grab(
|
||||
new Element('div').adopt(
|
||||
self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', {
|
||||
'src': info.images.poster[0],
|
||||
'height': null,
|
||||
'width': null
|
||||
}) : null,
|
||||
new Element('div', {
|
||||
'class': self.info.in_wanted && self.info.in_wanted.profile || in_library ? 'in_library_wanted' : ''
|
||||
}).adopt(
|
||||
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
|
||||
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
|
||||
}) : (in_library ? new Element('span.in_library', {
|
||||
|
||||
@@ -5,7 +5,7 @@ from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParams, getParam
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Profile, ProfileType
|
||||
from couchpotato.core.settings.model import Profile, ProfileType, Movie
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -30,6 +30,21 @@ class ProfilePlugin(Plugin):
|
||||
})
|
||||
|
||||
addEvent('app.initialize', self.fill, priority = 90)
|
||||
addEvent('app.load', self.forceDefaults)
|
||||
|
||||
def forceDefaults(self):
|
||||
|
||||
# Get all active movies without profile
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
|
||||
db = get_session()
|
||||
movies = db.query(Movie).filter(Movie.status_id == active_status.get('id'), Movie.profile == None).all()
|
||||
|
||||
if len(movies) > 0:
|
||||
default_profile = self.default()
|
||||
for movie in movies:
|
||||
movie.profile_id = default_profile.get('id')
|
||||
db.commit()
|
||||
|
||||
def allView(self):
|
||||
|
||||
@@ -47,6 +62,7 @@ class ProfilePlugin(Plugin):
|
||||
for profile in profiles:
|
||||
temp.append(profile.to_dict(self.to_dict))
|
||||
|
||||
db.expire_all()
|
||||
return temp
|
||||
|
||||
def save(self):
|
||||
@@ -94,6 +110,7 @@ class ProfilePlugin(Plugin):
|
||||
default = db.query(Profile).first()
|
||||
default_dict = default.to_dict(self.to_dict)
|
||||
|
||||
db.expire_all()
|
||||
return default_dict
|
||||
|
||||
def saveOrder(self):
|
||||
@@ -129,10 +146,14 @@ class ProfilePlugin(Plugin):
|
||||
db.delete(p)
|
||||
db.commit()
|
||||
|
||||
# Force defaults on all empty profile movies
|
||||
self.forceDefaults()
|
||||
|
||||
success = True
|
||||
except Exception, e:
|
||||
message = log.error('Failed deleting Profile: %s', e)
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
'success': success,
|
||||
'message': message
|
||||
|
||||
@@ -6,19 +6,30 @@
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.profile { border-bottom: 1px solid rgba(255,255,255,0.2) }
|
||||
.profile {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.profile > .delete {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: absolute;
|
||||
margin-left: 690px;
|
||||
padding: 14px;
|
||||
background-position: center;
|
||||
padding: 16px;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
color: #fd5353;
|
||||
}
|
||||
.profile > .delete:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.profile .ctrlHolder:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.profile .qualities {
|
||||
min-height: 80px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.profile .formHint {
|
||||
@@ -34,7 +45,8 @@
|
||||
|
||||
.profile .wait_for {
|
||||
position: absolute;
|
||||
margin: -45px 0 0 437px;
|
||||
right: 60px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.profile .wait_for input {
|
||||
@@ -61,6 +73,10 @@
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.profile .type .check {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.profile .quality_type select {
|
||||
width: 186px;
|
||||
margin-left: -1px;
|
||||
@@ -71,22 +87,24 @@
|
||||
}
|
||||
|
||||
.profile .types .type .handle {
|
||||
background: url('./handle.png') center;
|
||||
background: url('../../static/profile_plugin/handle.png') center;
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile .types .type .delete {
|
||||
background-position: left center;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
line-height: 20px;
|
||||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #fd5353;
|
||||
}
|
||||
|
||||
.profile .types .type:hover:not(.is_empty) .delete {
|
||||
@@ -105,9 +123,9 @@
|
||||
}
|
||||
|
||||
#profile_ordering li {
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
padding: 0 5px;
|
||||
}
|
||||
@@ -126,7 +144,7 @@
|
||||
}
|
||||
|
||||
#profile_ordering li .handle {
|
||||
background: url('./handle.png') center;
|
||||
background: url('../../static/profile_plugin/handle.png') center;
|
||||
width: 20px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ var Profile = new Class({
|
||||
var data = self.data;
|
||||
|
||||
self.el = new Element('div.profile').adopt(
|
||||
self.delete_button = new Element('span.delete.icon', {
|
||||
self.delete_button = new Element('span.delete.icon2', {
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
@@ -256,7 +256,7 @@ Profile.Type = new Class({
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('span.delete.icon', {
|
||||
new Element('span.delete.icon2', {
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Quality, Profile, ProfileType
|
||||
from sqlalchemy.sql.expression import or_
|
||||
import os.path
|
||||
import re
|
||||
import time
|
||||
|
||||
@@ -19,13 +18,13 @@ class QualityPlugin(Plugin):
|
||||
|
||||
qualities = [
|
||||
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
|
||||
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
||||
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
||||
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
||||
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
|
||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'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']}
|
||||
@@ -164,7 +163,6 @@ class QualityPlugin(Plugin):
|
||||
if cached and extra is {}: return cached
|
||||
|
||||
for cur_file in files:
|
||||
size = (os.path.getsize(cur_file) / 1024 / 1024) if os.path.isfile(cur_file) else 0
|
||||
words = re.split('\W+', cur_file.lower())
|
||||
|
||||
for quality in self.all():
|
||||
@@ -188,29 +186,30 @@ class QualityPlugin(Plugin):
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
# Try again with loose testing
|
||||
quality = self.guessLoose(hash, extra = extra)
|
||||
quality = self.guessLoose(hash, files = files, extra = extra)
|
||||
if quality:
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
log.debug('Could not identify quality for: %s', files)
|
||||
return None
|
||||
|
||||
def guessLoose(self, hash, extra):
|
||||
def guessLoose(self, hash, files = None, extra = None):
|
||||
|
||||
for quality in self.all():
|
||||
if extra:
|
||||
for quality in self.all():
|
||||
|
||||
# Check width resolution, range 20
|
||||
if (quality.get('width', 720) - 20) <= extra.get('resolution_width', 0) <= (quality.get('width', 720) + 20):
|
||||
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 720), extra.get('resolution_width', 0)))
|
||||
return self.setCache(hash, quality)
|
||||
# Check width resolution, range 20
|
||||
if quality.get('width') and (quality.get('width') - 20) <= extra.get('resolution_width', 0) <= (quality.get('width') + 20):
|
||||
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width'), extra.get('resolution_width', 0)))
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
# Check height resolution, range 20
|
||||
if (quality.get('height', 480) - 20) <= extra.get('resolution_height', 0) <= (quality.get('height', 480) + 20):
|
||||
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height', 480), extra.get('resolution_height', 0)))
|
||||
return self.setCache(hash, quality)
|
||||
# Check height resolution, range 20
|
||||
if quality.get('height') and (quality.get('height') - 20) <= extra.get('resolution_height', 0) <= (quality.get('height') + 20):
|
||||
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height'), extra.get('resolution_height', 0)))
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
if 480 <= extra.get('resolution_width', 0) <= 720:
|
||||
log.debug('Found as dvdrip')
|
||||
return self.setCache(hash, self.single('dvdrip'))
|
||||
if 480 <= extra.get('resolution_width', 0) <= 720:
|
||||
log.debug('Found as dvdrip')
|
||||
return self.setCache(hash, self.single('dvdrip'))
|
||||
|
||||
return None
|
||||
|
||||
@@ -41,12 +41,15 @@ class Release(Plugin):
|
||||
addEvent('release.clean', self.clean)
|
||||
|
||||
def add(self, group):
|
||||
|
||||
db = get_session()
|
||||
|
||||
identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier'])
|
||||
|
||||
|
||||
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
|
||||
|
||||
# Add movie
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first()
|
||||
if not movie:
|
||||
movie = Movie(
|
||||
@@ -58,7 +61,6 @@ class Release(Plugin):
|
||||
db.commit()
|
||||
|
||||
# Add Release
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
rel = db.query(Relea).filter(
|
||||
or_(
|
||||
Relea.identifier == identifier,
|
||||
@@ -76,15 +78,19 @@ class Release(Plugin):
|
||||
db.commit()
|
||||
|
||||
# Add each file type
|
||||
added_files = []
|
||||
for type in group['files']:
|
||||
for cur_file in group['files'][type]:
|
||||
added_file = self.saveFile(cur_file, type = type, include_media_info = type is 'movie')
|
||||
try:
|
||||
added_file = db.query(File).filter_by(id = added_file.get('id')).one()
|
||||
rel.files.append(added_file)
|
||||
db.commit()
|
||||
except Exception, e:
|
||||
log.debug('Failed to attach "%s" to release: %s', (cur_file, e))
|
||||
added_files.append(added_file.get('id'))
|
||||
|
||||
# Add the release files in batch
|
||||
try:
|
||||
added_files = db.query(File).filter(or_(*[File.id == x for x in added_files])).all()
|
||||
rel.files.extend(added_files)
|
||||
db.commit()
|
||||
except Exception, e:
|
||||
log.debug('Failed to attach "%s" to release: %s', (cur_file, e))
|
||||
|
||||
fireEvent('movie.restatus', movie.id)
|
||||
|
||||
@@ -147,8 +153,7 @@ class Release(Plugin):
|
||||
|
||||
rel = db.query(Relea).filter_by(id = id).first()
|
||||
if rel:
|
||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True)
|
||||
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
@@ -160,7 +165,8 @@ class Release(Plugin):
|
||||
|
||||
db = get_session()
|
||||
id = getParam('id')
|
||||
status_snatched = fireEvent('status.add', 'snatched', single = True)
|
||||
|
||||
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
|
||||
|
||||
rel = db.query(Relea).filter_by(id = id).first()
|
||||
if rel:
|
||||
@@ -168,11 +174,13 @@ class Release(Plugin):
|
||||
for info in rel.info:
|
||||
item[info.identifier] = info.value
|
||||
|
||||
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Snatching "%s"' % item['name'])
|
||||
|
||||
# Get matching provider
|
||||
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
|
||||
|
||||
if item['type'] != 'torrent_magnet':
|
||||
item['download'] = provider.download
|
||||
item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
|
||||
|
||||
success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
|
||||
'profile': {'types': {'quality': {}}},
|
||||
@@ -182,8 +190,14 @@ class Release(Plugin):
|
||||
}), manual = True, single = True)
|
||||
|
||||
if success:
|
||||
rel.status_id = status_snatched.get('id')
|
||||
db.commit()
|
||||
db.expunge_all()
|
||||
rel = db.query(Relea).filter_by(id = id).first() # Get release again
|
||||
|
||||
if rel.status_id != done_status.get('id'):
|
||||
rel.status_id = snatched_status.get('id')
|
||||
db.commit()
|
||||
|
||||
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
|
||||
|
||||
return jsonified({
|
||||
'success': success
|
||||
|
||||
@@ -13,7 +13,7 @@ rename_options = {
|
||||
'thename': 'The Moviename',
|
||||
'year': 'Year (2011)',
|
||||
'first': 'First letter (M)',
|
||||
'quality': 'Quality (720P)',
|
||||
'quality': 'Quality (720p)',
|
||||
'video': 'Video (x264)',
|
||||
'audio': 'Audio (DTS)',
|
||||
'group': 'Releasegroup name',
|
||||
@@ -112,6 +112,15 @@ config = [{
|
||||
'label': 'Separator',
|
||||
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
|
||||
},
|
||||
{
|
||||
'name': 'file_action',
|
||||
'label': 'Torrent File Action',
|
||||
'default': 'move',
|
||||
'type': 'dropdown',
|
||||
'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink'), ('Move & Sym link', 'move_symlink')],
|
||||
'description': 'Define which kind of file operation you want to use for torrents. Before you start using <a href="http://en.wikipedia.org/wiki/Hard_link">hard links</a> or <a href="http://en.wikipedia.org/wiki/Sym_link">sym links</a>, PLEASE read about their possible drawbacks.',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
'name': 'ntfs_permission',
|
||||
|
||||
@@ -2,12 +2,13 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||
getImdb
|
||||
getImdb, link, symlink, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, File, Profile, Release
|
||||
from couchpotato.core.settings.model import Library, File, Profile, Release, \
|
||||
ReleaseInfo
|
||||
from couchpotato.environment import Env
|
||||
import errno
|
||||
import os
|
||||
@@ -18,16 +19,20 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Renamer(Plugin):
|
||||
|
||||
renaming_started = False
|
||||
checking_snatched = False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('renamer.scan', self.scanView, docs = {
|
||||
'desc': 'For the renamer to check for new files to rename',
|
||||
'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.'},
|
||||
'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'},
|
||||
'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'},
|
||||
'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'},
|
||||
},
|
||||
})
|
||||
|
||||
addEvent('renamer.scan', self.scan)
|
||||
@@ -35,22 +40,45 @@ class Renamer(Plugin):
|
||||
|
||||
addEvent('app.load', self.scan)
|
||||
addEvent('app.load', self.checkSnatched)
|
||||
addEvent('app.load', self.setCrons)
|
||||
|
||||
if self.conf('run_every') > 0:
|
||||
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
|
||||
# Enable / disable interval
|
||||
addEvent('setting.save.renamer.enabled.after', self.setCrons)
|
||||
addEvent('setting.save.renamer.run_every.after', self.setCrons)
|
||||
addEvent('setting.save.renamer.force_every.after', self.setCrons)
|
||||
|
||||
if self.conf('force_every') > 0:
|
||||
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'))
|
||||
def setCrons(self):
|
||||
|
||||
fireEvent('schedule.remove', 'renamer.check_snatched')
|
||||
if self.isEnabled() and self.conf('run_every') > 0:
|
||||
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'), single = True)
|
||||
|
||||
fireEvent('schedule.remove', 'renamer.check_snatched_forced')
|
||||
if self.isEnabled() and self.conf('force_every') > 0:
|
||||
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'), single = True)
|
||||
|
||||
return True
|
||||
|
||||
def scanView(self):
|
||||
|
||||
fireEventAsync('renamer.scan')
|
||||
params = getParams()
|
||||
async = tryInt(params.get('async', None))
|
||||
movie_folder = params.get('movie_folder', None)
|
||||
downloader = params.get('downloader', None)
|
||||
download_id = params.get('download_id', None)
|
||||
|
||||
fire_handle = fireEvent if not async else fireEventAsync
|
||||
|
||||
fire_handle('renamer.scan',
|
||||
movie_folder = movie_folder,
|
||||
download_info = {'id': download_id, 'downloader': downloader} if download_id else None
|
||||
)
|
||||
|
||||
return jsonified({
|
||||
'success': True
|
||||
})
|
||||
|
||||
def scan(self):
|
||||
def scan(self, movie_folder = None, download_info = None):
|
||||
|
||||
if self.isDisabled():
|
||||
return
|
||||
@@ -60,17 +88,42 @@ class Renamer(Plugin):
|
||||
return
|
||||
|
||||
# Check to see if the "to" folder is inside the "from" folder.
|
||||
if not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')):
|
||||
log.debug('"To" and "From" have to exist.')
|
||||
if movie_folder and not os.path.isdir(movie_folder) or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')):
|
||||
l = log.debug if movie_folder else log.error
|
||||
l('Both the "To" and "From" have to exist.')
|
||||
return
|
||||
elif self.conf('from') in self.conf('to'):
|
||||
log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.')
|
||||
return
|
||||
|
||||
groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True)
|
||||
elif (movie_folder and movie_folder in [self.conf('to'), self.conf('from')]):
|
||||
log.error('The "to" and "from" folders can\'t be inside of or the same as the provided movie folder.')
|
||||
return
|
||||
|
||||
self.renaming_started = True
|
||||
|
||||
# make sure the movie folder name is included in the search
|
||||
folder = None
|
||||
files = []
|
||||
if movie_folder:
|
||||
log.info('Scanning movie folder %s...', movie_folder)
|
||||
movie_folder = movie_folder.rstrip(os.path.sep)
|
||||
folder = os.path.dirname(movie_folder)
|
||||
|
||||
# Get all files from the specified folder
|
||||
try:
|
||||
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', (movie_folder, traceback.format_exc()))
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Extend the download info with info stored in the downloaded release
|
||||
download_info = self.extendDownloadInfo(download_info)
|
||||
|
||||
groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'),
|
||||
files = files, download_info = download_info, return_ignored = False, single = True)
|
||||
|
||||
destination = self.conf('to')
|
||||
folder_name = self.conf('folder_name')
|
||||
file_name = self.conf('file_name')
|
||||
@@ -79,12 +132,8 @@ class Renamer(Plugin):
|
||||
separator = self.conf('separator')
|
||||
|
||||
# Statusses
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
downloaded_status = fireEvent('status.get', 'downloaded', single = True)
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
|
||||
db = get_session()
|
||||
done_status, active_status, downloaded_status, snatched_status = \
|
||||
fireEvent('status.get', ['done', 'active', 'downloaded', 'snatched'], single = True)
|
||||
|
||||
for group_identifier in groups:
|
||||
|
||||
@@ -158,6 +207,7 @@ class Renamer(Plugin):
|
||||
cd = 1 if multiple else 0
|
||||
|
||||
for current_file in sorted(list(group['files'][file_type])):
|
||||
current_file = toUnicode(current_file)
|
||||
|
||||
# Original filename
|
||||
replacements['original'] = os.path.splitext(os.path.basename(current_file))[0]
|
||||
@@ -304,7 +354,7 @@ class Renamer(Plugin):
|
||||
else:
|
||||
log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label))
|
||||
|
||||
# Add _EXISTS_ to the parent dir
|
||||
# Add exists tag to the .ignore file
|
||||
self.tagDir(group, 'exists')
|
||||
|
||||
# Notify on rename fail
|
||||
@@ -325,7 +375,8 @@ class Renamer(Plugin):
|
||||
db.commit()
|
||||
|
||||
# Remove leftover files
|
||||
if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers:
|
||||
if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers and \
|
||||
not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)):
|
||||
log.debug('Removing leftover files')
|
||||
for current_file in group['files']['leftover']:
|
||||
remove_files.append(current_file)
|
||||
@@ -350,7 +401,7 @@ class Renamer(Plugin):
|
||||
os.remove(src)
|
||||
|
||||
parent_dir = os.path.normpath(os.path.dirname(src))
|
||||
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and destination != parent_dir:
|
||||
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not parent_dir in [destination, movie_folder] and not self.conf('from') in parent_dir:
|
||||
delete_folders.append(parent_dir)
|
||||
|
||||
except:
|
||||
@@ -375,12 +426,15 @@ class Renamer(Plugin):
|
||||
self.makeDir(os.path.dirname(dst))
|
||||
|
||||
try:
|
||||
self.moveFile(src, dst)
|
||||
self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(download_info))
|
||||
group['renamed_files'].append(dst)
|
||||
except:
|
||||
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
|
||||
self.tagDir(group, 'failed_rename')
|
||||
|
||||
if self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info):
|
||||
self.tagDir(group, 'renamed already')
|
||||
|
||||
# Remove matching releases
|
||||
for release in remove_releases:
|
||||
log.debug('Removing release %s', release.identifier)
|
||||
@@ -426,36 +480,40 @@ class Renamer(Plugin):
|
||||
|
||||
return rename_files
|
||||
|
||||
# This adds a file to ignore / tag a release so it is ignored later
|
||||
def tagDir(self, group, tag):
|
||||
|
||||
rename_files = {}
|
||||
ignore_file = None
|
||||
for movie_file in sorted(list(group['files']['movie'])):
|
||||
ignore_file = '%s.ignore' % os.path.splitext(movie_file)[0]
|
||||
break
|
||||
|
||||
if group['dirname']:
|
||||
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_%s_%s' % (tag.upper(), group['dirname']))
|
||||
else: # Add it to filename
|
||||
for file_type in group['files']:
|
||||
for rename_me in group['files'][file_type]:
|
||||
filename = os.path.basename(rename_me)
|
||||
rename_files[rename_me] = rename_me.replace(filename, '_%s_%s' % (tag.upper(), filename))
|
||||
text = """This file is from CouchPotato
|
||||
It has marked this release as "%s"
|
||||
This file hides the release from the renamer
|
||||
Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
""" % tag
|
||||
|
||||
for src in rename_files:
|
||||
if rename_files[src]:
|
||||
dst = rename_files[src]
|
||||
log.info('Renaming "%s" to "%s"', (src, dst))
|
||||
if ignore_file:
|
||||
self.createFile(ignore_file, text)
|
||||
|
||||
# Create dir
|
||||
self.makeDir(os.path.dirname(dst))
|
||||
|
||||
try:
|
||||
self.moveFile(src, dst)
|
||||
except:
|
||||
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
|
||||
raise
|
||||
|
||||
def moveFile(self, old, dest):
|
||||
def moveFile(self, old, dest, forcemove = False):
|
||||
dest = ss(dest)
|
||||
try:
|
||||
shutil.move(old, dest)
|
||||
if forcemove:
|
||||
shutil.move(old, dest)
|
||||
elif self.conf('file_action') == 'hardlink':
|
||||
link(old, dest)
|
||||
elif self.conf('file_action') == 'symlink':
|
||||
symlink(old, dest)
|
||||
elif self.conf('file_action') == 'copy':
|
||||
shutil.copy(old, dest)
|
||||
elif self.conf('file_action') == 'move_symlink':
|
||||
shutil.move(old, dest)
|
||||
symlink(dest, old)
|
||||
else:
|
||||
shutil.move(old, dest)
|
||||
|
||||
try:
|
||||
os.chmod(dest, Env.getPermission('file'))
|
||||
@@ -524,16 +582,14 @@ class Renamer(Plugin):
|
||||
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
|
||||
|
||||
def checkSnatched(self):
|
||||
|
||||
if self.checking_snatched:
|
||||
log.debug('Already checking snatched')
|
||||
|
||||
self.checking_snatched = True
|
||||
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
||||
failed_status = fireEvent('status.get', 'failed', single = True)
|
||||
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
snatched_status, ignored_status, failed_status, done_status = \
|
||||
fireEvent('status.get', ['snatched', 'ignored', 'failed', 'done'], single = True)
|
||||
|
||||
db = get_session()
|
||||
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
|
||||
@@ -571,8 +627,16 @@ class Renamer(Plugin):
|
||||
|
||||
found = False
|
||||
for item in statuses:
|
||||
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
|
||||
found_release = False
|
||||
if rel_dict['info'].get('download_id'):
|
||||
if item['id'] == rel_dict['info']['download_id'] and item['downloader'] == rel_dict['info']['download_downloader']:
|
||||
log.debug('Found release by id: %s', item['id'])
|
||||
found_release = True
|
||||
else:
|
||||
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
|
||||
found_release = True
|
||||
|
||||
if found_release:
|
||||
timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
|
||||
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
|
||||
|
||||
@@ -580,16 +644,18 @@ class Renamer(Plugin):
|
||||
pass
|
||||
elif item['status'] == 'failed':
|
||||
fireEvent('download.remove_failed', item, single = True)
|
||||
rel.status_id = failed_status.get('id')
|
||||
rel.last_edit = int(time.time())
|
||||
db.commit()
|
||||
|
||||
if self.conf('next_on_failed'):
|
||||
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
|
||||
else:
|
||||
rel.status_id = failed_status.get('id')
|
||||
rel.last_edit = int(time.time())
|
||||
db.commit()
|
||||
elif item['status'] == 'completed':
|
||||
log.info('Download of %s completed!', item['name'])
|
||||
scan_required = True
|
||||
if item['id'] and item['downloader'] and item['folder']:
|
||||
fireEventAsync('renamer.scan', movie_folder = item['folder'], download_info = item)
|
||||
else:
|
||||
scan_required = True
|
||||
|
||||
found = True
|
||||
break
|
||||
@@ -606,3 +672,38 @@ class Renamer(Plugin):
|
||||
self.checking_snatched = False
|
||||
|
||||
return True
|
||||
|
||||
def extendDownloadInfo(self, download_info):
|
||||
|
||||
rls = None
|
||||
|
||||
if download_info and download_info.get('id') and download_info.get('downloader'):
|
||||
|
||||
db = get_session()
|
||||
|
||||
rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = download_info.get('downloader')).all()
|
||||
rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_info.get('id')).all()
|
||||
|
||||
for rlsnfo_dwnld in rlsnfo_dwnlds:
|
||||
for rlsnfo_id in rlsnfo_ids:
|
||||
if rlsnfo_id.release == rlsnfo_dwnld.release:
|
||||
rls = rlsnfo_id.release
|
||||
break
|
||||
if rls: break
|
||||
|
||||
if not rls:
|
||||
log.error('Download ID %s from downloader %s not found in releases', (download_info.get('id'), download_info.get('downloader')))
|
||||
|
||||
if rls:
|
||||
|
||||
rls_dict = rls.to_dict({'info':{}})
|
||||
download_info.update({
|
||||
'imdb_id': rls.movie.library.identifier,
|
||||
'quality': rls.quality.identifier,
|
||||
'type': rls_dict.get('info', {}).get('type')
|
||||
})
|
||||
|
||||
return download_info
|
||||
|
||||
def downloadIsTorrent(self, download_info):
|
||||
return download_info and download_info.get('type') in ['torrent', 'torrent_magnet']
|
||||
|
||||
@@ -11,6 +11,7 @@ from subliminal.videos import Video
|
||||
import enzyme
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
@@ -74,7 +75,7 @@ class Scanner(Plugin):
|
||||
'hdtv': ['hdtv']
|
||||
}
|
||||
|
||||
clean = '[ _\,\.\(\)\[\]\-](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
|
||||
@@ -100,7 +101,7 @@ class Scanner(Plugin):
|
||||
addEvent('scanner.name_year', self.getReleaseNameYear)
|
||||
addEvent('scanner.partnumber', self.getPartNumber)
|
||||
|
||||
def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None):
|
||||
def scan(self, folder = None, files = None, download_info = None, simple = False, newer_than = 0, return_ignored = True, on_found = None):
|
||||
|
||||
folder = ss(os.path.normpath(folder))
|
||||
|
||||
@@ -118,8 +119,7 @@ class Scanner(Plugin):
|
||||
try:
|
||||
files = []
|
||||
for root, dirs, walk_files in os.walk(folder):
|
||||
for filename in walk_files:
|
||||
files.append(os.path.join(root, filename))
|
||||
files.extend(os.path.join(root, filename) for filename in walk_files)
|
||||
except:
|
||||
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
|
||||
else:
|
||||
@@ -177,17 +177,25 @@ class Scanner(Plugin):
|
||||
|
||||
|
||||
# Group files minus extension
|
||||
ignored_identifiers = []
|
||||
for identifier, group in movie_files.iteritems():
|
||||
if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier)
|
||||
|
||||
log.debug('Grouping files: %s', identifier)
|
||||
|
||||
has_ignored = 0
|
||||
for file_path in group['unsorted_files']:
|
||||
wo_ext = file_path[:-(len(getExt(file_path)) + 1)]
|
||||
ext = getExt(file_path)
|
||||
wo_ext = file_path[:-(len(ext) + 1)]
|
||||
found_files = set([i for i in leftovers if wo_ext in i])
|
||||
group['unsorted_files'].extend(found_files)
|
||||
leftovers = leftovers - found_files
|
||||
|
||||
has_ignored += 1 if ext == 'ignore' else 0
|
||||
|
||||
if has_ignored > 0:
|
||||
ignored_identifiers.append(identifier)
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
break
|
||||
@@ -313,6 +321,11 @@ class Scanner(Plugin):
|
||||
|
||||
del movie_files
|
||||
|
||||
# Make sure only one movie was found if a download ID is provided
|
||||
if download_info and not len(valid_files) == 1:
|
||||
log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_info.get('imdb_id'), len(valid_files)))
|
||||
download_info = None
|
||||
|
||||
# Determine file types
|
||||
processed_movies = {}
|
||||
total_found = len(valid_files)
|
||||
@@ -322,15 +335,17 @@ class Scanner(Plugin):
|
||||
except:
|
||||
break
|
||||
|
||||
if return_ignored is False and identifier in ignored_identifiers:
|
||||
log.debug('Ignore file found, ignoring release: %s', identifier)
|
||||
continue
|
||||
|
||||
# Group extra (and easy) files first
|
||||
# images = self.getImages(group['unsorted_files'])
|
||||
group['files'] = {
|
||||
'movie_extra': self.getMovieExtras(group['unsorted_files']),
|
||||
'subtitle': self.getSubtitles(group['unsorted_files']),
|
||||
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
|
||||
'nfo': self.getNfo(group['unsorted_files']),
|
||||
'trailer': self.getTrailers(group['unsorted_files']),
|
||||
#'backdrop': images['backdrop'],
|
||||
'leftover': set(group['unsorted_files']),
|
||||
}
|
||||
|
||||
@@ -345,7 +360,7 @@ class Scanner(Plugin):
|
||||
continue
|
||||
|
||||
log.debug('Getting metadata for %s', identifier)
|
||||
group['meta_data'] = self.getMetaData(group, folder = folder)
|
||||
group['meta_data'] = self.getMetaData(group, folder = folder, download_info = download_info)
|
||||
|
||||
# Subtitle meta
|
||||
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
|
||||
@@ -370,12 +385,14 @@ class Scanner(Plugin):
|
||||
for file_type in group['files']:
|
||||
if not file_type is 'leftover':
|
||||
group['files']['leftover'] -= set(group['files'][file_type])
|
||||
group['files'][file_type] = list(group['files'][file_type])
|
||||
group['files']['leftover'] = list(group['files']['leftover'])
|
||||
|
||||
# Delete the unsorted list
|
||||
del group['unsorted_files']
|
||||
|
||||
# Determine movie
|
||||
group['library'] = self.determineMovie(group)
|
||||
group['library'] = self.determineMovie(group, download_info = download_info)
|
||||
if not group['library']:
|
||||
log.error('Unable to determine movie: %s', group['identifiers'])
|
||||
else:
|
||||
@@ -388,6 +405,11 @@ class Scanner(Plugin):
|
||||
if on_found:
|
||||
on_found(group, total_found, total_found - len(processed_movies))
|
||||
|
||||
# Wait for all the async events calm down a bit
|
||||
while threading.activeCount() > 100 and not self.shuttingDown():
|
||||
log.debug('Too many threads active, waiting a few seconds')
|
||||
time.sleep(10)
|
||||
|
||||
if len(processed_movies) > 0:
|
||||
log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
|
||||
else:
|
||||
@@ -395,7 +417,7 @@ class Scanner(Plugin):
|
||||
|
||||
return processed_movies
|
||||
|
||||
def getMetaData(self, group, folder = ''):
|
||||
def getMetaData(self, group, folder = '', download_info = None):
|
||||
|
||||
data = {}
|
||||
files = list(group['files']['movie'])
|
||||
@@ -417,9 +439,16 @@ class Scanner(Plugin):
|
||||
|
||||
if data.get('audio'): break
|
||||
|
||||
data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
|
||||
# Use the quality guess first, if that failes use the quality we wanted to download
|
||||
data['quality'] = None
|
||||
if download_info and download_info.get('quality'):
|
||||
data['quality'] = fireEvent('quality.single', download_info.get('quality'), single = True)
|
||||
|
||||
if not data['quality']:
|
||||
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
|
||||
data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
|
||||
|
||||
if not data['quality']:
|
||||
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
|
||||
|
||||
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
|
||||
|
||||
@@ -495,17 +524,22 @@ class Scanner(Plugin):
|
||||
|
||||
return detected_languages
|
||||
|
||||
def determineMovie(self, group):
|
||||
imdb_id = None
|
||||
def determineMovie(self, group, download_info = None):
|
||||
|
||||
# Get imdb id from downloader
|
||||
imdb_id = download_info and download_info.get('imdb_id')
|
||||
if imdb_id:
|
||||
log.debug('Found movie via imdb id from it\'s download id: %s', download_info.get('imdb_id'))
|
||||
|
||||
files = group['files']
|
||||
|
||||
# Check for CP(imdb_id) string in the file paths
|
||||
for cur_file in files['movie']:
|
||||
imdb_id = self.getCPImdb(cur_file)
|
||||
if imdb_id:
|
||||
log.debug('Found movie via CP tag: %s', cur_file)
|
||||
break
|
||||
if not imdb_id:
|
||||
for cur_file in files['movie']:
|
||||
imdb_id = self.getCPImdb(cur_file)
|
||||
if imdb_id:
|
||||
log.debug('Found movie via CP tag: %s', cur_file)
|
||||
break
|
||||
|
||||
# Check and see if nfo contains the imdb-id
|
||||
if not imdb_id:
|
||||
|
||||
@@ -116,13 +116,13 @@ def sizeScore(size):
|
||||
|
||||
|
||||
def providerScore(provider):
|
||||
if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']:
|
||||
return 20
|
||||
|
||||
if provider in ['Newznab']:
|
||||
return 10
|
||||
try:
|
||||
score = tryInt(Env.setting('extra_score', section = provider.lower(), default = 0))
|
||||
except:
|
||||
score = 0
|
||||
|
||||
return 0
|
||||
return score
|
||||
|
||||
|
||||
def duplicateScore(nzb_name, movie_name):
|
||||
|
||||
@@ -25,12 +25,13 @@ config = [{
|
||||
'label': 'Required words',
|
||||
'default': '',
|
||||
'placeholder': 'Example: DTS, AC3 & English',
|
||||
'description': 'Ignore releases that don\'t contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
|
||||
'description': 'A release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
|
||||
},
|
||||
{
|
||||
'name': 'ignored_words',
|
||||
'label': 'Ignored words',
|
||||
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
|
||||
'description': 'Ignores releases that match any of these sets. (Works like explained above)'
|
||||
},
|
||||
{
|
||||
'name': 'preferred_method',
|
||||
@@ -40,6 +41,14 @@ config = [{
|
||||
'type': 'dropdown',
|
||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
|
||||
},
|
||||
{
|
||||
'name': 'always_search',
|
||||
'default': False,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'label': 'Always search',
|
||||
'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.',
|
||||
},
|
||||
],
|
||||
}, {
|
||||
'tab': 'searcher',
|
||||
|
||||
@@ -50,7 +50,12 @@ class Searcher(Plugin):
|
||||
}"""},
|
||||
})
|
||||
|
||||
# Schedule cronjob
|
||||
addEvent('app.load', self.setCrons)
|
||||
addEvent('setting.save.searcher.cron_day.after', self.setCrons)
|
||||
addEvent('setting.save.searcher.cron_hour.after', self.setCrons)
|
||||
addEvent('setting.save.searcher.cron_minute.after', self.setCrons)
|
||||
|
||||
def setCrons(self):
|
||||
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
||||
|
||||
def allMoviesView(self):
|
||||
@@ -141,8 +146,7 @@ class Searcher(Plugin):
|
||||
|
||||
pre_releases = fireEvent('quality.pre_releases', single = True)
|
||||
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
||||
available_status, ignored_status = fireEvent('status.get', ['available', 'ignored'], single = True)
|
||||
|
||||
found_releases = []
|
||||
|
||||
@@ -157,7 +161,7 @@ class Searcher(Plugin):
|
||||
|
||||
ret = False
|
||||
for quality_type in movie['profile']['types']:
|
||||
if not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
|
||||
if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
|
||||
log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title))
|
||||
continue
|
||||
|
||||
@@ -165,7 +169,7 @@ class Searcher(Plugin):
|
||||
|
||||
# See if better quality is available
|
||||
for release in movie['releases']:
|
||||
if release['quality']['order'] < quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]:
|
||||
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]:
|
||||
has_better_quality += 1
|
||||
|
||||
# Don't search for quality lower then already available.
|
||||
@@ -285,10 +289,10 @@ class Searcher(Plugin):
|
||||
if filedata == 'try_next':
|
||||
return filedata
|
||||
|
||||
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
|
||||
|
||||
if successful:
|
||||
download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
|
||||
log.debug('Downloader result: %s', download_result)
|
||||
|
||||
if download_result:
|
||||
try:
|
||||
# Mark release as snatched
|
||||
db = get_session()
|
||||
@@ -298,6 +302,15 @@ class Searcher(Plugin):
|
||||
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
|
||||
|
||||
# Save download-id info if returned
|
||||
if isinstance(download_result, dict):
|
||||
for key in download_result:
|
||||
rls_info = ReleaseInfo(
|
||||
identifier = 'download_%s' % key,
|
||||
value = toUnicode(download_result.get(key))
|
||||
)
|
||||
rls.info.append(rls_info)
|
||||
db.commit()
|
||||
|
||||
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
||||
@@ -333,7 +346,7 @@ class Searcher(Plugin):
|
||||
|
||||
return True
|
||||
|
||||
log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', '')))
|
||||
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('type', '')))
|
||||
|
||||
return False
|
||||
|
||||
@@ -357,7 +370,7 @@ class Searcher(Plugin):
|
||||
|
||||
return search_types
|
||||
|
||||
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
|
||||
def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
|
||||
|
||||
imdb_results = kwargs.get('imdb_results', False)
|
||||
retention = Env.setting('retention', section = 'nzb')
|
||||
@@ -370,30 +383,36 @@ class Searcher(Plugin):
|
||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||
nzb_name = simplifyString(nzb['name'])
|
||||
nzb_words = re.split('\W+', nzb_name)
|
||||
required_words = splitString(self.conf('required_words').lower())
|
||||
|
||||
# Make sure it has required words
|
||||
required_words = splitString(self.conf('required_words').lower())
|
||||
req_match = 0
|
||||
for req_set in required_words:
|
||||
req = splitString(req_set, '&')
|
||||
req_match += len(list(set(nzb_words) & set(req))) == len(req)
|
||||
|
||||
if self.conf('required_words') and req_match == 0:
|
||||
log.info2("Wrong: Required word missing: %s" % nzb['name'])
|
||||
log.info2('Wrong: Required word missing: %s', nzb['name'])
|
||||
return False
|
||||
|
||||
# Ignore releases
|
||||
ignored_words = splitString(self.conf('ignored_words').lower())
|
||||
blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words))
|
||||
if self.conf('ignored_words') and blacklisted:
|
||||
log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
|
||||
ignored_match = 0
|
||||
for ignored_set in ignored_words:
|
||||
ignored = splitString(ignored_set, '&')
|
||||
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
|
||||
|
||||
if self.conf('ignored_words') and ignored_match:
|
||||
log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name']))
|
||||
return False
|
||||
|
||||
# Ignore porn stuff
|
||||
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
|
||||
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
|
||||
if pron_words:
|
||||
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
||||
return False
|
||||
|
||||
#qualities = fireEvent('quality.all', single = True)
|
||||
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
|
||||
|
||||
# Contains lower quality string
|
||||
@@ -443,7 +462,7 @@ class Searcher(Plugin):
|
||||
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
|
||||
return True
|
||||
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year']))
|
||||
return False
|
||||
|
||||
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}):
|
||||
|
||||
@@ -25,13 +25,14 @@ class StatusPlugin(Plugin):
|
||||
'available': 'Available',
|
||||
'suggest': 'Suggest',
|
||||
}
|
||||
status_cached = {}
|
||||
|
||||
def __init__(self):
|
||||
addEvent('status.add', self.add)
|
||||
addEvent('status.get', self.add) # Alias for .add
|
||||
addEvent('status.get', self.get)
|
||||
addEvent('status.get_by_id', self.getById)
|
||||
addEvent('status.all', self.all)
|
||||
addEvent('app.initialize', self.fill)
|
||||
addEvent('app.load', self.all) # Cache all statuses
|
||||
|
||||
addApiView('status.list', self.list, docs = {
|
||||
'desc': 'Check for available update',
|
||||
@@ -67,26 +68,40 @@ class StatusPlugin(Plugin):
|
||||
s = status.to_dict()
|
||||
temp.append(s)
|
||||
|
||||
#db.close()
|
||||
# Update cache
|
||||
self.status_cached[status.identifier] = s
|
||||
|
||||
return temp
|
||||
|
||||
def add(self, identifier):
|
||||
def get(self, identifiers):
|
||||
|
||||
if not isinstance(identifiers, (list)):
|
||||
identifiers = [identifiers]
|
||||
|
||||
db = get_session()
|
||||
return_list = []
|
||||
|
||||
s = db.query(Status).filter_by(identifier = identifier).first()
|
||||
if not s:
|
||||
s = Status(
|
||||
identifier = identifier,
|
||||
label = toUnicode(identifier.capitalize())
|
||||
)
|
||||
db.add(s)
|
||||
db.commit()
|
||||
for identifier in identifiers:
|
||||
|
||||
status_dict = s.to_dict()
|
||||
if self.status_cached.get(identifier):
|
||||
return_list.append(self.status_cached.get(identifier))
|
||||
continue
|
||||
|
||||
#db.close()
|
||||
return status_dict
|
||||
s = db.query(Status).filter_by(identifier = identifier).first()
|
||||
if not s:
|
||||
s = Status(
|
||||
identifier = identifier,
|
||||
label = toUnicode(identifier.capitalize())
|
||||
)
|
||||
db.add(s)
|
||||
db.commit()
|
||||
|
||||
status_dict = s.to_dict()
|
||||
|
||||
self.status_cached[identifier] = status_dict
|
||||
return_list.append(status_dict)
|
||||
|
||||
return return_list if len(identifiers) > 1 else return_list[0]
|
||||
|
||||
def fill(self):
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ config = [{
|
||||
},
|
||||
{
|
||||
'name': 'languages',
|
||||
'description': 'Comma separated, 2 letter country code. Example: en, nl',
|
||||
'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',
|
||||
|
||||
@@ -59,7 +59,7 @@ class Subtitle(Plugin):
|
||||
|
||||
for d_sub in downloaded:
|
||||
log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files))
|
||||
group['files']['subtitle'].add(d_sub.path)
|
||||
group['files']['subtitle'].append(d_sub.path)
|
||||
group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2]
|
||||
|
||||
return True
|
||||
|
||||
@@ -22,14 +22,14 @@ config = [{
|
||||
'name': 'quality',
|
||||
'default': '720p',
|
||||
'type': 'dropdown',
|
||||
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
|
||||
'values': [('1080p', '1080p'), ('720p', '720p'), ('480P', '480p')],
|
||||
},
|
||||
{
|
||||
'name': 'name',
|
||||
'label': 'Naming',
|
||||
'default': '<filename>-trailer',
|
||||
'advanced': True,
|
||||
'description': 'Use <filename> to use above settings.'
|
||||
'description': 'Use <strong><filename></strong> to use above settings.'
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -22,10 +22,15 @@ class Trailer(Plugin):
|
||||
return False
|
||||
|
||||
for trailer in trailers.get(self.conf('quality'), []):
|
||||
filename = self.conf('name').replace('<filename>', group['filename']) + ('.%s' % getExt(trailer))
|
||||
|
||||
ext = getExt(trailer)
|
||||
filename = self.conf('name').replace('<filename>', group['filename']) + ('.%s' % ('mp4' if len(ext) > 5 else ext))
|
||||
destination = os.path.join(group['destination_dir'], filename)
|
||||
if not os.path.isfile(destination):
|
||||
fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
|
||||
trailer_file = fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
|
||||
if os.path.getsize(trailer_file) < (1024 * 1024): # Don't trust small trailers (1MB), try next one
|
||||
os.unlink(trailer_file)
|
||||
continue
|
||||
else:
|
||||
log.debug('Trailer already exists: %s', destination)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page.userscript .frame.loading {
|
||||
@@ -12,3 +13,26 @@
|
||||
font-size: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page.userscript .movie_result {
|
||||
height: 140px;
|
||||
}
|
||||
.page.userscript .movie_result .thumbnail {
|
||||
width: 90px;
|
||||
}
|
||||
.page.userscript .movie_result .options {
|
||||
left: 90px;
|
||||
padding: 54px 15px;
|
||||
}
|
||||
|
||||
.page.userscript .movie_result .year {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page.userscript .movie_result .options select[name="title"] {
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.page.userscript .movie_result .options select[name="profile"] {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
@@ -63,28 +63,19 @@ var UserscriptSettingTab = new Class({
|
||||
self.settings = App.getPage('Settings')
|
||||
self.settings.addEvent('create', function(){
|
||||
|
||||
// See if userscript can be installed
|
||||
var userscript = false;
|
||||
try {
|
||||
if(Components.interfaces.gmIGreasemonkeyService)
|
||||
userscript = true
|
||||
}
|
||||
catch(e){
|
||||
userscript = Browser.chrome === true;
|
||||
}
|
||||
|
||||
var host_url = window.location.protocol + '//' + window.location.host;
|
||||
|
||||
self.settings.createGroup({
|
||||
'name': 'userscript',
|
||||
'label': 'Install the bookmarklet' + (userscript ? ' or userscript' : ''),
|
||||
'label': 'Install the bookmarklet or userscript',
|
||||
'description': 'Easily add movies via imdb.com, appletrailers and more'
|
||||
}).inject(self.settings.tabs.automation.content, 'top').adopt(
|
||||
(userscript ? [new Element('a.userscript.button', {
|
||||
new Element('a.userscript.button', {
|
||||
'text': 'Install userscript',
|
||||
'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js',
|
||||
'target': '_self'
|
||||
}), new Element('span.or[text=or]')] : null),
|
||||
'target': '_blank'
|
||||
}),
|
||||
new Element('span.or[text=or]'),
|
||||
new Element('span.bookmarklet').adopt(
|
||||
new Element('a.button.green', {
|
||||
'text': '+CouchPotato',
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
// ==UserScript==
|
||||
//
|
||||
// If you can read this, you need to enable or install the Greasemonkey add-on for firefox
|
||||
// If you are using Chrome, download this file and drag it to the extensions tab
|
||||
// Other browsers, use the bookmarklet
|
||||
//
|
||||
// @name CouchPotato UserScript
|
||||
// @description Add movies like a real CouchPotato
|
||||
// @grant none
|
||||
|
||||
@@ -1,49 +1,60 @@
|
||||
.page.wizard .uniForm {
|
||||
width: 80%;
|
||||
margin: 0 auto 30px;
|
||||
margin: 0 0 30px;
|
||||
width: 83%;
|
||||
}
|
||||
|
||||
.page.wizard h1 {
|
||||
padding: 10px 30px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
margin: 0 5px;
|
||||
display: block;
|
||||
font-size: 30px;
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
.page.wizard .description {
|
||||
padding: 10px 30px;
|
||||
font-size: 18px;
|
||||
padding: 10px 5px;
|
||||
font-size: 1.45em;
|
||||
line-height: 1.4em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page.wizard .tab_wrapper {
|
||||
background: #5c697b;
|
||||
padding: 10px 0;
|
||||
font-size: 18px;
|
||||
height: 65px;
|
||||
font-size: 1.75em;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
min-width: 960px;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.page.wizard .tab_wrapper .tabs {
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.page.wizard .tabs li {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
}
|
||||
.page.wizard .tabs li a {
|
||||
padding: 20px 10px;
|
||||
height: 100%;
|
||||
display: block;
|
||||
color: #FFF;
|
||||
font-weight: normal;
|
||||
border-bottom: 4px solid transparent;
|
||||
}
|
||||
|
||||
.page.wizard .tabs li:hover a { border-color: #047792; }
|
||||
.page.wizard .tabs li.done a { border-color: #04bce6; }
|
||||
|
||||
.page.wizard .tab_wrapper .pointer {
|
||||
border-right: 10px solid transparent;
|
||||
@@ -61,27 +72,13 @@
|
||||
.page.wizard form > div {
|
||||
min-height: 300px;
|
||||
}
|
||||
.page.wizard .wgroup_finish {
|
||||
height: 300px;
|
||||
}
|
||||
.page.wizard .wgroup_finish h1 {
|
||||
text-align: center;
|
||||
}
|
||||
.page.wizard .wgroup_finish .wizard_support,
|
||||
.page.wizard .wgroup_finish .description {
|
||||
font-size: 25px;
|
||||
line-height: 120%;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page.wizard .button.green {
|
||||
padding: 20px;
|
||||
font-size: 25px;
|
||||
margin: 10px 30px 80px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
.page.wizard .button.green {
|
||||
padding: 20px;
|
||||
font-size: 25px;
|
||||
margin: 10px 0 80px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page.wizard .tab_nzb_providers {
|
||||
margin: 20px 0 0 0;
|
||||
|
||||
@@ -9,27 +9,12 @@ Page.Wizard = new Class({
|
||||
headers: {
|
||||
'welcome': {
|
||||
'title': 'Welcome to the new CouchPotato',
|
||||
'description': 'To get started, fill in each of the following settings as much as you can. <br />Maybe first start with importing your movies from the previous CouchPotato',
|
||||
'description': 'To get started, fill in each of the following settings as much as you can.',
|
||||
'content': new Element('div', {
|
||||
'styles': {
|
||||
'margin': '0 0 0 30px'
|
||||
}
|
||||
}).adopt(
|
||||
new Element('div', {
|
||||
'html': 'Select the <strong>data.db</strong>. It should be in your CouchPotato root directory.'
|
||||
}),
|
||||
self.import_iframe = new Element('iframe', {
|
||||
'styles': {
|
||||
'height': 40,
|
||||
'width': 300,
|
||||
'border': 0,
|
||||
'overflow': 'hidden'
|
||||
}
|
||||
})
|
||||
),
|
||||
'event': function(){
|
||||
self.import_iframe.set('src', Api.createUrl('v1.import'))
|
||||
}
|
||||
})
|
||||
},
|
||||
'general': {
|
||||
'title': 'General',
|
||||
@@ -178,7 +163,7 @@ Page.Wizard = new Class({
|
||||
'href': App.createUrl('wizard/'+group),
|
||||
'text': (self.headers[group].label || group).capitalize()
|
||||
})
|
||||
).inject(tabs);
|
||||
).inject(tabs)
|
||||
|
||||
}
|
||||
else
|
||||
@@ -214,13 +199,7 @@ Page.Wizard = new Class({
|
||||
self.el.getElement('.t_searcher').hide();
|
||||
|
||||
// Add pointer
|
||||
new Element('.tab_wrapper').wraps(tabs).adopt(
|
||||
self.pointer = new Element('.pointer', {
|
||||
'tween': {
|
||||
'transition': 'quint:in:out'
|
||||
}
|
||||
})
|
||||
);
|
||||
new Element('.tab_wrapper').wraps(tabs);
|
||||
|
||||
// Add nav
|
||||
var minimum = self.el.getSize().y-window.getSize().y;
|
||||
@@ -232,16 +211,18 @@ Page.Wizard = new Class({
|
||||
if(!t) return;
|
||||
|
||||
var func = function(){
|
||||
var ct = t.getCoordinates();
|
||||
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
|
||||
// Activate all previous ones
|
||||
self.groups.each(function(groups2, nr2){
|
||||
var t2 = self.el.getElement('.t_'+groups2);
|
||||
t2[nr2 > nr ? 'removeClass' : 'addClass' ]('done');
|
||||
})
|
||||
g.tween('opacity', 1);
|
||||
}
|
||||
|
||||
if(nr == 0)
|
||||
func();
|
||||
|
||||
|
||||
var ss = new ScrollSpy( {
|
||||
new ScrollSpy( {
|
||||
min: function(){
|
||||
var c = g.getCoordinates();
|
||||
var top = c.top-(window.getSize().y/2);
|
||||
|
||||
@@ -2,6 +2,7 @@ from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import Provider
|
||||
from couchpotato.environment import Env
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -10,6 +11,7 @@ log = CPLog(__name__)
|
||||
class Automation(Provider):
|
||||
|
||||
enabled_option = 'automation_enabled'
|
||||
http_time_between_calls = 2
|
||||
|
||||
interval = 86400
|
||||
last_checked = 0
|
||||
@@ -59,7 +61,26 @@ class Automation(Provider):
|
||||
type_value = movie.get(minimal_type, 0)
|
||||
type_min = self.getMinimal(minimal_type)
|
||||
if type_value < type_min:
|
||||
log.info('%s too low for %s, need %s has %s', (minimal_type, movie['imdb'], type_min, type_value))
|
||||
log.info('%s too low for %s, need %s has %s', (minimal_type, movie['original_title'], type_min, type_value))
|
||||
return False
|
||||
|
||||
movie_genres = [genre.lower() for genre in movie['genres']]
|
||||
required_genres = splitString(self.getMinimal('required_genres').lower())
|
||||
ignored_genres = splitString(self.getMinimal('ignored_genres').lower())
|
||||
|
||||
req_match = 0
|
||||
for req_set in required_genres:
|
||||
req = splitString(req_set, '&')
|
||||
req_match += len(list(set(movie_genres) & set(req))) == len(req)
|
||||
|
||||
if self.getMinimal('required_genres') and req_match == 0:
|
||||
log.info2('Required genre(s) missing for %s', movie['original_title'])
|
||||
return False
|
||||
|
||||
for ign_set in ignored_genres:
|
||||
ign = splitString(ign_set, '&')
|
||||
if len(list(set(movie_genres) & set(ign))) == len(ign):
|
||||
log.info2('%s has blacklisted genre(s): %s', (movie['original_title'], ign))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.automation.base import Automation
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Goodfilms(Automation):
|
||||
|
||||
url = 'http://goodfil.ms/%s/queue'
|
||||
url = 'http://goodfil.ms/%s/queue?page=%d&without_layout=1'
|
||||
|
||||
def getIMDBids(self):
|
||||
|
||||
@@ -25,12 +25,25 @@ class Goodfilms(Automation):
|
||||
|
||||
def getWatchlist(self):
|
||||
|
||||
url = self.url % self.conf('automation_username')
|
||||
soup = BeautifulSoup(self.getHTMLData(url))
|
||||
|
||||
movies = []
|
||||
page = 1
|
||||
|
||||
for movie in soup.find_all('div', attrs = { 'class': 'movie', 'data-film-title': True }):
|
||||
movies.append({ 'title': movie['data-film-title'], 'year': movie['data-film-year'] })
|
||||
while True:
|
||||
url = self.url % (self.conf('automation_username'), page)
|
||||
data = self.getHTMLData(url)
|
||||
soup = BeautifulSoup(data)
|
||||
|
||||
this_watch_list = soup.find_all('div', attrs = { 'class': 'movie', 'data-film-title': True })
|
||||
|
||||
if not this_watch_list: # No Movies
|
||||
break
|
||||
|
||||
for movie in this_watch_list:
|
||||
movies.append({ 'title': movie['data-film-title'], 'year': movie['data-film-year'] })
|
||||
|
||||
if not 'next page' in data.lower():
|
||||
break
|
||||
|
||||
page += 1
|
||||
|
||||
return movies
|
||||
|
||||
34
couchpotato/core/providers/automation/letterboxd/__init__.py
Normal file
34
couchpotato/core/providers/automation/letterboxd/__init__.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from .main import Letterboxd
|
||||
|
||||
def start():
|
||||
return Letterboxd()
|
||||
|
||||
config = [{
|
||||
'name': 'letterboxd',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'automation',
|
||||
'list': 'watchlist_providers',
|
||||
'name': 'letterboxd_automation',
|
||||
'label': 'Letterboxd',
|
||||
'description': 'Import movies from any public <a href="http://letterboxd.com/">Letterboxd</a> watchlist',
|
||||
'options': [
|
||||
{
|
||||
'name': 'automation_enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'automation_urls_use',
|
||||
'label': 'Use',
|
||||
},
|
||||
{
|
||||
'name': 'automation_urls',
|
||||
'label': 'Username',
|
||||
'type': 'combined',
|
||||
'combine': ['automation_urls_use', 'automation_urls'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
49
couchpotato/core/providers/automation/letterboxd/main.py
Normal file
49
couchpotato/core/providers/automation/letterboxd/main.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.automation.base import Automation
|
||||
import re
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Letterboxd(Automation):
|
||||
|
||||
url = 'http://letterboxd.com/%s/watchlist/'
|
||||
pattern = re.compile(r'(.*)\((\d*)\)')
|
||||
|
||||
def getIMDBids(self):
|
||||
|
||||
urls = splitString(self.conf('automation_urls'))
|
||||
|
||||
if len(urls) == 0:
|
||||
return []
|
||||
|
||||
movies = []
|
||||
|
||||
for movie in self.getWatchlist():
|
||||
imdb_id = self.search(movie.get('title'), movie.get('year'), imdb_only = True)
|
||||
movies.append(imdb_id)
|
||||
|
||||
return movies
|
||||
|
||||
def getWatchlist(self):
|
||||
|
||||
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
|
||||
urls = splitString(self.conf('automation_urls'))
|
||||
|
||||
index = -1
|
||||
movies = []
|
||||
for username in urls:
|
||||
|
||||
index += 1
|
||||
if not enablers[index]:
|
||||
continue
|
||||
|
||||
soup = BeautifulSoup(self.getHTMLData(self.url % username))
|
||||
|
||||
for movie in soup.find_all('a', attrs = { 'class': 'frame' }):
|
||||
match = filter(None, self.pattern.split(movie['title']))
|
||||
movies.append({'title': match[0], 'year': match[1] })
|
||||
|
||||
return movies
|
||||
@@ -35,10 +35,10 @@ class Rottentomatoes(Automation, RSS):
|
||||
name = result.group(0)
|
||||
|
||||
if rating < tryInt(self.conf('tomatometer_percent')):
|
||||
log.info2('%s seems to be rotten...' % name)
|
||||
log.info2('%s seems to be rotten...', name)
|
||||
else:
|
||||
|
||||
log.info2('Found %s fresh enough movies, enqueuing: %s' % (rating, name))
|
||||
log.info2('Found %s fresh enough movies, enqueuing: %s', (rating, name))
|
||||
year = datetime.datetime.now().strftime("%Y")
|
||||
imdb = self.search(name, year)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class Provider(Plugin):
|
||||
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
|
||||
data = self.getCache(cache_key, url, **kwargs)
|
||||
|
||||
if data:
|
||||
if data and len(data) > 0:
|
||||
try:
|
||||
data = XMLTree.fromstring(data)
|
||||
return self.getElements(data, item_path)
|
||||
@@ -104,6 +104,7 @@ class YarrProvider(Provider):
|
||||
try:
|
||||
cookiejar = cookielib.CookieJar()
|
||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
|
||||
opener.addheaders = [('User-Agent', self.user_agent)]
|
||||
urllib2.install_opener(opener)
|
||||
log.info2('Logging into %s', self.urls['login'])
|
||||
f = opener.open(self.urls['login'], self.getLoginParams())
|
||||
@@ -113,9 +114,12 @@ class YarrProvider(Provider):
|
||||
if self.loginSuccess(output):
|
||||
self.login_opener = opener
|
||||
return True
|
||||
except:
|
||||
log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
error = 'unknown'
|
||||
except:
|
||||
error = traceback.format_exc()
|
||||
|
||||
log.error('Failed to login %s: %s', (self.getName(), error))
|
||||
return False
|
||||
|
||||
def loginSuccess(self, output):
|
||||
@@ -178,7 +182,7 @@ class YarrProvider(Provider):
|
||||
if hostname in download_url:
|
||||
return self
|
||||
except:
|
||||
log.debug('Url % s doesn\'t belong to %s', (url, self.getName()))
|
||||
log.debug('Url %s doesn\'t belong to %s', (url, self.getName()))
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from couchpotato.core.helpers.variable import mergeDicts, randomString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -11,6 +12,25 @@ log = CPLog(__name__)
|
||||
|
||||
class MovieResultModifier(Plugin):
|
||||
|
||||
default_info = {
|
||||
'tmdb_id': 0,
|
||||
'titles': [],
|
||||
'original_title': '',
|
||||
'year': 0,
|
||||
'images': {
|
||||
'poster': [],
|
||||
'backdrop': [],
|
||||
'poster_original': [],
|
||||
'backdrop_original': []
|
||||
},
|
||||
'runtime': 0,
|
||||
'plot': '',
|
||||
'tagline': '',
|
||||
'imdb': '',
|
||||
'genres': [],
|
||||
'release_date': {}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
addEvent('result.modify.movie.search', self.combineOnIMDB)
|
||||
addEvent('result.modify.movie.info', self.checkLibrary)
|
||||
@@ -52,8 +72,7 @@ class MovieResultModifier(Plugin):
|
||||
if l:
|
||||
|
||||
# Statuses
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||
|
||||
for movie in l.movies:
|
||||
if movie.status_id == active_status['id']:
|
||||
@@ -68,6 +87,9 @@ class MovieResultModifier(Plugin):
|
||||
return temp
|
||||
|
||||
def checkLibrary(self, result):
|
||||
|
||||
result = mergeDicts(copy.deepcopy(self.default_info), copy.deepcopy(result))
|
||||
|
||||
if result and result.get('imdb'):
|
||||
return mergeDicts(result, self.getLibraryTags(result['imdb']))
|
||||
return result
|
||||
|
||||
@@ -5,6 +5,7 @@ from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.movie.base import MovieProvider
|
||||
from couchpotato.core.settings.model import Movie
|
||||
from couchpotato.environment import Env
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -13,24 +14,42 @@ log = CPLog(__name__)
|
||||
class CouchPotatoApi(MovieProvider):
|
||||
|
||||
urls = {
|
||||
'search': 'https://couchpota.to/api/search/%s/',
|
||||
'info': 'https://couchpota.to/api/info/%s/',
|
||||
'is_movie': 'https://couchpota.to/api/ismovie/%s/',
|
||||
'eta': 'https://couchpota.to/api/eta/%s/',
|
||||
'suggest': 'https://couchpota.to/api/suggest/',
|
||||
'search': 'https://api.couchpota.to/search/%s/',
|
||||
'info': 'https://api.couchpota.to/info/%s/',
|
||||
'is_movie': 'https://api.couchpota.to/ismovie/%s/',
|
||||
'eta': 'https://api.couchpota.to/eta/%s/',
|
||||
'suggest': 'https://api.couchpota.to/suggest/',
|
||||
'updater': 'https://api.couchpota.to/updater/?%s',
|
||||
'messages': 'https://api.couchpota.to/messages/?%s',
|
||||
}
|
||||
http_time_between_calls = 0
|
||||
api_version = 1
|
||||
|
||||
def __init__(self):
|
||||
#addApiView('movie.suggest', self.suggestView)
|
||||
|
||||
addEvent('movie.info', self.getInfo, priority = 1)
|
||||
addEvent('movie.search', self.search, priority = 1)
|
||||
addEvent('movie.release_date', self.getReleaseDate)
|
||||
addEvent('movie.suggest', self.suggest)
|
||||
addEvent('movie.is_movie', self.isMovie)
|
||||
|
||||
addEvent('cp.source_url', self.getSourceUrl)
|
||||
addEvent('cp.messages', self.getMessages)
|
||||
|
||||
def getMessages(self, last_check = 0):
|
||||
|
||||
data = self.getJsonData(self.urls['messages'] % tryUrlencode({
|
||||
'last_check': last_check,
|
||||
}), headers = self.getRequestHeaders(), cache_timeout = 10)
|
||||
|
||||
return data
|
||||
|
||||
def getSourceUrl(self, repo = None, repo_name = None, branch = None):
|
||||
return self.getJsonData(self.urls['updater'] % tryUrlencode({
|
||||
'repo': repo,
|
||||
'name': repo_name,
|
||||
'branch': branch,
|
||||
}), headers = self.getRequestHeaders())
|
||||
|
||||
def search(self, q, limit = 12):
|
||||
return self.getJsonData(self.urls['search'] % tryUrlencode(q), headers = self.getRequestHeaders())
|
||||
|
||||
@@ -51,7 +70,8 @@ class CouchPotatoApi(MovieProvider):
|
||||
return
|
||||
|
||||
result = self.getJsonData(self.urls['info'] % identifier, headers = self.getRequestHeaders())
|
||||
if result: return result
|
||||
if result:
|
||||
return dict((k, v) for k, v in result.iteritems() if v)
|
||||
|
||||
return {}
|
||||
|
||||
@@ -96,4 +116,5 @@ class CouchPotatoApi(MovieProvider):
|
||||
'X-CP-Version': fireEvent('app.version', single = True),
|
||||
'X-CP-API': self.api_version,
|
||||
'X-CP-Time': time.time(),
|
||||
'X-CP-Identifier': '+%s' % Env.setting('api_key', 'core')[:10], # Use first 10 as identifier, so we don't need to use IP address in api stats
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class OMDBAPI(MovieProvider):
|
||||
movie_data = {
|
||||
'via_imdb': True,
|
||||
'titles': [movie.get('Title')] if movie.get('Title') else [],
|
||||
'original_title': movie.get('Title', ''),
|
||||
'original_title': movie.get('Title'),
|
||||
'images': {
|
||||
'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [],
|
||||
},
|
||||
@@ -96,14 +96,15 @@ class OMDBAPI(MovieProvider):
|
||||
},
|
||||
'imdb': str(movie.get('imdbID', '')),
|
||||
'runtime': self.runtimeToMinutes(movie.get('Runtime', '')),
|
||||
'released': movie.get('Released', ''),
|
||||
'released': movie.get('Released'),
|
||||
'year': year if isinstance(year, (int)) else None,
|
||||
'plot': movie.get('Plot', ''),
|
||||
'plot': movie.get('Plot'),
|
||||
'genres': splitString(movie.get('Genre', '')),
|
||||
'directors': splitString(movie.get('Director', '')),
|
||||
'writers': splitString(movie.get('Writer', '')),
|
||||
'actors': splitString(movie.get('Actors', '')),
|
||||
}
|
||||
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
|
||||
except:
|
||||
log.error('Failed parsing IMDB API json: %s', traceback.format_exc())
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class TheMovieDb(MovieProvider):
|
||||
if raw:
|
||||
try:
|
||||
results = self.parseMovie(raw)
|
||||
log.info('Found: %s', results['titles'][0] + ' (' + str(results['year']) + ')')
|
||||
log.info('Found: %s', results['titles'][0] + ' (' + str(results.get('year', 0)) + ')')
|
||||
|
||||
self.setCache(cache_key, results)
|
||||
return results
|
||||
@@ -81,7 +81,7 @@ class TheMovieDb(MovieProvider):
|
||||
if nr == limit:
|
||||
break
|
||||
|
||||
log.info('Found: %s', [result['titles'][0] + ' (' + str(result['year']) + ')' for result in results])
|
||||
log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
|
||||
|
||||
self.setCache(cache_key, results)
|
||||
return results
|
||||
@@ -170,11 +170,12 @@ class TheMovieDb(MovieProvider):
|
||||
'runtime': movie.get('runtime'),
|
||||
'released': movie.get('released'),
|
||||
'year': year,
|
||||
'plot': movie.get('overview', ''),
|
||||
'tagline': '',
|
||||
'plot': movie.get('overview'),
|
||||
'genres': genres,
|
||||
}
|
||||
|
||||
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
|
||||
|
||||
# Add alternative names
|
||||
for alt in ['original_name', 'alternative_name']:
|
||||
alt_name = toUnicode(movie.get(alt))
|
||||
|
||||
@@ -18,6 +18,14 @@ config = [{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -27,6 +27,14 @@ config = [{
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3,7 +3,6 @@ from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
import json
|
||||
import traceback
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@ config = [{
|
||||
'list': 'nzb_providers',
|
||||
'name': 'newznab',
|
||||
'order': 10,
|
||||
'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab providers</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \
|
||||
'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \
|
||||
<a href="https://nzbs.org" target="_blank">NZBs.org</a>, <a href="http://dognzb.cr/" target="_blank">DOGnzb.cr</a>, \
|
||||
<a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a> or <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>',
|
||||
<a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a>, <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>, \
|
||||
<a href="https://smackdownonyou.com" target="_blank">SmackDown</a>, <a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
@@ -23,20 +24,27 @@ config = [{
|
||||
},
|
||||
{
|
||||
'name': 'use',
|
||||
'default': '0,0,0,0'
|
||||
'default': '0,0,0,0,0,0'
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info',
|
||||
'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws',
|
||||
'description': 'The hostname of your newznab provider',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'default': '0,0,0,0,0,0',
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'default': ',,,',
|
||||
'default': ',,,,,',
|
||||
'label': 'Api Key',
|
||||
'description': 'Can be found on your profile page',
|
||||
'type': 'combined',
|
||||
'combine': ['use', 'host', 'api_key'],
|
||||
'combine': ['use', 'host', 'api_key', 'extra_score'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import cleanHost, splitString
|
||||
from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import ResultList
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
@@ -76,6 +76,7 @@ class Newznab(NZBProvider, RSS):
|
||||
'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'],
|
||||
})
|
||||
|
||||
def getHosts(self):
|
||||
@@ -83,13 +84,15 @@ class Newznab(NZBProvider, RSS):
|
||||
uses = splitString(str(self.conf('use')))
|
||||
hosts = splitString(self.conf('host'))
|
||||
api_keys = splitString(self.conf('api_key'))
|
||||
extra_score = splitString(self.conf('extra_score'))
|
||||
|
||||
list = []
|
||||
for nr in range(len(hosts)):
|
||||
list.append({
|
||||
'use': uses[nr],
|
||||
'host': hosts[nr],
|
||||
'api_key': api_keys[nr]
|
||||
'api_key': api_keys[nr],
|
||||
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
|
||||
})
|
||||
|
||||
return list
|
||||
|
||||
@@ -18,6 +18,14 @@ config = [{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -19,6 +19,14 @@ config = [{
|
||||
'type': 'enabler',
|
||||
'default': True,
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -35,6 +35,14 @@ config = [{
|
||||
'label': 'English only',
|
||||
'description': 'Only search for English spoken movies on Nzbsrus',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -19,6 +19,14 @@ config = [{
|
||||
'type': 'enabler',
|
||||
'default': True,
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -27,6 +27,14 @@ config = [{
|
||||
'label': 'Api Key',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'default': 20,
|
||||
'type': 'int',
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
45
couchpotato/core/providers/torrent/hdbits/__init__.py
Normal file
45
couchpotato/core/providers/torrent/hdbits/__init__.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from .main import HDBits
|
||||
|
||||
def start():
|
||||
return HDBits()
|
||||
|
||||
config = [{
|
||||
'name': 'hdbits',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'HDBits',
|
||||
'description': 'See <a href="http://hdbits.org">HDBits</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'passkey',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
55
couchpotato/core/providers/torrent/hdbits/main.py
Normal file
55
couchpotato/core/providers/torrent/hdbits/main.py
Normal file
@@ -0,0 +1,55 @@
|
||||
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
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class HDBits(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test' : 'https://hdbits.org/',
|
||||
'login' : 'https://hdbits.org/login/doLogin/',
|
||||
'detail' : 'https://hdbits.org/details.php?id=%s&source=browse',
|
||||
'search' : 'https://hdbits.org/json_search.php?imdb=%s',
|
||||
'download' : 'https://hdbits.org/download.php/%s.torrent?id=%s&passkey=%s&source=details.browse',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
data = self.getJsonData(self.urls['search'] % movie['library']['identifier'], opener = self.login_opener)
|
||||
|
||||
if data:
|
||||
try:
|
||||
for result in data:
|
||||
results.append({
|
||||
'id': result['id'],
|
||||
'name': result['title'],
|
||||
'url': self.urls['download'] % (result['title'], result['id'], self.conf('passkey')),
|
||||
'detail_url': self.urls['detail'] % result['id'],
|
||||
'size': self.parseSize(result['size']),
|
||||
'seeders': tryInt(result['seeder']),
|
||||
'leechers': tryInt(result['leecher'])
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
data = self.getHTMLData('https://hdbits.org/login')
|
||||
bs = BeautifulSoup(data)
|
||||
secret = bs.find('input', attrs = {'name': 'lol'})['value']
|
||||
|
||||
return tryUrlencode({
|
||||
'uname': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'lol': secret
|
||||
})
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/logout.php' in output.lower()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user