Compare commits
161 Commits
tv_old
...
build/2.2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d08c7c57a8 | ||
|
|
eeeb845ef3 | ||
|
|
651a063f94 | ||
|
|
f20aaa2d9d | ||
|
|
ba925ec191 | ||
|
|
3b7376fd18 | ||
|
|
c31b10c798 | ||
|
|
acda664686 | ||
|
|
e2852407ea | ||
|
|
88e738c6cd | ||
|
|
eaae8bdb0b | ||
|
|
821f68909d | ||
|
|
2b8dfed475 | ||
|
|
0a749ce913 | ||
|
|
dfd2c33657 | ||
|
|
7aad27c3d2 | ||
|
|
7a5588d5de | ||
|
|
f1dde5c925 | ||
|
|
0eff4f0096 | ||
|
|
4d7fa08805 | ||
|
|
f0af184262 | ||
|
|
5a23be2224 | ||
|
|
7f87b255f9 | ||
|
|
5ac1118db3 | ||
|
|
2c46279617 | ||
|
|
5d6a9ad2d0 | ||
|
|
607b5ea766 | ||
|
|
88579cd71a | ||
|
|
6c57316ce6 | ||
|
|
6702683da3 | ||
|
|
b9c2b42725 | ||
|
|
1ed58586a1 | ||
|
|
a8369b4e93 | ||
|
|
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 | ||
|
|
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/about/'
|
||||
webbrowser.open(url)
|
||||
|
||||
def onTaskBarClose(self, evt):
|
||||
if self.closed:
|
||||
return
|
||||
|
||||
self.closed = True
|
||||
|
||||
self.RemoveIcon()
|
||||
wx.CallAfter(self.frame.Close)
|
||||
|
||||
|
||||
def makeIcon(self, img):
|
||||
if "wxMSW" in wx.PlatformInfo:
|
||||
img = img.Scale(16, 16)
|
||||
elif "wxGTK" in wx.PlatformInfo:
|
||||
img = img.Scale(22, 22)
|
||||
|
||||
icon = wx.IconFromBitmap(img.CopyFromBitmap())
|
||||
return icon
|
||||
|
||||
|
||||
class MainFrame(wx.Frame):
|
||||
|
||||
def __init__(self, parent):
|
||||
wx.Frame.__init__(self, None, style = wx.FRAME_NO_TASKBAR)
|
||||
|
||||
self.parent = parent
|
||||
self.tbicon = TaskBarIcon(self)
|
||||
|
||||
|
||||
class WorkerThread(Thread):
|
||||
|
||||
def __init__(self, desktop):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self._desktop = desktop
|
||||
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
|
||||
# Get options via arg
|
||||
from couchpotato.runner import getOptions
|
||||
args = ['--quiet']
|
||||
self.options = getOptions(base_path, args)
|
||||
|
||||
# Load settings
|
||||
settings = Env.get('settings')
|
||||
settings.setFile(self.options.config_file)
|
||||
|
||||
# Create data dir if needed
|
||||
self.data_dir = os.path.expanduser(Env.setting('data_dir'))
|
||||
if self.data_dir == '':
|
||||
from couchpotato.core.helpers.variable import getDataDir
|
||||
self.data_dir = getDataDir()
|
||||
|
||||
if not os.path.isdir(self.data_dir):
|
||||
os.makedirs(self.data_dir)
|
||||
|
||||
# Create logging dir
|
||||
self.log_dir = os.path.join(self.data_dir, 'logs');
|
||||
if not os.path.isdir(self.log_dir):
|
||||
os.mkdir(self.log_dir)
|
||||
|
||||
try:
|
||||
from couchpotato.runner import runCouchPotato
|
||||
runCouchPotato(self.options, base_path, args, data_dir = self.data_dir, log_dir = self.log_dir, Env = Env, desktop = self._desktop)
|
||||
except:
|
||||
pass
|
||||
|
||||
self._desktop.frame.Close()
|
||||
|
||||
|
||||
class CouchPotatoApp(wx.App, SoftwareUpdate):
|
||||
|
||||
settings = {}
|
||||
events = {}
|
||||
restart = False
|
||||
closing = False
|
||||
|
||||
def OnInit(self):
|
||||
|
||||
# Updater
|
||||
base_url = 'https://couchpota.to/updates/%s'
|
||||
self.InitUpdates(base_url % VERSION + '/', base_url % 'changelog.html',
|
||||
icon = wx.Icon('icon.png'))
|
||||
|
||||
self.frame = MainFrame(self)
|
||||
self.frame.Bind(wx.EVT_CLOSE, self.onClose)
|
||||
|
||||
# CouchPotato thread
|
||||
self.worker = WorkerThread(self)
|
||||
|
||||
return True
|
||||
|
||||
def onAppLoad(self):
|
||||
self.frame.tbicon.enable()
|
||||
|
||||
def setSettings(self, settings = {}):
|
||||
self.settings = settings
|
||||
|
||||
def getSetting(self, name):
|
||||
return self.settings.get(name)
|
||||
|
||||
def addEvents(self, events = {}):
|
||||
for name in events.iterkeys():
|
||||
self.events[name] = events[name]
|
||||
|
||||
def onClose(self, event):
|
||||
|
||||
if not self.closing:
|
||||
self.closing = True
|
||||
self.frame.tbicon.onTaskBarClose(event)
|
||||
|
||||
onClose = self.events.get('onClose')
|
||||
onClose(event)
|
||||
|
||||
def afterShutdown(self, restart = False):
|
||||
self.frame.Destroy()
|
||||
self.restart = restart
|
||||
self.ExitMainLoop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
app = CouchPotatoApp(redirect = False)
|
||||
app.MainLoop()
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if app.restart:
|
||||
|
||||
def appexe_from_executable(exepath):
|
||||
appdir = appdir_from_executable(exepath)
|
||||
exename = os.path.basename(exepath)
|
||||
|
||||
if sys.platform == "darwin":
|
||||
if os.path.isdir(os.path.join(appdir, "Contents", "MacOS")):
|
||||
return os.path.join(appdir, "Contents", "MacOS", exename)
|
||||
|
||||
return os.path.join(appdir, exename)
|
||||
|
||||
exe = appexe_from_executable(sys.executable)
|
||||
os.chdir(os.path.dirname(exe))
|
||||
|
||||
os.execv(exe, [exe] + sys.argv[1:])
|
||||
@@ -1,25 +1,15 @@
|
||||
## Got a issue/feature request or submitting a pull request?
|
||||
#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem!
|
||||
|
||||
Make sure you think of the following things:
|
||||
##Just make sure you think of the following things:
|
||||
|
||||
## Issue
|
||||
* Search through the existing (and closed) issues first, see if you can get your answer there.
|
||||
* Search through the existing (and closed) issues first. See if you can get your answer there.
|
||||
* Double check the result manually, because it could be an external issue.
|
||||
* Post logs! Without seeing what is going on, I can't reproduce the error.
|
||||
* Also check the logs before submitting, obvious errors like permission or http errors are often not related to CP.
|
||||
* What is the movie + quality you are searching for?
|
||||
* What are you're settings for the specific problem?
|
||||
* What providers are you using? (While you're logs include these, scanning through hundred of lines of log isn't our hobby)
|
||||
* Post the logs from config directory, please do not copy paste the UI. Use pastebin to store these logs!
|
||||
* Give a short step by step of how to reproduce the error.
|
||||
* What is the movie + quality you are searching for.
|
||||
* What are you settings for the specific problem.
|
||||
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby).
|
||||
* Give me a short step by step of how to reproduce.
|
||||
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
|
||||
* I will mark issues with the "can't reproduce" tag. Don't go asking "why closed" if it clearly says the issue in the tag ;)
|
||||
* If you're running on a NAS (QNAP, Austor etc..) with pre-made packages, make sure these are setup to use our source repo (RuudBurger/CouchPotatoServer) and nothing else!!
|
||||
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
|
||||
|
||||
## Pull Request
|
||||
* Make sure you're pull request is made for develop branch (or relevant feature branch)
|
||||
* Have you tested your PR? If not, why?
|
||||
* Are there any limitations of your PR we should know of?
|
||||
* Make sure to keep you're PR up-to-date with the branch you're trying to push into.
|
||||
|
||||
**If we don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
|
||||
**If I don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
|
||||
|
||||
@@ -44,13 +44,12 @@ class NonBlockHandler(RequestHandler):
|
||||
|
||||
def onNewMessage(self, response):
|
||||
if self.request.connection.stream.closed():
|
||||
self.on_connection_close()
|
||||
return
|
||||
|
||||
try:
|
||||
self.finish(response)
|
||||
except:
|
||||
log.debug('Failed doing nonblock request, probably already closed: %s', (traceback.format_exc()))
|
||||
log.error('Failed doing nonblock request: %s', (traceback.format_exc()))
|
||||
try: self.finish({'success': False, 'error': 'Failed returning results'})
|
||||
except: pass
|
||||
|
||||
@@ -110,7 +109,6 @@ class ApiHandler(RequestHandler):
|
||||
|
||||
if jsonp_callback:
|
||||
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
||||
self.set_header("Content-Type", "text/javascript")
|
||||
elif isinstance(result, tuple) and result[0] == 'redirect':
|
||||
self.redirect(result[1])
|
||||
else:
|
||||
|
||||
@@ -55,10 +55,6 @@ class Core(Plugin):
|
||||
if not Env.get('desktop'):
|
||||
self.signalHandler()
|
||||
|
||||
# Set default urlopen timeout
|
||||
import socket
|
||||
socket.setdefaulttimeout(30)
|
||||
|
||||
def md5Password(self, value):
|
||||
return md5(value) if value else ''
|
||||
|
||||
|
||||
@@ -34,8 +34,6 @@ class ClientScript(Plugin):
|
||||
'scripts/library/question.js',
|
||||
'scripts/library/scrollspy.js',
|
||||
'scripts/library/spin.js',
|
||||
'scripts/library/Array.stableSort.js',
|
||||
'scripts/library/async.js',
|
||||
'scripts/couchpotato.js',
|
||||
'scripts/api.js',
|
||||
'scripts/library/history.js',
|
||||
|
||||
@@ -31,13 +31,13 @@ class Scheduler(Plugin):
|
||||
pass
|
||||
|
||||
def doShutdown(self):
|
||||
super(Scheduler, self).doShutdown()
|
||||
self.stop()
|
||||
return super(Scheduler, self).doShutdown()
|
||||
|
||||
def stop(self):
|
||||
if self.started:
|
||||
log.debug('Stopping scheduler')
|
||||
self.sched.shutdown(wait = False)
|
||||
self.sched.shutdown()
|
||||
log.debug('Scheduler stopped')
|
||||
self.started = False
|
||||
|
||||
|
||||
@@ -183,6 +183,9 @@ class GitUpdater(BaseUpdater):
|
||||
def doUpdate(self):
|
||||
|
||||
try:
|
||||
log.debug('Stashing local changes')
|
||||
self.repo.saveStash()
|
||||
|
||||
log.info('Updating to latest version')
|
||||
self.repo.pull()
|
||||
|
||||
@@ -295,7 +298,6 @@ class SourceUpdater(BaseUpdater):
|
||||
|
||||
def replaceWith(self, path):
|
||||
app_dir = ss(Env.get('app_dir'))
|
||||
data_dir = ss(Env.get('data_dir'))
|
||||
|
||||
# Get list of files we want to overwrite
|
||||
self.deletePyc()
|
||||
@@ -327,15 +329,12 @@ class SourceUpdater(BaseUpdater):
|
||||
log.error('Failed overwriting file "%s": %s', (tofile, traceback.format_exc()))
|
||||
return False
|
||||
|
||||
for still_exists in existing_files:
|
||||
|
||||
if data_dir in still_exists:
|
||||
continue
|
||||
|
||||
try:
|
||||
os.remove(still_exists)
|
||||
except:
|
||||
log.error('Failed removing non-used file: %s', traceback.format_exc())
|
||||
if Env.get('app_dir') not in Env.get('data_dir'):
|
||||
for still_exists in existing_files:
|
||||
try:
|
||||
os.remove(still_exists)
|
||||
except:
|
||||
log.error('Failed removing non-used file: %s', traceback.format_exc())
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
|
||||
self.doUpdate();
|
||||
else {
|
||||
App.unBlockPage();
|
||||
App.on('message', 'No updates available');
|
||||
App.fireEvent('message', 'No updates available');
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
config = [{
|
||||
config = {
|
||||
'name': 'download_providers',
|
||||
'groups': [
|
||||
{
|
||||
@@ -10,4 +10,4 @@ config = [{
|
||||
'options': [],
|
||||
},
|
||||
],
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ class Downloader(Provider):
|
||||
|
||||
protocol = []
|
||||
http_time_between_calls = 0
|
||||
status_support = True
|
||||
|
||||
torrent_sources = [
|
||||
'http://torrage.com/torrent/%s.torrent',
|
||||
@@ -50,58 +49,53 @@ class Downloader(Provider):
|
||||
|
||||
return []
|
||||
|
||||
def _download(self, data = None, media = None, manual = False, filedata = None):
|
||||
if not media: media = {}
|
||||
def _download(self, data = None, movie = None, manual = False, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
if self.isDisabled(manual, data):
|
||||
return
|
||||
return self.download(data = data, media = media, filedata = filedata)
|
||||
return self.download(data = data, movie = movie, filedata = filedata)
|
||||
|
||||
def _getAllDownloadStatus(self, download_ids):
|
||||
def _getAllDownloadStatus(self):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
|
||||
ids = [download_id['id'] for download_id in download_ids if download_id['downloader'] == self.getName()]
|
||||
return self.getAllDownloadStatus()
|
||||
|
||||
if ids:
|
||||
return self.getAllDownloadStatus(ids)
|
||||
else:
|
||||
return
|
||||
def getAllDownloadStatus(self):
|
||||
return
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
return []
|
||||
|
||||
def _removeFailed(self, release_download):
|
||||
def _removeFailed(self, item):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
|
||||
if release_download and release_download.get('downloader') == self.getName():
|
||||
if item and item.get('downloader') == self.getName():
|
||||
if self.conf('delete_failed'):
|
||||
return self.removeFailed(release_download)
|
||||
return self.removeFailed(item)
|
||||
|
||||
return False
|
||||
return
|
||||
|
||||
def removeFailed(self, release_download):
|
||||
def removeFailed(self, item):
|
||||
return
|
||||
|
||||
def _processComplete(self, release_download):
|
||||
def _processComplete(self, item):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
|
||||
if release_download and release_download.get('downloader') == self.getName():
|
||||
if item and item.get('downloader') == self.getName():
|
||||
if self.conf('remove_complete', default = False):
|
||||
return self.processComplete(release_download = release_download, delete_files = self.conf('delete_files', default = False))
|
||||
return self.processComplete(item = item, delete_files = self.conf('delete_files', default = False))
|
||||
|
||||
return False
|
||||
return
|
||||
|
||||
def processComplete(self, release_download, delete_files):
|
||||
def processComplete(self, item, delete_files):
|
||||
return
|
||||
|
||||
def isCorrectProtocol(self, protocol):
|
||||
is_correct = protocol in self.protocol
|
||||
def isCorrectProtocol(self, item_protocol):
|
||||
is_correct = item_protocol in self.protocol
|
||||
|
||||
if not is_correct:
|
||||
log.debug("Downloader doesn't support this protocol")
|
||||
@@ -134,7 +128,6 @@ class Downloader(Provider):
|
||||
def downloadReturnId(self, download_id):
|
||||
return {
|
||||
'downloader': self.getName(),
|
||||
'status_support': self.status_support,
|
||||
'id': download_id
|
||||
}
|
||||
|
||||
@@ -158,20 +151,20 @@ class Downloader(Provider):
|
||||
(d_manual and manual or d_manual is False) and \
|
||||
(not data or self.isCorrectProtocol(data.get('protocol')))
|
||||
|
||||
def _pause(self, release_download, pause = True):
|
||||
def _pause(self, item, pause = True):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
|
||||
if release_download and release_download.get('downloader') == self.getName():
|
||||
self.pause(release_download, pause)
|
||||
if item and item.get('downloader') == self.getName():
|
||||
self.pause(item, pause)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def pause(self, release_download, pause):
|
||||
def pause(self, item, pause):
|
||||
return
|
||||
|
||||
class ReleaseDownloadList(list):
|
||||
class StatusList(list):
|
||||
|
||||
provider = None
|
||||
|
||||
@@ -180,7 +173,7 @@ class ReleaseDownloadList(list):
|
||||
self.provider = provider
|
||||
self.kwargs = kwargs
|
||||
|
||||
super(ReleaseDownloadList, self).__init__()
|
||||
super(StatusList, self).__init__()
|
||||
|
||||
def extend(self, results):
|
||||
for r in results:
|
||||
@@ -188,7 +181,7 @@ class ReleaseDownloadList(list):
|
||||
|
||||
def append(self, result):
|
||||
new_result = self.fillResult(result)
|
||||
super(ReleaseDownloadList, self).append(new_result)
|
||||
super(StatusList, self).append(new_result)
|
||||
|
||||
def fillResult(self, result):
|
||||
|
||||
@@ -197,7 +190,6 @@ class ReleaseDownloadList(list):
|
||||
'status': 'busy',
|
||||
'downloader': self.provider.getName(),
|
||||
'folder': '',
|
||||
'files': '',
|
||||
}
|
||||
|
||||
return mergeDicts(defaults, result)
|
||||
|
||||
@@ -13,7 +13,7 @@ config = [{
|
||||
'list': 'download_providers',
|
||||
'name': 'blackhole',
|
||||
'label': 'Black hole',
|
||||
'description': 'Download the NZB/Torrent to a specific folder. <em>Note: Seeding and copying/linking features do <strong>not</strong> work with Black hole</em>.',
|
||||
'description': 'Download the NZB/Torrent to a specific folder.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
@@ -35,13 +35,6 @@ config = [{
|
||||
'type': 'dropdown',
|
||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||
},
|
||||
{
|
||||
'name': 'create_subdir',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Create a sub directory when saving the .nzb (or .torrent).',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
|
||||
@@ -11,10 +11,9 @@ log = CPLog(__name__)
|
||||
class Blackhole(Downloader):
|
||||
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
status_support = False
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
@@ -34,28 +33,18 @@ class Blackhole(Downloader):
|
||||
log.error('No nzb/torrent available: %s', data.get('url'))
|
||||
return False
|
||||
|
||||
file_name = self.createFileName(data, filedata, media)
|
||||
full_path = os.path.join(directory, file_name)
|
||||
|
||||
if self.conf('create_subdir'):
|
||||
try:
|
||||
new_path = os.path.splitext(full_path)[0]
|
||||
if not os.path.exists(new_path):
|
||||
os.makedirs(new_path)
|
||||
full_path = os.path.join(new_path, file_name)
|
||||
except:
|
||||
log.error('Couldnt create sub dir, reverting to old one: %s', full_path)
|
||||
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
|
||||
|
||||
try:
|
||||
if not os.path.isfile(full_path):
|
||||
log.info('Downloading %s to %s.', (data.get('protocol'), full_path))
|
||||
with open(full_path, 'wb') as f:
|
||||
if not os.path.isfile(fullPath):
|
||||
log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
|
||||
with open(fullPath, 'wb') as f:
|
||||
f.write(filedata)
|
||||
os.chmod(full_path, Env.getPermission('file'))
|
||||
return self.downloadReturnId('')
|
||||
os.chmod(fullPath, Env.getPermission('file'))
|
||||
return True
|
||||
else:
|
||||
log.info('File %s already exists.', full_path)
|
||||
return self.downloadReturnId('')
|
||||
log.info('File %s already exists.', fullPath)
|
||||
return True
|
||||
|
||||
except:
|
||||
log.error('Failed to download to blackhole %s', traceback.format_exc())
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from base64 import b64encode, b16encode, b32decode
|
||||
from bencode import bencode as benc, bdecode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import isInt, sp
|
||||
from base64 import b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import isInt, ss
|
||||
from couchpotato.core.helpers.variable import tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from synchronousdeluge import DelugeClient
|
||||
import os.path
|
||||
import re
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -32,10 +30,7 @@ class Deluge(Downloader):
|
||||
|
||||
return self.drpc
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
|
||||
|
||||
if not self.connect():
|
||||
@@ -76,8 +71,8 @@ class Deluge(Downloader):
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
|
||||
else:
|
||||
filename = self.createFileName(data, filedata, media)
|
||||
remote_torrent = self.drpc.add_torrent_file(filename, filedata, options)
|
||||
filename = self.createFileName(data, filedata, movie)
|
||||
remote_torrent = self.drpc.add_torrent_file(filename, b64encode(filedata), options)
|
||||
|
||||
if not remote_torrent:
|
||||
log.error('Failed sending torrent to Deluge')
|
||||
@@ -86,71 +81,70 @@ class Deluge(Downloader):
|
||||
log.info('Torrent sent to Deluge successfully.')
|
||||
return self.downloadReturnId(remote_torrent)
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking Deluge download status.')
|
||||
|
||||
if not os.path.isdir(Env.setting('from', 'renamer')):
|
||||
log.error('Renamer "from" folder doesn\'t to exist.')
|
||||
return
|
||||
|
||||
if not self.connect():
|
||||
return []
|
||||
return False
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
statuses = StatusList(self)
|
||||
|
||||
queue = self.drpc.get_alltorrents(ids)
|
||||
queue = self.drpc.get_alltorrents()
|
||||
|
||||
if not queue:
|
||||
log.debug('Nothing in queue or error')
|
||||
return []
|
||||
return False
|
||||
|
||||
for torrent_id in queue:
|
||||
torrent = queue[torrent_id]
|
||||
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
|
||||
|
||||
item = queue[torrent_id]
|
||||
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (item['name'], item['hash'], item['save_path'], item['move_completed_path'], item['hash'], item['progress'], item['state'], item['eta'], item['ratio'], item['stop_ratio'], item['is_seed'], item['is_finished'], item['paused']))
|
||||
|
||||
# Deluge has no easy way to work out if a torrent is stalled or failing.
|
||||
#status = 'failed'
|
||||
status = 'busy'
|
||||
if torrent['is_seed'] and tryFloat(torrent['ratio']) < tryFloat(torrent['stop_ratio']):
|
||||
# We have torrent['seeding_time'] to work out what the seeding time is, but we do not
|
||||
if item['is_seed'] and tryFloat(item['ratio']) < tryFloat(item['stop_ratio']):
|
||||
# We have item['seeding_time'] to work out what the seeding time is, but we do not
|
||||
# have access to the downloader seed_time, as with deluge we have no way to pass it
|
||||
# when the torrent is added. So Deluge will only look at the ratio.
|
||||
# See above comment in download().
|
||||
status = 'seeding'
|
||||
elif torrent['is_seed'] and torrent['is_finished'] and torrent['paused'] and torrent['state'] == 'Paused':
|
||||
elif item['is_seed'] and item['is_finished'] and item['paused'] and item['state'] == 'Paused':
|
||||
status = 'completed'
|
||||
|
||||
download_dir = sp(torrent['save_path'])
|
||||
if torrent['move_on_completed']:
|
||||
download_dir = torrent['move_completed_path']
|
||||
|
||||
torrent_files = []
|
||||
for file_item in torrent['files']:
|
||||
torrent_files.append(sp(os.path.join(download_dir, file_item['path'])))
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent['hash'],
|
||||
'name': torrent['name'],
|
||||
download_dir = item['save_path']
|
||||
if item['move_on_completed']:
|
||||
download_dir = item['move_completed_path']
|
||||
|
||||
statuses.append({
|
||||
'id': item['hash'],
|
||||
'name': item['name'],
|
||||
'status': status,
|
||||
'original_status': torrent['state'],
|
||||
'seed_ratio': torrent['ratio'],
|
||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||
'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])),
|
||||
'files': '|'.join(torrent_files),
|
||||
'original_status': item['state'],
|
||||
'seed_ratio': item['ratio'],
|
||||
'timeleft': str(timedelta(seconds = item['eta'])),
|
||||
'folder': ss(os.path.join(download_dir, item['name'])),
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
return statuses
|
||||
|
||||
def pause(self, release_download, pause = True):
|
||||
def pause(self, item, pause = True):
|
||||
if pause:
|
||||
return self.drpc.pause_torrent([release_download['id']])
|
||||
return self.drpc.pause_torrent([item['id']])
|
||||
else:
|
||||
return self.drpc.resume_torrent([release_download['id']])
|
||||
return self.drpc.resume_torrent([item['id']])
|
||||
|
||||
def removeFailed(self, release_download):
|
||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||
return self.drpc.remove_torrent(release_download['id'], True)
|
||||
def removeFailed(self, item):
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
return self.drpc.remove_torrent(item['id'], True)
|
||||
|
||||
def processComplete(self, release_download, delete_files = False):
|
||||
log.debug('Requesting Deluge to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
return self.drpc.remove_torrent(release_download['id'], remove_local_data = delete_files)
|
||||
def processComplete(self, item, delete_files = False):
|
||||
log.debug('Requesting Deluge to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
return self.drpc.remove_torrent(item['id'], remove_local_data = delete_files)
|
||||
|
||||
class DelugeRPC(object):
|
||||
|
||||
@@ -177,10 +171,7 @@ class DelugeRPC(object):
|
||||
try:
|
||||
self.connect()
|
||||
torrent_id = self.client.core.add_torrent_magnet(torrent, options).get()
|
||||
if not torrent_id:
|
||||
torrent_id = self._check_torrent(True, torrent)
|
||||
|
||||
if torrent_id and options['label']:
|
||||
if options['label']:
|
||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||
except Exception, err:
|
||||
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
|
||||
@@ -194,11 +185,8 @@ class DelugeRPC(object):
|
||||
torrent_id = False
|
||||
try:
|
||||
self.connect()
|
||||
torrent_id = self.client.core.add_torrent_file(filename, b64encode(torrent), options).get()
|
||||
if not torrent_id:
|
||||
torrent_id = self._check_torrent(False, torrent)
|
||||
|
||||
if torrent_id and options['label']:
|
||||
torrent_id = self.client.core.add_torrent_file(filename, torrent, options).get()
|
||||
if options['label']:
|
||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||
except Exception, err:
|
||||
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
|
||||
@@ -208,11 +196,11 @@ class DelugeRPC(object):
|
||||
|
||||
return torrent_id
|
||||
|
||||
def get_alltorrents(self, ids):
|
||||
def get_alltorrents(self):
|
||||
ret = False
|
||||
try:
|
||||
self.connect()
|
||||
ret = self.client.core.get_torrents_status({'id': ids}, {}).get()
|
||||
ret = self.client.core.get_torrents_status({}, {}).get()
|
||||
except Exception, err:
|
||||
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
@@ -254,22 +242,3 @@ class DelugeRPC(object):
|
||||
|
||||
def disconnect(self):
|
||||
self.client.disconnect()
|
||||
|
||||
def _check_torrent(self, magnet, torrent):
|
||||
# Torrent not added, check if it already existed.
|
||||
if magnet:
|
||||
torrent_hash = re.findall('urn:btih:([\w]{32,40})', torrent)[0]
|
||||
else:
|
||||
info = bdecode(torrent)["info"]
|
||||
torrent_hash = sha1(benc(info)).hexdigest()
|
||||
|
||||
# Convert base 32 to hex
|
||||
if len(torrent_hash) == 32:
|
||||
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||
|
||||
torrent_hash = torrent_hash.lower()
|
||||
torrent_check = self.client.core.get_torrent_status(torrent_hash, {}).get()
|
||||
if torrent_check['hash']:
|
||||
return torrent_hash
|
||||
|
||||
return False
|
||||
|
||||
@@ -12,7 +12,6 @@ config = [{
|
||||
'name': 'nzbget',
|
||||
'label': 'NZBGet',
|
||||
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
@@ -25,13 +24,6 @@ config = [{
|
||||
'default': 'localhost:6789',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'ssl',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': 'nzbget',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from base64 import standard_b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import ss, sp
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.variable import tryInt, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
@@ -17,10 +17,10 @@ class NZBGet(Downloader):
|
||||
|
||||
protocol = ['nzb']
|
||||
|
||||
url = '%(protocol)s://%(username)s:%(password)s@%(host)s/xmlrpc'
|
||||
url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc'
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
if not filedata:
|
||||
@@ -29,8 +29,8 @@ class NZBGet(Downloader):
|
||||
|
||||
log.info('Sending "%s" to NZBGet.', data.get('name'))
|
||||
|
||||
url = self.url % {'protocol': 'https' if self.conf('ssl') else 'http', 'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
nzb_name = ss('%s.nzb' % self.createNzbName(data, media))
|
||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
@@ -67,11 +67,11 @@ class NZBGet(Downloader):
|
||||
log.error('NZBGet could not add %s to the queue.', nzb_name)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking NZBGet download status.')
|
||||
|
||||
url = self.url % {'protocol': 'https' if self.conf('ssl') else 'http', 'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
@@ -81,13 +81,13 @@ class NZBGet(Downloader):
|
||||
log.info('Successfully connected to NZBGet, but unable to send a message')
|
||||
except socket.error:
|
||||
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
|
||||
return []
|
||||
return False
|
||||
except xmlrpclib.ProtocolError, e:
|
||||
if e.errcode == 401:
|
||||
log.error('Password is incorrect.')
|
||||
else:
|
||||
log.error('Protocol Error: %s', e)
|
||||
return []
|
||||
return False
|
||||
|
||||
# Get NZBGet data
|
||||
try:
|
||||
@@ -97,65 +97,62 @@ class NZBGet(Downloader):
|
||||
history = rpc.history()
|
||||
except:
|
||||
log.error('Failed getting data: %s', traceback.format_exc(1))
|
||||
return []
|
||||
return False
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
statuses = StatusList(self)
|
||||
|
||||
for nzb in groups:
|
||||
for item in groups:
|
||||
log.debug('Found %s in NZBGet download queue', item['NZBFilename'])
|
||||
try:
|
||||
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
except:
|
||||
nzb_id = nzb['NZBID']
|
||||
nzb_id = item['NZBID']
|
||||
|
||||
if nzb_id in ids:
|
||||
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
|
||||
timeleft = -1
|
||||
try:
|
||||
if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
||||
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||
except:
|
||||
pass
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED',
|
||||
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
|
||||
'timeleft': timeleft,
|
||||
})
|
||||
|
||||
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||
if nzb['NZBID'] in ids:
|
||||
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
|
||||
release_downloads.append({
|
||||
'id': nzb['NZBID'],
|
||||
'name': nzb['NZBFilename'],
|
||||
'original_status': nzb['Stage'],
|
||||
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
|
||||
})
|
||||
|
||||
for nzb in history:
|
||||
timeleft = -1
|
||||
try:
|
||||
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
if item['ActiveDownloads'] > 0 and item['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
||||
timeleft = str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||
except:
|
||||
nzb_id = nzb['NZBID']
|
||||
pass
|
||||
|
||||
if nzb_id in ids:
|
||||
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
|
||||
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(nzb['DestDir'])
|
||||
})
|
||||
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': timeleft,
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
for item in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||
log.debug('Found %s in NZBGet postprocessing queue', item['NZBFilename'])
|
||||
statuses.append({
|
||||
'id': item['NZBID'],
|
||||
'name': item['NZBFilename'],
|
||||
'original_status': item['Stage'],
|
||||
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
|
||||
})
|
||||
|
||||
def removeFailed(self, release_download):
|
||||
for item in history:
|
||||
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log']))
|
||||
try:
|
||||
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
except:
|
||||
nzb_id = item['NZBID']
|
||||
statuses.append({
|
||||
'id': nzb_id,
|
||||
'name': item['NZBFilename'],
|
||||
'status': 'completed' if item['ParStatus'] in ['SUCCESS','NONE'] and item['ScriptStatus'] in ['SUCCESS','NONE'] else 'failed',
|
||||
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': ss(item['DestDir'])
|
||||
})
|
||||
|
||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||
return statuses
|
||||
|
||||
def removeFailed(self, item):
|
||||
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
|
||||
@@ -182,9 +179,9 @@ class NZBGet(Downloader):
|
||||
|
||||
for hist in history:
|
||||
for param in hist['Parameters']:
|
||||
if param['Name'] == 'couchpotato' and param['Value'] == release_download['id']:
|
||||
if param['Name'] == 'couchpotato' and param['Value'] == item['id']:
|
||||
nzb_id = hist['ID']
|
||||
path = hist['DestDir']
|
||||
path = hist['DestDir']
|
||||
|
||||
if nzb_id and path and rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
|
||||
shutil.rmtree(path, True)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from base64 import b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, sp
|
||||
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
|
||||
from urllib2 import URLError
|
||||
@@ -8,11 +8,9 @@ from uuid import uuid4
|
||||
import hashlib
|
||||
import httplib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
@@ -25,55 +23,53 @@ class NZBVortex(Downloader):
|
||||
api_level = None
|
||||
session_id = None
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
# Send the nzb
|
||||
try:
|
||||
nzb_filename = self.createFileName(data, filedata, media)
|
||||
self.call('nzb/add', files = {'file': (nzb_filename, filedata)})
|
||||
nzb_filename = self.createFileName(data, filedata, movie)
|
||||
self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
|
||||
|
||||
time.sleep(10)
|
||||
raw_statuses = self.call('nzb')
|
||||
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(item['nzbFileName']) == nzb_filename][0]
|
||||
nzb_id = [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
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
raw_statuses = self.call('nzb')
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
for nzb in raw_statuses.get('nzbs', []):
|
||||
if nzb['id'] in ids:
|
||||
statuses = StatusList(self)
|
||||
for item in raw_statuses.get('nzbs', []):
|
||||
|
||||
# Check status
|
||||
status = 'busy'
|
||||
if nzb['state'] == 20:
|
||||
status = 'completed'
|
||||
elif nzb['state'] in [21, 22, 24]:
|
||||
status = 'failed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['id'],
|
||||
'name': nzb['uiTitle'],
|
||||
'status': status,
|
||||
'original_status': nzb['state'],
|
||||
'timeleft':-1,
|
||||
'folder': sp(nzb['destinationPath']),
|
||||
})
|
||||
# Check status
|
||||
status = 'busy'
|
||||
if item['state'] == 20:
|
||||
status = 'completed'
|
||||
elif item['state'] in [21, 22, 24]:
|
||||
status = 'failed'
|
||||
|
||||
return release_downloads
|
||||
statuses.append({
|
||||
'id': item['id'],
|
||||
'name': item['uiTitle'],
|
||||
'status': status,
|
||||
'original_status': item['state'],
|
||||
'timeleft':-1,
|
||||
'folder': ss(item['destinationPath']),
|
||||
})
|
||||
|
||||
def removeFailed(self, release_download):
|
||||
return statuses
|
||||
|
||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||
def removeFailed(self, item):
|
||||
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
|
||||
try:
|
||||
self.call('nzb/%s/cancel' % release_download['id'])
|
||||
self.call('nzb/%s/cancel' % item['id'])
|
||||
except:
|
||||
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||
return False
|
||||
@@ -117,9 +113,10 @@ class NZBVortex(Downloader):
|
||||
params = tryUrlencode(parameters)
|
||||
|
||||
url = cleanHost(self.conf('host')) + 'api/' + call
|
||||
url_opener = urllib2.build_opener(HTTPSHandler())
|
||||
|
||||
try:
|
||||
data = self.urlopen('%s?%s' % (url, params), *args, **kwargs)
|
||||
data = self.urlopen('%s?%s' % (url, params), opener = url_opener, *args, **kwargs)
|
||||
|
||||
if data:
|
||||
return json.loads(data)
|
||||
@@ -141,9 +138,10 @@ class NZBVortex(Downloader):
|
||||
if not self.api_level:
|
||||
|
||||
url = cleanHost(self.conf('host')) + 'api/app/apilevel'
|
||||
url_opener = urllib2.build_opener(HTTPSHandler())
|
||||
|
||||
try:
|
||||
data = self.urlopen(url, show_error = False)
|
||||
data = self.urlopen(url, opener = url_opener, show_error = False)
|
||||
self.api_level = float(json.loads(data).get('apilevel'))
|
||||
except URLError, e:
|
||||
if hasattr(e, 'code') and e.code == 403:
|
||||
|
||||
@@ -11,10 +11,9 @@ class Pneumatic(Downloader):
|
||||
|
||||
protocol = ['nzb']
|
||||
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
|
||||
status_support = False
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
@@ -26,7 +25,7 @@ class Pneumatic(Downloader):
|
||||
log.error('No nzb available!')
|
||||
return False
|
||||
|
||||
fullPath = os.path.join(directory, self.createFileName(data, filedata, media))
|
||||
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
|
||||
|
||||
try:
|
||||
if not os.path.isfile(fullPath):
|
||||
@@ -34,7 +33,7 @@ class Pneumatic(Downloader):
|
||||
with open(fullPath, 'wb') as f:
|
||||
f.write(filedata)
|
||||
|
||||
nzb_name = self.createNzbName(data, media)
|
||||
nzb_name = self.createNzbName(data, movie)
|
||||
strm_path = os.path.join(directory, nzb_name)
|
||||
|
||||
strm_file = open(strm_path + '.strm', 'wb')
|
||||
@@ -42,11 +41,11 @@ class Pneumatic(Downloader):
|
||||
strm_file.write(strmContent)
|
||||
strm_file.close()
|
||||
|
||||
return self.downloadReturnId('')
|
||||
return True
|
||||
|
||||
else:
|
||||
log.info('File %s already exists.', fullPath)
|
||||
return self.downloadReturnId('')
|
||||
return True
|
||||
|
||||
except:
|
||||
log.error('Failed to download .strm: %s', traceback.format_exc())
|
||||
|
||||
@@ -23,8 +23,6 @@ config = [{
|
||||
{
|
||||
'name': 'url',
|
||||
'default': 'http://localhost:80/RPC2',
|
||||
'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
|
||||
'or <strong>http://localhost:80/RPC2</strong>'
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
@@ -37,11 +35,6 @@ config = [{
|
||||
'name': 'label',
|
||||
'description': 'Label to apply on added torrents.',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Download to this directory. Keep empty for default rTorrent download directory.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from base64 import b16encode, b32decode
|
||||
from bencode import bencode, bdecode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from rtorrent import RTorrent
|
||||
from rtorrent.err import MethodError
|
||||
import os
|
||||
import shutil
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -71,16 +71,13 @@ class rTorrent(Downloader):
|
||||
group.set_command()
|
||||
group.disable()
|
||||
except MethodError, err:
|
||||
log.error('Unable to set group options: %s', err.msg)
|
||||
log.error('Unable to set group options: %s', err.message)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
|
||||
|
||||
if not self.connect():
|
||||
@@ -94,7 +91,6 @@ class rTorrent(Downloader):
|
||||
if self.conf('label'):
|
||||
torrent_params['label'] = self.conf('label')
|
||||
|
||||
|
||||
if not filedata and data.get('protocol') == 'torrent':
|
||||
log.error('Failed sending torrent, no data')
|
||||
return False
|
||||
@@ -120,17 +116,10 @@ class rTorrent(Downloader):
|
||||
# Send torrent to rTorrent
|
||||
torrent = self.rt.load_torrent(filedata)
|
||||
|
||||
if not torrent:
|
||||
log.error('Unable to find the torrent, did it fail to load?')
|
||||
return False
|
||||
|
||||
# Set label
|
||||
if self.conf('label'):
|
||||
torrent.set_custom(1, self.conf('label'))
|
||||
|
||||
if self.conf('directory'):
|
||||
torrent.set_directory(self.conf('directory'))
|
||||
|
||||
# Set Ratio Group
|
||||
torrent.set_visible(group_name)
|
||||
|
||||
@@ -143,52 +132,46 @@ class rTorrent(Downloader):
|
||||
log.error('Failed to send torrent to rTorrent: %s', err)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
def getAllDownloadStatus(self):
|
||||
log.debug('Checking rTorrent download status.')
|
||||
|
||||
if not self.connect():
|
||||
return []
|
||||
return False
|
||||
|
||||
try:
|
||||
torrents = self.rt.get_torrents()
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
statuses = StatusList(self)
|
||||
|
||||
for torrent in torrents:
|
||||
if torrent.info_hash in ids:
|
||||
torrent_files = []
|
||||
for file_item in torrent.get_files():
|
||||
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
|
||||
|
||||
status = 'busy'
|
||||
if torrent.complete:
|
||||
if torrent.active:
|
||||
status = 'seeding'
|
||||
else:
|
||||
status = 'completed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent.info_hash,
|
||||
'name': torrent.name,
|
||||
'status': status,
|
||||
'seed_ratio': torrent.ratio,
|
||||
'original_status': torrent.state,
|
||||
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
|
||||
'folder': sp(torrent.directory),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
for item in torrents:
|
||||
status = 'busy'
|
||||
if item.complete:
|
||||
if item.active:
|
||||
status = 'seeding'
|
||||
else:
|
||||
status = 'completed'
|
||||
|
||||
return release_downloads
|
||||
statuses.append({
|
||||
'id': item.info_hash,
|
||||
'name': item.name,
|
||||
'status': status,
|
||||
'seed_ratio': item.ratio,
|
||||
'original_status': item.state,
|
||||
'timeleft': str(timedelta(seconds = float(item.left_bytes) / item.down_rate)) if item.down_rate > 0 else -1,
|
||||
'folder': ss(item.directory)
|
||||
})
|
||||
|
||||
return statuses
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed to get status from rTorrent: %s', err)
|
||||
return []
|
||||
return False
|
||||
|
||||
def pause(self, release_download, pause = True):
|
||||
def pause(self, download_info, pause = True):
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent = self.rt.find_torrent(release_download['id'])
|
||||
torrent = self.rt.find_torrent(download_info['id'])
|
||||
if torrent is None:
|
||||
return False
|
||||
|
||||
@@ -196,34 +179,23 @@ class rTorrent(Downloader):
|
||||
return torrent.pause()
|
||||
return torrent.resume()
|
||||
|
||||
def removeFailed(self, release_download):
|
||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||
return self.processComplete(release_download, delete_files = True)
|
||||
def removeFailed(self, item):
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
return self.processComplete(item, delete_files = True)
|
||||
|
||||
def processComplete(self, release_download, delete_files):
|
||||
def processComplete(self, item, delete_files):
|
||||
log.debug('Requesting rTorrent to remove the torrent %s%s.',
|
||||
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
|
||||
(item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent = self.rt.find_torrent(release_download['id'])
|
||||
|
||||
torrent = self.rt.find_torrent(item['id'])
|
||||
if torrent is None:
|
||||
return False
|
||||
|
||||
if delete_files:
|
||||
for file_item in torrent.get_files(): # will only delete files, not dir/sub-dir
|
||||
os.unlink(os.path.join(torrent.directory, file_item.path))
|
||||
|
||||
if torrent.is_multi_file() and torrent.directory.endswith(torrent.name):
|
||||
# Remove empty directories bottom up
|
||||
try:
|
||||
for path, _, _ in os.walk(torrent.directory, topdown = False):
|
||||
os.rmdir(path)
|
||||
except OSError:
|
||||
log.info('Directory "%s" contains extra files, unable to remove', torrent.directory)
|
||||
|
||||
torrent.erase() # just removes the torrent, doesn't delete data
|
||||
|
||||
if delete_files:
|
||||
shutil.rmtree(item['folder'], True)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp
|
||||
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 os
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -16,8 +15,8 @@ class Sabnzbd(Downloader):
|
||||
|
||||
protocol = ['nzb']
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
||||
@@ -25,7 +24,7 @@ class Sabnzbd(Downloader):
|
||||
req_params = {
|
||||
'cat': self.conf('category'),
|
||||
'mode': 'addurl',
|
||||
'nzbname': self.createNzbName(data, media),
|
||||
'nzbname': self.createNzbName(data, movie),
|
||||
'priority': self.conf('priority'),
|
||||
}
|
||||
|
||||
@@ -36,14 +35,14 @@ class Sabnzbd(Downloader):
|
||||
return False
|
||||
|
||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||
nzb_filename = self.createFileName(data, filedata, media)
|
||||
nzb_filename = self.createFileName(data, filedata, movie)
|
||||
req_params['mode'] = 'addfile'
|
||||
else:
|
||||
req_params['name'] = data.get('url')
|
||||
|
||||
try:
|
||||
if nzb_filename and req_params.get('mode') is 'addfile':
|
||||
sab_data = self.call(req_params, files = {'nzbfile': (ss(nzb_filename), filedata)})
|
||||
sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
|
||||
else:
|
||||
sab_data = self.call(req_params)
|
||||
except URLError:
|
||||
@@ -64,7 +63,7 @@ class Sabnzbd(Downloader):
|
||||
log.error('Error getting data from SABNZBd: %s', sab_data)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking SABnzbd download status.')
|
||||
|
||||
@@ -75,7 +74,7 @@ class Sabnzbd(Downloader):
|
||||
})
|
||||
except:
|
||||
log.error('Failed getting queue: %s', traceback.format_exc(1))
|
||||
return []
|
||||
return False
|
||||
|
||||
# Go through history items
|
||||
try:
|
||||
@@ -85,61 +84,49 @@ class Sabnzbd(Downloader):
|
||||
})
|
||||
except:
|
||||
log.error('Failed getting history json: %s', traceback.format_exc(1))
|
||||
return []
|
||||
return False
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
statuses = StatusList(self)
|
||||
|
||||
# Get busy releases
|
||||
for nzb in queue.get('slots', []):
|
||||
if nzb['nzo_id'] in ids:
|
||||
status = 'busy'
|
||||
if 'ENCRYPTED / ' in nzb['filename']:
|
||||
status = 'failed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['filename'],
|
||||
'status': status,
|
||||
'original_status': nzb['status'],
|
||||
'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
|
||||
})
|
||||
for item in queue.get('slots', []):
|
||||
statuses.append({
|
||||
'id': item['nzo_id'],
|
||||
'name': item['filename'],
|
||||
'original_status': item['status'],
|
||||
'timeleft': item['timeleft'] if not queue['paused'] else -1,
|
||||
})
|
||||
|
||||
# Get old releases
|
||||
for nzb in history.get('slots', []):
|
||||
if nzb['nzo_id'] in ids:
|
||||
status = 'busy'
|
||||
if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()):
|
||||
status = 'failed'
|
||||
elif nzb['status'] == 'Completed':
|
||||
status = 'completed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['name'],
|
||||
'status': status,
|
||||
'original_status': nzb['status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']),
|
||||
})
|
||||
for item in history.get('slots', []):
|
||||
|
||||
return release_downloads
|
||||
status = 'busy'
|
||||
if item['status'] == 'Failed' or (item['status'] == 'Completed' and item['fail_message'].strip()):
|
||||
status = 'failed'
|
||||
elif item['status'] == 'Completed':
|
||||
status = 'completed'
|
||||
|
||||
def removeFailed(self, release_download):
|
||||
statuses.append({
|
||||
'id': item['nzo_id'],
|
||||
'name': item['name'],
|
||||
'status': status,
|
||||
'original_status': item['status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': ss(item['storage']),
|
||||
})
|
||||
|
||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||
return statuses
|
||||
|
||||
def removeFailed(self, item):
|
||||
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
|
||||
try:
|
||||
self.call({
|
||||
'mode': 'queue',
|
||||
'name': 'delete',
|
||||
'del_files': '1',
|
||||
'value': release_download['id']
|
||||
}, use_json = False)
|
||||
self.call({
|
||||
'mode': 'history',
|
||||
'name': 'delete',
|
||||
'del_files': '1',
|
||||
'value': release_download['id']
|
||||
'value': item['id']
|
||||
}, use_json = False)
|
||||
except:
|
||||
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||
@@ -147,15 +134,15 @@ class Sabnzbd(Downloader):
|
||||
|
||||
return True
|
||||
|
||||
def processComplete(self, release_download, delete_files = False):
|
||||
log.debug('Requesting SabNZBd to remove the NZB %s.', release_download['name'])
|
||||
def processComplete(self, item, delete_files = False):
|
||||
log.debug('Requesting SabNZBd to remove the NZB %s.', item['name'])
|
||||
|
||||
try:
|
||||
self.call({
|
||||
'mode': 'history',
|
||||
'name': 'delete',
|
||||
'del_files': '0',
|
||||
'value': release_download['id']
|
||||
'value': item['id']
|
||||
}, use_json = False)
|
||||
except:
|
||||
log.error('Failed removing: %s', traceback.format_exc(0))
|
||||
|
||||
@@ -3,7 +3,6 @@ from couchpotato.core.helpers.encoding import isInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
import json
|
||||
import requests
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -11,10 +10,10 @@ log = CPLog(__name__)
|
||||
class Synology(Downloader):
|
||||
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
status_support = False
|
||||
log = CPLog(__name__)
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
response = False
|
||||
@@ -35,14 +34,14 @@ class Synology(Downloader):
|
||||
elif data['protocol'] in ['nzb', 'torrent']:
|
||||
log.info('Adding %s' % data['protocol'])
|
||||
if not filedata:
|
||||
log.error('No %s data found', data['protocol'])
|
||||
log.error('No %s data found' % data['protocol'])
|
||||
else:
|
||||
filename = data['name'] + '.' + data['protocol']
|
||||
response = srpc.create_task(filename = filename, filedata = filedata)
|
||||
except:
|
||||
log.error('Exception while adding torrent: %s', traceback.format_exc())
|
||||
except Exception, err:
|
||||
log.error('Exception while adding torrent: %s', err)
|
||||
finally:
|
||||
return self.downloadReturnId('') if response else False
|
||||
return response
|
||||
|
||||
def getEnabledProtocol(self):
|
||||
if self.conf('use_for') == 'both':
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from base64 import b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import isInt, sp
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import isInt, ss
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from datetime import timedelta
|
||||
import httplib
|
||||
import json
|
||||
@@ -31,9 +32,7 @@ class Transmission(Downloader):
|
||||
|
||||
return self.trpc
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
def download(self, data, movie, filedata = None):
|
||||
|
||||
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol')))
|
||||
|
||||
@@ -83,67 +82,65 @@ class Transmission(Downloader):
|
||||
log.info('Torrent sent to Transmission successfully.')
|
||||
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking Transmission download status.')
|
||||
|
||||
if not self.connect():
|
||||
return []
|
||||
return False
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
statuses = StatusList(self)
|
||||
|
||||
return_params = {
|
||||
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit', 'files']
|
||||
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit']
|
||||
}
|
||||
|
||||
queue = self.trpc.get_alltorrents(return_params)
|
||||
if not (queue and queue.get('torrents')):
|
||||
log.debug('Nothing in queue or error')
|
||||
return []
|
||||
return False
|
||||
|
||||
for torrent in queue['torrents']:
|
||||
if torrent['hashString'] in ids:
|
||||
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / isStalled=%s / eta=%s / uploadRatio=%s / isFinished=%s',
|
||||
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent.get('isStalled', 'N/A'), torrent['eta'], torrent['uploadRatio'], torrent['isFinished']))
|
||||
|
||||
torrent_files = []
|
||||
for file_item in torrent['files']:
|
||||
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name'])))
|
||||
|
||||
status = 'busy'
|
||||
if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'):
|
||||
status = 'failed'
|
||||
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
|
||||
status = 'completed'
|
||||
elif torrent['status'] in [5, 6]:
|
||||
status = 'seeding'
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent['hashString'],
|
||||
'name': torrent['name'],
|
||||
'status': status,
|
||||
'original_status': torrent['status'],
|
||||
'seed_ratio': torrent['uploadRatio'],
|
||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
for item in queue['torrents']:
|
||||
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s',
|
||||
(item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], item['isFinished']))
|
||||
|
||||
return release_downloads
|
||||
if not os.path.isdir(Env.setting('from', 'renamer')):
|
||||
log.error('Renamer "from" folder doesn\'t to exist.')
|
||||
return
|
||||
|
||||
def pause(self, release_download, pause = True):
|
||||
status = 'busy'
|
||||
if item['isStalled'] and self.conf('stalled_as_failed'):
|
||||
status = 'failed'
|
||||
elif item['status'] == 0 and item['percentDone'] == 1:
|
||||
status = 'completed'
|
||||
elif item['status'] in [5, 6]:
|
||||
status = 'seeding'
|
||||
|
||||
statuses.append({
|
||||
'id': item['hashString'],
|
||||
'name': item['name'],
|
||||
'status': status,
|
||||
'original_status': item['status'],
|
||||
'seed_ratio': item['uploadRatio'],
|
||||
'timeleft': str(timedelta(seconds = item['eta'])),
|
||||
'folder': ss(os.path.join(item['downloadDir'], item['name'])),
|
||||
})
|
||||
|
||||
return statuses
|
||||
|
||||
def pause(self, item, pause = True):
|
||||
if pause:
|
||||
return self.trpc.stop_torrent(release_download['id'])
|
||||
return self.trpc.stop_torrent(item['id'])
|
||||
else:
|
||||
return self.trpc.start_torrent(release_download['id'])
|
||||
return self.trpc.start_torrent(item['id'])
|
||||
|
||||
def removeFailed(self, release_download):
|
||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||
return self.trpc.remove_torrent(release_download['id'], True)
|
||||
def removeFailed(self, item):
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
return self.trpc.remove_torrent(item['hashString'], True)
|
||||
|
||||
def processComplete(self, release_download, delete_files = False):
|
||||
log.debug('Requesting Transmission to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
return self.trpc.remove_torrent(release_download['id'], delete_files)
|
||||
def processComplete(self, item, delete_files = False):
|
||||
log.debug('Requesting Transmission to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
return self.trpc.remove_torrent(item['hashString'], delete_files)
|
||||
|
||||
class TransmissionRPC(object):
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from base64 import b16encode, b32decode
|
||||
from bencode import bencode as benc, bdecode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import isInt, ss, sp
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import isInt, ss
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
@@ -24,16 +24,6 @@ class uTorrent(Downloader):
|
||||
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
utorrent_api = None
|
||||
status_flags = {
|
||||
'STARTED' : 1,
|
||||
'CHECKING' : 2,
|
||||
'CHECK-START' : 4,
|
||||
'CHECKED' : 8,
|
||||
'ERROR' : 16,
|
||||
'PAUSED' : 32,
|
||||
'QUEUED' : 64,
|
||||
'LOADED' : 128
|
||||
}
|
||||
|
||||
def connect(self):
|
||||
# Load host from config and split out port.
|
||||
@@ -46,11 +36,11 @@ class uTorrent(Downloader):
|
||||
|
||||
return self.utorrent_api
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.debug("Sending '%s' (%s) to uTorrent.", (data.get('name'), data.get('protocol')))
|
||||
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('protocol')))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
@@ -85,10 +75,9 @@ class uTorrent(Downloader):
|
||||
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
|
||||
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
|
||||
else:
|
||||
info = bdecode(filedata)['info']
|
||||
info = bdecode(filedata)["info"]
|
||||
torrent_hash = sha1(benc(info)).hexdigest().upper()
|
||||
|
||||
torrent_filename = self.createFileName(data, filedata, media)
|
||||
torrent_filename = self.createFileName(data, filedata, movie)
|
||||
|
||||
if data.get('seed_ratio'):
|
||||
torrent_params['seed_override'] = 1
|
||||
@@ -113,90 +102,111 @@ class uTorrent(Downloader):
|
||||
if self.conf('paused', default = 0):
|
||||
self.utorrent_api.pause_torrent(torrent_hash)
|
||||
|
||||
count = 0
|
||||
while True:
|
||||
|
||||
count += 1
|
||||
# Check if torrent is saved in subfolder of torrent name
|
||||
data = self.utorrent_api.get_files(torrent_hash)
|
||||
|
||||
torrent_files = json.loads(data)
|
||||
if torrent_files.get('error'):
|
||||
log.error('Error getting data from uTorrent: %s', torrent_files.get('error'))
|
||||
return False
|
||||
|
||||
if (torrent_files.get('files') and len(torrent_files['files'][1]) > 0) or count > 60:
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# Torrent has only one file, so uTorrent wont create a folder for it
|
||||
if len(torrent_files['files'][1]) == 1:
|
||||
# Remove torrent and try again
|
||||
self.utorrent_api.remove_torrent(torrent_hash, remove_data = True)
|
||||
|
||||
# Send request to uTorrent
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'), add_folder = True)
|
||||
else:
|
||||
self.utorrent_api.add_torrent_file(torrent_filename, filedata, add_folder = True)
|
||||
|
||||
# Change settings of added torrent
|
||||
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
||||
if self.conf('paused', default = 0):
|
||||
self.utorrent_api.pause_torrent(torrent_hash)
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking uTorrent download status.')
|
||||
|
||||
if not self.connect():
|
||||
return []
|
||||
return False
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
statuses = StatusList(self)
|
||||
|
||||
data = self.utorrent_api.get_status()
|
||||
if not data:
|
||||
log.error('Error getting data from uTorrent')
|
||||
return []
|
||||
return False
|
||||
|
||||
queue = json.loads(data)
|
||||
if queue.get('error'):
|
||||
log.error('Error getting data from uTorrent: %s', queue.get('error'))
|
||||
return []
|
||||
return False
|
||||
|
||||
if not queue.get('torrents'):
|
||||
log.debug('Nothing in queue')
|
||||
return []
|
||||
return False
|
||||
|
||||
# Get torrents
|
||||
for torrent in queue['torrents']:
|
||||
if torrent[0] in ids:
|
||||
for item in queue['torrents']:
|
||||
|
||||
#Get files of the torrent
|
||||
torrent_files = []
|
||||
try:
|
||||
torrent_files = json.loads(self.utorrent_api.get_files(torrent[0]))
|
||||
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]]
|
||||
except:
|
||||
log.debug('Failed getting files from torrent: %s', torrent[2])
|
||||
|
||||
status = 'busy'
|
||||
if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000:
|
||||
status = 'seeding'
|
||||
elif (torrent[1] & self.status_flags['ERROR']):
|
||||
status = 'failed'
|
||||
elif torrent[4] == 1000:
|
||||
status = 'completed'
|
||||
|
||||
if not status == 'busy':
|
||||
self.removeReadOnly(torrent_files)
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent[0],
|
||||
'name': torrent[2],
|
||||
'status': status,
|
||||
'seed_ratio': float(torrent[7]) / 1000,
|
||||
'original_status': torrent[1],
|
||||
'timeleft': str(timedelta(seconds = torrent[10])),
|
||||
'folder': sp(torrent[26]),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
# item[21] = Paused | Downloading | Seeding | Finished
|
||||
status = 'busy'
|
||||
if 'Finished' in item[21]:
|
||||
status = 'completed'
|
||||
self.removeReadOnly(item[26])
|
||||
elif 'Seeding' in item[21]:
|
||||
status = 'seeding'
|
||||
self.removeReadOnly(item[26])
|
||||
|
||||
return release_downloads
|
||||
statuses.append({
|
||||
'id': item[0],
|
||||
'name': item[2],
|
||||
'status': status,
|
||||
'seed_ratio': float(item[7]) / 1000,
|
||||
'original_status': item[1],
|
||||
'timeleft': str(timedelta(seconds = item[10])),
|
||||
'folder': ss(item[26]),
|
||||
})
|
||||
|
||||
def pause(self, release_download, pause = True):
|
||||
return statuses
|
||||
|
||||
def pause(self, item, pause = True):
|
||||
if not self.connect():
|
||||
return False
|
||||
return self.utorrent_api.pause_torrent(release_download['id'], pause)
|
||||
return self.utorrent_api.pause_torrent(item['id'], pause)
|
||||
|
||||
def removeFailed(self, release_download):
|
||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||
def removeFailed(self, item):
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
if not self.connect():
|
||||
return False
|
||||
return self.utorrent_api.remove_torrent(release_download['id'], remove_data = True)
|
||||
return self.utorrent_api.remove_torrent(item['id'], remove_data = True)
|
||||
|
||||
def processComplete(self, release_download, delete_files = False):
|
||||
log.debug('Requesting uTorrent to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
def processComplete(self, item, delete_files = False):
|
||||
log.debug('Requesting uTorrent to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
if not self.connect():
|
||||
return False
|
||||
return self.utorrent_api.remove_torrent(release_download['id'], remove_data = delete_files)
|
||||
|
||||
def removeReadOnly(self, files):
|
||||
#Removes all read-on ly flags in a for all files
|
||||
for filepath in files:
|
||||
if os.path.isfile(filepath):
|
||||
#Windows only needs S_IWRITE, but we bitwise-or with current perms to preserve other permission bits on Linux
|
||||
os.chmod(filepath, stat.S_IWRITE | os.stat(filepath).st_mode)
|
||||
return self.utorrent_api.remove_torrent(item['id'], remove_data = delete_files)
|
||||
|
||||
def removeReadOnly(self, folder):
|
||||
#Removes all read-only flags in a folder
|
||||
if folder and os.path.isdir(folder):
|
||||
for root, folders, filenames in os.walk(folder):
|
||||
for filename in filenames:
|
||||
os.chmod(os.path.join(root, filename), stat.S_IWRITE)
|
||||
|
||||
class uTorrentAPI(object):
|
||||
|
||||
@@ -223,7 +233,7 @@ class uTorrentAPI(object):
|
||||
if time.time() > self.last_time + 1800:
|
||||
self.last_time = time.time()
|
||||
self.token = self.get_token()
|
||||
request = urllib2.Request(self.url + '?token=' + self.token + '&' + action, data)
|
||||
request = urllib2.Request(self.url + "?token=" + self.token + "&" + action, data)
|
||||
try:
|
||||
open_request = self.opener.open(request)
|
||||
response = open_request.read()
|
||||
@@ -243,64 +253,64 @@ class uTorrentAPI(object):
|
||||
return False
|
||||
|
||||
def get_token(self):
|
||||
request = self.opener.open(self.url + 'token.html')
|
||||
token = re.findall('<div.*?>(.*?)</', request.read())[0]
|
||||
request = self.opener.open(self.url + "token.html")
|
||||
token = re.findall("<div.*?>(.*?)</", request.read())[0]
|
||||
return token
|
||||
|
||||
def add_torrent_uri(self, filename, torrent, add_folder = False):
|
||||
action = 'action=add-url&s=%s' % urllib.quote(torrent)
|
||||
action = "action=add-url&s=%s" % urllib.quote(torrent)
|
||||
if add_folder:
|
||||
action += '&path=%s' % urllib.quote(filename)
|
||||
action += "&path=%s" % urllib.quote(filename)
|
||||
return self._request(action)
|
||||
|
||||
def add_torrent_file(self, filename, filedata, add_folder = False):
|
||||
action = 'action=add-file'
|
||||
action = "action=add-file"
|
||||
if add_folder:
|
||||
action += '&path=%s' % urllib.quote(filename)
|
||||
return self._request(action, {'torrent_file': (ss(filename), filedata)})
|
||||
action += "&path=%s" % urllib.quote(filename)
|
||||
return self._request(action, {"torrent_file": (ss(filename), filedata)})
|
||||
|
||||
def set_torrent(self, hash, params):
|
||||
action = 'action=setprops&hash=%s' % hash
|
||||
action = "action=setprops&hash=%s" % hash
|
||||
for k, v in params.iteritems():
|
||||
action += '&s=%s&v=%s' % (k, v)
|
||||
action += "&s=%s&v=%s" % (k, v)
|
||||
return self._request(action)
|
||||
|
||||
def pause_torrent(self, hash, pause = True):
|
||||
if pause:
|
||||
action = 'action=pause&hash=%s' % hash
|
||||
action = "action=pause&hash=%s" % hash
|
||||
else:
|
||||
action = 'action=unpause&hash=%s' % hash
|
||||
action = "action=unpause&hash=%s" % hash
|
||||
return self._request(action)
|
||||
|
||||
def stop_torrent(self, hash):
|
||||
action = 'action=stop&hash=%s' % hash
|
||||
action = "action=stop&hash=%s" % hash
|
||||
return self._request(action)
|
||||
|
||||
def remove_torrent(self, hash, remove_data = False):
|
||||
if remove_data:
|
||||
action = 'action=removedata&hash=%s' % hash
|
||||
action = "action=removedata&hash=%s" % hash
|
||||
else:
|
||||
action = 'action=remove&hash=%s' % hash
|
||||
action = "action=remove&hash=%s" % hash
|
||||
return self._request(action)
|
||||
|
||||
def get_status(self):
|
||||
action = 'list=1'
|
||||
action = "list=1"
|
||||
return self._request(action)
|
||||
|
||||
def get_settings(self):
|
||||
action = 'action=getsettings'
|
||||
action = "action=getsettings"
|
||||
settings_dict = {}
|
||||
try:
|
||||
utorrent_settings = json.loads(self._request(action))
|
||||
|
||||
# Create settings dict
|
||||
for setting in utorrent_settings['settings']:
|
||||
if setting[1] == 0: # int
|
||||
settings_dict[setting[0]] = int(setting[2] if not setting[2].strip() == '' else '0')
|
||||
elif setting[1] == 1: # bool
|
||||
settings_dict[setting[0]] = True if setting[2] == 'true' else False
|
||||
elif setting[1] == 2: # string
|
||||
settings_dict[setting[0]] = setting[2]
|
||||
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)
|
||||
|
||||
@@ -320,5 +330,5 @@ class uTorrentAPI(object):
|
||||
return self._request(action)
|
||||
|
||||
def get_files(self, hash):
|
||||
action = 'action=getfiles&hash=%s' % hash
|
||||
action = "action=getfiles&hash=%s" % hash
|
||||
return self._request(action)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from string import ascii_letters, digits
|
||||
from urllib import quote_plus
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
import unicodedata
|
||||
@@ -39,39 +38,8 @@ def toUnicode(original, *args):
|
||||
return toUnicode(ascii_text)
|
||||
|
||||
def ss(original, *args):
|
||||
|
||||
u_original = toUnicode(original, *args)
|
||||
try:
|
||||
from couchpotato.environment import Env
|
||||
return u_original.encode(Env.get('encoding'))
|
||||
except Exception, e:
|
||||
log.debug('Failed ss encoding char, force UTF8: %s', e)
|
||||
return u_original.encode('UTF-8')
|
||||
|
||||
def sp(path, *args):
|
||||
|
||||
# Standardise encoding, normalise case, path and strip trailing '/' or '\'
|
||||
if not path or len(path) == 0:
|
||||
return path
|
||||
|
||||
# convert windows path (from remote box) to *nix path
|
||||
if os.path.sep == '/' and '\\' in path:
|
||||
path = '/' + path.replace(':', '').replace('\\', '/')
|
||||
|
||||
path = os.path.normcase(os.path.normpath(ss(path, *args)))
|
||||
|
||||
# Remove any trailing path separators
|
||||
if path != os.path.sep:
|
||||
path = path.rstrip(os.path.sep)
|
||||
|
||||
# Add a trailing separator in case it is a root folder on windows (crashes guessit)
|
||||
if len(path) == 2 and path[1] == ':':
|
||||
path = path + os.path.sep
|
||||
|
||||
# Replace *NIX ambiguous '//' at the beginning of a path with '/' (crashes guessit)
|
||||
path = re.sub('^//', '/', path)
|
||||
|
||||
return path
|
||||
from couchpotato.environment import Env
|
||||
return toUnicode(original, *args).encode(Env.get('encoding'))
|
||||
|
||||
def ek(original, *args):
|
||||
if isinstance(original, (str, unicode)):
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
|
||||
from couchpotato.core.logger import CPLog
|
||||
import collections
|
||||
import hashlib
|
||||
import os
|
||||
import os.path
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
@@ -11,9 +10,6 @@ import sys
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
def fnEscape(pattern):
|
||||
return pattern.replace('[','[[').replace(']','[]]').replace('[[','[[]')
|
||||
|
||||
def link(src, dst):
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
@@ -140,20 +136,18 @@ def getImdb(txt, check_inside = False, multiple = False):
|
||||
output.close()
|
||||
|
||||
try:
|
||||
ids = re.findall('(tt\d{4,7})', txt)
|
||||
|
||||
ids = re.findall('(tt\d{7})', txt)
|
||||
if multiple:
|
||||
return list(set(['tt%07d' % tryInt(x[2:]) for x in ids])) if len(ids) > 0 else []
|
||||
|
||||
return 'tt%07d' % tryInt(ids[0][2:])
|
||||
return list(set(ids)) if len(ids) > 0 else []
|
||||
return ids[0]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def tryInt(s, default = 0):
|
||||
def tryInt(s):
|
||||
try: return int(s)
|
||||
except: return default
|
||||
except: return 0
|
||||
|
||||
def tryFloat(s):
|
||||
try:
|
||||
@@ -169,11 +163,6 @@ def natsortKey(s):
|
||||
def natcmp(a, b):
|
||||
return cmp(natsortKey(a), natsortKey(b))
|
||||
|
||||
def toIterable(value):
|
||||
if type(value) in [list, tuple]:
|
||||
return value
|
||||
return [value]
|
||||
|
||||
def getTitle(library_dict):
|
||||
try:
|
||||
try:
|
||||
@@ -216,10 +205,3 @@ def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||
def splitString(str, split_on = ',', clean = True):
|
||||
list = [x.strip() for x in str.split(split_on)] if str else []
|
||||
return filter(None, list) if clean else list
|
||||
|
||||
def dictIsSubset(a, b):
|
||||
return all([k in b and b[k] == v for k, v in a.items()])
|
||||
|
||||
def isSubFolder(sub_folder, base_folder):
|
||||
# Returns True is sub_folder is the same as or in base_folder
|
||||
return base_folder.rstrip(os.path.sep) + os.path.sep in sub_folder.rstrip(os.path.sep) + os.path.sep
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from importlib import import_module
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -13,6 +12,17 @@ class Loader(object):
|
||||
providers = {}
|
||||
modules = {}
|
||||
|
||||
def addPath(self, root, base_path, priority, recursive = False):
|
||||
for filename in os.listdir(os.path.join(root, *base_path)):
|
||||
path = os.path.join(os.path.join(root, *base_path), filename)
|
||||
if os.path.isdir(path) and filename[:2] != '__':
|
||||
if u'__init__.py' in os.listdir(path):
|
||||
new_base_path = ''.join(s + '.' for s in base_path) + filename
|
||||
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
|
||||
|
||||
if recursive:
|
||||
self.addPath(root, base_path + [filename], priority, recursive = True)
|
||||
|
||||
def preload(self, root = ''):
|
||||
core = os.path.join(root, 'couchpotato', 'core')
|
||||
|
||||
@@ -29,14 +39,6 @@ class Loader(object):
|
||||
# Add media to loader
|
||||
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
|
||||
|
||||
# Add custom plugin folder
|
||||
from couchpotato.environment import Env
|
||||
custom_plugin_dir = os.path.join(Env.get('data_dir'), 'custom_plugins')
|
||||
if os.path.isdir(custom_plugin_dir):
|
||||
sys.path.insert(0, custom_plugin_dir)
|
||||
self.paths['custom_plugins'] = (30, '', custom_plugin_dir)
|
||||
|
||||
# Loop over all paths and add to module list
|
||||
for plugin_type, plugin_tuple in self.paths.iteritems():
|
||||
priority, module, dir_name = plugin_tuple
|
||||
self.addFromDir(plugin_type, priority, module, dir_name)
|
||||
@@ -44,9 +46,8 @@ class Loader(object):
|
||||
def run(self):
|
||||
did_save = 0
|
||||
|
||||
for priority in sorted(self.modules):
|
||||
for priority in self.modules:
|
||||
for module_name, plugin in sorted(self.modules[priority].iteritems()):
|
||||
|
||||
# Load module
|
||||
try:
|
||||
if plugin.get('name')[:2] == '__':
|
||||
@@ -55,6 +56,7 @@ class Loader(object):
|
||||
m = self.loadModule(module_name)
|
||||
if m is None:
|
||||
continue
|
||||
m = getattr(m, plugin.get('name'))
|
||||
|
||||
log.info('Loading %s: %s', (plugin['type'], plugin['name']))
|
||||
|
||||
@@ -76,26 +78,20 @@ class Loader(object):
|
||||
if did_save:
|
||||
fireEvent('settings.save')
|
||||
|
||||
def addPath(self, root, base_path, priority, recursive = False):
|
||||
root_path = os.path.join(root, *base_path)
|
||||
for filename in os.listdir(root_path):
|
||||
path = os.path.join(root_path, filename)
|
||||
if os.path.isdir(path) and filename[:2] != '__':
|
||||
if u'__init__.py' in os.listdir(path):
|
||||
new_base_path = ''.join(s + '.' for s in base_path) + filename
|
||||
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
|
||||
|
||||
if recursive:
|
||||
self.addPath(root, base_path + [filename], priority, recursive = True)
|
||||
|
||||
def addFromDir(self, plugin_type, priority, module, dir_name):
|
||||
|
||||
# Load dir module
|
||||
if module and len(module) > 0:
|
||||
self.addModule(priority, plugin_type, module, os.path.basename(dir_name))
|
||||
try:
|
||||
m = __import__(module)
|
||||
splitted = module.split('.')
|
||||
for sub in splitted[1:]:
|
||||
m = getattr(m, sub)
|
||||
except:
|
||||
raise
|
||||
|
||||
for name in os.listdir(dir_name):
|
||||
if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(dir_name, name, '__init__.py')):
|
||||
for cur_file in glob.glob(os.path.join(dir_name, '*')):
|
||||
name = os.path.basename(cur_file)
|
||||
if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(cur_file, '__init__.py')):
|
||||
module_name = '%s.%s' % (module, name)
|
||||
self.addModule(priority, plugin_type, module_name, name)
|
||||
|
||||
@@ -135,7 +131,6 @@ class Loader(object):
|
||||
if not self.modules.get(priority):
|
||||
self.modules[priority] = {}
|
||||
|
||||
module = module.lstrip('.')
|
||||
self.modules[priority][module] = {
|
||||
'priority': priority,
|
||||
'module': module,
|
||||
@@ -145,7 +140,11 @@ class Loader(object):
|
||||
|
||||
def loadModule(self, name):
|
||||
try:
|
||||
return import_module(name)
|
||||
m = __import__(name)
|
||||
splitted = name.split('.')
|
||||
for sub in splitted[1:-1]:
|
||||
m = getattr(m, sub)
|
||||
return m
|
||||
except ImportError:
|
||||
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
|
||||
return None
|
||||
|
||||
@@ -1,52 +1,13 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Media
|
||||
|
||||
|
||||
class MediaBase(Plugin):
|
||||
|
||||
_type = None
|
||||
|
||||
default_dict = {
|
||||
'profile': {'types': {'quality': {}}},
|
||||
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
'status': {},
|
||||
'category': {},
|
||||
}
|
||||
|
||||
search_dict = mergeDicts({
|
||||
'library': {
|
||||
'related_libraries': {},
|
||||
'root_library': {}
|
||||
},
|
||||
}, default_dict)
|
||||
|
||||
def initType(self):
|
||||
addEvent('media.types', self.getType)
|
||||
|
||||
def getType(self):
|
||||
return self._type
|
||||
|
||||
def createOnComplete(self, id):
|
||||
|
||||
def onComplete():
|
||||
db = get_session()
|
||||
media = db.query(Media).filter_by(id = id).first()
|
||||
fireEventAsync('%s.searcher.single' % media.type, media.to_dict(self.search_dict), on_complete = self.createNotifyFront(id))
|
||||
db.expire_all()
|
||||
|
||||
return onComplete
|
||||
|
||||
def createNotifyFront(self, media_id):
|
||||
|
||||
def notifyFront():
|
||||
db = get_session()
|
||||
media = db.query(Media).filter_by(id = media_id).first()
|
||||
fireEvent('notify.frontend', type = '%s.update' % media.type, data = media.to_dict(self.default_dict))
|
||||
db.expire_all()
|
||||
|
||||
return notifyFront
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
from .main import Library
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
def start():
|
||||
return Library()
|
||||
|
||||
config = []
|
||||
class LibraryBase(Plugin):
|
||||
|
||||
_type = None
|
||||
|
||||
def initType(self):
|
||||
addEvent('library.types', self.getType)
|
||||
|
||||
def getType(self):
|
||||
return self._type
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
|
||||
class LibraryBase(Plugin):
|
||||
|
||||
_type = None
|
||||
|
||||
def initType(self):
|
||||
addEvent('library.types', self.getType)
|
||||
|
||||
def getType(self):
|
||||
return self._type
|
||||
@@ -1,18 +0,0 @@
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
|
||||
|
||||
class Library(LibraryBase):
|
||||
def __init__(self):
|
||||
addEvent('library.title', self.title)
|
||||
|
||||
def title(self, library):
|
||||
return fireEvent(
|
||||
'library.query',
|
||||
library,
|
||||
|
||||
condense = False,
|
||||
include_year = False,
|
||||
include_identifier = False,
|
||||
single = True
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import Matcher
|
||||
|
||||
def start():
|
||||
return Matcher()
|
||||
|
||||
config = []
|
||||
@@ -1,84 +0,0 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class MatcherBase(Plugin):
|
||||
type = None
|
||||
|
||||
def __init__(self):
|
||||
if self.type:
|
||||
addEvent('%s.matcher.correct' % self.type, self.correct)
|
||||
|
||||
def correct(self, chain, release, media, quality):
|
||||
raise NotImplementedError()
|
||||
|
||||
def flattenInfo(self, info):
|
||||
# Flatten dictionary of matches (chain info)
|
||||
if isinstance(info, dict):
|
||||
return dict([(key, self.flattenInfo(value)) for key, value in info.items()])
|
||||
|
||||
# Flatten matches
|
||||
result = None
|
||||
|
||||
for match in info:
|
||||
if isinstance(match, dict):
|
||||
if result is None:
|
||||
result = {}
|
||||
|
||||
for key, value in match.items():
|
||||
if key not in result:
|
||||
result[key] = []
|
||||
|
||||
result[key].append(value)
|
||||
else:
|
||||
if result is None:
|
||||
result = []
|
||||
|
||||
result.append(match)
|
||||
|
||||
return result
|
||||
|
||||
def constructFromRaw(self, match):
|
||||
if not match:
|
||||
return None
|
||||
|
||||
parts = [
|
||||
''.join([
|
||||
y for y in x[1:] if y
|
||||
]) for x in match
|
||||
]
|
||||
|
||||
return ''.join(parts)[:-1].strip()
|
||||
|
||||
def simplifyValue(self, value):
|
||||
if not value:
|
||||
return value
|
||||
|
||||
if isinstance(value, basestring):
|
||||
return simplifyString(value)
|
||||
|
||||
if isinstance(value, list):
|
||||
return [self.simplifyValue(x) for x in value]
|
||||
|
||||
raise ValueError("Unsupported value type")
|
||||
|
||||
def chainMatch(self, chain, group, tags):
|
||||
info = self.flattenInfo(chain.info[group])
|
||||
|
||||
found_tags = []
|
||||
for tag, accepted in tags.items():
|
||||
values = [self.simplifyValue(x) for x in info.get(tag, [None])]
|
||||
|
||||
if any([val in accepted for val in values]):
|
||||
found_tags.append(tag)
|
||||
|
||||
log.debug('tags found: %s, required: %s' % (found_tags, tags.keys()))
|
||||
|
||||
if set(tags.keys()) == set(found_tags):
|
||||
return True
|
||||
|
||||
return all([key in found_tags for key, value in tags.items()])
|
||||
@@ -1,88 +0,0 @@
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import possibleTitles
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.matcher.base import MatcherBase
|
||||
from caper import Caper
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Matcher(MatcherBase):
|
||||
def __init__(self):
|
||||
super(Matcher, self).__init__()
|
||||
|
||||
self.caper = Caper()
|
||||
|
||||
addEvent('matcher.parse', self.parse)
|
||||
addEvent('matcher.match', self.match)
|
||||
|
||||
addEvent('matcher.flatten_info', self.flattenInfo)
|
||||
addEvent('matcher.construct_from_raw', self.constructFromRaw)
|
||||
|
||||
addEvent('matcher.correct_title', self.correctTitle)
|
||||
addEvent('matcher.correct_quality', self.correctQuality)
|
||||
|
||||
def parse(self, name, parser='scene'):
|
||||
return self.caper.parse(name, parser)
|
||||
|
||||
def match(self, release, media, quality):
|
||||
match = fireEvent('matcher.parse', release['name'], single = True)
|
||||
|
||||
if len(match.chains) < 1:
|
||||
log.info2('Wrong: %s, unable to parse release name (no chains)', release['name'])
|
||||
return False
|
||||
|
||||
for chain in match.chains:
|
||||
if fireEvent('%s.matcher.correct' % media['type'], chain, release, media, quality, single = True):
|
||||
return chain
|
||||
|
||||
return False
|
||||
|
||||
def correctTitle(self, chain, media):
|
||||
root_library = media['library']['root_library']
|
||||
|
||||
if 'show_name' not in chain.info or not len(chain.info['show_name']):
|
||||
log.info('Wrong: missing show name in parsed result')
|
||||
return False
|
||||
|
||||
# Get the lower-case parsed show name from the chain
|
||||
chain_words = [x.lower() for x in chain.info['show_name']]
|
||||
|
||||
# Build a list of possible titles of the media we are searching for
|
||||
titles = root_library['info']['titles']
|
||||
|
||||
# Add year suffix titles (will result in ['<name_one>', '<name_one> <suffix_one>', '<name_two>', ...])
|
||||
suffixes = [None, root_library['info']['year']]
|
||||
|
||||
titles = [
|
||||
title + ((' %s' % suffix) if suffix else '')
|
||||
for title in titles
|
||||
for suffix in suffixes
|
||||
]
|
||||
|
||||
# Check show titles match
|
||||
# TODO check xem names
|
||||
for title in titles:
|
||||
for valid_words in [x.split(' ') for x in possibleTitles(title)]:
|
||||
|
||||
if valid_words == chain_words:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def correctQuality(self, chain, quality, quality_map):
|
||||
if quality['identifier'] not in quality_map:
|
||||
log.info2('Wrong: unknown preferred quality %s', quality['identifier'])
|
||||
return False
|
||||
|
||||
if 'video' not in chain.info:
|
||||
log.info2('Wrong: no video tags found')
|
||||
return False
|
||||
|
||||
video_tags = quality_map[quality['identifier']]
|
||||
|
||||
if not self.chainMatch(chain, 'video', video_tags):
|
||||
log.info2('Wrong: %s tags not in chain', video_tags)
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import MediaPlugin
|
||||
|
||||
def start():
|
||||
return MediaPlugin()
|
||||
|
||||
config = []
|
||||
@@ -1,458 +0,0 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media import MediaBase
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, Release, \
|
||||
Media
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
from sqlalchemy.sql.expression import or_, asc, not_, desc
|
||||
from string import ascii_lowercase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class MediaPlugin(MediaBase):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('media.refresh', self.refresh, docs = {
|
||||
'desc': 'Refresh a any media type by ID',
|
||||
'params': {
|
||||
'id': {'desc': 'Movie, Show, Season or Episode ID(s) you want to refresh.', 'type': 'int (comma separated)'},
|
||||
}
|
||||
})
|
||||
|
||||
addApiView('media.list', self.listView, docs = {
|
||||
'desc': 'List media',
|
||||
'params': {
|
||||
'type': {'type': 'string', 'desc': 'Media type to filter on.'},
|
||||
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
|
||||
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
|
||||
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
|
||||
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
|
||||
'search': {'desc': 'Search movie title'},
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'empty': bool, any movies returned or not,
|
||||
'media': array, media found,
|
||||
}"""}
|
||||
})
|
||||
|
||||
addApiView('media.get', self.getView, docs = {
|
||||
'desc': 'Get media by id',
|
||||
'params': {
|
||||
'id': {'desc': 'The id of the media'},
|
||||
}
|
||||
})
|
||||
|
||||
addApiView('media.delete', self.deleteView, docs = {
|
||||
'desc': 'Delete a media from the wanted list',
|
||||
'params': {
|
||||
'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'},
|
||||
'delete_from': {'desc': 'Delete media from this page', 'type': 'string: all (default), wanted, manage'},
|
||||
}
|
||||
})
|
||||
|
||||
addApiView('media.available_chars', self.charView)
|
||||
|
||||
addEvent('app.load', self.addSingleRefreshView)
|
||||
addEvent('app.load', self.addSingleListView)
|
||||
addEvent('app.load', self.addSingleCharView)
|
||||
addEvent('app.load', self.addSingleDeleteView)
|
||||
|
||||
addEvent('media.get', self.get)
|
||||
addEvent('media.list', self.list)
|
||||
addEvent('media.delete', self.delete)
|
||||
addEvent('media.restatus', self.restatus)
|
||||
|
||||
def refresh(self, id = '', **kwargs):
|
||||
db = get_session()
|
||||
|
||||
for x in splitString(id):
|
||||
media = db.query(Media).filter_by(id = x).first()
|
||||
|
||||
if media:
|
||||
# Get current selected title
|
||||
default_title = ''
|
||||
for title in media.library.titles:
|
||||
if title.default: default_title = title.title
|
||||
|
||||
fireEvent('notify.frontend', type = '%s.busy' % media.type, data = {'id': x})
|
||||
fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||
|
||||
db.expire_all()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def addSingleRefreshView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
addApiView('%s.refresh' % media_type, self.refresh)
|
||||
|
||||
def get(self, media_id):
|
||||
|
||||
db = get_session()
|
||||
|
||||
imdb_id = getImdb(str(media_id))
|
||||
|
||||
if imdb_id:
|
||||
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
|
||||
else:
|
||||
m = db.query(Media).filter_by(id = media_id).first()
|
||||
|
||||
results = None
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return results
|
||||
|
||||
def getView(self, id = None, **kwargs):
|
||||
|
||||
media = self.get(id) if id else None
|
||||
|
||||
return {
|
||||
'success': media is not None,
|
||||
'media': media,
|
||||
}
|
||||
|
||||
def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if status and not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
if types and not isinstance(types, (list, tuple)):
|
||||
types = [types]
|
||||
|
||||
# query movie ids
|
||||
q = db.query(Media) \
|
||||
.with_entities(Media.id) \
|
||||
.group_by(Media.id)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Media.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.join(Media.releases)
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Release.status_id.in_(statuses))
|
||||
|
||||
# Filter on type
|
||||
if types and len(types) > 0:
|
||||
try: q = q.filter(Media.type.in_(types))
|
||||
except: pass
|
||||
|
||||
# Only join when searching / ordering
|
||||
if starts_with or search or order != 'release_order':
|
||||
q = q.join(Media.library, Library.titles) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
# Add search filters
|
||||
filter_or = []
|
||||
if starts_with:
|
||||
starts_with = toUnicode(starts_with.lower())
|
||||
if starts_with in ascii_lowercase:
|
||||
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
|
||||
else:
|
||||
ignore = []
|
||||
for letter in ascii_lowercase:
|
||||
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
|
||||
filter_or.append(not_(or_(*ignore)))
|
||||
|
||||
if search:
|
||||
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
||||
|
||||
if len(filter_or) > 0:
|
||||
q = q.filter(or_(*filter_or))
|
||||
|
||||
total_count = q.count()
|
||||
if total_count == 0:
|
||||
return 0, []
|
||||
|
||||
if order == 'release_order':
|
||||
q = q.order_by(desc(Release.last_edit))
|
||||
else:
|
||||
q = q.order_by(asc(LibraryTitle.simple_title))
|
||||
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q = q.limit(limit).offset(offset)
|
||||
|
||||
# Get all media_ids in sorted order
|
||||
media_ids = [m.id for m in q.all()]
|
||||
|
||||
# List release statuses
|
||||
releases = db.query(Release) \
|
||||
.filter(Release.media_id.in_(media_ids)) \
|
||||
.all()
|
||||
|
||||
release_statuses = dict((m, set()) for m in media_ids)
|
||||
releases_count = dict((m, 0) for m in media_ids)
|
||||
for release in releases:
|
||||
release_statuses[release.media_id].add('%d,%d' % (release.status_id, release.quality_id))
|
||||
releases_count[release.media_id] += 1
|
||||
|
||||
# Get main movie data
|
||||
q2 = db.query(Media) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
q2 = q2.filter(Media.id.in_(media_ids))
|
||||
|
||||
results = q2.all()
|
||||
|
||||
# Create dict by movie id
|
||||
movie_dict = {}
|
||||
for movie in results:
|
||||
movie_dict[movie.id] = movie
|
||||
|
||||
# List movies based on media_ids order
|
||||
movies = []
|
||||
for media_id in media_ids:
|
||||
|
||||
releases = []
|
||||
for r in release_statuses.get(media_id):
|
||||
x = splitString(r)
|
||||
releases.append({'status_id': x[0], 'quality_id': x[1]})
|
||||
|
||||
# Merge releases with movie dict
|
||||
movies.append(mergeDicts(movie_dict[media_id].to_dict({
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
}), {
|
||||
'releases': releases,
|
||||
'releases_count': releases_count.get(media_id),
|
||||
}))
|
||||
|
||||
db.expire_all()
|
||||
return total_count, movies
|
||||
|
||||
def listView(self, **kwargs):
|
||||
|
||||
types = splitString(kwargs.get('types'))
|
||||
status = splitString(kwargs.get('status'))
|
||||
release_status = splitString(kwargs.get('release_status'))
|
||||
limit_offset = kwargs.get('limit_offset')
|
||||
starts_with = kwargs.get('starts_with')
|
||||
search = kwargs.get('search')
|
||||
order = kwargs.get('order')
|
||||
|
||||
total_movies, movies = self.list(
|
||||
types = types,
|
||||
status = status,
|
||||
release_status = release_status,
|
||||
limit_offset = limit_offset,
|
||||
starts_with = starts_with,
|
||||
search = search,
|
||||
order = order
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
}
|
||||
|
||||
def addSingleListView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
def tempList(*args, **kwargs):
|
||||
return self.listView(types = media_type, *args, **kwargs)
|
||||
addApiView('%s.list' % media_type, tempList)
|
||||
|
||||
def availableChars(self, types = None, status = None, release_status = None):
|
||||
|
||||
types = types or []
|
||||
status = status or []
|
||||
release_status = release_status or []
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
if types and not isinstance(types, (list, tuple)):
|
||||
types = [types]
|
||||
|
||||
q = db.query(Media)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Media.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.join(Media.releases) \
|
||||
.filter(Release.status_id.in_(statuses))
|
||||
|
||||
# Filter on type
|
||||
if types and len(types) > 0:
|
||||
try: q = q.filter(Media.type.in_(types))
|
||||
except: pass
|
||||
|
||||
q = q.join(Library, LibraryTitle) \
|
||||
.with_entities(LibraryTitle.simple_title) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
titles = q.all()
|
||||
|
||||
chars = set()
|
||||
for title in titles:
|
||||
try:
|
||||
char = title[0][0]
|
||||
char = char if char in ascii_lowercase else '#'
|
||||
chars.add(str(char))
|
||||
except:
|
||||
log.error('Failed getting title for %s', title.libraries_id)
|
||||
|
||||
if len(chars) == 25:
|
||||
break
|
||||
|
||||
db.expire_all()
|
||||
return ''.join(sorted(chars))
|
||||
|
||||
def charView(self, **kwargs):
|
||||
|
||||
type = splitString(kwargs.get('type', 'movie'))
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
chars = self.availableChars(type, status, release_status)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(chars) == 0,
|
||||
'chars': chars,
|
||||
}
|
||||
|
||||
def addSingleCharView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
def tempChar(*args, **kwargs):
|
||||
return self.charView(types = media_type, *args, **kwargs)
|
||||
addApiView('%s.available_chars' % media_type, tempChar)
|
||||
|
||||
def delete(self, media_id, delete_from = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
media = db.query(Media).filter_by(id = media_id).first()
|
||||
if media:
|
||||
deleted = False
|
||||
if delete_from == 'all':
|
||||
db.delete(media)
|
||||
db.commit()
|
||||
deleted = True
|
||||
else:
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
total_releases = len(media.releases)
|
||||
total_deleted = 0
|
||||
new_movie_status = None
|
||||
for release in media.releases:
|
||||
if delete_from in ['wanted', 'snatched', 'late']:
|
||||
if release.status_id != done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'done'
|
||||
elif delete_from == 'manage':
|
||||
if release.status_id == done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'active'
|
||||
db.commit()
|
||||
|
||||
if total_releases == total_deleted:
|
||||
db.delete(media)
|
||||
db.commit()
|
||||
deleted = True
|
||||
elif new_movie_status:
|
||||
new_status = fireEvent('status.get', new_movie_status, single = True)
|
||||
media.profile_id = None
|
||||
media.status_id = new_status.get('id')
|
||||
db.commit()
|
||||
else:
|
||||
fireEvent('media.restatus', media.id, single = True)
|
||||
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict())
|
||||
|
||||
db.expire_all()
|
||||
return True
|
||||
|
||||
def deleteView(self, id = '', **kwargs):
|
||||
|
||||
ids = splitString(id)
|
||||
for media_id in ids:
|
||||
self.delete(media_id, delete_from = kwargs.get('delete_from', 'all'))
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def addSingleDeleteView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
def tempDelete(*args, **kwargs):
|
||||
return self.deleteView(types = media_type, *args, **kwargs)
|
||||
addApiView('%s.delete' % media_type, tempDelete)
|
||||
|
||||
def restatus(self, media_id):
|
||||
|
||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
m = db.query(Media).filter_by(id = media_id).first()
|
||||
if not m or len(m.library.titles) == 0:
|
||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||
return False
|
||||
|
||||
log.debug('Changing status for %s', m.library.titles[0].title)
|
||||
if not m.profile:
|
||||
m.status_id = done_status.get('id')
|
||||
else:
|
||||
move_to_wanted = True
|
||||
|
||||
for t in m.profile.types:
|
||||
for release in m.releases:
|
||||
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
|
||||
move_to_wanted = False
|
||||
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import Search
|
||||
|
||||
def start():
|
||||
return Search()
|
||||
|
||||
config = []
|
||||
@@ -1,59 +0,0 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Search(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('search', self.search, docs = {
|
||||
'desc': 'Search the info in providers for a movie',
|
||||
'params': {
|
||||
'q': {'desc': 'The (partial) movie name you want to search for'},
|
||||
'type': {'desc': 'Search for a specific media type. Leave empty to search all.'},
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'movies': array,
|
||||
'show': array,
|
||||
etc
|
||||
}"""}
|
||||
})
|
||||
|
||||
addEvent('app.load', self.addSingleSearches)
|
||||
|
||||
def search(self, q = '', types = None, **kwargs):
|
||||
|
||||
# Make sure types is the correct instance
|
||||
if isinstance(types, (str, unicode)):
|
||||
types = [types]
|
||||
elif isinstance(types, (list, tuple, set)):
|
||||
types = list(types)
|
||||
|
||||
if not types:
|
||||
result = fireEvent('info.search', q = q, merge = True)
|
||||
else:
|
||||
result = {}
|
||||
for media_type in types:
|
||||
result[media_type] = fireEvent('%s.search' % media_type)
|
||||
|
||||
return mergeDicts({
|
||||
'success': True,
|
||||
}, result)
|
||||
|
||||
def createSingleSearch(self, media_type):
|
||||
|
||||
def singleSearch(q, **kwargs):
|
||||
return self.search(q, type = media_type, **kwargs)
|
||||
|
||||
return singleSearch
|
||||
|
||||
def addSingleSearches(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
addApiView('%s.search' % media_type, self.createSingleSearch(media_type))
|
||||
@@ -1,188 +0,0 @@
|
||||
Block.Search = new Class({
|
||||
|
||||
Extends: BlockBase,
|
||||
|
||||
cache: {},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var focus_timer = 0;
|
||||
self.el = new Element('div.search_form').adopt(
|
||||
new Element('div.input').adopt(
|
||||
self.input = new Element('input', {
|
||||
'placeholder': 'Search & add a new media',
|
||||
'events': {
|
||||
'keyup': self.keyup.bind(self),
|
||||
'focus': function(){
|
||||
if(focus_timer) clearTimeout(focus_timer);
|
||||
self.el.addClass('focused')
|
||||
if(this.get('value'))
|
||||
self.hideResults(false)
|
||||
},
|
||||
'blur': function(){
|
||||
focus_timer = (function(){
|
||||
self.el.removeClass('focused')
|
||||
}).delay(100);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('a.icon2', {
|
||||
'events': {
|
||||
'click': self.clear.bind(self),
|
||||
'touchend': self.clear.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
self.result_container = new Element('div.results_container', {
|
||||
'tween': {
|
||||
'duration': 200
|
||||
},
|
||||
'events': {
|
||||
'mousewheel': function(e){
|
||||
(e).stopPropagation();
|
||||
}
|
||||
}
|
||||
}).adopt(
|
||||
self.results = new Element('div.results')
|
||||
)
|
||||
);
|
||||
|
||||
self.mask = new Element('div.mask').inject(self.result_container).fade('hide');
|
||||
|
||||
},
|
||||
|
||||
clear: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
if(self.last_q === ''){
|
||||
self.input.blur()
|
||||
self.last_q = null;
|
||||
}
|
||||
else {
|
||||
|
||||
self.last_q = '';
|
||||
self.input.set('value', '');
|
||||
self.input.focus()
|
||||
|
||||
self.media = {}
|
||||
self.results.empty()
|
||||
self.el.removeClass('filled')
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
hideResults: function(bool){
|
||||
var self = this;
|
||||
|
||||
if(self.hidden == bool) return;
|
||||
|
||||
self.el[bool ? 'removeClass' : 'addClass']('shown');
|
||||
|
||||
if(bool){
|
||||
History.removeEvent('change', self.hideResults.bind(self, !bool));
|
||||
self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool));
|
||||
}
|
||||
else {
|
||||
History.addEvent('change', self.hideResults.bind(self, !bool));
|
||||
self.el.addEvent('outerClick', self.hideResults.bind(self, !bool));
|
||||
}
|
||||
|
||||
self.hidden = bool;
|
||||
},
|
||||
|
||||
keyup: function(e){
|
||||
var self = this;
|
||||
|
||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
||||
|
||||
if(self.q() != self.last_q){
|
||||
if(self.api_request && self.api_request.isRunning())
|
||||
self.api_request.cancel();
|
||||
|
||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
||||
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
autocomplete: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.q()){
|
||||
self.hideResults(true)
|
||||
return
|
||||
}
|
||||
|
||||
self.list()
|
||||
},
|
||||
|
||||
list: function(){
|
||||
var self = this,
|
||||
q = self.q(),
|
||||
cache = self.cache[q];
|
||||
|
||||
self.hideResults(false);
|
||||
|
||||
if(!cache){
|
||||
self.mask.fade('in');
|
||||
|
||||
if(!self.spinner)
|
||||
self.spinner = createSpinner(self.mask);
|
||||
|
||||
self.api_request = Api.request('search', {
|
||||
'data': {
|
||||
'q': q
|
||||
},
|
||||
'onComplete': self.fill.bind(self, q)
|
||||
})
|
||||
}
|
||||
else
|
||||
self.fill(q, cache)
|
||||
|
||||
self.last_q = q;
|
||||
|
||||
},
|
||||
|
||||
fill: function(q, json){
|
||||
var self = this;
|
||||
|
||||
self.cache[q] = json
|
||||
|
||||
self.media = {}
|
||||
self.results.empty()
|
||||
|
||||
Object.each(json, function(media, type){
|
||||
if(typeOf(media) == 'array'){
|
||||
Object.each(media, function(m){
|
||||
|
||||
var m = new Block.Search[m.type.capitalize() + 'Item'](m);
|
||||
$(m).inject(self.results)
|
||||
self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
|
||||
|
||||
if(q == m.imdb)
|
||||
m.showOptions()
|
||||
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate result heights
|
||||
var w = window.getSize(),
|
||||
rc = self.result_container.getCoordinates();
|
||||
|
||||
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
|
||||
self.mask.fade('out')
|
||||
|
||||
},
|
||||
|
||||
loading: function(bool){
|
||||
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
||||
},
|
||||
|
||||
q: function(){
|
||||
return this.input.get('value').trim();
|
||||
}
|
||||
|
||||
});
|
||||
@@ -47,7 +47,7 @@ config = [{
|
||||
{
|
||||
'name': 'ignored_words',
|
||||
'label': 'Ignored',
|
||||
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs, vain',
|
||||
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
|
||||
'description': 'Ignores releases that match any of these sets. (Works like explained above)'
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,10 +2,10 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.helpers.variable import md5, getTitle, splitString
|
||||
from couchpotato.core.helpers.variable import md5, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||
from couchpotato.core.settings.model import Media, Release, ReleaseInfo
|
||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||
from couchpotato.environment import Env
|
||||
from inspect import ismethod, isfunction
|
||||
import datetime
|
||||
@@ -23,8 +23,7 @@ class Searcher(SearcherBase):
|
||||
addEvent('searcher.contains_other_quality', self.containsOtherQuality)
|
||||
addEvent('searcher.correct_year', self.correctYear)
|
||||
addEvent('searcher.correct_name', self.correctName)
|
||||
addEvent('searcher.correct_words', self.correctWords)
|
||||
addEvent('searcher.search', self.search)
|
||||
addEvent('searcher.download', self.download)
|
||||
|
||||
addApiView('searcher.full_search', self.searchAllView, docs = {
|
||||
'desc': 'Starts a full search for all media',
|
||||
@@ -50,21 +49,86 @@ class Searcher(SearcherBase):
|
||||
progress = fireEvent('searcher.progress', merge = True)
|
||||
return progress
|
||||
|
||||
def search(self, protocols, media, quality):
|
||||
results = []
|
||||
def download(self, data, movie, manual = False):
|
||||
|
||||
for search_protocol in protocols:
|
||||
protocol_results = fireEvent('provider.search.%s.%s' % (search_protocol, media['type']), media, quality, merge = True)
|
||||
if protocol_results:
|
||||
results += protocol_results
|
||||
if not data.get('protocol'):
|
||||
data['protocol'] = data['type']
|
||||
data['type'] = 'movie'
|
||||
|
||||
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
|
||||
# Test to see if any downloaders are enabled for this type
|
||||
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
|
||||
|
||||
download_preference = self.conf('preferred_method', section = 'searcher')
|
||||
if download_preference != 'both':
|
||||
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
|
||||
if downloader_enabled:
|
||||
|
||||
return sorted_results
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
|
||||
# Download movie to temp
|
||||
filedata = None
|
||||
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
|
||||
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||
if filedata == 'try_next':
|
||||
return filedata
|
||||
|
||||
download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
|
||||
log.debug('Downloader result: %s', download_result)
|
||||
|
||||
if download_result:
|
||||
try:
|
||||
# Mark release as snatched
|
||||
db = get_session()
|
||||
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
||||
if rls:
|
||||
renamer_enabled = Env.setting('enabled', 'renamer')
|
||||
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
|
||||
|
||||
# Save download-id info if returned
|
||||
if isinstance(download_result, dict):
|
||||
for key in download_result:
|
||||
rls_info = ReleaseInfo(
|
||||
identifier = 'download_%s' % key,
|
||||
value = toUnicode(download_result.get(key))
|
||||
)
|
||||
rls.info.append(rls_info)
|
||||
db.commit()
|
||||
|
||||
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||
log.info(snatch_message)
|
||||
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
|
||||
|
||||
# If renamer isn't used, mark movie done
|
||||
if not renamer_enabled:
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
try:
|
||||
if movie['status_id'] == active_status.get('id'):
|
||||
for profile_type in movie['profile']['types']:
|
||||
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
||||
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
||||
|
||||
# Mark release done
|
||||
rls.status_id = done_status.get('id')
|
||||
rls.last_edit = int(time.time())
|
||||
db.commit()
|
||||
|
||||
# Mark movie done
|
||||
mvie = db.query(Movie).filter_by(id = movie['id']).first()
|
||||
mvie.status_id = done_status.get('id')
|
||||
mvie.last_edit = int(time.time())
|
||||
db.commit()
|
||||
except:
|
||||
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
|
||||
|
||||
except:
|
||||
log.error('Failed marking movie finished: %s', traceback.format_exc())
|
||||
|
||||
return True
|
||||
|
||||
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('protocol')))
|
||||
|
||||
return False
|
||||
|
||||
def getSearchProtocols(self):
|
||||
|
||||
@@ -153,7 +217,7 @@ class Searcher(SearcherBase):
|
||||
except: pass
|
||||
|
||||
# Match longest name between []
|
||||
try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', check_name), key = len).strip())
|
||||
try: check_names.append(max(check_name.split('['), key = len))
|
||||
except: pass
|
||||
|
||||
for check_name in list(set(check_names)):
|
||||
@@ -170,49 +234,5 @@ class Searcher(SearcherBase):
|
||||
|
||||
return False
|
||||
|
||||
def correctWords(self, rel_name, media):
|
||||
media_title = fireEvent('library.title', media['library'], single = True)
|
||||
media_words = re.split('\W+', simplifyString(media_title))
|
||||
|
||||
rel_name = simplifyString(rel_name)
|
||||
rel_words = re.split('\W+', rel_name)
|
||||
|
||||
# Make sure it has required words
|
||||
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
|
||||
try: required_words = list(set(required_words + splitString(media['category']['required'].lower())))
|
||||
except: pass
|
||||
|
||||
req_match = 0
|
||||
for req_set in required_words:
|
||||
req = splitString(req_set, '&')
|
||||
req_match += len(list(set(rel_words) & set(req))) == len(req)
|
||||
|
||||
if len(required_words) > 0 and req_match == 0:
|
||||
log.info2('Wrong: Required word missing: %s', rel_name)
|
||||
return False
|
||||
|
||||
# Ignore releases
|
||||
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
|
||||
try: ignored_words = list(set(ignored_words + splitString(media['category']['ignored'].lower())))
|
||||
except: pass
|
||||
|
||||
ignored_match = 0
|
||||
for ignored_set in ignored_words:
|
||||
ignored = splitString(ignored_set, '&')
|
||||
ignored_match += len(list(set(rel_words) & set(ignored))) == len(ignored)
|
||||
|
||||
if len(ignored_words) > 0 and ignored_match:
|
||||
log.info2("Wrong: '%s' contains 'ignored words'", rel_name)
|
||||
return False
|
||||
|
||||
# Ignore porn stuff
|
||||
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic', 'cock', 'dick']
|
||||
pron_words = list(set(rel_words) & set(pron_tags) - set(media_words))
|
||||
if pron_words:
|
||||
log.info('Wrong: %s, probably pr0n', rel_name)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
class SearchSetupError(Exception):
|
||||
pass
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
|
||||
mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.movie import MovieTypeBase
|
||||
from couchpotato.core.settings.model import Media
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \
|
||||
Release
|
||||
from couchpotato.environment import Env
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
from sqlalchemy.sql.expression import or_, asc, not_, desc
|
||||
from string import ascii_lowercase
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -13,7 +19,14 @@ log = CPLog(__name__)
|
||||
|
||||
class MovieBase(MovieTypeBase):
|
||||
|
||||
_type = 'movie'
|
||||
default_dict = {
|
||||
'profile': {'types': {'quality': {}}},
|
||||
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
'status': {},
|
||||
'category': {},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -21,12 +34,50 @@ class MovieBase(MovieTypeBase):
|
||||
super(MovieBase, self).__init__()
|
||||
self.initType()
|
||||
|
||||
addApiView('movie.search', self.search, docs = {
|
||||
'desc': 'Search the movie providers for a movie',
|
||||
'params': {
|
||||
'q': {'desc': 'The (partial) movie name you want to search for'},
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'empty': bool, any movies returned or not,
|
||||
'movies': array, movies found,
|
||||
}"""}
|
||||
})
|
||||
addApiView('movie.list', self.listView, docs = {
|
||||
'desc': 'List movies in wanted list',
|
||||
'params': {
|
||||
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
|
||||
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
|
||||
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
|
||||
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
|
||||
'search': {'desc': 'Search movie title'},
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'empty': bool, any movies returned or not,
|
||||
'movies': array, movies found,
|
||||
}"""}
|
||||
})
|
||||
addApiView('movie.get', self.getView, docs = {
|
||||
'desc': 'Get a movie by id',
|
||||
'params': {
|
||||
'id': {'desc': 'The id of the movie'},
|
||||
}
|
||||
})
|
||||
addApiView('movie.refresh', self.refresh, docs = {
|
||||
'desc': 'Refresh a movie by id',
|
||||
'params': {
|
||||
'id': {'desc': 'Movie ID(s) you want to refresh.', 'type': 'int (comma separated)'},
|
||||
}
|
||||
})
|
||||
addApiView('movie.available_chars', self.charView)
|
||||
addApiView('movie.add', self.addView, docs = {
|
||||
'desc': 'Add new movie to the wanted list',
|
||||
'params': {
|
||||
'identifier': {'desc': 'IMDB id of the movie your want to add.'},
|
||||
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
|
||||
'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'},
|
||||
'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
|
||||
}
|
||||
})
|
||||
@@ -35,12 +86,327 @@ class MovieBase(MovieTypeBase):
|
||||
'params': {
|
||||
'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'},
|
||||
'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'},
|
||||
'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'},
|
||||
'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
|
||||
}
|
||||
})
|
||||
addApiView('movie.delete', self.deleteView, docs = {
|
||||
'desc': 'Delete a movie from the wanted list',
|
||||
'params': {
|
||||
'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'},
|
||||
'delete_from': {'desc': 'Delete movie from this page', 'type': 'string: all (default), wanted, manage'},
|
||||
}
|
||||
})
|
||||
|
||||
addEvent('movie.add', self.add)
|
||||
addEvent('movie.delete', self.delete)
|
||||
addEvent('movie.get', self.get)
|
||||
addEvent('movie.list', self.list)
|
||||
addEvent('movie.restatus', self.restatus)
|
||||
|
||||
# Clean releases that didn't have activity in the last week
|
||||
addEvent('app.load', self.cleanReleases)
|
||||
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanReleases, hours = 4)
|
||||
|
||||
def cleanReleases(self):
|
||||
|
||||
log.debug('Removing releases from dashboard')
|
||||
|
||||
now = time.time()
|
||||
week = 262080
|
||||
|
||||
done_status, available_status, snatched_status = \
|
||||
fireEvent('status.get', ['done', 'available', 'snatched'], single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
# get movies last_edit more than a week ago
|
||||
movies = db.query(Movie) \
|
||||
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
|
||||
.all()
|
||||
|
||||
for movie in movies:
|
||||
for rel in movie.releases:
|
||||
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
|
||||
fireEvent('release.delete', id = rel.id, single = True)
|
||||
|
||||
db.expire_all()
|
||||
|
||||
def getView(self, id = None, **kwargs):
|
||||
|
||||
movie = self.get(id) if id else None
|
||||
|
||||
return {
|
||||
'success': movie is not None,
|
||||
'movie': movie,
|
||||
}
|
||||
|
||||
def get(self, movie_id):
|
||||
|
||||
db = get_session()
|
||||
|
||||
imdb_id = getImdb(str(movie_id))
|
||||
|
||||
if imdb_id:
|
||||
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
|
||||
else:
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
|
||||
results = None
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return results
|
||||
|
||||
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if status and not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
# query movie ids
|
||||
q = db.query(Movie) \
|
||||
.with_entities(Movie.id) \
|
||||
.group_by(Movie.id)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Movie.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.join(Movie.releases)
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Release.status_id.in_(statuses))
|
||||
|
||||
# Only join when searching / ordering
|
||||
if starts_with or search or order != 'release_order':
|
||||
q = q.join(Movie.library, Library.titles) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
# Add search filters
|
||||
filter_or = []
|
||||
if starts_with:
|
||||
starts_with = toUnicode(starts_with.lower())
|
||||
if starts_with in ascii_lowercase:
|
||||
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
|
||||
else:
|
||||
ignore = []
|
||||
for letter in ascii_lowercase:
|
||||
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
|
||||
filter_or.append(not_(or_(*ignore)))
|
||||
|
||||
if search:
|
||||
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
||||
|
||||
if len(filter_or) > 0:
|
||||
q = q.filter(or_(*filter_or))
|
||||
|
||||
total_count = q.count()
|
||||
if total_count == 0:
|
||||
return 0, []
|
||||
|
||||
if order == 'release_order':
|
||||
q = q.order_by(desc(Release.last_edit))
|
||||
else:
|
||||
q = q.order_by(asc(LibraryTitle.simple_title))
|
||||
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q = q.limit(limit).offset(offset)
|
||||
|
||||
# Get all movie_ids in sorted order
|
||||
movie_ids = [m.id for m in q.all()]
|
||||
|
||||
# List release statuses
|
||||
releases = db.query(Release) \
|
||||
.filter(Release.movie_id.in_(movie_ids)) \
|
||||
.all()
|
||||
|
||||
release_statuses = dict((m, set()) for m in movie_ids)
|
||||
releases_count = dict((m, 0) for m in movie_ids)
|
||||
for release in releases:
|
||||
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id))
|
||||
releases_count[release.movie_id] += 1
|
||||
|
||||
# Get main movie data
|
||||
q2 = db.query(Movie) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
q2 = q2.filter(Movie.id.in_(movie_ids))
|
||||
|
||||
results = q2.all()
|
||||
|
||||
# Create dict by movie id
|
||||
movie_dict = {}
|
||||
for movie in results:
|
||||
movie_dict[movie.id] = movie
|
||||
|
||||
# List movies based on movie_ids order
|
||||
movies = []
|
||||
for movie_id in movie_ids:
|
||||
|
||||
releases = []
|
||||
for r in release_statuses.get(movie_id):
|
||||
x = splitString(r)
|
||||
releases.append({'status_id': x[0], 'quality_id': x[1]})
|
||||
|
||||
# Merge releases with movie dict
|
||||
movies.append(mergeDicts(movie_dict[movie_id].to_dict({
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
}), {
|
||||
'releases': releases,
|
||||
'releases_count': releases_count.get(movie_id),
|
||||
}))
|
||||
|
||||
db.expire_all()
|
||||
return total_count, movies
|
||||
|
||||
def availableChars(self, status = None, release_status = None):
|
||||
|
||||
status = status or []
|
||||
release_status = release_status or []
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
q = db.query(Movie)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Movie.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.join(Movie.releases) \
|
||||
.filter(Release.status_id.in_(statuses))
|
||||
|
||||
q = q.join(Library, LibraryTitle) \
|
||||
.with_entities(LibraryTitle.simple_title) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
titles = q.all()
|
||||
|
||||
chars = set()
|
||||
for title in titles:
|
||||
try:
|
||||
char = title[0][0]
|
||||
char = char if char in ascii_lowercase else '#'
|
||||
chars.add(str(char))
|
||||
except:
|
||||
log.error('Failed getting title for %s', title.libraries_id)
|
||||
|
||||
if len(chars) == 25:
|
||||
break
|
||||
|
||||
db.expire_all()
|
||||
return ''.join(sorted(chars))
|
||||
|
||||
def listView(self, **kwargs):
|
||||
|
||||
status = splitString(kwargs.get('status'))
|
||||
release_status = splitString(kwargs.get('release_status'))
|
||||
limit_offset = kwargs.get('limit_offset')
|
||||
starts_with = kwargs.get('starts_with')
|
||||
search = kwargs.get('search')
|
||||
order = kwargs.get('order')
|
||||
|
||||
total_movies, movies = self.list(
|
||||
status = status,
|
||||
release_status = release_status,
|
||||
limit_offset = limit_offset,
|
||||
starts_with = starts_with,
|
||||
search = search,
|
||||
order = order
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
}
|
||||
|
||||
def charView(self, **kwargs):
|
||||
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
chars = self.availableChars(status, release_status)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(chars) == 0,
|
||||
'chars': chars,
|
||||
}
|
||||
|
||||
def refresh(self, id = '', **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
for x in splitString(id):
|
||||
movie = db.query(Movie).filter_by(id = x).first()
|
||||
|
||||
if movie:
|
||||
|
||||
# Get current selected title
|
||||
default_title = ''
|
||||
for title in movie.library.titles:
|
||||
if title.default: default_title = title.title
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
|
||||
fireEventAsync('library.update.movie', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def search(self, q = '', **kwargs):
|
||||
|
||||
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
||||
movies = Env.get('cache').get(cache_key)
|
||||
|
||||
if not movies:
|
||||
|
||||
if getImdb(q):
|
||||
movies = [fireEvent('movie.info', identifier = q, merge = True)]
|
||||
else:
|
||||
movies = fireEvent('movie.search', q = q, merge = True)
|
||||
Env.get('cache').set(cache_key, movies)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0 if movies else 0,
|
||||
'movies': movies,
|
||||
}
|
||||
|
||||
def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||
if not params: params = {}
|
||||
@@ -72,12 +438,12 @@ class MovieBase(MovieTypeBase):
|
||||
cat_id = params.get('category_id')
|
||||
|
||||
db = get_session()
|
||||
m = db.query(Media).filter_by(library_id = library.get('id')).first()
|
||||
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
|
||||
added = True
|
||||
do_search = False
|
||||
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
|
||||
if not m:
|
||||
m = Media(
|
||||
m = Movie(
|
||||
library_id = library.get('id'),
|
||||
profile_id = params.get('profile_id', default_profile.get('id')),
|
||||
status_id = status_id if status_id else status_active.get('id'),
|
||||
@@ -134,12 +500,15 @@ class MovieBase(MovieTypeBase):
|
||||
db.expire_all()
|
||||
return movie_dict
|
||||
|
||||
|
||||
def addView(self, **kwargs):
|
||||
add_dict = self.add(params = kwargs)
|
||||
|
||||
movie_dict = self.add(params = kwargs)
|
||||
|
||||
return {
|
||||
'success': True if add_dict else False,
|
||||
'movie': add_dict,
|
||||
'success': True,
|
||||
'added': True if movie_dict else False,
|
||||
'movie': movie_dict,
|
||||
}
|
||||
|
||||
def edit(self, id = '', **kwargs):
|
||||
@@ -149,9 +518,9 @@ class MovieBase(MovieTypeBase):
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
|
||||
ids = splitString(id)
|
||||
for media_id in ids:
|
||||
for movie_id in ids:
|
||||
|
||||
m = db.query(Media).filter_by(id = media_id).first()
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
if not m:
|
||||
continue
|
||||
|
||||
@@ -174,12 +543,119 @@ class MovieBase(MovieTypeBase):
|
||||
|
||||
db.commit()
|
||||
|
||||
fireEvent('media.restatus', m.id)
|
||||
fireEvent('movie.restatus', m.id)
|
||||
|
||||
movie_dict = m.to_dict(self.search_dict)
|
||||
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id))
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def deleteView(self, id = '', **kwargs):
|
||||
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def delete(self, movie_id, delete_from = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
if movie:
|
||||
deleted = False
|
||||
if delete_from == 'all':
|
||||
db.delete(movie)
|
||||
db.commit()
|
||||
deleted = True
|
||||
else:
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
total_releases = len(movie.releases)
|
||||
total_deleted = 0
|
||||
new_movie_status = None
|
||||
for release in movie.releases:
|
||||
if delete_from in ['wanted', 'snatched', 'late']:
|
||||
if release.status_id != done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'done'
|
||||
elif delete_from == 'manage':
|
||||
if release.status_id == done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'active'
|
||||
db.commit()
|
||||
|
||||
if total_releases == total_deleted:
|
||||
db.delete(movie)
|
||||
db.commit()
|
||||
deleted = True
|
||||
elif new_movie_status:
|
||||
new_status = fireEvent('status.get', new_movie_status, single = True)
|
||||
movie.profile_id = None
|
||||
movie.status_id = new_status.get('id')
|
||||
db.commit()
|
||||
else:
|
||||
fireEvent('movie.restatus', movie.id, single = True)
|
||||
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
||||
|
||||
db.expire_all()
|
||||
return True
|
||||
|
||||
def restatus(self, movie_id):
|
||||
|
||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
if not m or len(m.library.titles) == 0:
|
||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||
return False
|
||||
|
||||
log.debug('Changing status for %s', m.library.titles[0].title)
|
||||
if not m.profile:
|
||||
m.status_id = done_status.get('id')
|
||||
else:
|
||||
move_to_wanted = True
|
||||
|
||||
for t in m.profile.types:
|
||||
for release in m.releases:
|
||||
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
|
||||
move_to_wanted = False
|
||||
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
|
||||
def createOnComplete(self, movie_id):
|
||||
|
||||
def onComplete():
|
||||
db = get_session()
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
fireEventAsync('movie.searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
|
||||
db.expire_all()
|
||||
|
||||
return onComplete
|
||||
|
||||
|
||||
def createNotifyFront(self, movie_id):
|
||||
|
||||
def notifyFront():
|
||||
db = get_session()
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
|
||||
db.expire_all()
|
||||
|
||||
return notifyFront
|
||||
|
||||
@@ -52,8 +52,8 @@ var MovieList = new Class({
|
||||
|
||||
self.getMovies();
|
||||
|
||||
App.on('movie.added', self.movieAdded.bind(self))
|
||||
App.on('movie.deleted', self.movieDeleted.bind(self))
|
||||
App.addEvent('movie.added', self.movieAdded.bind(self))
|
||||
App.addEvent('movie.deleted', self.movieDeleted.bind(self))
|
||||
},
|
||||
|
||||
movieDeleted: function(notification){
|
||||
@@ -65,7 +65,6 @@ var MovieList = new Class({
|
||||
movie.destroy();
|
||||
delete self.movies_added[notification.data.id];
|
||||
self.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -76,7 +75,6 @@ var MovieList = new Class({
|
||||
movieAdded: function(notification){
|
||||
var self = this;
|
||||
|
||||
self.fireEvent('movieAdded', notification);
|
||||
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
|
||||
window.scroll(0,0);
|
||||
self.createMovie(notification.data, 'top');
|
||||
@@ -281,7 +279,7 @@ var MovieList = new Class({
|
||||
|
||||
// Get available chars and highlight
|
||||
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
|
||||
Api.request('media.available_chars', {
|
||||
Api.request('movie.available_chars', {
|
||||
'data': Object.merge({
|
||||
'status': self.options.status
|
||||
}, self.filter),
|
||||
@@ -372,7 +370,7 @@ var MovieList = new Class({
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
this.set('text', 'Deleting..')
|
||||
Api.request('media.delete', {
|
||||
Api.request('movie.delete', {
|
||||
'data': {
|
||||
'id': ids.join(','),
|
||||
'delete_from': self.options.identifier
|
||||
@@ -392,7 +390,6 @@ var MovieList = new Class({
|
||||
self.movies.erase(movie);
|
||||
movie.destroy();
|
||||
self.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
});
|
||||
|
||||
self.calculateSelected();
|
||||
@@ -425,7 +422,7 @@ var MovieList = new Class({
|
||||
var self = this;
|
||||
var ids = self.getSelectedMovies()
|
||||
|
||||
Api.request('media.refresh', {
|
||||
Api.request('movie.refresh', {
|
||||
'data': {
|
||||
'id': ids.join(','),
|
||||
}
|
||||
@@ -550,9 +547,8 @@ var MovieList = new Class({
|
||||
|
||||
}
|
||||
|
||||
Api.request(self.options.api_call || 'media.list', {
|
||||
Api.request(self.options.api_call || 'movie.list', {
|
||||
'data': Object.merge({
|
||||
'type': 'movie',
|
||||
'status': self.options.status,
|
||||
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
|
||||
}, self.filter),
|
||||
|
||||
@@ -18,13 +18,11 @@ var MovieAction = new Class({
|
||||
create: function(){},
|
||||
|
||||
disable: function(){
|
||||
if(this.el)
|
||||
this.el.addClass('disable')
|
||||
this.el.addClass('disable')
|
||||
},
|
||||
|
||||
enable: function(){
|
||||
if(this.el)
|
||||
this.el.removeClass('disable')
|
||||
this.el.removeClass('disable')
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
@@ -126,9 +124,7 @@ MA.Release = new Class({
|
||||
else
|
||||
self.showHelper();
|
||||
|
||||
App.on('movie.searcher.ended', function(notification){
|
||||
if(self.movie.data.id != notification.data.id) return;
|
||||
|
||||
App.addEvent('movie.searcher.ended.'+self.movie.data.id, function(notification){
|
||||
self.releases = null;
|
||||
if(self.options_container){
|
||||
self.options_container.destroy();
|
||||
@@ -243,6 +239,7 @@ MA.Release = new Class({
|
||||
}
|
||||
})
|
||||
).inject(self.release_container);
|
||||
|
||||
release['el'] = item;
|
||||
|
||||
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
|
||||
@@ -252,44 +249,18 @@ MA.Release = new Class({
|
||||
else if(!self.next_release && status.identifier == 'available'){
|
||||
self.next_release = release;
|
||||
}
|
||||
|
||||
var update_handle = function(notification) {
|
||||
if(notification.data.id != release.id) return;
|
||||
|
||||
var q = self.movie.quality.getElement('.q_id' + release.quality_id),
|
||||
status = Status.get(release.status_id),
|
||||
new_status = Status.get(notification.data.status_id);
|
||||
|
||||
release.status_id = new_status.id
|
||||
release.el.set('class', 'item ' + new_status.identifier);
|
||||
|
||||
var status_el = release.el.getElement('.release_status');
|
||||
status_el.set('class', 'release_status ' + new_status.identifier);
|
||||
status_el.set('text', new_status.identifier);
|
||||
|
||||
if(!q && (new_status.identifier == 'snatched' || new_status.identifier == 'seeding' || new_status.identifier == 'done'))
|
||||
var q = self.addQuality(release.quality_id);
|
||||
|
||||
if(new_status && q && !q.hasClass(new_status.identifier)) {
|
||||
q.removeClass(status.identifier).addClass(new_status.identifier);
|
||||
q.set('title', q.get('title').replace(status.label, new_status.label));
|
||||
}
|
||||
}
|
||||
|
||||
App.on('release.update_status', update_handle);
|
||||
|
||||
});
|
||||
|
||||
if(self.last_release)
|
||||
self.release_container.getElements('#release_'+self.last_release.id).addClass('last_release');
|
||||
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
|
||||
|
||||
if(self.next_release)
|
||||
self.release_container.getElements('#release_'+self.next_release.id).addClass('next_release');
|
||||
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
|
||||
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
|
||||
|
||||
|
||||
var nr = self.next_release,
|
||||
lr = self.last_release;
|
||||
|
||||
@@ -385,7 +356,7 @@ MA.Release = new Class({
|
||||
},
|
||||
|
||||
get: function(release, type){
|
||||
return release.info[type] !== undefined ? release.info[type] : 'n/a'
|
||||
return release.info[type] || 'n/a'
|
||||
},
|
||||
|
||||
download: function(release){
|
||||
@@ -394,25 +365,21 @@ MA.Release = new Class({
|
||||
var release_el = self.release_container.getElement('#release_'+release.id),
|
||||
icon = release_el.getElement('.download.icon2');
|
||||
|
||||
if(icon)
|
||||
icon.addClass('icon spinner').removeClass('download');
|
||||
icon.addClass('icon spinner').removeClass('download');
|
||||
|
||||
Api.request('release.manual_download', {
|
||||
Api.request('release.download', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
if(icon)
|
||||
icon.removeClass('icon spinner');
|
||||
icon.removeClass('icon spinner');
|
||||
|
||||
if(json.success){
|
||||
if(icon)
|
||||
icon.addClass('completed');
|
||||
icon.addClass('completed');
|
||||
release_el.getElement('.release_status').set('text', 'snatched');
|
||||
}
|
||||
else
|
||||
if(icon)
|
||||
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
|
||||
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -424,6 +391,17 @@ MA.Release = new Class({
|
||||
'data': {
|
||||
'id': release.id
|
||||
},
|
||||
'onComplete': function(){
|
||||
var el = release.el;
|
||||
if(el.hasClass('failed') || el.hasClass('ignored')){
|
||||
el.removeClass('failed').removeClass('ignored');
|
||||
el.getElement('.release_status').set('text', 'available');
|
||||
}
|
||||
else {
|
||||
el.addClass('ignored');
|
||||
el.getElement('.release_status').set('text', 'ignored');
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
@@ -431,7 +409,7 @@ MA.Release = new Class({
|
||||
markMovieDone: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('media.delete', {
|
||||
Api.request('movie.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': 'wanted'
|
||||
@@ -450,7 +428,7 @@ MA.Release = new Class({
|
||||
|
||||
},
|
||||
|
||||
tryNextRelease: function(){
|
||||
tryNextRelease: function(movie_id){
|
||||
var self = this;
|
||||
|
||||
Api.request('movie.searcher.try_next', {
|
||||
@@ -710,7 +688,7 @@ MA.Refresh = new Class({
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
Api.request('media.refresh', {
|
||||
Api.request('movie.refresh', {
|
||||
'data': {
|
||||
'id': self.movie.get('id')
|
||||
}
|
||||
@@ -821,7 +799,7 @@ MA.Delete = new Class({
|
||||
self.callChain();
|
||||
},
|
||||
function(){
|
||||
Api.request('media.delete', {
|
||||
Api.request('movie.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': self.movie.list.options.identifier
|
||||
|
||||
@@ -419,25 +419,22 @@
|
||||
}
|
||||
|
||||
.movies .data .quality .available,
|
||||
.movies .data .quality .snatched,
|
||||
.movies .data .quality .seeding {
|
||||
.movies .data .quality .snatched {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.movies .data .quality .available { background-color: #578bc3; }
|
||||
.movies .data .quality .failed,
|
||||
.movies .data .quality .missing,
|
||||
.movies .data .quality .ignored { background-color: #a43d34; }
|
||||
.movies .data .quality .failed { background-color: #a43d34; }
|
||||
.movies .data .quality .snatched { background-color: #a2a232; }
|
||||
.movies .data .quality .seeding { background-color: #0a6819; }
|
||||
.movies .data .quality .done {
|
||||
background-color: #369545;
|
||||
opacity: 1;
|
||||
}
|
||||
.movies .data .quality .seeding { background-color: #0a6819; }
|
||||
.movies .data .quality .finish {
|
||||
background-image: url('../../images/sprite.png');
|
||||
background-repeat: no-repeat;
|
||||
background-image: url('../images/sprite.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 2px;
|
||||
padding-left: 14px;
|
||||
background-size: 14px
|
||||
@@ -649,7 +646,7 @@
|
||||
margin-top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.trailer_container.hide {
|
||||
height: 0 !important;
|
||||
}
|
||||
@@ -992,7 +989,7 @@
|
||||
}
|
||||
|
||||
.movies .empty_wanted {
|
||||
background-image: url('../../images/emptylist.png');
|
||||
background-image: url('../images/emptylist.png');
|
||||
background-position: 80% 0;
|
||||
height: 750px;
|
||||
width: 100%;
|
||||
@@ -1032,11 +1029,11 @@
|
||||
.movies .progress > div .folder {
|
||||
display: inline-block;
|
||||
padding: 5px 20px 5px 0;
|
||||
white-space: nowrap;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 85%;
|
||||
direction: ltr;
|
||||
direction: rtl;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,49 +23,23 @@ var Movie = new Class({
|
||||
addEvents: function(){
|
||||
var self = this;
|
||||
|
||||
self.global_events = {}
|
||||
|
||||
// Do refresh with new data
|
||||
self.global_events['movie.update'] = function(notification){
|
||||
if(self.data.id != notification.data.id) return;
|
||||
|
||||
App.addEvent('movie.update.'+self.data.id, function(notification){
|
||||
self.busy(false);
|
||||
self.removeView();
|
||||
self.update.delay(2000, self, notification);
|
||||
}
|
||||
App.on('movie.update', self.global_events['movie.update']);
|
||||
});
|
||||
|
||||
// Add spinner on load / search
|
||||
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||
self.global_events[listener] = function(notification){
|
||||
if(notification.data && self.data.id == notification.data.id)
|
||||
App.addEvent(listener+'.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
self.busy(true)
|
||||
}
|
||||
App.on(listener, self.global_events[listener]);
|
||||
});
|
||||
})
|
||||
|
||||
// Remove spinner
|
||||
self.global_events['movie.searcher.ended'] = function(notification){
|
||||
if(notification.data && self.data.id == notification.data.id)
|
||||
App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
self.busy(false)
|
||||
}
|
||||
App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
|
||||
|
||||
// Reload when releases have updated
|
||||
self.global_events['release.update_status'] = function(notification){
|
||||
var data = notification.data
|
||||
if(data && self.data.id == data.media_id){
|
||||
|
||||
if(!self.data.releases)
|
||||
self.data.releases = [];
|
||||
|
||||
self.data.releases.push({'quality_id': data.quality_id, 'status_id': data.status_id});
|
||||
self.updateReleases();
|
||||
}
|
||||
}
|
||||
|
||||
App.on('release.update_status', self.global_events['release.update_status']);
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function(){
|
||||
@@ -78,9 +52,10 @@ var Movie = new Class({
|
||||
self.list.checkIfEmpty();
|
||||
|
||||
// Remove events
|
||||
Object.each(self.global_events, function(handle, listener){
|
||||
App.off(listener, handle);
|
||||
});
|
||||
App.removeEvents('movie.update.'+self.data.id);
|
||||
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||
App.removeEvents(listener+'.'+self.data.id);
|
||||
})
|
||||
},
|
||||
|
||||
busy: function(set_busy, timeout){
|
||||
@@ -204,7 +179,21 @@ var Movie = new Class({
|
||||
});
|
||||
|
||||
// Add releases
|
||||
self.updateReleases();
|
||||
if(self.data.releases)
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||
status = Status.get(release.status_id);
|
||||
|
||||
if(!q && (status.identifier == 'snatched' || status.identifier == 'done'))
|
||||
var q = self.addQuality(release.quality_id)
|
||||
|
||||
if (status && q && !q.hasClass(status.identifier)){
|
||||
q.addClass(status.identifier);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
|
||||
@@ -214,26 +203,6 @@ var Movie = new Class({
|
||||
|
||||
},
|
||||
|
||||
updateReleases: function(){
|
||||
var self = this;
|
||||
if(!self.data.releases || self.data.releases.length == 0) return;
|
||||
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||
status = Status.get(release.status_id);
|
||||
|
||||
if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done'))
|
||||
var q = self.addQuality(release.quality_id)
|
||||
|
||||
if (status && q && !q.hasClass(status.identifier)){
|
||||
q.addClass(status.identifier);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
addQuality: function(quality_id){
|
||||
var self = this;
|
||||
|
||||
@@ -287,8 +256,7 @@ var Movie = new Class({
|
||||
self.el.removeEvents('outerClick')
|
||||
|
||||
setTimeout(function(){
|
||||
if(self.el)
|
||||
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
|
||||
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
|
||||
}, 600);
|
||||
|
||||
self.data_container.removeClass('hide_right');
|
||||
@@ -298,10 +266,9 @@ var Movie = new Class({
|
||||
changeView: function(new_view){
|
||||
var self = this;
|
||||
|
||||
if(self.el)
|
||||
self.el
|
||||
.removeClass(self.view+'_view')
|
||||
.addClass(new_view+'_view')
|
||||
self.el
|
||||
.removeClass(self.view+'_view')
|
||||
.addClass(new_view+'_view')
|
||||
|
||||
self.view = new_view;
|
||||
},
|
||||
|
||||
@@ -129,13 +129,13 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.media_result {
|
||||
.movie_result {
|
||||
overflow: hidden;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.media_result .options {
|
||||
.movie_result .options {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
@@ -147,48 +147,48 @@
|
||||
border-radius: 0;
|
||||
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
|
||||
}
|
||||
.media_result .options > .in_library_wanted {
|
||||
.movie_result .options > .in_library_wanted {
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
.media_result .options > div {
|
||||
.movie_result .options > div {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.media_result .options .thumbnail {
|
||||
.movie_result .options .thumbnail {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.media_result .options select {
|
||||
.movie_result .options select {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.media_result .options select[name=title] { width: 170px; }
|
||||
.media_result .options select[name=profile] { width: 90px; }
|
||||
.media_result .options select[name=category] { width: 80px; }
|
||||
.movie_result .options select[name=title] { width: 170px; }
|
||||
.movie_result .options select[name=profile] { width: 90px; }
|
||||
.movie_result .options select[name=category] { width: 80px; }
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
|
||||
.media_result .options select[name=title] { width: 90px; }
|
||||
.media_result .options select[name=profile] { width: 50px; }
|
||||
.media_result .options select[name=category] { width: 50px; }
|
||||
.movie_result .options select[name=title] { width: 90px; }
|
||||
.movie_result .options select[name=profile] { width: 50px; }
|
||||
.movie_result .options select[name=category] { width: 50px; }
|
||||
|
||||
}
|
||||
|
||||
.media_result .options .button {
|
||||
.movie_result .options .button {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.media_result .options .message {
|
||||
.movie_result .options .message {
|
||||
height: 100%;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.media_result .data {
|
||||
.movie_result .data {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
@@ -199,20 +199,20 @@
|
||||
border-top: 1px solid rgba(255,255,255, 0.08);
|
||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||
}
|
||||
.media_result .data.open {
|
||||
.movie_result .data.open {
|
||||
left: 100% !important;
|
||||
}
|
||||
|
||||
.media_result:last-child .data { border-bottom: 0; }
|
||||
.movie_result:last-child .data { border-bottom: 0; }
|
||||
|
||||
.media_result .in_wanted, .media_result .in_library {
|
||||
.movie_result .in_wanted, .movie_result .in_library {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 14px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.media_result .thumbnail {
|
||||
.movie_result .thumbnail {
|
||||
width: 34px;
|
||||
min-height: 100%;
|
||||
display: block;
|
||||
@@ -220,7 +220,7 @@
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.media_result .info {
|
||||
.movie_result .info {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
left: 15px;
|
||||
@@ -228,7 +228,7 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.media_result .info h2 {
|
||||
.movie_result .info h2 {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
font-size: 20px;
|
||||
@@ -240,7 +240,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media_result .info h2 .title {
|
||||
.movie_result .info h2 .title {
|
||||
display: block;
|
||||
margin: 0;
|
||||
text-overflow: ellipsis;
|
||||
@@ -253,7 +253,7 @@
|
||||
width: 88%;
|
||||
}
|
||||
|
||||
.media_result .info h2 .year {
|
||||
.movie_result .info h2 .year {
|
||||
padding: 0 5px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
@@ -271,7 +271,7 @@
|
||||
}
|
||||
|
||||
.search_form .mask,
|
||||
.media_result .mask {
|
||||
.movie_result .mask {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@@ -1,4 +1,189 @@
|
||||
Block.Search.MovieItem = new Class({
|
||||
Block.Search = new Class({
|
||||
|
||||
Extends: BlockBase,
|
||||
|
||||
cache: {},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var focus_timer = 0;
|
||||
self.el = new Element('div.search_form').adopt(
|
||||
new Element('div.input').adopt(
|
||||
self.input = new Element('input', {
|
||||
'placeholder': 'Search & add a new movie',
|
||||
'events': {
|
||||
'keyup': self.keyup.bind(self),
|
||||
'focus': function(){
|
||||
if(focus_timer) clearTimeout(focus_timer);
|
||||
self.el.addClass('focused')
|
||||
if(this.get('value'))
|
||||
self.hideResults(false)
|
||||
},
|
||||
'blur': function(){
|
||||
focus_timer = (function(){
|
||||
self.el.removeClass('focused')
|
||||
}).delay(100);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('a.icon2', {
|
||||
'events': {
|
||||
'click': self.clear.bind(self),
|
||||
'touchend': self.clear.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
self.result_container = new Element('div.results_container', {
|
||||
'tween': {
|
||||
'duration': 200
|
||||
},
|
||||
'events': {
|
||||
'mousewheel': function(e){
|
||||
(e).stopPropagation();
|
||||
}
|
||||
}
|
||||
}).adopt(
|
||||
self.results = new Element('div.results')
|
||||
)
|
||||
);
|
||||
|
||||
self.mask = new Element('div.mask').inject(self.result_container).fade('hide');
|
||||
|
||||
},
|
||||
|
||||
clear: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
if(self.last_q === ''){
|
||||
self.input.blur()
|
||||
self.last_q = null;
|
||||
}
|
||||
else {
|
||||
|
||||
self.last_q = '';
|
||||
self.input.set('value', '');
|
||||
self.input.focus()
|
||||
|
||||
self.movies = []
|
||||
self.results.empty()
|
||||
self.el.removeClass('filled')
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
hideResults: function(bool){
|
||||
var self = this;
|
||||
|
||||
if(self.hidden == bool) return;
|
||||
|
||||
self.el[bool ? 'removeClass' : 'addClass']('shown');
|
||||
|
||||
if(bool){
|
||||
History.removeEvent('change', self.hideResults.bind(self, !bool));
|
||||
self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool));
|
||||
}
|
||||
else {
|
||||
History.addEvent('change', self.hideResults.bind(self, !bool));
|
||||
self.el.addEvent('outerClick', self.hideResults.bind(self, !bool));
|
||||
}
|
||||
|
||||
self.hidden = bool;
|
||||
},
|
||||
|
||||
keyup: function(e){
|
||||
var self = this;
|
||||
|
||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
||||
|
||||
if(self.q() != self.last_q){
|
||||
if(self.api_request && self.api_request.isRunning())
|
||||
self.api_request.cancel();
|
||||
|
||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
||||
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
autocomplete: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.q()){
|
||||
self.hideResults(true)
|
||||
return
|
||||
}
|
||||
|
||||
self.list()
|
||||
},
|
||||
|
||||
list: function(){
|
||||
var self = this,
|
||||
q = self.q(),
|
||||
cache = self.cache[q];
|
||||
|
||||
self.hideResults(false);
|
||||
|
||||
if(!cache){
|
||||
self.mask.fade('in');
|
||||
|
||||
if(!self.spinner)
|
||||
self.spinner = createSpinner(self.mask);
|
||||
|
||||
self.api_request = Api.request('movie.search', {
|
||||
'data': {
|
||||
'q': q
|
||||
},
|
||||
'onComplete': self.fill.bind(self, q)
|
||||
})
|
||||
}
|
||||
else
|
||||
self.fill(q, cache)
|
||||
|
||||
self.last_q = q;
|
||||
|
||||
},
|
||||
|
||||
fill: function(q, json){
|
||||
var self = this;
|
||||
|
||||
self.cache[q] = json
|
||||
|
||||
self.movies = {}
|
||||
self.results.empty()
|
||||
|
||||
Object.each(json.movies, function(movie){
|
||||
|
||||
var m = new Block.Search.Item(movie);
|
||||
$(m).inject(self.results)
|
||||
self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
|
||||
|
||||
if(q == movie.imdb)
|
||||
m.showOptions()
|
||||
|
||||
});
|
||||
|
||||
// Calculate result heights
|
||||
var w = window.getSize(),
|
||||
rc = self.result_container.getCoordinates();
|
||||
|
||||
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
|
||||
self.mask.fade('out')
|
||||
|
||||
},
|
||||
|
||||
loading: function(bool){
|
||||
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
||||
},
|
||||
|
||||
q: function(){
|
||||
return this.input.get('value').trim();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Block.Search.Item = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
@@ -16,7 +201,7 @@ Block.Search.MovieItem = new Class({
|
||||
var self = this,
|
||||
info = self.info;
|
||||
|
||||
self.el = new Element('div.media_result', {
|
||||
self.el = new Element('div.movie_result', {
|
||||
'id': info.imdb
|
||||
}).adopt(
|
||||
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||
@@ -107,7 +292,7 @@ Block.Search.MovieItem = new Class({
|
||||
self.options_el.empty();
|
||||
self.options_el.adopt(
|
||||
new Element('div.message', {
|
||||
'text': json.success ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs'
|
||||
'text': json.added ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs'
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
@@ -181,7 +366,7 @@ Block.Search.MovieItem = new Class({
|
||||
if(categories.length == 0)
|
||||
self.category_select.hide();
|
||||
else {
|
||||
self.category_select.movie();
|
||||
self.category_select.show();
|
||||
categories.each(function(category){
|
||||
new Element('option', {
|
||||
'value': category.data.id,
|
||||
|
||||
@@ -2,7 +2,7 @@ from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
from couchpotato.core.media._base.library import LibraryBase
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
||||
from string import ascii_letters
|
||||
import time
|
||||
@@ -16,46 +16,27 @@ class MovieLibraryPlugin(LibraryBase):
|
||||
default_dict = {'titles': {}, 'files':{}}
|
||||
|
||||
def __init__(self):
|
||||
addEvent('library.query', self.query)
|
||||
addEvent('library.add.movie', self.add)
|
||||
addEvent('library.update.movie', self.update)
|
||||
addEvent('library.update.movie.release_date', self.updateReleaseDate)
|
||||
|
||||
def query(self, library, first = True, include_year = True, **kwargs):
|
||||
if library.get('type') != 'movie':
|
||||
return
|
||||
def add(self, attrs = None, update_after = True):
|
||||
if not attrs: attrs = {}
|
||||
|
||||
titles = [title['title'] for title in library['titles']]
|
||||
|
||||
# Add year identifier to titles
|
||||
if include_year:
|
||||
titles = [title + (' %s' % str(library['year'])) for title in titles]
|
||||
|
||||
if first:
|
||||
return titles[0] if titles else None
|
||||
|
||||
return titles
|
||||
|
||||
def add(self, attrs = {}, update_after = True):
|
||||
# movies don't yet contain these, so lets make sure to set defaults
|
||||
type = attrs.get('type', 'movie')
|
||||
primary_provider = attrs.get('primary_provider', 'imdb')
|
||||
|
||||
db = get_session()
|
||||
|
||||
l = db.query(Library).filter_by(type = type, identifier = attrs.get('identifier')).first()
|
||||
l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
|
||||
if not l:
|
||||
status = fireEvent('status.get', 'needs_update', single = True)
|
||||
l = Library(
|
||||
type = type,
|
||||
primary_provider = primary_provider,
|
||||
year = attrs.get('year'),
|
||||
identifier = attrs.get('identifier'),
|
||||
plot = toUnicode(attrs.get('plot')),
|
||||
tagline = toUnicode(attrs.get('tagline')),
|
||||
status_id = status.get('id'),
|
||||
info = {},
|
||||
parent = None
|
||||
info = {}
|
||||
)
|
||||
|
||||
title = LibraryTitle(
|
||||
@@ -170,7 +151,7 @@ class MovieLibraryPlugin(LibraryBase):
|
||||
else:
|
||||
dates = library.info.get('release_date')
|
||||
|
||||
if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates:
|
||||
if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||
library.info.update({'release_date': dates })
|
||||
db.commit()
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss
|
||||
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
|
||||
possibleTitles, getImdb
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||
from couchpotato.core.media.movie import MovieTypeBase
|
||||
from couchpotato.core.settings.model import Media, Release
|
||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||
from couchpotato.environment import Env
|
||||
from datetime import date
|
||||
from sqlalchemy.exc import InterfaceError
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
@@ -27,9 +29,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
addEvent('movie.searcher.all', self.searchAll)
|
||||
addEvent('movie.searcher.all_view', self.searchAllView)
|
||||
addEvent('movie.searcher.single', self.single)
|
||||
addEvent('movie.searcher.correct_movie', self.correctMovie)
|
||||
addEvent('movie.searcher.try_next_release', self.tryNextRelease)
|
||||
addEvent('movie.searcher.could_be_released', self.couldBeReleased)
|
||||
addEvent('searcher.correct_release', self.correctRelease)
|
||||
|
||||
addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
|
||||
'desc': 'Marks the snatched results as ignored and try the next best release',
|
||||
@@ -72,8 +74,8 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
|
||||
db = get_session()
|
||||
|
||||
movies = db.query(Media).filter(
|
||||
Media.status.has(identifier = 'active')
|
||||
movies = db.query(Movie).filter(
|
||||
Movie.status.has(identifier = 'active')
|
||||
).all()
|
||||
random.shuffle(movies)
|
||||
|
||||
@@ -115,10 +117,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
|
||||
def single(self, movie, search_protocols = None, manual = False):
|
||||
|
||||
# movies don't contain 'type' yet, so just set to default here
|
||||
if not movie.has_key('type'):
|
||||
movie['type'] = 'movie'
|
||||
|
||||
# Find out search type
|
||||
try:
|
||||
if not search_protocols:
|
||||
@@ -144,10 +142,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
default_title = getTitle(movie['library'])
|
||||
if not default_title:
|
||||
log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
|
||||
fireEvent('media.delete', movie['id'], single = True)
|
||||
fireEvent('movie.delete', movie['id'], single = True)
|
||||
return
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title)
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
|
||||
|
||||
|
||||
ret = False
|
||||
@@ -169,20 +167,82 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
|
||||
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
|
||||
|
||||
results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or []
|
||||
if len(results) == 0:
|
||||
results = []
|
||||
for search_protocol in search_protocols:
|
||||
protocol_results = fireEvent('provider.search.%s.movie' % search_protocol, movie, quality, merge = True)
|
||||
if protocol_results:
|
||||
results += protocol_results
|
||||
|
||||
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
|
||||
if len(sorted_results) == 0:
|
||||
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
|
||||
|
||||
download_preference = self.conf('preferred_method', section = 'searcher')
|
||||
if download_preference != 'both':
|
||||
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
|
||||
|
||||
# Check if movie isn't deleted while searching
|
||||
if not db.query(Media).filter_by(id = movie.get('id')).first():
|
||||
if not db.query(Movie).filter_by(id = movie.get('id')).first():
|
||||
break
|
||||
|
||||
# Add them to this movie releases list
|
||||
found_releases += fireEvent('release.create_from_search', results, movie, quality_type, single = True)
|
||||
for nzb in sorted_results:
|
||||
|
||||
# Try find a valid result and download it
|
||||
if fireEvent('release.try_download_result', results, movie, quality_type, manual, single = True):
|
||||
ret = True
|
||||
nzb_identifier = md5(nzb['url'])
|
||||
found_releases.append(nzb_identifier)
|
||||
|
||||
rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
|
||||
if not rls:
|
||||
rls = Release(
|
||||
identifier = nzb_identifier,
|
||||
movie_id = movie.get('id'),
|
||||
quality_id = quality_type.get('quality_id'),
|
||||
status_id = available_status.get('id')
|
||||
)
|
||||
db.add(rls)
|
||||
else:
|
||||
[db.delete(old_info) for old_info in rls.info]
|
||||
rls.last_edit = int(time.time())
|
||||
|
||||
db.commit()
|
||||
|
||||
for info in nzb:
|
||||
try:
|
||||
if not isinstance(nzb[info], (str, unicode, int, long, float)):
|
||||
continue
|
||||
|
||||
rls_info = ReleaseInfo(
|
||||
identifier = info,
|
||||
value = toUnicode(nzb[info])
|
||||
)
|
||||
rls.info.append(rls_info)
|
||||
except InterfaceError:
|
||||
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
||||
|
||||
db.commit()
|
||||
|
||||
nzb['status_id'] = rls.status_id
|
||||
|
||||
|
||||
for nzb in sorted_results:
|
||||
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
|
||||
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
||||
continue
|
||||
|
||||
if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
|
||||
log.info('Ignored: %s', nzb['name'])
|
||||
continue
|
||||
|
||||
if nzb['score'] <= 0:
|
||||
log.info('Ignored, score to low: %s', nzb['name'])
|
||||
continue
|
||||
|
||||
downloaded = fireEvent('searcher.download', data = nzb, movie = movie, manual = manual, single = True)
|
||||
if downloaded is True:
|
||||
ret = True
|
||||
break
|
||||
elif downloaded != 'try_next':
|
||||
break
|
||||
|
||||
# Remove releases that aren't found anymore
|
||||
for release in movie.get('releases', []):
|
||||
@@ -191,7 +251,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
|
||||
else:
|
||||
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
|
||||
fireEvent('media.restatus', movie['id'])
|
||||
fireEvent('movie.restatus', movie['id'])
|
||||
break
|
||||
|
||||
# Break if CP wants to shut down
|
||||
@@ -201,15 +261,11 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
if len(too_early_to_search) > 0:
|
||||
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['id']})
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.ended.%s' % movie['id'], data = True)
|
||||
|
||||
return ret
|
||||
|
||||
def correctRelease(self, nzb = None, media = None, quality = None, **kwargs):
|
||||
|
||||
if media.get('type') != 'movie': return
|
||||
|
||||
media_title = fireEvent('library.title', media['library'], single = True)
|
||||
def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
|
||||
|
||||
imdb_results = kwargs.get('imdb_results', False)
|
||||
retention = Env.setting('retention', section = 'nzb')
|
||||
@@ -218,14 +274,50 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
||||
return False
|
||||
|
||||
# Check for required and ignored words
|
||||
if not fireEvent('searcher.correct_words', nzb['name'], media, single = True):
|
||||
movie_name = getTitle(movie['library'])
|
||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||
nzb_name = simplifyString(nzb['name'])
|
||||
nzb_words = re.split('\W+', nzb_name)
|
||||
|
||||
# Make sure it has required words
|
||||
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
|
||||
try: required_words = list(set(required_words + splitString(movie['category']['required'].lower())))
|
||||
except: pass
|
||||
|
||||
req_match = 0
|
||||
for req_set in required_words:
|
||||
req = splitString(req_set, '&')
|
||||
req_match += len(list(set(nzb_words) & set(req))) == len(req)
|
||||
|
||||
if len(required_words) > 0 and req_match == 0:
|
||||
log.info2('Wrong: Required word missing: %s', nzb['name'])
|
||||
return False
|
||||
|
||||
# Ignore releases
|
||||
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
|
||||
try: ignored_words = list(set(ignored_words + splitString(movie['category']['ignored'].lower())))
|
||||
except: pass
|
||||
|
||||
ignored_match = 0
|
||||
for ignored_set in ignored_words:
|
||||
ignored = splitString(ignored_set, '&')
|
||||
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
|
||||
|
||||
if len(ignored_words) > 0 and ignored_match:
|
||||
log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name']))
|
||||
return False
|
||||
|
||||
# Ignore porn stuff
|
||||
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic', 'cock', 'dick']
|
||||
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
|
||||
if pron_words:
|
||||
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
||||
return False
|
||||
|
||||
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
|
||||
|
||||
# Contains lower quality string
|
||||
if fireEvent('searcher.contains_other_quality', nzb, movie_year = media['library']['year'], preferred_quality = preferred_quality, single = True):
|
||||
if fireEvent('searcher.contains_other_quality', nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single = True):
|
||||
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
|
||||
return False
|
||||
|
||||
@@ -255,23 +347,23 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
return True
|
||||
|
||||
# Check if nzb contains imdb link
|
||||
if getImdb(nzb.get('description', '')) == media['library']['identifier']:
|
||||
if getImdb(nzb.get('description', '')) == movie['library']['identifier']:
|
||||
return True
|
||||
|
||||
for raw_title in media['library']['titles']:
|
||||
for raw_title in movie['library']['titles']:
|
||||
for movie_title in possibleTitles(raw_title['title']):
|
||||
movie_words = re.split('\W+', simplifyString(movie_title))
|
||||
|
||||
if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
|
||||
# if no IMDB link, at least check year range 1
|
||||
if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 1, single = True):
|
||||
if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 1, single = True):
|
||||
return True
|
||||
|
||||
# if no IMDB link, at least check year
|
||||
if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 0, single = True):
|
||||
if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 0, single = True):
|
||||
return True
|
||||
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], media_title, media['library']['year']))
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year']))
|
||||
return False
|
||||
|
||||
def couldBeReleased(self, is_pre_release, dates, year = None):
|
||||
@@ -283,10 +375,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
return True
|
||||
else:
|
||||
|
||||
# Don't allow movies with years to far in the future
|
||||
if year is not None and year > now_year + 1:
|
||||
return False
|
||||
|
||||
# For movies before 1972
|
||||
if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
|
||||
return True
|
||||
@@ -321,14 +409,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
'success': trynext
|
||||
}
|
||||
|
||||
def tryNextRelease(self, media_id, manual = False):
|
||||
def tryNextRelease(self, movie_id, manual = False):
|
||||
|
||||
snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True)
|
||||
|
||||
try:
|
||||
db = get_session()
|
||||
rels = db.query(Release) \
|
||||
.filter_by(media_id = media_id) \
|
||||
.filter_by(movie_id = movie_id) \
|
||||
.filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \
|
||||
.all()
|
||||
|
||||
@@ -336,7 +424,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
rel.status_id = ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
movie_dict = fireEvent('media.get', media_id = media_id, single = True)
|
||||
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||
log.info('Trying next release for: %s', getTitle(movie_dict['library']))
|
||||
fireEvent('movie.searcher.single', movie_dict, manual = manual)
|
||||
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
.suggestions {
|
||||
}
|
||||
|
||||
.suggestions > h2 {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.suggestions .media_result {
|
||||
display: inline-block;
|
||||
width: 33.333%;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 960px) {
|
||||
.suggestions .media_result {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 600px) {
|
||||
.suggestions .media_result {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.suggestions .media_result .data {
|
||||
left: 100px;
|
||||
background: #4e5969;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.suggestions .media_result .data .info {
|
||||
top: 10px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
bottom: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.suggestions .media_result .data .info h2 {
|
||||
white-space: normal;
|
||||
max-height: 120px;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.suggestions .media_result .data .info .rating,
|
||||
.suggestions .media_result .data .info .genres,
|
||||
.suggestions .media_result .data .info .year {
|
||||
position: static;
|
||||
display: block;
|
||||
padding: 0;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.suggestions .media_result .data .info .year {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.suggestions .media_result .data .info .rating {
|
||||
font-size: 20px;
|
||||
float: right;
|
||||
margin-top: -20px;
|
||||
}
|
||||
.suggestions .media_result .data .info .rating:before {
|
||||
content: "\e031";
|
||||
font-family: 'Elusive-Icons';
|
||||
font-size: 14px;
|
||||
margin: 0 5px 0 0;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.suggestions .media_result .data .info .genres {
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.suggestions .media_result .data .info .plot {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-align: justify;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
top: 64px;
|
||||
position: absolute;
|
||||
background: #4e5969;
|
||||
cursor: pointer;
|
||||
transition: all .4s ease-in-out;
|
||||
padding: 0 3px 10px 0;
|
||||
}
|
||||
.suggestions .media_result .data:before {
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 10px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 10px;
|
||||
position: absolute;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(78, 89, 105, 1) 0%,
|
||||
rgba(78, 89, 105, 0) 100%
|
||||
);
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.suggestions .media_result .data .info .plot.full {
|
||||
top: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.suggestions .media_result .data {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.suggestions .media_result .options {
|
||||
left: 100px;
|
||||
}
|
||||
.suggestions .media_result .options select[name=title] { width: 100%; }
|
||||
.suggestions .media_result .options select[name=profile] { width: 100%; }
|
||||
.suggestions .media_result .options select[name=category] { width: 100%; }
|
||||
|
||||
.suggestions .media_result .button {
|
||||
position: absolute;
|
||||
margin: 2px 0 0 0;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
}
|
||||
|
||||
|
||||
.suggestions .media_result .thumbnail {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.suggestions .media_result .actions {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: none;
|
||||
width: 140px;
|
||||
}
|
||||
.suggestions .media_result:hover .actions {
|
||||
display: block;
|
||||
}
|
||||
.suggestions .media_result:hover h2 .title {
|
||||
opacity: 0;
|
||||
}
|
||||
.suggestions .media_result .data.open .actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.suggestions .media_result .actions a {
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
var SuggestList = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.create();
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('div.suggestions', {
|
||||
'events': {
|
||||
'click:relay(a.delete)': function(e, el){
|
||||
(e).stop();
|
||||
|
||||
$(el).getParent('.media_result').destroy();
|
||||
|
||||
Api.request('suggestion.ignore', {
|
||||
'data': {
|
||||
'imdb': el.get('data-ignore')
|
||||
},
|
||||
'onComplete': self.fill.bind(self)
|
||||
});
|
||||
|
||||
},
|
||||
'click:relay(a.eye-open)': function(e, el){
|
||||
(e).stop();
|
||||
|
||||
$(el).getParent('.media_result').destroy();
|
||||
|
||||
Api.request('suggestion.ignore', {
|
||||
'data': {
|
||||
'imdb': el.get('data-seen'),
|
||||
'mark_seen': 1
|
||||
},
|
||||
'onComplete': self.fill.bind(self)
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}).grab(
|
||||
new Element('h2', {
|
||||
'text': 'You might like these'
|
||||
})
|
||||
);
|
||||
|
||||
self.api_request = Api.request('suggestion.view', {
|
||||
'onComplete': self.fill.bind(self)
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
fill: function(json){
|
||||
|
||||
var self = this;
|
||||
|
||||
if(!json || json.count == 0){
|
||||
self.el.hide();
|
||||
}
|
||||
else {
|
||||
|
||||
Object.each(json.suggestions, function(movie){
|
||||
|
||||
var m = new Block.Search.MovieItem(movie, {
|
||||
'onAdded': function(){
|
||||
self.afterAdded(m, movie)
|
||||
}
|
||||
});
|
||||
m.data_container.grab(
|
||||
new Element('div.actions').adopt(
|
||||
new Element('a.add.icon2', {
|
||||
'title': 'Add movie with your default quality',
|
||||
'data-add': movie.imdb,
|
||||
'events': {
|
||||
'click': m.showOptions.bind(m)
|
||||
}
|
||||
}),
|
||||
$(new MA.IMDB(m)),
|
||||
$(new MA.Trailer(m, {
|
||||
'height': 150
|
||||
})),
|
||||
new Element('a.delete.icon2', {
|
||||
'title': 'Don\'t suggest this movie again',
|
||||
'data-ignore': movie.imdb
|
||||
}),
|
||||
new Element('a.eye-open.icon2', {
|
||||
'title': 'Seen it, like it, don\'t add',
|
||||
'data-seen': movie.imdb
|
||||
})
|
||||
)
|
||||
);
|
||||
m.data_container.removeEvents('click');
|
||||
|
||||
var plot = false;
|
||||
if(m.info.plot && m.info.plot.length > 0)
|
||||
plot = m.info.plot;
|
||||
|
||||
// Add rating
|
||||
m.info_container.adopt(
|
||||
m.rating = m.info.rating && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', {
|
||||
'text': parseFloat(m.info.rating.imdb[0]),
|
||||
'title': parseInt(m.info.rating.imdb[1]) + ' votes'
|
||||
}) : null,
|
||||
m.genre = m.info.genres && m.info.genres.length > 0 ? new Element('span.genres', {
|
||||
'text': m.info.genres.slice(0, 3).join(', ')
|
||||
}) : null,
|
||||
m.plot = plot ? new Element('span.plot', {
|
||||
'text': plot,
|
||||
'events': {
|
||||
'click': function(){
|
||||
this.toggleClass('full')
|
||||
}
|
||||
}
|
||||
}) : null
|
||||
)
|
||||
|
||||
$(m).inject(self.el);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
self.fireEvent('loaded');
|
||||
|
||||
},
|
||||
|
||||
afterAdded: function(m, movie){
|
||||
var self = this;
|
||||
|
||||
setTimeout(function(){
|
||||
$(m).destroy();
|
||||
|
||||
Api.request('suggestion.ignore', {
|
||||
'data': {
|
||||
'imdb': movie.imdb,
|
||||
'remove_only': true
|
||||
},
|
||||
'onComplete': self.fill.bind(self)
|
||||
});
|
||||
|
||||
}, 3000);
|
||||
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el;
|
||||
}
|
||||
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import ShowBase
|
||||
|
||||
def start():
|
||||
return ShowBase()
|
||||
|
||||
config = []
|
||||
@@ -1,239 +0,0 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media import MediaBase
|
||||
from couchpotato.core.settings.model import Media
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class ShowBase(MediaBase):
|
||||
|
||||
_type = 'show'
|
||||
|
||||
def __init__(self):
|
||||
super(ShowBase, self).__init__()
|
||||
|
||||
addApiView('show.add', self.addView, docs = {
|
||||
'desc': 'Add new movie to the wanted list',
|
||||
'params': {
|
||||
'identifier': {'desc': 'IMDB id of the movie your want to add.'},
|
||||
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
|
||||
'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
|
||||
}
|
||||
})
|
||||
|
||||
addEvent('show.add', self.add)
|
||||
|
||||
def addView(self, **kwargs):
|
||||
add_dict = self.add(params = kwargs)
|
||||
|
||||
return {
|
||||
'success': True if add_dict else False,
|
||||
'show': add_dict,
|
||||
}
|
||||
|
||||
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||
"""
|
||||
params
|
||||
{'category_id': u'-1',
|
||||
'identifier': u'tt1519931',
|
||||
'profile_id': u'12',
|
||||
'thetvdb_id': u'158661',
|
||||
'title': u'Haven'}
|
||||
"""
|
||||
log.debug("show.add")
|
||||
|
||||
# Add show parent to db first; need to update library so maps will be in place (if any)
|
||||
parent = self.addToDatabase(params = params, update_library = True, type = 'show')
|
||||
|
||||
# TODO: add by airdate
|
||||
|
||||
# Add by Season/Episode numbers
|
||||
self.addBySeasonEpisode(parent,
|
||||
params = params,
|
||||
force_readd = force_readd,
|
||||
search_after = search_after,
|
||||
update_library = update_library,
|
||||
status_id = status_id
|
||||
)
|
||||
|
||||
def addBySeasonEpisode(self, parent, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||
identifier = params.get('id')
|
||||
# 'tvdb' will always be the master for our purpose. All mapped data can be mapped
|
||||
# to another source for downloading, but it will always be remapped back to tvdb numbering
|
||||
# when renamed so media can be used in media players that use tvdb for info provider
|
||||
#
|
||||
# This currently means the episode must actually exist in tvdb in order to be found but
|
||||
# the numbering can be different
|
||||
|
||||
#master = 'tvdb'
|
||||
#destination = 'scene'
|
||||
#destination = 'anidb'
|
||||
#destination = 'rage'
|
||||
#destination = 'trakt'
|
||||
# TODO: auto mode. if anime exists use it. if scene exists use it else use tvdb
|
||||
|
||||
# XXX: We should abort adding show, etc if either tvdb or xem is down or we will have incorrent mappings
|
||||
# I think if tvdb gets error we wont have anydata anyway, but we must make sure XEM returns!!!!
|
||||
|
||||
# Only the master should return results here; all other info providers should just return False
|
||||
# since we are just interested in the structure at this point.
|
||||
seasons = fireEvent('season.info', merge = True, identifier = identifier)
|
||||
if seasons is not None:
|
||||
for season in seasons:
|
||||
# Make sure we are only dealing with 'tvdb' responses at this point
|
||||
if season.get('primary_provider', None) != 'thetvdb':
|
||||
continue
|
||||
season_id = season.get('id', None)
|
||||
if season_id is None: continue
|
||||
|
||||
season_params = {'season_identifier': season_id}
|
||||
# Calling all info providers; merge your info now for individual season
|
||||
single_season = fireEvent('season.info', merge = True, identifier = identifier, params = season_params)
|
||||
single_season['category_id'] = params.get('category_id')
|
||||
single_season['profile_id'] = params.get('profile_id')
|
||||
single_season['title'] = single_season.get('original_title', None)
|
||||
single_season['identifier'] = season_id
|
||||
single_season['parent_identifier'] = identifier
|
||||
log.info("Adding Season %s" % season_id)
|
||||
s = self.addToDatabase(params = single_season, type = "season")
|
||||
|
||||
episode_params = {'season_identifier': season_id}
|
||||
episodes = fireEvent('episode.info', merge = True, identifier = identifier, params = episode_params)
|
||||
if episodes is not None:
|
||||
for episode in episodes:
|
||||
# Make sure we are only dealing with 'tvdb' responses at this point
|
||||
if episode.get('primary_provider', None) != 'thetvdb':
|
||||
continue
|
||||
episode_id = episode.get('id', None)
|
||||
if episode_id is None: continue
|
||||
try:
|
||||
episode_number = int(episode.get('episodenumber', None))
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
try:
|
||||
absolute_number = int(episode.get('absolute_number', None))
|
||||
except (ValueError, TypeError):
|
||||
absolute_number = None
|
||||
|
||||
episode_params = {'season_identifier': season_id,
|
||||
'episode_identifier': episode_id,
|
||||
'episode': episode_number}
|
||||
if absolute_number:
|
||||
episode_params['absolute'] = absolute_number
|
||||
# Calling all info providers; merge your info now for individual episode
|
||||
single_episode = fireEvent('episode.info', merge = True, identifier = identifier, params = episode_params)
|
||||
single_episode['category_id'] = params.get('category_id')
|
||||
single_episode['profile_id'] = params.get('profile_id')
|
||||
single_episode['title'] = single_episode.get('original_title', None)
|
||||
single_episode['identifier'] = episode_id
|
||||
single_episode['parent_identifier'] = single_season['identifier']
|
||||
log.info("Adding [%sx%s] %s - %s" % (season_id,
|
||||
episode_number,
|
||||
params['title'],
|
||||
single_episode.get('original_title', '')))
|
||||
e = self.addToDatabase(params = single_episode, type = "episode")
|
||||
|
||||
# Start searching now that all the media has been added
|
||||
if search_after:
|
||||
onComplete = self.createOnComplete(parent['id'])
|
||||
onComplete()
|
||||
|
||||
return parent
|
||||
|
||||
def addToDatabase(self, params = {}, type = "show", force_readd = True, search_after = False, update_library = False, status_id = None):
|
||||
log.debug("show.addToDatabase")
|
||||
|
||||
if not params.get('identifier'):
|
||||
msg = 'Can\'t add show without imdb identifier.'
|
||||
log.error(msg)
|
||||
fireEvent('notify.frontend', type = 'show.is_tvshow', message = msg)
|
||||
return False
|
||||
#else:
|
||||
#try:
|
||||
#is_show = fireEvent('movie.is_show', identifier = params.get('identifier'), single = True)
|
||||
#if not is_show:
|
||||
#msg = 'Can\'t add show, seems to be a TV show.'
|
||||
#log.error(msg)
|
||||
#fireEvent('notify.frontend', type = 'show.is_tvshow', message = msg)
|
||||
#return False
|
||||
#except:
|
||||
#pass
|
||||
|
||||
library = fireEvent('library.add.%s' % type, single = True, attrs = params, update_after = update_library)
|
||||
if not library:
|
||||
return False
|
||||
|
||||
# Status
|
||||
status_active, snatched_status, ignored_status, done_status, downloaded_status = \
|
||||
fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True)
|
||||
|
||||
default_profile = fireEvent('profile.default', single = True)
|
||||
cat_id = params.get('category_id', None)
|
||||
|
||||
db = get_session()
|
||||
m = db.query(Media).filter_by(library_id = library.get('id')).first()
|
||||
added = True
|
||||
do_search = False
|
||||
if not m:
|
||||
m = Media(
|
||||
type = type,
|
||||
library_id = library.get('id'),
|
||||
profile_id = params.get('profile_id', default_profile.get('id')),
|
||||
status_id = status_id if status_id else status_active.get('id'),
|
||||
category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None,
|
||||
)
|
||||
db.add(m)
|
||||
db.commit()
|
||||
|
||||
onComplete = None
|
||||
if search_after:
|
||||
onComplete = self.createOnComplete(m.id)
|
||||
|
||||
fireEventAsync('library.update.%s' % type, params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
|
||||
search_after = False
|
||||
elif force_readd:
|
||||
|
||||
# Clean snatched history
|
||||
for release in m.releases:
|
||||
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
|
||||
if params.get('ignore_previous', False):
|
||||
release.status_id = ignored_status.get('id')
|
||||
else:
|
||||
fireEvent('release.delete', release.id, single = True)
|
||||
|
||||
m.profile_id = params.get('profile_id', default_profile.get('id'))
|
||||
m.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None
|
||||
else:
|
||||
log.debug('Show already exists, not updating: %s', params)
|
||||
added = False
|
||||
|
||||
if force_readd:
|
||||
m.status_id = status_id if status_id else status_active.get('id')
|
||||
m.last_edit = int(time.time())
|
||||
do_search = True
|
||||
|
||||
db.commit()
|
||||
|
||||
# Remove releases
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
for rel in m.releases:
|
||||
if rel.status_id is available_status.get('id'):
|
||||
db.delete(rel)
|
||||
db.commit()
|
||||
|
||||
show_dict = m.to_dict(self.default_dict)
|
||||
|
||||
if do_search and search_after:
|
||||
onComplete = self.createOnComplete(m.id)
|
||||
onComplete()
|
||||
|
||||
if added:
|
||||
fireEvent('notify.frontend', type = 'show.added', data = show_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
|
||||
|
||||
db.expire_all()
|
||||
return show_dict
|
||||
@@ -1,232 +0,0 @@
|
||||
Block.Search.ShowItem = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
initialize: function(info, options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.info = info;
|
||||
self.alternative_titles = [];
|
||||
|
||||
self.create();
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this,
|
||||
info = self.info;
|
||||
|
||||
self.el = new Element('div.media_result', {
|
||||
'id': info.id
|
||||
}).adopt(
|
||||
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||
'src': info.images.poster[0],
|
||||
'height': null,
|
||||
'width': null
|
||||
}) : null,
|
||||
self.options_el = new Element('div.options.inlay'),
|
||||
self.data_container = new Element('div.data', {
|
||||
'events': {
|
||||
'click': self.showOptions.bind(self)
|
||||
}
|
||||
}).adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
new Element('h2').adopt(
|
||||
self.title = new Element('span.title', {
|
||||
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
|
||||
}),
|
||||
self.year = info.year ? new Element('span.year', {
|
||||
'text': info.year
|
||||
}) : null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if(info.titles)
|
||||
info.titles.each(function(title){
|
||||
self.alternativeTitle({
|
||||
'title': title
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
alternativeTitle: function(alternative){
|
||||
var self = this;
|
||||
|
||||
self.alternative_titles.include(alternative);
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
try {
|
||||
return self.info.original_title ? self.info.original_title : self.info.titles[0];
|
||||
}
|
||||
catch(e){
|
||||
return 'Unknown';
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key){
|
||||
return this.info[key]
|
||||
},
|
||||
|
||||
showOptions: function(){
|
||||
var self = this;
|
||||
|
||||
self.createOptions();
|
||||
|
||||
self.data_container.addClass('open');
|
||||
self.el.addEvent('outerClick', self.closeOptions.bind(self))
|
||||
|
||||
},
|
||||
|
||||
closeOptions: function(){
|
||||
var self = this;
|
||||
|
||||
self.data_container.removeClass('open');
|
||||
self.el.removeEvents('outerClick')
|
||||
},
|
||||
|
||||
add: function(e){
|
||||
var self = this;
|
||||
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.loadingMask();
|
||||
|
||||
Api.request('show.add', {
|
||||
'data': {
|
||||
'identifier': self.info.id,
|
||||
'id': self.info.id,
|
||||
'type': self.info.type,
|
||||
'primary_provider': self.info.primary_provider,
|
||||
'title': self.title_select.get('value'),
|
||||
'profile_id': self.profile_select.get('value'),
|
||||
'category_id': self.category_select.get('value')
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.options_el.empty();
|
||||
self.options_el.adopt(
|
||||
new Element('div.message', {
|
||||
'text': json.added ? 'Show successfully added.' : 'Show didn\'t add properly. Check logs'
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
|
||||
self.fireEvent('added');
|
||||
},
|
||||
'onFailure': function(){
|
||||
self.options_el.empty();
|
||||
self.options_el.adopt(
|
||||
new Element('div.message', {
|
||||
'text': 'Something went wrong, check the logs for more info.'
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createOptions: function(){
|
||||
var self = this,
|
||||
info = self.info;
|
||||
|
||||
if(!self.options_el.hasClass('set')){
|
||||
|
||||
if(self.info.in_library){
|
||||
var in_library = [];
|
||||
self.info.in_library.releases.each(function(release){
|
||||
in_library.include(release.quality.label)
|
||||
});
|
||||
}
|
||||
|
||||
self.options_el.grab(
|
||||
new Element('div', {
|
||||
'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
|
||||
}).adopt(
|
||||
self.info.in_wanted && self.info.in_wanted.profile_id ? new Element('span.in_wanted', {
|
||||
'text': 'Already in wanted list: ' + Quality.getProfile(self.info.in_wanted.profile_id).get('label')
|
||||
}) : (in_library ? new Element('span.in_library', {
|
||||
'text': 'Already in library: ' + in_library.join(', ')
|
||||
}) : null),
|
||||
self.title_select = new Element('select', {
|
||||
'name': 'title'
|
||||
}),
|
||||
self.profile_select = new Element('select', {
|
||||
'name': 'profile'
|
||||
}),
|
||||
self.category_select = new Element('select', {
|
||||
'name': 'category'
|
||||
}).grab(
|
||||
new Element('option', {'value': -1, 'text': 'None'})
|
||||
),
|
||||
self.add_button = new Element('a.button', {
|
||||
'text': 'Add',
|
||||
'events': {
|
||||
'click': self.add.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
Array.each(self.alternative_titles, function(alt){
|
||||
new Element('option', {
|
||||
'text': alt.title
|
||||
}).inject(self.title_select)
|
||||
})
|
||||
|
||||
|
||||
// Fill categories
|
||||
var categories = CategoryList.getAll();
|
||||
|
||||
if(categories.length == 0)
|
||||
self.category_select.hide();
|
||||
else {
|
||||
self.category_select.show();
|
||||
categories.each(function(category){
|
||||
new Element('option', {
|
||||
'value': category.data.id,
|
||||
'text': category.data.label
|
||||
}).inject(self.category_select);
|
||||
});
|
||||
}
|
||||
|
||||
// Fill profiles
|
||||
var profiles = Quality.getActiveProfiles();
|
||||
if(profiles.length == 1)
|
||||
self.profile_select.hide();
|
||||
|
||||
profiles.each(function(profile){
|
||||
new Element('option', {
|
||||
'value': profile.id ? profile.id : profile.data.id,
|
||||
'text': profile.label ? profile.label : profile.data.label
|
||||
}).inject(self.profile_select)
|
||||
});
|
||||
|
||||
self.options_el.addClass('set');
|
||||
|
||||
if(categories.length == 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 &&
|
||||
!(self.info.in_wanted && self.info.in_wanted.profile_id || in_library))
|
||||
self.add();
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
loadingMask: function(){
|
||||
var self = this;
|
||||
|
||||
self.mask = new Element('div.mask').inject(self.el).fade('hide')
|
||||
|
||||
createSpinner(self.mask)
|
||||
self.mask.fade('in')
|
||||
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import EpisodeLibraryPlugin
|
||||
|
||||
def start():
|
||||
return EpisodeLibraryPlugin()
|
||||
|
||||
config = []
|
||||
@@ -1,266 +0,0 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.settings.model import EpisodeLibrary, SeasonLibrary, LibraryTitle, File
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from string import ascii_letters
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class EpisodeLibraryPlugin(LibraryBase):
|
||||
|
||||
default_dict = {'titles': {}, 'files':{}}
|
||||
|
||||
def __init__(self):
|
||||
addEvent('library.query', self.query)
|
||||
addEvent('library.identifier', self.identifier)
|
||||
addEvent('library.add.episode', self.add)
|
||||
addEvent('library.update.episode', self.update)
|
||||
addEvent('library.update.episode_release_date', self.updateReleaseDate)
|
||||
|
||||
def query(self, library, first = True, condense = True, include_identifier = True, **kwargs):
|
||||
if library is list or library.get('type') != 'episode':
|
||||
return
|
||||
|
||||
# Get the titles of the season
|
||||
if not library.get('related_libraries', {}).get('season', []):
|
||||
log.warning('Invalid library, unable to determine title.')
|
||||
return
|
||||
|
||||
titles = fireEvent(
|
||||
'library.query',
|
||||
library['related_libraries']['season'][0],
|
||||
first=False,
|
||||
include_identifier=include_identifier,
|
||||
condense=condense,
|
||||
|
||||
single=True
|
||||
)
|
||||
|
||||
identifier = fireEvent('library.identifier', library, single = True)
|
||||
|
||||
# Add episode identifier to titles
|
||||
if include_identifier and identifier.get('episode'):
|
||||
titles = [title + ('E%02d' % identifier['episode']) for title in titles]
|
||||
|
||||
|
||||
if first:
|
||||
return titles[0] if titles else None
|
||||
|
||||
return titles
|
||||
|
||||
|
||||
def identifier(self, library):
|
||||
if library.get('type') != 'episode':
|
||||
return
|
||||
|
||||
identifier = {
|
||||
'season': None,
|
||||
'episode': None
|
||||
}
|
||||
|
||||
scene_map = library['info'].get('map_episode', {}).get('scene')
|
||||
|
||||
if scene_map:
|
||||
# Use scene mappings if they are available
|
||||
identifier['season'] = scene_map.get('season')
|
||||
identifier['episode'] = scene_map.get('episode')
|
||||
else:
|
||||
# Fallback to normal season/episode numbers
|
||||
identifier['season'] = library.get('season_number')
|
||||
identifier['episode'] = library.get('episode_number')
|
||||
|
||||
|
||||
# Cast identifiers to integers
|
||||
# TODO this will need changing to support identifiers with trailing 'a', 'b' characters
|
||||
identifier['season'] = tryInt(identifier['season'], None)
|
||||
identifier['episode'] = tryInt(identifier['episode'], None)
|
||||
|
||||
return identifier
|
||||
|
||||
def add(self, attrs = {}, update_after = True):
|
||||
type = attrs.get('type', 'episode')
|
||||
primary_provider = attrs.get('primary_provider', 'thetvdb')
|
||||
|
||||
db = get_session()
|
||||
parent_identifier = attrs.get('parent_identifier', None)
|
||||
|
||||
parent = None
|
||||
if parent_identifier:
|
||||
parent = db.query(SeasonLibrary).filter_by(primary_provider = primary_provider, identifier = attrs.get('parent_identifier')).first()
|
||||
|
||||
l = db.query(EpisodeLibrary).filter_by(type = type, identifier = attrs.get('identifier')).first()
|
||||
if not l:
|
||||
status = fireEvent('status.get', 'needs_update', single = True)
|
||||
l = EpisodeLibrary(
|
||||
type = type,
|
||||
primary_provider = primary_provider,
|
||||
year = attrs.get('year'),
|
||||
identifier = attrs.get('identifier'),
|
||||
plot = toUnicode(attrs.get('plot')),
|
||||
tagline = toUnicode(attrs.get('tagline')),
|
||||
status_id = status.get('id'),
|
||||
info = {},
|
||||
parent = parent,
|
||||
season_number = tryInt(attrs.get('seasonnumber', None)),
|
||||
episode_number = tryInt(attrs.get('episodenumber', None)),
|
||||
absolute_number = tryInt(attrs.get('absolute_number', None))
|
||||
)
|
||||
|
||||
title = LibraryTitle(
|
||||
title = toUnicode(attrs.get('title')),
|
||||
simple_title = self.simplifyTitle(attrs.get('title')),
|
||||
)
|
||||
|
||||
l.titles.append(title)
|
||||
|
||||
db.add(l)
|
||||
db.commit()
|
||||
|
||||
# Update library info
|
||||
if update_after is not False:
|
||||
handle = fireEventAsync if update_after is 'async' else fireEvent
|
||||
handle('library.update.episode', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
|
||||
|
||||
library_dict = l.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def update(self, identifier, default_title = '', force = False):
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
db = get_session()
|
||||
library = db.query(EpisodeLibrary).filter_by(identifier = identifier).first()
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
if library:
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
|
||||
do_update = True
|
||||
|
||||
parent_identifier = None
|
||||
if library.parent is not None:
|
||||
parent_identifier = library.parent.identifier
|
||||
|
||||
if library.status_id == done_status.get('id') and not force:
|
||||
do_update = False
|
||||
|
||||
episode_params = {'season_identifier': parent_identifier,
|
||||
'episode_identifier': identifier,
|
||||
'episode': library.episode_number,
|
||||
'absolute': library.absolute_number,}
|
||||
info = fireEvent('episode.info', merge = True, params = episode_params)
|
||||
|
||||
# Don't need those here
|
||||
try: del info['in_wanted']
|
||||
except: pass
|
||||
try: del info['in_library']
|
||||
except: pass
|
||||
|
||||
if not info or len(info) == 0:
|
||||
log.error('Could not update, no movie info to work with: %s', identifier)
|
||||
return False
|
||||
|
||||
# Main info
|
||||
if do_update:
|
||||
library.plot = toUnicode(info.get('plot', ''))
|
||||
library.tagline = toUnicode(info.get('tagline', ''))
|
||||
library.year = info.get('year', 0)
|
||||
library.status_id = done_status.get('id')
|
||||
library.season_number = tryInt(info.get('seasonnumber', None))
|
||||
library.episode_number = tryInt(info.get('episodenumber', None))
|
||||
library.absolute_number = tryInt(info.get('absolute_number', None))
|
||||
try:
|
||||
library.last_updated = int(info.get('lastupdated'))
|
||||
except:
|
||||
library.last_updated = int(time.time())
|
||||
library.info.update(info)
|
||||
db.commit()
|
||||
|
||||
# Titles
|
||||
[db.delete(title) for title in library.titles]
|
||||
db.commit()
|
||||
|
||||
titles = info.get('titles', [])
|
||||
log.debug('Adding titles: %s', titles)
|
||||
counter = 0
|
||||
for title in titles:
|
||||
if not title:
|
||||
continue
|
||||
title = toUnicode(title)
|
||||
t = LibraryTitle(
|
||||
title = title,
|
||||
simple_title = self.simplifyTitle(title),
|
||||
default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
|
||||
)
|
||||
library.titles.append(t)
|
||||
counter += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
# Files
|
||||
images = info.get('images', [])
|
||||
for image_type in ['poster']:
|
||||
for image in images.get(image_type, []):
|
||||
if not isinstance(image, (str, unicode)):
|
||||
continue
|
||||
|
||||
file_path = fireEvent('file.download', url = image, single = True)
|
||||
if file_path:
|
||||
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
|
||||
try:
|
||||
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
|
||||
library.files.append(file_obj)
|
||||
db.commit()
|
||||
|
||||
break
|
||||
except:
|
||||
log.debug('Failed to attach to library: %s', traceback.format_exc())
|
||||
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def updateReleaseDate(self, identifier):
|
||||
'''XXX: Not sure what this is for yet in relation to an episode'''
|
||||
pass
|
||||
#db = get_session()
|
||||
#library = db.query(EpisodeLibrary).filter_by(identifier = identifier).first()
|
||||
|
||||
#if not library.info:
|
||||
#library_dict = self.update(identifier, force = True)
|
||||
#dates = library_dict.get('info', {}).get('release_date')
|
||||
#else:
|
||||
#dates = library.info.get('release_date')
|
||||
|
||||
#if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||
#dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||
#library.info.update({'release_date': dates })
|
||||
#db.commit()
|
||||
|
||||
#db.expire_all()
|
||||
#return dates
|
||||
|
||||
|
||||
#TODO: Add to base class
|
||||
def simplifyTitle(self, title):
|
||||
|
||||
title = toUnicode(title)
|
||||
|
||||
nr_prefix = '' if title[0] in ascii_letters else '#'
|
||||
title = simplifyString(title)
|
||||
|
||||
for prefix in ['the ']:
|
||||
if prefix == title[:len(prefix)]:
|
||||
title = title[len(prefix):]
|
||||
break
|
||||
|
||||
return nr_prefix + title
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import SeasonLibraryPlugin
|
||||
|
||||
def start():
|
||||
return SeasonLibraryPlugin()
|
||||
|
||||
config = []
|
||||
@@ -1,242 +0,0 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.settings.model import SeasonLibrary, ShowLibrary, LibraryTitle, File
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from string import ascii_letters
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class SeasonLibraryPlugin(LibraryBase):
|
||||
|
||||
default_dict = {'titles': {}, 'files':{}}
|
||||
|
||||
def __init__(self):
|
||||
addEvent('library.query', self.query)
|
||||
addEvent('library.identifier', self.identifier)
|
||||
addEvent('library.add.season', self.add)
|
||||
addEvent('library.update.season', self.update)
|
||||
addEvent('library.update.season_release_date', self.updateReleaseDate)
|
||||
|
||||
def query(self, library, first = True, condense = True, include_identifier = True, **kwargs):
|
||||
if library is list or library.get('type') != 'season':
|
||||
return
|
||||
|
||||
# Get the titles of the show
|
||||
if not library.get('related_libraries', {}).get('show', []):
|
||||
log.warning('Invalid library, unable to determine title.')
|
||||
return
|
||||
|
||||
titles = fireEvent(
|
||||
'library.query',
|
||||
library['related_libraries']['show'][0],
|
||||
first=False,
|
||||
condense=condense,
|
||||
|
||||
single=True
|
||||
)
|
||||
|
||||
# Add season map_names if they exist
|
||||
if 'map_names' in library['info']:
|
||||
season_names = library['info']['map_names'].get(str(library['season_number']), {})
|
||||
|
||||
# Add titles from all locations
|
||||
# TODO only add name maps from a specific location
|
||||
for location, names in season_names.items():
|
||||
titles += [name for name in names if name and name not in titles]
|
||||
|
||||
|
||||
identifier = fireEvent('library.identifier', library, single = True)
|
||||
|
||||
# Add season identifier to titles
|
||||
if include_identifier and identifier.get('season') is not None:
|
||||
titles = [title + (' S%02d' % identifier['season']) for title in titles]
|
||||
|
||||
|
||||
if first:
|
||||
return titles[0] if titles else None
|
||||
|
||||
return titles
|
||||
|
||||
def identifier(self, library):
|
||||
if library.get('type') != 'season':
|
||||
return
|
||||
|
||||
return {
|
||||
'season': tryInt(library['season_number'], None)
|
||||
}
|
||||
|
||||
def add(self, attrs = {}, update_after = True):
|
||||
type = attrs.get('type', 'season')
|
||||
primary_provider = attrs.get('primary_provider', 'thetvdb')
|
||||
|
||||
db = get_session()
|
||||
parent_identifier = attrs.get('parent_identifier', None)
|
||||
|
||||
parent = None
|
||||
if parent_identifier:
|
||||
parent = db.query(ShowLibrary).filter_by(primary_provider = primary_provider, identifier = attrs.get('parent_identifier')).first()
|
||||
|
||||
l = db.query(SeasonLibrary).filter_by(type = type, identifier = attrs.get('identifier')).first()
|
||||
if not l:
|
||||
status = fireEvent('status.get', 'needs_update', single = True)
|
||||
l = SeasonLibrary(
|
||||
type = type,
|
||||
primary_provider = primary_provider,
|
||||
year = attrs.get('year'),
|
||||
identifier = attrs.get('identifier'),
|
||||
plot = toUnicode(attrs.get('plot')),
|
||||
tagline = toUnicode(attrs.get('tagline')),
|
||||
status_id = status.get('id'),
|
||||
info = {},
|
||||
parent = parent,
|
||||
)
|
||||
|
||||
title = LibraryTitle(
|
||||
title = toUnicode(attrs.get('title')),
|
||||
simple_title = self.simplifyTitle(attrs.get('title')),
|
||||
)
|
||||
|
||||
l.titles.append(title)
|
||||
|
||||
db.add(l)
|
||||
db.commit()
|
||||
|
||||
# Update library info
|
||||
if update_after is not False:
|
||||
handle = fireEventAsync if update_after is 'async' else fireEvent
|
||||
handle('library.update.season', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
|
||||
|
||||
library_dict = l.to_dict(self.default_dict)
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def update(self, identifier, default_title = '', force = False):
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
db = get_session()
|
||||
library = db.query(SeasonLibrary).filter_by(identifier = identifier).first()
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
if library:
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
|
||||
do_update = True
|
||||
|
||||
parent_identifier = None
|
||||
if library.parent is not None:
|
||||
parent_identifier = library.parent.identifier
|
||||
|
||||
if library.status_id == done_status.get('id') and not force:
|
||||
do_update = False
|
||||
|
||||
season_params = {'season_identifier': identifier}
|
||||
info = fireEvent('season.info', merge = True, identifier = parent_identifier, params = season_params)
|
||||
|
||||
# Don't need those here
|
||||
try: del info['in_wanted']
|
||||
except: pass
|
||||
try: del info['in_library']
|
||||
except: pass
|
||||
|
||||
if not info or len(info) == 0:
|
||||
log.error('Could not update, no movie info to work with: %s', identifier)
|
||||
return False
|
||||
|
||||
# Main info
|
||||
if do_update:
|
||||
library.plot = toUnicode(info.get('plot', ''))
|
||||
library.tagline = toUnicode(info.get('tagline', ''))
|
||||
library.year = info.get('year', 0)
|
||||
library.status_id = done_status.get('id')
|
||||
library.season_number = tryInt(info.get('seasonnumber', None))
|
||||
library.info.update(info)
|
||||
db.commit()
|
||||
|
||||
# Titles
|
||||
[db.delete(title) for title in library.titles]
|
||||
db.commit()
|
||||
|
||||
titles = info.get('titles', [])
|
||||
log.debug('Adding titles: %s', titles)
|
||||
counter = 0
|
||||
for title in titles:
|
||||
if not title:
|
||||
continue
|
||||
title = toUnicode(title)
|
||||
t = LibraryTitle(
|
||||
title = title,
|
||||
simple_title = self.simplifyTitle(title),
|
||||
# XXX: default was None; so added a quick hack since we don't really need titiles for seasons anyway
|
||||
#default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
|
||||
default = True,
|
||||
)
|
||||
library.titles.append(t)
|
||||
counter += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
# Files
|
||||
images = info.get('images', [])
|
||||
for image_type in ['poster']:
|
||||
for image in images.get(image_type, []):
|
||||
if not isinstance(image, (str, unicode)):
|
||||
continue
|
||||
|
||||
file_path = fireEvent('file.download', url = image, single = True)
|
||||
if file_path:
|
||||
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
|
||||
try:
|
||||
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
|
||||
library.files.append(file_obj)
|
||||
db.commit()
|
||||
break
|
||||
except:
|
||||
log.debug('Failed to attach to library: %s', traceback.format_exc())
|
||||
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def updateReleaseDate(self, identifier):
|
||||
'''XXX: Not sure what this is for yet in relation to a tvshow'''
|
||||
pass
|
||||
#db = get_session()
|
||||
#library = db.query(SeasonLibrary).filter_by(identifier = identifier).first()
|
||||
|
||||
#if not library.info:
|
||||
#library_dict = self.update(identifier, force = True)
|
||||
#dates = library_dict.get('info', {}).get('release_date')
|
||||
#else:
|
||||
#dates = library.info.get('release_date')
|
||||
|
||||
#if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||
#dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||
#library.info.update({'release_date': dates })
|
||||
#db.commit()
|
||||
|
||||
#db.expire_all()
|
||||
#return dates
|
||||
|
||||
|
||||
#TODO: Add to base class
|
||||
def simplifyTitle(self, title):
|
||||
|
||||
title = toUnicode(title)
|
||||
|
||||
nr_prefix = '' if title[0] in ascii_letters else '#'
|
||||
title = simplifyString(title)
|
||||
|
||||
for prefix in ['the ']:
|
||||
if prefix == title[:len(prefix)]:
|
||||
title = title[len(prefix):]
|
||||
break
|
||||
|
||||
return nr_prefix + title
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import ShowLibraryPlugin
|
||||
|
||||
def start():
|
||||
return ShowLibraryPlugin()
|
||||
|
||||
config = []
|
||||
@@ -1,229 +0,0 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.settings.model import ShowLibrary, LibraryTitle, File
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
from qcond.helpers import simplify
|
||||
from qcond import QueryCondenser
|
||||
from string import ascii_letters
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class ShowLibraryPlugin(LibraryBase):
|
||||
|
||||
default_dict = {'titles': {}, 'files':{}}
|
||||
|
||||
def __init__(self):
|
||||
self.query_condenser = QueryCondenser()
|
||||
|
||||
addEvent('library.query', self.query)
|
||||
addEvent('library.add.show', self.add)
|
||||
addEvent('library.update.show', self.update)
|
||||
addEvent('library.update.show_release_date', self.updateReleaseDate)
|
||||
|
||||
def query(self, library, first = True, condense = True, **kwargs):
|
||||
if library is list or library.get('type') != 'show':
|
||||
return
|
||||
|
||||
titles = [title['title'] for title in library['titles']]
|
||||
|
||||
if condense:
|
||||
# Use QueryCondenser to build a list of optimal search titles
|
||||
condensed_titles = self.query_condenser.distinct(titles)
|
||||
|
||||
if condensed_titles:
|
||||
# Use condensed titles if we got a valid result
|
||||
titles = condensed_titles
|
||||
else:
|
||||
# Fallback to simplifying titles
|
||||
titles = [simplify(title) for title in titles]
|
||||
|
||||
if first:
|
||||
return titles[0] if titles else None
|
||||
|
||||
return titles
|
||||
|
||||
def add(self, attrs = {}, update_after = True):
|
||||
type = attrs.get('type', 'show')
|
||||
primary_provider = attrs.get('primary_provider', 'thetvdb')
|
||||
|
||||
db = get_session()
|
||||
|
||||
l = db.query(ShowLibrary).filter_by(type = type, identifier = attrs.get('identifier')).first()
|
||||
if not l:
|
||||
status = fireEvent('status.get', 'needs_update', single = True)
|
||||
l = ShowLibrary(
|
||||
type = type,
|
||||
primary_provider = primary_provider,
|
||||
year = attrs.get('year'),
|
||||
identifier = attrs.get('identifier'),
|
||||
plot = toUnicode(attrs.get('plot')),
|
||||
tagline = toUnicode(attrs.get('tagline')),
|
||||
status_id = status.get('id'),
|
||||
info = {},
|
||||
parent = None,
|
||||
)
|
||||
|
||||
title = LibraryTitle(
|
||||
title = toUnicode(attrs.get('title')),
|
||||
simple_title = self.simplifyTitle(attrs.get('title')),
|
||||
)
|
||||
|
||||
l.titles.append(title)
|
||||
|
||||
db.add(l)
|
||||
db.commit()
|
||||
|
||||
# Update library info
|
||||
if update_after is not False:
|
||||
handle = fireEventAsync if update_after is 'async' else fireEvent
|
||||
handle('library.update.show', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
|
||||
|
||||
library_dict = l.to_dict(self.default_dict)
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def update(self, identifier, default_title = '', force = False):
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
db = get_session()
|
||||
library = db.query(ShowLibrary).filter_by(identifier = identifier).first()
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
if library:
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
|
||||
do_update = True
|
||||
|
||||
info = fireEvent('show.info', merge = True, identifier = identifier)
|
||||
|
||||
# Don't need those here
|
||||
try: del info['in_wanted']
|
||||
except: pass
|
||||
try: del info['in_library']
|
||||
except: pass
|
||||
|
||||
if not info or len(info) == 0:
|
||||
log.error('Could not update, no show info to work with: %s', identifier)
|
||||
return False
|
||||
|
||||
# Main info
|
||||
if do_update:
|
||||
library.plot = toUnicode(info.get('plot', ''))
|
||||
library.tagline = toUnicode(info.get('tagline', ''))
|
||||
library.year = info.get('year', 0)
|
||||
library.status_id = done_status.get('id')
|
||||
library.show_status = toUnicode(info.get('status', '').lower())
|
||||
library.airs_time = info.get('airs_time', None)
|
||||
|
||||
# Bits
|
||||
days_of_week_map = {
|
||||
u'Monday': 1,
|
||||
u'Tuesday': 2,
|
||||
u'Wednesday': 4,
|
||||
u'Thursday': 8,
|
||||
u'Friday': 16,
|
||||
u'Saturday': 32,
|
||||
u'Sunday': 64,
|
||||
u'Daily': 127,
|
||||
}
|
||||
try:
|
||||
library.airs_dayofweek = days_of_week_map.get(info.get('airs_dayofweek'))
|
||||
except:
|
||||
library.airs_dayofweek = 0
|
||||
|
||||
try:
|
||||
library.last_updated = int(info.get('lastupdated'))
|
||||
except:
|
||||
library.last_updated = int(time.time())
|
||||
|
||||
library.info.update(info)
|
||||
|
||||
db.commit()
|
||||
|
||||
# Titles
|
||||
[db.delete(title) for title in library.titles]
|
||||
db.commit()
|
||||
|
||||
titles = info.get('titles', [])
|
||||
log.debug('Adding titles: %s', titles)
|
||||
counter = 0
|
||||
for title in titles:
|
||||
if not title:
|
||||
continue
|
||||
title = toUnicode(title)
|
||||
t = LibraryTitle(
|
||||
title = title,
|
||||
simple_title = self.simplifyTitle(title),
|
||||
default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
|
||||
)
|
||||
library.titles.append(t)
|
||||
counter += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
# Files
|
||||
images = info.get('images', [])
|
||||
for image_type in ['poster']:
|
||||
for image in images.get(image_type, []):
|
||||
if not isinstance(image, (str, unicode)):
|
||||
continue
|
||||
|
||||
file_path = fireEvent('file.download', url = image, single = True)
|
||||
if file_path:
|
||||
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
|
||||
try:
|
||||
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
|
||||
library.files.append(file_obj)
|
||||
db.commit()
|
||||
|
||||
break
|
||||
except:
|
||||
log.debug('Failed to attach to library: %s', traceback.format_exc())
|
||||
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def updateReleaseDate(self, identifier):
|
||||
'''XXX: Not sure what this is for yet in relation to a show'''
|
||||
pass
|
||||
#db = get_session()
|
||||
#library = db.query(ShowLibrary).filter_by(identifier = identifier).first()
|
||||
|
||||
#if not library.info:
|
||||
#library_dict = self.update(identifier, force = True)
|
||||
#dates = library_dict.get('info', {}).get('release_date')
|
||||
#else:
|
||||
#dates = library.info.get('release_date')
|
||||
|
||||
#if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||
#dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||
#library.info.update({'release_date': dates })
|
||||
#db.commit()
|
||||
|
||||
#db.expire_all()
|
||||
#return dates
|
||||
|
||||
|
||||
#TODO: Add to base class
|
||||
def simplifyTitle(self, title):
|
||||
|
||||
title = toUnicode(title)
|
||||
|
||||
nr_prefix = '' if title[0] in ascii_letters else '#'
|
||||
title = simplifyString(title)
|
||||
|
||||
for prefix in ['the ']:
|
||||
if prefix == title[:len(prefix)]:
|
||||
title = title[len(prefix):]
|
||||
break
|
||||
|
||||
return nr_prefix + title
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import ShowMatcher
|
||||
|
||||
def start():
|
||||
return ShowMatcher()
|
||||
|
||||
config = []
|
||||
@@ -1,127 +0,0 @@
|
||||
from couchpotato import CPLog
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import dictIsSubset, tryInt, toIterable
|
||||
from couchpotato.core.media._base.matcher.base import MatcherBase
|
||||
from couchpotato.core.providers.base import MultiProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class ShowMatcher(MultiProvider):
|
||||
|
||||
def getTypes(self):
|
||||
return [Season, Episode]
|
||||
|
||||
|
||||
class Base(MatcherBase):
|
||||
# TODO come back to this later, think this could be handled better, this is starting to get out of hand....
|
||||
quality_map = {
|
||||
'bluray_1080p': {'resolution': ['1080p'], 'source': ['bluray']},
|
||||
'bluray_720p': {'resolution': ['720p'], 'source': ['bluray']},
|
||||
|
||||
'bdrip_1080p': {'resolution': ['1080p'], 'source': ['BDRip']},
|
||||
'bdrip_720p': {'resolution': ['720p'], 'source': ['BDRip']},
|
||||
|
||||
'brrip_1080p': {'resolution': ['1080p'], 'source': ['BRRip']},
|
||||
'brrip_720p': {'resolution': ['720p'], 'source': ['BRRip']},
|
||||
|
||||
'webdl_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]},
|
||||
'webdl_720p': {'resolution': ['720p'], 'source': ['webdl', ['web', 'dl']]},
|
||||
'webdl_480p': {'resolution': ['480p'], 'source': ['webdl', ['web', 'dl']]},
|
||||
|
||||
'hdtv_720p': {'resolution': ['720p'], 'source': ['hdtv']},
|
||||
'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super(Base, self).__init__()
|
||||
|
||||
addEvent('%s.matcher.correct_identifier' % self.type, self.correctIdentifier)
|
||||
|
||||
def correct(self, chain, release, media, quality):
|
||||
log.info("Checking if '%s' is valid", release['name'])
|
||||
log.info2('Release parsed as: %s', chain.info)
|
||||
|
||||
if not fireEvent('matcher.correct_quality', chain, quality, self.quality_map, single = True):
|
||||
log.info('Wrong: %s, quality does not match', release['name'])
|
||||
return False
|
||||
|
||||
if not fireEvent('%s.matcher.correct_identifier' % self.type, chain, media):
|
||||
log.info('Wrong: %s, identifier does not match', release['name'])
|
||||
return False
|
||||
|
||||
if not fireEvent('matcher.correct_title', chain, media):
|
||||
log.info("Wrong: '%s', undetermined naming.", (' '.join(chain.info['show_name'])))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def correctIdentifier(self, chain, media):
|
||||
raise NotImplementedError()
|
||||
|
||||
def getChainIdentifier(self, chain):
|
||||
if 'identifier' not in chain.info:
|
||||
return None
|
||||
|
||||
identifier = self.flattenInfo(chain.info['identifier'])
|
||||
|
||||
# Try cast values to integers
|
||||
for key, value in identifier.items():
|
||||
if isinstance(value, list):
|
||||
if len(value) <= 1:
|
||||
value = value[0]
|
||||
else:
|
||||
log.warning('Wrong: identifier contains multiple season or episode values, unsupported')
|
||||
return None
|
||||
|
||||
identifier[key] = tryInt(value, value)
|
||||
|
||||
return identifier
|
||||
|
||||
|
||||
class Episode(Base):
|
||||
type = 'episode'
|
||||
|
||||
def correctIdentifier(self, chain, media):
|
||||
identifier = self.getChainIdentifier(chain)
|
||||
if not identifier:
|
||||
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
|
||||
return False
|
||||
|
||||
# TODO - Parse episode ranges from identifier to determine if they are multi-part episodes
|
||||
if any([x in identifier for x in ['episode_from', 'episode_to']]):
|
||||
log.info2('Wrong: releases with identifier ranges are not supported yet')
|
||||
return False
|
||||
|
||||
required = fireEvent('library.identifier', media['library'], single = True)
|
||||
|
||||
# TODO - Support air by date episodes
|
||||
# TODO - Support episode parts
|
||||
|
||||
if identifier != required:
|
||||
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
class Season(Base):
|
||||
type = 'season'
|
||||
|
||||
def correctIdentifier(self, chain, media):
|
||||
identifier = self.getChainIdentifier(chain)
|
||||
if not identifier:
|
||||
log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)')
|
||||
return False
|
||||
|
||||
# TODO - Parse episode ranges from identifier to determine if they are season packs
|
||||
if any([x in identifier for x in ['episode_from', 'episode_to']]):
|
||||
log.info2('Wrong: releases with identifier ranges are not supported yet')
|
||||
return False
|
||||
|
||||
required = fireEvent('library.identifier', media['library'], single = True)
|
||||
|
||||
if identifier != required:
|
||||
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,7 +0,0 @@
|
||||
from .main import ShowSearcher
|
||||
import random
|
||||
|
||||
def start():
|
||||
return ShowSearcher()
|
||||
|
||||
config = []
|
||||
@@ -1,189 +0,0 @@
|
||||
from couchpotato import Env, get_session
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import getTitle, toIterable
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.searcher.main import SearchSetupError
|
||||
from couchpotato.core.media.show._base import ShowBase
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Media
|
||||
from qcond import QueryCondenser
|
||||
from qcond.helpers import simplify
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class ShowSearcher(Plugin):
|
||||
|
||||
type = ['show', 'season', 'episode']
|
||||
|
||||
in_progress = False
|
||||
|
||||
def __init__(self):
|
||||
super(ShowSearcher, self).__init__()
|
||||
|
||||
self.query_condenser = QueryCondenser()
|
||||
|
||||
for type in toIterable(self.type):
|
||||
addEvent('%s.searcher.single' % type, self.single)
|
||||
|
||||
addEvent('searcher.correct_release', self.correctRelease)
|
||||
|
||||
def single(self, media, search_protocols = None, manual = False):
|
||||
show, season, episode = self.getLibraries(media['library'])
|
||||
|
||||
db = get_session()
|
||||
|
||||
if media['type'] == 'show':
|
||||
for library in season:
|
||||
# TODO ideally we shouldn't need to fetch the media for each season library here
|
||||
m = db.query(Media).filter_by(library_id = library['library_id']).first()
|
||||
|
||||
fireEvent('season.searcher.single', m.to_dict(ShowBase.search_dict))
|
||||
|
||||
return
|
||||
|
||||
# Find out search type
|
||||
try:
|
||||
if not search_protocols:
|
||||
search_protocols = fireEvent('searcher.protocols', single = True)
|
||||
except SearchSetupError:
|
||||
return
|
||||
|
||||
done_status, available_status, ignored_status, failed_status = fireEvent('status.get', ['done', 'available', 'ignored', 'failed'], single = True)
|
||||
|
||||
if not media['profile'] or media['status_id'] == done_status.get('id'):
|
||||
log.debug('Episode doesn\'t have a profile or already done, assuming in manage tab.')
|
||||
return
|
||||
|
||||
#pre_releases = fireEvent('quality.pre_releases', single = True)
|
||||
|
||||
found_releases = []
|
||||
too_early_to_search = []
|
||||
|
||||
default_title = fireEvent('library.query', media['library'], condense = False, single=True)
|
||||
if not default_title:
|
||||
log.error('No proper info found for episode, removing it from library to cause it from having more issues.')
|
||||
#fireEvent('episode.delete', episode['id'], single = True)
|
||||
return
|
||||
|
||||
if not show or not season:
|
||||
log.error('Unable to find show or season library in database, missing required data for searching')
|
||||
return
|
||||
|
||||
fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['id'], data = True, message = 'Searching for "%s"' % default_title)
|
||||
|
||||
ret = False
|
||||
has_better_quality = None
|
||||
|
||||
for quality_type in media['profile']['types']:
|
||||
# TODO check air date?
|
||||
#if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
|
||||
# too_early_to_search.append(quality_type['quality']['identifier'])
|
||||
# continue
|
||||
|
||||
has_better_quality = 0
|
||||
|
||||
# See if better quality is available
|
||||
for release in media['releases']:
|
||||
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]:
|
||||
has_better_quality += 1
|
||||
|
||||
# Don't search for quality lower then already available.
|
||||
if has_better_quality is 0:
|
||||
|
||||
log.info('Search for %s S%02d%s in %s', (
|
||||
getTitle(show),
|
||||
season['season_number'],
|
||||
"E%02d" % episode['episode_number'] if episode and len(episode) == 1 else "",
|
||||
quality_type['quality']['label'])
|
||||
)
|
||||
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
|
||||
|
||||
results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
|
||||
if len(results) == 0:
|
||||
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
|
||||
|
||||
# Check if movie isn't deleted while searching
|
||||
if not db.query(Media).filter_by(id = media.get('id')).first():
|
||||
break
|
||||
|
||||
# Add them to this movie releases list
|
||||
found_releases += fireEvent('release.create_from_search', results, media, quality_type, single = True)
|
||||
|
||||
# Try find a valid result and download it
|
||||
if fireEvent('release.try_download_result', results, media, quality_type, manual, single = True):
|
||||
ret = True
|
||||
|
||||
# Remove releases that aren't found anymore
|
||||
for release in media.get('releases', []):
|
||||
if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases:
|
||||
fireEvent('release.delete', release.get('id'), single = True)
|
||||
else:
|
||||
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
|
||||
fireEvent('media.restatus', media['id'])
|
||||
break
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown() or ret:
|
||||
break
|
||||
|
||||
if len(too_early_to_search) > 0:
|
||||
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
|
||||
elif media['type'] == 'season' and not ret and has_better_quality is 0:
|
||||
# If nothing was found, start searching for episodes individually
|
||||
log.info('No season pack found, starting individual episode search')
|
||||
|
||||
for library in episode:
|
||||
# TODO ideally we shouldn't need to fetch the media for each episode library here
|
||||
m = db.query(Media).filter_by(library_id = library['library_id']).first()
|
||||
|
||||
fireEvent('episode.searcher.single', m.to_dict(ShowBase.search_dict))
|
||||
|
||||
|
||||
fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['id'], data = True)
|
||||
|
||||
return ret
|
||||
|
||||
def correctRelease(self, release = None, media = None, quality = None, **kwargs):
|
||||
|
||||
if media.get('type') not in ['season', 'episode']: return
|
||||
|
||||
retention = Env.setting('retention', section = 'nzb')
|
||||
|
||||
if release.get('seeders') is None and 0 < retention < release.get('age', 0):
|
||||
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (release['age'], retention, release['name']))
|
||||
return False
|
||||
|
||||
# Check for required and ignored words
|
||||
if not fireEvent('searcher.correct_words', release['name'], media, single = True):
|
||||
return False
|
||||
|
||||
# TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations)
|
||||
match = fireEvent('matcher.match', release, media, quality, single = True)
|
||||
if match:
|
||||
return match.weight
|
||||
|
||||
return False
|
||||
|
||||
def getLibraries(self, library):
|
||||
if 'related_libraries' not in library:
|
||||
log.warning("'related_libraries' missing from media library, unable to continue searching")
|
||||
return None, None, None
|
||||
|
||||
libraries = library['related_libraries']
|
||||
|
||||
# Show always collapses as there can never be any multiples
|
||||
show = libraries.get('show', [])
|
||||
show = show[0] if len(show) else None
|
||||
|
||||
# Season collapses if the subject is a season or episode
|
||||
season = libraries.get('season', [])
|
||||
if library['type'] in ['season', 'episode']:
|
||||
season = season[0] if len(season) else None
|
||||
|
||||
# Episode collapses if the subject is a episode
|
||||
episode = libraries.get('episode', [])
|
||||
if library['type'] == 'episode':
|
||||
episode = episode[0] if len(episode) else None
|
||||
|
||||
return show, season, episode
|
||||
@@ -1,4 +1,4 @@
|
||||
config = [{
|
||||
config = {
|
||||
'name': 'notification_providers',
|
||||
'groups': [
|
||||
{
|
||||
@@ -10,4 +10,4 @@ config = [{
|
||||
'options': [],
|
||||
},
|
||||
],
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class Notification(Provider):
|
||||
listen_to = [
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
'core.message.important',
|
||||
'core.message',
|
||||
]
|
||||
dont_listen_to = []
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ class Boxcar(Notification):
|
||||
try:
|
||||
message = message.strip()
|
||||
|
||||
data = {
|
||||
params = {
|
||||
'email': self.conf('email'),
|
||||
'notification[from_screen_name]': self.default_title,
|
||||
'notification[message]': toUnicode(message),
|
||||
'notification[from_remote_service_id]': int(time.time()),
|
||||
}
|
||||
|
||||
self.urlopen(self.url, data = data)
|
||||
self.urlopen(self.url, params = params)
|
||||
except:
|
||||
log.error('Check your email and added services on boxcar.io')
|
||||
return False
|
||||
|
||||
@@ -21,12 +21,6 @@ class CoreNotifier(Notification):
|
||||
|
||||
m_lock = None
|
||||
|
||||
listen_to = [
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
'core.message', 'core.message.important',
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(CoreNotifier, self).__init__()
|
||||
|
||||
@@ -127,10 +121,7 @@ class CoreNotifier(Notification):
|
||||
|
||||
for message in messages:
|
||||
if message.get('time') > last_check:
|
||||
message['sticky'] = True # Always sticky core messages
|
||||
|
||||
message_type = 'core.message.important' if message.get('important') else 'core.message'
|
||||
fireEvent(message_type, message = message.get('message'), data = message)
|
||||
fireEvent('core.message', message = message.get('message'), data = message)
|
||||
|
||||
if last_check < message.get('time'):
|
||||
last_check = message.get('time')
|
||||
@@ -207,16 +198,13 @@ class CoreNotifier(Notification):
|
||||
def removeListener(self, callback):
|
||||
|
||||
self.m_lock.acquire()
|
||||
new_listeners = []
|
||||
for list_tuple in self.listeners:
|
||||
try:
|
||||
listener, last_id = list_tuple
|
||||
if listener != callback:
|
||||
new_listeners.append(list_tuple)
|
||||
if listener == callback:
|
||||
self.listeners.remove(list_tuple)
|
||||
except:
|
||||
log.debug('Failed removing listener: %s', traceback.format_exc())
|
||||
|
||||
self.listeners = new_listeners
|
||||
self.m_lock.release()
|
||||
|
||||
def cleanMessages(self):
|
||||
|
||||
@@ -10,8 +10,8 @@ var NotificationBase = new Class({
|
||||
// Listener
|
||||
App.addEvent('unload', self.stopPoll.bind(self));
|
||||
App.addEvent('reload', self.startInterval.bind(self, [true]));
|
||||
App.on('notification', self.notify.bind(self));
|
||||
App.on('message', self.showMessage.bind(self));
|
||||
App.addEvent('notification', self.notify.bind(self));
|
||||
App.addEvent('message', self.showMessage.bind(self));
|
||||
|
||||
// Add test buttons to settings page
|
||||
App.addEvent('load', self.addTestButtons.bind(self));
|
||||
@@ -50,9 +50,9 @@ var NotificationBase = new Class({
|
||||
, 'top');
|
||||
self.notifications.include(result);
|
||||
|
||||
if((result.data.important !== undefined || result.data.sticky !== undefined) && !result.read){
|
||||
if(result.data.important !== undefined && !result.read){
|
||||
var sticky = true
|
||||
App.trigger('message', [result.message, sticky, result])
|
||||
App.fireEvent('message', [result.message, sticky, result])
|
||||
}
|
||||
else if(!result.read){
|
||||
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
|
||||
@@ -147,7 +147,7 @@ var NotificationBase = new Class({
|
||||
// Process data
|
||||
if(json){
|
||||
Array.each(json.result, function(result){
|
||||
App.trigger(result.type, result);
|
||||
App.fireEvent(result.type, result);
|
||||
if(result.message && result.read === undefined)
|
||||
self.showMessage(result.message);
|
||||
})
|
||||
|
||||
@@ -28,23 +28,12 @@ config = [{
|
||||
'name': 'smtp_server',
|
||||
'label': 'SMTP server',
|
||||
},
|
||||
{ 'name': 'smtp_port',
|
||||
'label': 'SMTP server port',
|
||||
'default': '25',
|
||||
'type': 'int',
|
||||
},
|
||||
{
|
||||
'name': 'ssl',
|
||||
'label': 'Enable SSL',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
},
|
||||
{
|
||||
'name': 'starttls',
|
||||
'label': 'Enable StartTLS',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
},
|
||||
{
|
||||
'name': 'smtp_user',
|
||||
'label': 'SMTP user',
|
||||
|
||||
@@ -2,9 +2,7 @@ from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from couchpotato.environment import Env
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate, make_msgid
|
||||
import smtplib
|
||||
import traceback
|
||||
|
||||
@@ -23,30 +21,18 @@ class Email(Notification):
|
||||
smtp_server = self.conf('smtp_server')
|
||||
smtp_user = self.conf('smtp_user')
|
||||
smtp_pass = self.conf('smtp_pass')
|
||||
smtp_port = self.conf('smtp_port')
|
||||
starttls = self.conf('starttls')
|
||||
|
||||
# Make the basic message
|
||||
message = MIMEText(toUnicode(message), _charset = Env.get('encoding'))
|
||||
message = MIMEText(toUnicode(message))
|
||||
message['Subject'] = self.default_title
|
||||
message['From'] = from_address
|
||||
message['To'] = to_address
|
||||
message['Date'] = formatdate(localtime = 1)
|
||||
message['Message-ID'] = make_msgid()
|
||||
|
||||
try:
|
||||
# Open the SMTP connection, via SSL if requested
|
||||
log.debug("Connecting to host %s on port %s" % (smtp_server, smtp_port))
|
||||
log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled"))
|
||||
mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server)
|
||||
|
||||
if (starttls):
|
||||
log.debug("Using StartTLS to initiate the connection with the SMTP server")
|
||||
mailserver.starttls()
|
||||
|
||||
# Say hello to the server
|
||||
mailserver.ehlo()
|
||||
|
||||
# Check too see if an login attempt should be attempted
|
||||
if len(smtp_user) > 0:
|
||||
log.debug("Logging on to SMTP server using username \'%s\'%s", (smtp_user, " and a password" if len(smtp_pass) > 0 else ""))
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from .main import Pushbullet
|
||||
from .main import Notifo
|
||||
|
||||
def start():
|
||||
return Pushbullet()
|
||||
return Notifo()
|
||||
|
||||
config = [{
|
||||
'name': 'pushbullet',
|
||||
'name': 'notifo',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'pushbullet',
|
||||
'name': 'notifo',
|
||||
'description': 'Keep in mind that Notifo service will end soon.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
@@ -17,14 +18,10 @@ config = [{
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'User API Key'
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'devices',
|
||||
'default': '',
|
||||
'advanced': True,
|
||||
'description': 'IDs of devices to send notifications to, empty = all devices'
|
||||
'name': 'api_key',
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
39
couchpotato/core/notifications/notifo/main.py
Normal file
39
couchpotato/core/notifications/notifo/main.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import base64
|
||||
import json
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Notifo(Notification):
|
||||
|
||||
url = 'https://api.notifo.com/v1/send_notification'
|
||||
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
try:
|
||||
params = {
|
||||
'label': self.default_title,
|
||||
'msg': toUnicode(message),
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('api_key')))[:-1]
|
||||
}
|
||||
|
||||
handle = self.urlopen(self.url, params = params, headers = headers)
|
||||
result = json.loads(handle)
|
||||
|
||||
if result['status'] != 'success' or result['response_message'] != 'OK':
|
||||
raise Exception
|
||||
|
||||
except:
|
||||
log.error('Notification failed: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
log.info('Notifo notification successful.')
|
||||
return True
|
||||
11
couchpotato/core/notifications/plex/__init__.py
Executable file → Normal file
11
couchpotato/core/notifications/plex/__init__.py
Executable file → Normal file
@@ -17,15 +17,10 @@ config = [{
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'media_server',
|
||||
'label': 'Media Server',
|
||||
'name': 'host',
|
||||
'default': 'localhost',
|
||||
'description': 'Hostname/IP, default localhost'
|
||||
},
|
||||
{
|
||||
'name': 'clients',
|
||||
'default': '',
|
||||
'description': 'Comma separated list of client names\'s (computer names). Top right when you start Plex'
|
||||
'description': 'Default should be on localhost',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import json
|
||||
from couchpotato import CPLog
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
import requests
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class PlexClientProtocol(object):
|
||||
def __init__(self, plex):
|
||||
self.plex = plex
|
||||
|
||||
addEvent('notify.plex.notifyClient', self.notify)
|
||||
|
||||
def notify(self, client, message):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class PlexClientHTTP(PlexClientProtocol):
|
||||
def request(self, command, client):
|
||||
url = 'http://%s:%s/xbmcCmds/xbmcHttp/?%s' % (
|
||||
client['address'],
|
||||
client['port'],
|
||||
tryUrlencode(command)
|
||||
)
|
||||
|
||||
headers = {}
|
||||
|
||||
try:
|
||||
self.plex.urlopen(url, headers = headers, timeout = 3, show_error = False)
|
||||
except Exception, err:
|
||||
log.error("Couldn't sent command to Plex: %s", err)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def notify(self, client, message):
|
||||
if client.get('protocol') != 'xbmchttp':
|
||||
return None
|
||||
|
||||
data = {
|
||||
'command': 'ExecBuiltIn',
|
||||
'parameter': 'Notification(CouchPotato, %s)' % message
|
||||
}
|
||||
|
||||
return self.request(data, client)
|
||||
|
||||
|
||||
class PlexClientJSON(PlexClientProtocol):
|
||||
def request(self, method, params, client):
|
||||
log.debug('sendJSON("%s", %s, %s)', (method, params, client))
|
||||
url = 'http://%s:%s/jsonrpc' % (
|
||||
client['address'],
|
||||
client['port']
|
||||
)
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
request = {
|
||||
'id': 1,
|
||||
'jsonrpc': '2.0',
|
||||
'method': method,
|
||||
'params': params
|
||||
}
|
||||
|
||||
try:
|
||||
requests.post(url, headers = headers, timeout = 3, data = json.dumps(request))
|
||||
except Exception, err:
|
||||
log.error("Couldn't sent command to Plex: %s", err)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def notify(self, client, message):
|
||||
if client.get('protocol') not in ['xbmcjson', 'plex']:
|
||||
return None
|
||||
|
||||
params = {
|
||||
'title': 'CouchPotato',
|
||||
'message': message
|
||||
}
|
||||
return self.request('GUI.ShowNotification', params, client)
|
||||
114
couchpotato/core/notifications/plex/main.py
Executable file → Normal file
114
couchpotato/core/notifications/plex/main.py
Executable file → Normal file
@@ -1,64 +1,78 @@
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import cleanHost, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from .client import PlexClientHTTP, PlexClientJSON
|
||||
from .server import PlexServer
|
||||
from urllib2 import URLError
|
||||
from urlparse import urlparse
|
||||
from xml.dom import minidom
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Plex(Notification):
|
||||
|
||||
http_time_between_calls = 0
|
||||
|
||||
def __init__(self):
|
||||
super(Plex, self).__init__()
|
||||
|
||||
self.server = PlexServer(self)
|
||||
|
||||
self.client_protocols = {
|
||||
'http': PlexClientHTTP(self),
|
||||
'json': PlexClientJSON(self)
|
||||
}
|
||||
|
||||
addEvent('renamer.after', self.addToLibrary)
|
||||
|
||||
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
def addToLibrary(self, message = None, group = None):
|
||||
if self.isDisabled(): return
|
||||
if not group: group = {}
|
||||
|
||||
return self.server.refresh()
|
||||
log.info('Sending notification to Plex')
|
||||
hosts = self.getHosts(port = 32400)
|
||||
|
||||
def getClientNames(self):
|
||||
return [
|
||||
x.strip().lower()
|
||||
for x in self.conf('clients').split(',')
|
||||
]
|
||||
for host in hosts:
|
||||
|
||||
def notifyClients(self, message, client_names):
|
||||
success = True
|
||||
source_type = ['movie']
|
||||
base_url = '%s/library/sections' % host
|
||||
refresh_url = '%s/%%s/refresh' % base_url
|
||||
|
||||
for client_name in client_names:
|
||||
try:
|
||||
sections_xml = self.urlopen(base_url)
|
||||
xml_sections = minidom.parseString(sections_xml)
|
||||
sections = xml_sections.getElementsByTagName('Directory')
|
||||
|
||||
client_success = False
|
||||
client = self.server.clients.get(client_name)
|
||||
for s in sections:
|
||||
if s.getAttribute('type') in source_type:
|
||||
url = refresh_url % s.getAttribute('key')
|
||||
self.urlopen(url)
|
||||
|
||||
if client and client['found']:
|
||||
client_success = fireEvent('notify.plex.notifyClient', client, message, single = True)
|
||||
except:
|
||||
log.error('Plex library update failed for %s, Media Server not running: %s', (host, traceback.format_exc(1)))
|
||||
return False
|
||||
|
||||
if not client_success:
|
||||
if self.server.staleClients() or not client:
|
||||
log.info('Failed to send notification to client "%s". '
|
||||
'Client list is stale, updating the client list and retrying.', client_name)
|
||||
self.server.updateClients(self.getClientNames())
|
||||
else:
|
||||
log.warning('Failed to send notification to client %s, skipping this time', client_name)
|
||||
success = False
|
||||
return True
|
||||
|
||||
return success
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
return self.notifyClients(message, self.getClientNames())
|
||||
hosts = self.getHosts(port = 3000)
|
||||
successful = 0
|
||||
for host in hosts:
|
||||
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
|
||||
successful += 1
|
||||
|
||||
return successful == len(hosts)
|
||||
|
||||
def send(self, command, host):
|
||||
|
||||
url = '%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
|
||||
headers = {}
|
||||
|
||||
try:
|
||||
self.urlopen(url, headers = headers, show_error = False)
|
||||
except URLError:
|
||||
log.error("Couldn't sent command to Plex, probably just running Media Server")
|
||||
return False
|
||||
except:
|
||||
log.error("Couldn't sent command to Plex: %s", traceback.format_exc())
|
||||
return False
|
||||
|
||||
log.info('Plex notification to %s successful.', host)
|
||||
return True
|
||||
|
||||
def test(self, **kwargs):
|
||||
|
||||
@@ -66,12 +80,28 @@ class Plex(Notification):
|
||||
|
||||
log.info('Sending test to %s', test_type)
|
||||
|
||||
notify_success = self.notify(
|
||||
success = self.notify(
|
||||
message = self.test_message,
|
||||
data = {},
|
||||
listener = 'test'
|
||||
)
|
||||
success2 = self.addToLibrary()
|
||||
|
||||
refresh_success = self.addToLibrary()
|
||||
return {
|
||||
'success': success or success2
|
||||
}
|
||||
|
||||
return {'success': notify_success or refresh_success}
|
||||
def getHosts(self, port = None):
|
||||
|
||||
raw_hosts = splitString(self.conf('host'))
|
||||
hosts = []
|
||||
|
||||
for h in raw_hosts:
|
||||
h = cleanHost(h)
|
||||
p = urlparse(h)
|
||||
h = h.rstrip('/')
|
||||
if port and not p.port:
|
||||
h += ':%s' % port
|
||||
hosts.append(h)
|
||||
|
||||
return hosts
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
from datetime import timedelta, datetime
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato import CPLog
|
||||
from urlparse import urlparse
|
||||
import traceback
|
||||
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as etree
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class PlexServer(object):
|
||||
def __init__(self, plex):
|
||||
self.plex = plex
|
||||
|
||||
self.clients = {}
|
||||
self.last_clients_update = None
|
||||
|
||||
def staleClients(self):
|
||||
if not self.last_clients_update:
|
||||
return True
|
||||
|
||||
return self.last_clients_update + timedelta(minutes=15) < datetime.now()
|
||||
|
||||
def request(self, path, data_type='xml'):
|
||||
if not self.plex.conf('media_server'):
|
||||
log.warning("Plex media server hostname is required")
|
||||
return None
|
||||
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
|
||||
data = self.plex.urlopen('%s/%s' % (
|
||||
self.createHost(self.plex.conf('media_server'), port = 32400),
|
||||
path
|
||||
))
|
||||
|
||||
if data_type == 'xml':
|
||||
return etree.fromstring(data)
|
||||
else:
|
||||
return data
|
||||
|
||||
def updateClients(self, client_names):
|
||||
log.info('Searching for clients on Plex Media Server')
|
||||
|
||||
self.clients = {}
|
||||
|
||||
result = self.request('clients')
|
||||
if not result:
|
||||
return
|
||||
|
||||
found_clients = [
|
||||
c for c in result.findall('Server')
|
||||
if c.get('name') and c.get('name').lower() in client_names
|
||||
]
|
||||
|
||||
# Store client details in cache
|
||||
for client in found_clients:
|
||||
name = client.get('name').lower()
|
||||
|
||||
self.clients[name] = {
|
||||
'name': client.get('name'),
|
||||
'found': True,
|
||||
'address': client.get('address'),
|
||||
'port': client.get('port'),
|
||||
'protocol': client.get('protocol', 'xbmchttp')
|
||||
}
|
||||
|
||||
client_names.remove(name)
|
||||
|
||||
# Store dummy info for missing clients
|
||||
for client_name in client_names:
|
||||
self.clients[client_name] = {
|
||||
'found': False
|
||||
}
|
||||
|
||||
if len(client_names) > 0:
|
||||
log.debug('Unable to find clients: %s', ', '.join(client_names))
|
||||
|
||||
self.last_clients_update = datetime.now()
|
||||
|
||||
def refresh(self, section_types=None):
|
||||
if not section_types:
|
||||
section_types = ['movie']
|
||||
|
||||
sections = self.request('library/sections')
|
||||
|
||||
try:
|
||||
for section in sections.findall('Directory'):
|
||||
if section.get('type') not in section_types:
|
||||
continue
|
||||
|
||||
self.request('library/sections/%s/refresh' % section.get('key'), 'text')
|
||||
except:
|
||||
log.error('Plex library update failed for %s, Media Server not running: %s',
|
||||
(self.plex.conf('media_server'), traceback.format_exc(1)))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def createHost(self, host, port = None):
|
||||
|
||||
h = cleanHost(host)
|
||||
p = urlparse(h)
|
||||
h = h.rstrip('/')
|
||||
|
||||
if port and not p.port:
|
||||
h += ':%s' % port
|
||||
|
||||
return h
|
||||
@@ -26,7 +26,7 @@ class Prowl(Notification):
|
||||
}
|
||||
|
||||
try:
|
||||
self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False)
|
||||
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
|
||||
log.info('Prowl notifications sent.')
|
||||
return True
|
||||
except:
|
||||
|
||||
@@ -29,7 +29,7 @@ class Pushalot(Notification):
|
||||
}
|
||||
|
||||
try:
|
||||
self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False)
|
||||
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
|
||||
return True
|
||||
except:
|
||||
log.error('PushAlot failed: %s', traceback.format_exc())
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import base64
|
||||
import json
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Pushbullet(Notification):
|
||||
|
||||
url = 'https://api.pushbullet.com/api/%s'
|
||||
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
devices = self.getDevices()
|
||||
if devices is None:
|
||||
return False
|
||||
|
||||
# Get all the device IDs linked to this user
|
||||
if not len(devices):
|
||||
response = self.request('devices')
|
||||
if not response:
|
||||
return False
|
||||
|
||||
devices += [device.get('id') for device in response['devices']]
|
||||
|
||||
successful = 0
|
||||
for device in devices:
|
||||
response = self.request(
|
||||
'pushes',
|
||||
cache = False,
|
||||
device_id = device,
|
||||
type = 'note',
|
||||
title = self.default_title,
|
||||
body = toUnicode(message)
|
||||
)
|
||||
|
||||
if response:
|
||||
successful += 1
|
||||
else:
|
||||
log.error('Unable to push notification to Pushbullet device with ID %s' % device)
|
||||
|
||||
return successful == len(devices)
|
||||
|
||||
def getDevices(self):
|
||||
devices = [d.strip() for d in self.conf('devices').split(',')]
|
||||
|
||||
# Remove empty items
|
||||
devices = [d for d in devices if len(d)]
|
||||
|
||||
# Break on any ids that aren't integers
|
||||
valid_devices = []
|
||||
|
||||
for device_id in devices:
|
||||
d = tryInt(device_id, None)
|
||||
|
||||
if not d:
|
||||
log.error('Device ID "%s" is not valid', device_id)
|
||||
return None
|
||||
|
||||
valid_devices.append(d)
|
||||
|
||||
return valid_devices
|
||||
|
||||
def request(self, method, cache = True, **kwargs):
|
||||
try:
|
||||
base64string = base64.encodestring('%s:' % self.conf('api_key'))[:-1]
|
||||
|
||||
headers = {
|
||||
"Authorization": "Basic %s" % base64string
|
||||
}
|
||||
|
||||
if cache:
|
||||
return self.getJsonData(self.url % method, headers = headers, data = kwargs)
|
||||
else:
|
||||
data = self.urlopen(self.url % method, headers = headers, data = kwargs)
|
||||
return json.loads(data)
|
||||
|
||||
except Exception, ex:
|
||||
log.error('Pushbullet request failed')
|
||||
log.debug(ex)
|
||||
|
||||
return None
|
||||
@@ -35,7 +35,7 @@ class Trakt(Notification):
|
||||
def call(self, method_url, post_data):
|
||||
|
||||
try:
|
||||
response = self.getJsonData(self.urls['base'] % method_url, data = post_data, cache_timeout = 1)
|
||||
response = self.getJsonData(self.urls['base'] % method_url, params = post_data, cache_timeout = 1)
|
||||
if response:
|
||||
if response.get('status') == "success":
|
||||
log.info('Successfully called Trakt')
|
||||
|
||||
@@ -46,14 +46,6 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.',
|
||||
},
|
||||
{
|
||||
'name': 'force_full_scan',
|
||||
'label': 'Always do a full scan',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Do a full scan instead of only the new movie. Useful if the XBMC path is different from the path CPS uses.',
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
|
||||
@@ -7,7 +7,6 @@ import json
|
||||
import socket
|
||||
import traceback
|
||||
import urllib
|
||||
import requests
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -37,7 +36,7 @@ class XBMC(Notification):
|
||||
|
||||
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
|
||||
param = {}
|
||||
if not self.conf('force_full_scan') and (self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0])):
|
||||
if self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0]):
|
||||
param = {'directory': data['destination_dir']}
|
||||
|
||||
calls.append(('VideoLibrary.Scan', param))
|
||||
@@ -168,18 +167,22 @@ class XBMC(Notification):
|
||||
# manually fake expected response array
|
||||
return [{'result': 'Error'}]
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||
return [{'result': 'Error'}]
|
||||
except URLError, e:
|
||||
if isinstance(e.reason, socket.timeout):
|
||||
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||
return [{'result': 'Error'}]
|
||||
else:
|
||||
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||
return [{'result': 'Error'}]
|
||||
except:
|
||||
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||
return [{'result': 'Error'}]
|
||||
|
||||
def request(self, host, do_requests):
|
||||
def request(self, host, requests):
|
||||
server = 'http://%s/jsonrpc' % host
|
||||
|
||||
data = []
|
||||
for req in do_requests:
|
||||
for req in requests:
|
||||
method, kwargs = req
|
||||
data.append({
|
||||
'method': method,
|
||||
@@ -199,13 +202,17 @@ class XBMC(Notification):
|
||||
|
||||
try:
|
||||
log.debug('Sending request to %s: %s', (host, data))
|
||||
response = self.getJsonData(server, headers = headers, data = data, timeout = 3, show_error = False)
|
||||
response = self.getJsonData(server, headers = headers, params = data, timeout = 3, show_error = False)
|
||||
log.debug('Returned from request %s: %s', (host, response))
|
||||
|
||||
return response
|
||||
except requests.exceptions.Timeout:
|
||||
log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||
return []
|
||||
except URLError, e:
|
||||
if isinstance(e.reason, socket.timeout):
|
||||
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||
return []
|
||||
else:
|
||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||
return []
|
||||
except:
|
||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||
return []
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
from .main import Xmpp
|
||||
|
||||
def start():
|
||||
return Xmpp()
|
||||
|
||||
config = [{
|
||||
'name': 'xmpp',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'xmpp',
|
||||
'label': 'XMPP',
|
||||
'description`': 'for Jabber, Hangouts (Google Talk), AIM...',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'description': 'User sending the message. For Hangouts, e-mail of a single-step authentication Google account.',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'Password',
|
||||
},
|
||||
{
|
||||
'name': 'hostname',
|
||||
'default': 'talk.google.com',
|
||||
},
|
||||
{
|
||||
'name': 'to',
|
||||
'description': 'Username (or e-mail for Hangouts) of the person to send the messages to.',
|
||||
},
|
||||
{
|
||||
'name': 'port',
|
||||
'type': 'int',
|
||||
'default': 5222,
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,43 +0,0 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from time import sleep
|
||||
import traceback
|
||||
import xmpp
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Xmpp(Notification):
|
||||
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
try:
|
||||
jid = xmpp.protocol.JID(self.conf('username'))
|
||||
client = xmpp.Client(jid.getDomain(), debug = [])
|
||||
|
||||
# Connect
|
||||
if not client.connect(server = (self.conf('hostname'), self.conf('port'))):
|
||||
log.error('XMPP failed: Connection to server failed.')
|
||||
return False
|
||||
|
||||
# Authenticate
|
||||
if not client.auth(jid.getNode(), self.conf('password'), resource = jid.getResource()):
|
||||
log.error('XMPP failed: Failed to authenticate.')
|
||||
return False
|
||||
|
||||
# Send message
|
||||
client.send(xmpp.protocol.Message(to = self.conf('to'), body = message, typ = 'chat'))
|
||||
|
||||
# Disconnect
|
||||
# some older servers will not send the message if you disconnect immediately after sending
|
||||
sleep(1)
|
||||
client.disconnect()
|
||||
|
||||
log.info('XMPP notifications sent.')
|
||||
return True
|
||||
|
||||
except:
|
||||
log.error('XMPP failed: %s', traceback.format_exc())
|
||||
|
||||
return False
|
||||
@@ -41,7 +41,7 @@ config = [{
|
||||
'label': 'Required Genres',
|
||||
'default': '',
|
||||
'placeholder': 'Example: Action, Crime & Drama',
|
||||
'description': ('Ignore movies that don\'t contain at least one set of genres.', 'Sets are separated by "," and each word within a set must be separated with "&"')
|
||||
'description': 'Ignore movies that don\'t contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"'
|
||||
},
|
||||
{
|
||||
'name': 'ignored_genres',
|
||||
|
||||
@@ -43,7 +43,7 @@ class Automation(Plugin):
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
movie_dict = fireEvent('media.get', movie_id, single = True)
|
||||
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||
fireEvent('movie.searcher.single', movie_dict)
|
||||
|
||||
return True
|
||||
return True
|
||||
@@ -1,15 +1,19 @@
|
||||
from StringIO import StringIO
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import ss, toSafeString, \
|
||||
toUnicode, sp
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
|
||||
toUnicode
|
||||
from couchpotato.core.helpers.variable import getExt, md5, isLocalIP
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
import requests
|
||||
from multipartpost import MultipartPostHandler
|
||||
from tornado import template
|
||||
from tornado.web import StaticFileHandler
|
||||
from urlparse import urlparse
|
||||
import cookielib
|
||||
import glob
|
||||
import gzip
|
||||
import inspect
|
||||
import math
|
||||
import os.path
|
||||
import re
|
||||
import time
|
||||
@@ -35,7 +39,6 @@ class Plugin(object):
|
||||
http_time_between_calls = 0
|
||||
http_failed_request = {}
|
||||
http_failed_disabled = {}
|
||||
http_opener = requests.Session()
|
||||
|
||||
def __new__(typ, *args, **kwargs):
|
||||
new_plugin = super(Plugin, typ).__new__(typ)
|
||||
@@ -103,9 +106,7 @@ class Plugin(object):
|
||||
f.close()
|
||||
os.chmod(path, Env.getPermission('file'))
|
||||
except Exception, e:
|
||||
log.error('Unable writing to file "%s": %s', (path, traceback.format_exc()))
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
log.error('Unable writing to file "%s": %s', (path, e))
|
||||
|
||||
def makeDir(self, path):
|
||||
path = ss(path)
|
||||
@@ -119,11 +120,11 @@ class Plugin(object):
|
||||
return False
|
||||
|
||||
# http request
|
||||
def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True, return_raw = False):
|
||||
url = urllib2.quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
|
||||
def urlopen(self, url, timeout = 30, params = None, headers = None, opener = None, multipart = False, show_error = True):
|
||||
url = ss(url)
|
||||
|
||||
if not headers: headers = {}
|
||||
if not data: data = {}
|
||||
if not params: params = {}
|
||||
|
||||
# Fill in some headers
|
||||
parsed_url = urlparse(url)
|
||||
@@ -136,8 +137,6 @@ class Plugin(object):
|
||||
headers['Connection'] = headers.get('Connection', 'keep-alive')
|
||||
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
|
||||
|
||||
r = self.http_opener
|
||||
|
||||
# Don't try for failed requests
|
||||
if self.http_failed_disabled.get(host, 0) > 0:
|
||||
if self.http_failed_disabled[host] > (time.time() - 900):
|
||||
@@ -153,18 +152,45 @@ class Plugin(object):
|
||||
self.wait(host)
|
||||
try:
|
||||
|
||||
kwargs = {
|
||||
'headers': headers,
|
||||
'data': data if len(data) > 0 else None,
|
||||
'timeout': timeout,
|
||||
'files': files,
|
||||
}
|
||||
method = 'post' if len(data) > 0 or files else 'get'
|
||||
# Make sure opener has the correct headers
|
||||
if opener:
|
||||
opener.add_headers = headers
|
||||
|
||||
log.info('Opening url: %s %s, data: %s', (method, url, [x for x in data.iterkeys()] if isinstance(data, dict) else 'with data'))
|
||||
response = r.request(method, url, verify = False, **kwargs)
|
||||
if multipart:
|
||||
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
request = urllib2.Request(url, params, headers)
|
||||
|
||||
data = response.content if return_raw else response.text
|
||||
if opener:
|
||||
opener.add_handler(MultipartPostHandler())
|
||||
else:
|
||||
cookies = cookielib.CookieJar()
|
||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
|
||||
|
||||
response = opener.open(request, timeout = timeout)
|
||||
else:
|
||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
|
||||
if isinstance(params, (str, unicode)) and len(params) > 0:
|
||||
data = params
|
||||
else:
|
||||
data = tryUrlencode(params) if len(params) > 0 else None
|
||||
|
||||
request = urllib2.Request(url, data, headers)
|
||||
|
||||
if opener:
|
||||
response = opener.open(request, timeout = timeout)
|
||||
else:
|
||||
response = urllib2.urlopen(request, timeout = timeout)
|
||||
|
||||
# unzip if needed
|
||||
if response.info().get('Content-Encoding') == 'gzip':
|
||||
buf = StringIO(response.read())
|
||||
f = gzip.GzipFile(fileobj = buf)
|
||||
data = f.read()
|
||||
f.close()
|
||||
else:
|
||||
data = response.read()
|
||||
response.close()
|
||||
|
||||
self.http_failed_request[host] = 0
|
||||
except IOError:
|
||||
@@ -192,19 +218,15 @@ class Plugin(object):
|
||||
return data
|
||||
|
||||
def wait(self, host = ''):
|
||||
if self.http_time_between_calls == 0:
|
||||
return
|
||||
|
||||
now = time.time()
|
||||
|
||||
last_use = self.http_last_use.get(host, 0)
|
||||
if last_use > 0:
|
||||
|
||||
wait = (last_use - now) + self.http_time_between_calls
|
||||
wait = math.ceil(last_use - now + self.http_time_between_calls)
|
||||
|
||||
if wait > 0:
|
||||
log.debug('Waiting for %s, %d seconds', (self.getName(), wait))
|
||||
time.sleep(wait)
|
||||
if wait > 0:
|
||||
log.debug('Waiting for %s, %d seconds', (self.getName(), wait))
|
||||
time.sleep(last_use - now + self.http_time_between_calls)
|
||||
|
||||
def beforeCall(self, handler):
|
||||
self.isRunning('%s.%s' % (self.getName(), handler.__name__))
|
||||
@@ -247,19 +269,18 @@ class Plugin(object):
|
||||
try:
|
||||
|
||||
cache_timeout = 300
|
||||
if kwargs.has_key('cache_timeout'):
|
||||
if kwargs.get('cache_timeout'):
|
||||
cache_timeout = kwargs.get('cache_timeout')
|
||||
del kwargs['cache_timeout']
|
||||
|
||||
data = self.urlopen(url, **kwargs)
|
||||
if data and cache_timeout > 0:
|
||||
if data:
|
||||
self.setCache(cache_key, data, timeout = cache_timeout)
|
||||
return data
|
||||
except:
|
||||
if not kwargs.get('show_error', True):
|
||||
raise
|
||||
|
||||
log.error('Failed getting cache: %s', (traceback.format_exc()))
|
||||
return ''
|
||||
|
||||
def setCache(self, cache_key, value, timeout = 300):
|
||||
@@ -268,19 +289,19 @@ class Plugin(object):
|
||||
Env.get('cache').set(cache_key_md5, value, timeout)
|
||||
return value
|
||||
|
||||
def createNzbName(self, data, media):
|
||||
tag = self.cpTag(media)
|
||||
return '%s%s' % (toSafeString(toUnicode(data.get('name'))[:127 - len(tag)]), tag)
|
||||
def createNzbName(self, data, movie):
|
||||
tag = self.cpTag(movie)
|
||||
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
|
||||
|
||||
def createFileName(self, data, filedata, media):
|
||||
name = sp(os.path.join(self.createNzbName(data, media)))
|
||||
def createFileName(self, data, filedata, movie):
|
||||
name = os.path.join(self.createNzbName(data, movie))
|
||||
if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
||||
return '%s.%s' % (name, 'rar')
|
||||
return '%s.%s' % (name, data.get('protocol'))
|
||||
|
||||
def cpTag(self, media):
|
||||
def cpTag(self, movie):
|
||||
if Env.setting('enabled', 'renamer'):
|
||||
return '.cp(' + media['library'].get('identifier') + ')' if media['library'].get('identifier') else ''
|
||||
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Media, Category
|
||||
from couchpotato.core.settings.model import Movie, Category
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -113,7 +113,7 @@ class CategoryPlugin(Plugin):
|
||||
def removeFromMovie(self, category_id):
|
||||
|
||||
db = get_session()
|
||||
movies = db.query(Media).filter(Media.category_id == category_id).all()
|
||||
movies = db.query(Movie).filter(Movie.category_id == category_id).all()
|
||||
|
||||
if len(movies) > 0:
|
||||
for movie in movies:
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import Custom
|
||||
|
||||
def start():
|
||||
return Custom()
|
||||
|
||||
config = []
|
||||
@@ -1,21 +0,0 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Custom(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('app.load', self.createStructure)
|
||||
|
||||
def createStructure(self):
|
||||
|
||||
custom_dir = os.path.join(Env.get('data_dir'), 'custom_plugins')
|
||||
|
||||
if not os.path.isdir(custom_dir):
|
||||
self.makeDir(custom_dir)
|
||||
self.createFile(os.path.join(custom_dir, '__init__.py'), '# Don\'t remove this file')
|
||||
@@ -4,10 +4,9 @@ from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Media, Library, LibraryTitle, \
|
||||
Release
|
||||
from couchpotato.core.settings.model import Movie, Library, LibraryTitle
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
from sqlalchemy.sql.expression import asc, or_
|
||||
from sqlalchemy.sql.expression import asc
|
||||
import random as rndm
|
||||
import time
|
||||
|
||||
@@ -49,14 +48,12 @@ class Dashboard(Plugin):
|
||||
limit = tryInt(splt[0])
|
||||
|
||||
# Get all active movies
|
||||
active_status, ignored_status = fireEvent('status.get', ['active', 'ignored'], single = True)
|
||||
q = db.query(Media) \
|
||||
active_status = fireEvent('status.get', ['active'], single = True)
|
||||
q = db.query(Movie) \
|
||||
.join(Library) \
|
||||
.outerjoin(Media.releases) \
|
||||
.filter(Media.status_id == active_status.get('id')) \
|
||||
.with_entities(Media.id, Media.profile_id, Library.info, Library.year) \
|
||||
.group_by(Media.id) \
|
||||
.filter(or_(Release.id == None, Release.status_id == ignored_status.get('id')))
|
||||
.filter(Movie.status_id == active_status.get('id')) \
|
||||
.with_entities(Movie.id, Movie.profile_id, Library.info, Library.year) \
|
||||
.group_by(Movie.id)
|
||||
|
||||
if not random:
|
||||
q = q.join(LibraryTitle) \
|
||||
@@ -101,11 +98,11 @@ class Dashboard(Plugin):
|
||||
if len(movie_ids) > 0:
|
||||
|
||||
# Get all movie information
|
||||
movies_raw = db.query(Media) \
|
||||
movies_raw = db.query(Movie) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('files')) \
|
||||
.filter(Media.id.in_(movie_ids)) \
|
||||
.filter(Movie.id.in_(movie_ids)) \
|
||||
.all()
|
||||
|
||||
# Create dict by movie id
|
||||
|
||||
@@ -93,7 +93,7 @@ class FileManager(Plugin):
|
||||
return dest
|
||||
|
||||
try:
|
||||
filedata = self.urlopen(url, return_raw = True, **urlopen_kwargs)
|
||||
filedata = self.urlopen(url, **urlopen_kwargs)
|
||||
except:
|
||||
log.error('Failed downloading file %s: %s', (url, traceback.format_exc()))
|
||||
return False
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.variable import splitString, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -79,7 +79,6 @@ class Manage(Plugin):
|
||||
try:
|
||||
|
||||
directories = self.directories()
|
||||
directories.sort()
|
||||
added_identifiers = []
|
||||
|
||||
# Add some progress
|
||||
@@ -112,20 +111,22 @@ class Manage(Plugin):
|
||||
if self.conf('cleanup') and full and not self.shuttingDown():
|
||||
|
||||
# Get movies with done status
|
||||
total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', single = True)
|
||||
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
|
||||
|
||||
for done_movie in done_movies:
|
||||
if done_movie['library']['identifier'] not in added_identifiers:
|
||||
fireEvent('media.delete', media_id = done_movie['id'], delete_from = 'all')
|
||||
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
|
||||
else:
|
||||
|
||||
releases = fireEvent('release.for_movie', id = done_movie.get('id'), single = True)
|
||||
|
||||
for release in releases:
|
||||
if len(release.get('files', [])) > 0:
|
||||
if len(release.get('files', [])) == 0:
|
||||
fireEvent('release.delete', release['id'])
|
||||
else:
|
||||
for release_file in release.get('files', []):
|
||||
# Remove release not available anymore
|
||||
if not os.path.isfile(sp(release_file['path'])):
|
||||
if not os.path.isfile(ss(release_file['path'])):
|
||||
fireEvent('release.clean', release['id'])
|
||||
break
|
||||
|
||||
@@ -200,7 +201,7 @@ class Manage(Plugin):
|
||||
|
||||
self.in_progress[folder]['to_go'] -= 1
|
||||
total = self.in_progress[folder]['total']
|
||||
movie_dict = fireEvent('media.get', identifier, single = True)
|
||||
movie_dict = fireEvent('movie.get', identifier, single = True)
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library']))
|
||||
|
||||
@@ -221,10 +222,9 @@ class Manage(Plugin):
|
||||
|
||||
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
|
||||
|
||||
if groups:
|
||||
for group in groups.itervalues():
|
||||
if group['library'] and group['library'].get('identifier'):
|
||||
fireEvent('release.add', group = group)
|
||||
for group in groups.itervalues():
|
||||
if group['library'] and group['library'].get('identifier'):
|
||||
fireEvent('release.add', group = group)
|
||||
|
||||
def getDiskSpace(self):
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Profile, ProfileType, Media
|
||||
from couchpotato.core.settings.model import Profile, ProfileType, Movie
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -38,7 +38,7 @@ class ProfilePlugin(Plugin):
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
|
||||
db = get_session()
|
||||
movies = db.query(Media).filter(Media.status_id == active_status.get('id'), Media.profile == None).all()
|
||||
movies = db.query(Movie).filter(Movie.status_id == active_status.get('id'), Movie.profile == None).all()
|
||||
|
||||
if len(movies) > 0:
|
||||
default_profile = self.default()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||
from couchpotato.core.helpers.variable import mergeDicts, getExt
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Quality, Profile, ProfileType
|
||||
@@ -17,40 +17,19 @@ 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': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']},
|
||||
{'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']},
|
||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
|
||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r')]},
|
||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':[], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': ['webrip', ('web', 'rip')]},
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':[]},
|
||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':[]},
|
||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':[]},
|
||||
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]},
|
||||
|
||||
# TODO come back to this later, think this could be handled better, this is starting to get out of hand....
|
||||
# BluRay
|
||||
{'identifier': 'bluray_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BluRay - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
|
||||
{'identifier': 'bluray_720p', 'hd': True, 'size': (800, 5000), 'label': 'BluRay - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
|
||||
# BDRip
|
||||
{'identifier': 'bdrip_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BDRip - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
|
||||
{'identifier': 'bdrip_720p', 'hd': True, 'size': (800, 5000), 'label': 'BDRip - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
|
||||
# BRRip
|
||||
{'identifier': 'brrip_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BRRip - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
|
||||
{'identifier': 'brrip_720p', 'hd': True, 'size': (800, 5000), 'label': 'BRRip - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
|
||||
# WEB-DL
|
||||
{'identifier': 'webdl_1080p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
|
||||
{'identifier': 'webdl_720p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
|
||||
{'identifier': 'webdl_480p', 'hd': True, 'size': (100, 5000), 'label': 'WEB-DL - 480p', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
|
||||
# HDTV
|
||||
{'identifier': 'hdtv_720p', 'hd': True, 'size': (800, 5000), 'label': 'HDTV - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
|
||||
{'identifier': 'hdtv_sd', 'hd': False, 'size': (100, 1000), 'label': 'HDTV - SD', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'mp4', 'avi']},
|
||||
{'identifier': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
||||
{'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
|
||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg'], 'tags': ['webrip', ('web', 'rip')]},
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
|
||||
]
|
||||
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
|
||||
|
||||
cached_qualities = None
|
||||
cached_order = None
|
||||
|
||||
def __init__(self):
|
||||
addEvent('quality.all', self.all)
|
||||
addEvent('quality.single', self.single)
|
||||
@@ -68,8 +47,6 @@ class QualityPlugin(Plugin):
|
||||
|
||||
addEvent('app.initialize', self.fill, priority = 10)
|
||||
|
||||
addEvent('app.test', self.doTest)
|
||||
|
||||
def preReleases(self):
|
||||
return self.pre_releases
|
||||
|
||||
@@ -82,9 +59,6 @@ class QualityPlugin(Plugin):
|
||||
|
||||
def all(self):
|
||||
|
||||
if self.cached_qualities:
|
||||
return self.cached_qualities
|
||||
|
||||
db = get_session()
|
||||
|
||||
qualities = db.query(Quality).all()
|
||||
@@ -94,7 +68,6 @@ class QualityPlugin(Plugin):
|
||||
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
|
||||
temp.append(q)
|
||||
|
||||
self.cached_qualities = temp
|
||||
return temp
|
||||
|
||||
def single(self, identifier = ''):
|
||||
@@ -123,8 +96,6 @@ class QualityPlugin(Plugin):
|
||||
setattr(quality, kwargs.get('value_type'), kwargs.get('value'))
|
||||
db.commit()
|
||||
|
||||
self.cached_qualities = None
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
||||
@@ -185,150 +156,73 @@ class QualityPlugin(Plugin):
|
||||
if not extra: extra = {}
|
||||
|
||||
# Create hash for cache
|
||||
cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files])
|
||||
cache_key = md5(str([f.replace('.' + getExt(f), '') for f in files]))
|
||||
cached = self.getCache(cache_key)
|
||||
if cached and len(extra) == 0:
|
||||
return cached
|
||||
if cached and len(extra) == 0: return cached
|
||||
|
||||
qualities = self.all()
|
||||
|
||||
# Start with 0
|
||||
score = {}
|
||||
for quality in qualities:
|
||||
score[quality.get('identifier')] = 0
|
||||
|
||||
for cur_file in files:
|
||||
words = re.split('\W+', cur_file.lower())
|
||||
|
||||
found = {}
|
||||
for quality in qualities:
|
||||
contains_score = self.containsTagScore(quality, words, cur_file)
|
||||
self.calcScore(score, quality, contains_score)
|
||||
contains = self.containsTag(quality, words, cur_file)
|
||||
if contains:
|
||||
found[quality['identifier']] = True
|
||||
|
||||
# Try again with loose testing
|
||||
for quality in qualities:
|
||||
loose_score = self.guessLooseScore(quality, files = files, extra = extra)
|
||||
self.calcScore(score, quality, loose_score)
|
||||
|
||||
|
||||
# Return nothing if all scores are 0
|
||||
has_non_zero = 0
|
||||
for s in score:
|
||||
if score[s] > 0:
|
||||
has_non_zero += 1
|
||||
|
||||
if not has_non_zero:
|
||||
return None
|
||||
|
||||
heighest_quality = max(score, key = score.get)
|
||||
if heighest_quality:
|
||||
for quality in qualities:
|
||||
if quality.get('identifier') == heighest_quality:
|
||||
|
||||
# Check identifier
|
||||
if quality['identifier'] in words:
|
||||
if len(found) == 0 or len(found) == 1 and found.get(quality['identifier']):
|
||||
log.debug('Found via identifier "%s" in %s', (quality['identifier'], cur_file))
|
||||
return self.setCache(cache_key, quality)
|
||||
|
||||
# Check alt and tags
|
||||
contains = self.containsTag(quality, words, cur_file)
|
||||
if contains:
|
||||
return self.setCache(cache_key, quality)
|
||||
|
||||
# Try again with loose testing
|
||||
quality = self.guessLoose(cache_key, files = files, extra = extra)
|
||||
if quality:
|
||||
return self.setCache(cache_key, quality)
|
||||
|
||||
log.debug('Could not identify quality for: %s', files)
|
||||
return None
|
||||
|
||||
def containsTagScore(self, quality, words, cur_file = ''):
|
||||
cur_file = ss(cur_file)
|
||||
score = 0
|
||||
|
||||
points = {
|
||||
'identifier': 10,
|
||||
'label': 10,
|
||||
'alternative': 9,
|
||||
'tags': 9,
|
||||
'ext': 3,
|
||||
}
|
||||
def containsTag(self, quality, words, cur_file = ''):
|
||||
|
||||
# Check alt and tags
|
||||
for tag_type in ['identifier', 'alternative', 'tags', 'label']:
|
||||
qualities = quality.get(tag_type, [])
|
||||
qualities = [qualities] if isinstance(qualities, (str, unicode)) else qualities
|
||||
|
||||
for alt in qualities:
|
||||
if (isinstance(alt, tuple)):
|
||||
if len(set(words) & set(alt)) == len(alt):
|
||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||
score += points.get(tag_type)
|
||||
|
||||
if (isinstance(alt, (str, unicode)) and ss(alt.lower()) in cur_file.lower()):
|
||||
for tag_type in ['alternative', 'tags']:
|
||||
for alt in quality.get(tag_type, []):
|
||||
if isinstance(alt, tuple) and '.'.join(alt) in '.'.join(words):
|
||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||
score += points.get(tag_type) / 2
|
||||
return True
|
||||
|
||||
if list(set(qualities) & set(words)):
|
||||
if list(set(quality.get(tag_type, [])) & set(words)):
|
||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||
score += points.get(tag_type)
|
||||
return True
|
||||
|
||||
# Check extention
|
||||
for ext in quality.get('ext', []):
|
||||
if ext == words[-1]:
|
||||
log.debug('Found %s extension in %s', (ext, cur_file))
|
||||
score += points['ext']
|
||||
return
|
||||
|
||||
return score
|
||||
|
||||
def guessLooseScore(self, quality, files = None, extra = None):
|
||||
|
||||
score = 0
|
||||
def guessLoose(self, cache_key, files = None, extra = None):
|
||||
|
||||
if extra:
|
||||
for quality in self.all():
|
||||
|
||||
# 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)))
|
||||
score += 5
|
||||
# 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(cache_key, 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)))
|
||||
score += 5
|
||||
|
||||
if quality.get('identifier') == 'dvdrip' and 480 <= extra.get('resolution_width', 0) <= 720:
|
||||
log.debug('Add point for correct dvdrip resolutions')
|
||||
score += 1
|
||||
|
||||
return score
|
||||
|
||||
def calcScore(self, score, quality, add_score):
|
||||
|
||||
score[quality['identifier']] += add_score
|
||||
|
||||
# Set order for allow calculation (and cache)
|
||||
if not self.cached_order:
|
||||
self.cached_order = {}
|
||||
for q in self.qualities:
|
||||
self.cached_order[q.get('identifier')] = self.qualities.index(q)
|
||||
|
||||
if add_score != 0:
|
||||
for allow in quality.get('allow', []):
|
||||
score[allow] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5
|
||||
|
||||
def doTest(self):
|
||||
|
||||
tests = {
|
||||
'Movie Name (1999)-DVD-Rip.avi': 'dvdrip',
|
||||
'Movie Name 1999 720p Bluray.mkv': '720p',
|
||||
'Movie Name 1999 BR-Rip 720p.avi': 'brrip',
|
||||
'Movie Name 1999 720p Web Rip.avi': 'scr',
|
||||
'Movie Name 1999 Web DL.avi': 'brrip',
|
||||
'Movie.Name.1999.1080p.WEBRip.H264-Group': 'scr',
|
||||
'Movie.Name.1999.DVDRip-Group': 'dvdrip',
|
||||
'Movie.Name.1999.DVD-Rip-Group': 'dvdrip',
|
||||
'Movie.Name.1999.DVD-R-Group': 'dvdr',
|
||||
'Movie.Name.Camelie.1999.720p.BluRay.x264-Group': '720p',
|
||||
}
|
||||
|
||||
correct = 0
|
||||
for name in tests:
|
||||
success = self.guess([name]).get('identifier') == tests[name]
|
||||
if not success:
|
||||
log.error('%s failed check, thinks it\'s %s', (name, self.guess([name]).get('identifier')))
|
||||
|
||||
correct += success
|
||||
|
||||
if correct == len(tests):
|
||||
log.info('Quality test successful')
|
||||
return True
|
||||
else:
|
||||
log.error('Quality test failed: %s out of %s succeeded', (correct, len(tests)))
|
||||
# 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(cache_key, quality)
|
||||
|
||||
if 480 <= extra.get('resolution_width', 0) <= 720:
|
||||
log.debug('Found as dvdrip')
|
||||
return self.setCache(cache_key, self.single('dvdrip'))
|
||||
|
||||
return None
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
from couchpotato import get_session, md5
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import ss, toUnicode
|
||||
from couchpotato.core.helpers.variable import getTitle
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.scanner.main import Scanner
|
||||
from couchpotato.core.settings.model import File, Release as Relea, Media, \
|
||||
ReleaseInfo
|
||||
from couchpotato.environment import Env
|
||||
from inspect import ismethod, isfunction
|
||||
from sqlalchemy.exc import InterfaceError
|
||||
from couchpotato.core.settings.model import File, Release as Relea, Movie
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
from sqlalchemy.sql.expression import and_, or_
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -25,7 +19,7 @@ class Release(Plugin):
|
||||
def __init__(self):
|
||||
addEvent('release.add', self.add)
|
||||
|
||||
addApiView('release.manual_download', self.manualDownload, docs = {
|
||||
addApiView('release.download', self.download, docs = {
|
||||
'desc': 'Send a release manually to the downloaders',
|
||||
'params': {
|
||||
'id': {'type': 'id', 'desc': 'ID of the release object in release-table'}
|
||||
@@ -50,45 +44,9 @@ class Release(Plugin):
|
||||
}
|
||||
})
|
||||
|
||||
addEvent('release.download', self.download)
|
||||
addEvent('release.try_download_result', self.tryDownloadResult)
|
||||
addEvent('release.create_from_search', self.createFromSearch)
|
||||
addEvent('release.for_movie', self.forMovie)
|
||||
addEvent('release.delete', self.delete)
|
||||
addEvent('release.clean', self.clean)
|
||||
addEvent('release.update_status', self.updateStatus)
|
||||
|
||||
# Clean releases that didn't have activity in the last week
|
||||
addEvent('app.load', self.cleanDone)
|
||||
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanDone, hours = 4)
|
||||
|
||||
def cleanDone(self):
|
||||
|
||||
log.debug('Removing releases from dashboard')
|
||||
|
||||
now = time.time()
|
||||
week = 262080
|
||||
|
||||
done_status, available_status, snatched_status, downloaded_status, ignored_status = \
|
||||
fireEvent('status.get', ['done', 'available', 'snatched', 'downloaded', 'ignored'], single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
# get movies last_edit more than a week ago
|
||||
media = db.query(Media) \
|
||||
.filter(Media.status_id == done_status.get('id'), Media.last_edit < (now - week)) \
|
||||
.all()
|
||||
|
||||
for item in media:
|
||||
for rel in item.releases:
|
||||
# Remove all available releases
|
||||
if rel.status_id in [available_status.get('id')]:
|
||||
fireEvent('release.delete', id = rel.id, single = True)
|
||||
# Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the move
|
||||
elif rel.status_id in [snatched_status.get('id'), downloaded_status.get('id')]:
|
||||
self.updateStatus(id = rel.id, status = ignored_status)
|
||||
|
||||
db.expire_all()
|
||||
|
||||
def add(self, group):
|
||||
|
||||
@@ -100,14 +58,14 @@ class Release(Plugin):
|
||||
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
|
||||
|
||||
# Add movie
|
||||
media = db.query(Media).filter_by(library_id = group['library'].get('id')).first()
|
||||
if not media:
|
||||
media = Media(
|
||||
movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first()
|
||||
if not movie:
|
||||
movie = Movie(
|
||||
library_id = group['library'].get('id'),
|
||||
profile_id = 0,
|
||||
status_id = done_status.get('id')
|
||||
)
|
||||
db.add(media)
|
||||
db.add(movie)
|
||||
db.commit()
|
||||
|
||||
# Add Release
|
||||
@@ -120,7 +78,7 @@ class Release(Plugin):
|
||||
if not rel:
|
||||
rel = Relea(
|
||||
identifier = identifier,
|
||||
media = media,
|
||||
movie = movie,
|
||||
quality_id = group['meta_data']['quality'].get('id'),
|
||||
status_id = done_status.get('id')
|
||||
)
|
||||
@@ -142,10 +100,11 @@ class Release(Plugin):
|
||||
except:
|
||||
log.debug('Failed to attach "%s" to release: %s', (added_files, traceback.format_exc()))
|
||||
|
||||
fireEvent('media.restatus', media.id)
|
||||
fireEvent('movie.restatus', movie.id)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def saveFile(self, filepath, type = 'unknown', include_media_info = False):
|
||||
|
||||
properties = {}
|
||||
@@ -200,218 +159,64 @@ class Release(Plugin):
|
||||
rel = db.query(Relea).filter_by(id = id).first()
|
||||
if rel:
|
||||
ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True)
|
||||
self.updateStatus(id, available_status if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status)
|
||||
rel.status_id = available_status.get('id') if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
||||
|
||||
def manualDownload(self, id = None, **kwargs):
|
||||
def download(self, id = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
|
||||
|
||||
rel = db.query(Relea).filter_by(id = id).first()
|
||||
if not rel:
|
||||
log.error('Couldn\'t find release with id: %s', id)
|
||||
return {
|
||||
'success': False
|
||||
}
|
||||
if rel:
|
||||
item = {}
|
||||
for info in rel.info:
|
||||
item[info.identifier] = info.value
|
||||
|
||||
item = {}
|
||||
for info in rel.info:
|
||||
item[info.identifier] = info.value
|
||||
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Snatching "%s"' % item['name'])
|
||||
|
||||
fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Snatching "%s"' % item['name'])
|
||||
# Get matching provider
|
||||
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
|
||||
|
||||
# Get matching provider
|
||||
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
|
||||
if not item.get('protocol'):
|
||||
item['protocol'] = item['type']
|
||||
item['type'] = 'movie'
|
||||
|
||||
# Backwards compatibility code
|
||||
if not item.get('protocol'):
|
||||
item['protocol'] = item['type']
|
||||
item['type'] = 'movie'
|
||||
if item.get('protocol') != 'torrent_magnet':
|
||||
item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
|
||||
|
||||
success = self.download(data = item, media = rel.media.to_dict({
|
||||
success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
|
||||
'profile': {'types': {'quality': {}}},
|
||||
'releases': {'status': {}, 'quality': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {}
|
||||
}), manual = True)
|
||||
if item.get('protocol') != 'torrent_magnet':
|
||||
item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
|
||||
}), manual = True, single = True)
|
||||
|
||||
success = self.download(data = item, media = rel.movie.to_dict({
|
||||
'profile': {'types': {'quality': {}}},
|
||||
'releases': {'status': {}, 'quality': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {}
|
||||
}), manual = True)
|
||||
if success:
|
||||
db.expunge_all()
|
||||
rel = db.query(Relea).filter_by(id = id).first() # Get release again
|
||||
|
||||
if success == True:
|
||||
db.expunge_all()
|
||||
rel = db.query(Relea).filter_by(id = id).first() # Get release again @RuudBurger why do we need to get it again??
|
||||
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 {
|
||||
'success': success
|
||||
}
|
||||
else:
|
||||
log.error('Couldn\'t find release with id: %s', id)
|
||||
|
||||
fireEvent('notify.frontend', type = 'release.manual_download', data = True, message = 'Successfully snatched "%s"' % item['name'])
|
||||
return {
|
||||
'success': success == True
|
||||
'success': False
|
||||
}
|
||||
|
||||
def download(self, data, media, manual = False):
|
||||
|
||||
# Backwards compatibility code
|
||||
if not data.get('protocol'):
|
||||
data['protocol'] = data['type']
|
||||
data['type'] = 'movie'
|
||||
|
||||
# Test to see if any downloaders are enabled for this type
|
||||
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
|
||||
if not downloader_enabled:
|
||||
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', data.get('protocol'))
|
||||
return False
|
||||
|
||||
# Download NZB or torrent file
|
||||
filedata = None
|
||||
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
|
||||
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||
log.info('Tried to download, but the "%s" provider gave an error', data.get('protocol'))
|
||||
if filedata == 'try_next':
|
||||
return filedata
|
||||
|
||||
# Send NZB or torrent file to downloader
|
||||
download_result = fireEvent('download', data = data, media = media, manual = manual, filedata = filedata, single = True)
|
||||
if not download_result:
|
||||
log.info('Tried to download, but the "%s" downloader gave an error', data.get('protocol'))
|
||||
return False
|
||||
log.debug('Downloader result: %s', download_result)
|
||||
|
||||
snatched_status, done_status, downloaded_status, active_status = fireEvent('status.get', ['snatched', 'done', 'downloaded', 'active'], single = True)
|
||||
|
||||
try:
|
||||
db = get_session()
|
||||
rls = db.query(Relea).filter_by(identifier = md5(data['url'])).first()
|
||||
if not rls:
|
||||
log.error('No release found to store download information in')
|
||||
return False
|
||||
|
||||
renamer_enabled = Env.setting('enabled', 'renamer')
|
||||
|
||||
# Save download-id info if returned
|
||||
if isinstance(download_result, dict):
|
||||
for key in download_result:
|
||||
rls_info = ReleaseInfo(
|
||||
identifier = 'download_%s' % key,
|
||||
value = toUnicode(download_result.get(key))
|
||||
)
|
||||
rls.info.append(rls_info)
|
||||
db.commit()
|
||||
|
||||
log_movie = '%s (%s) in %s' % (getTitle(media['library']), media['library']['year'], rls.quality.label)
|
||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||
log.info(snatch_message)
|
||||
fireEvent('%s.snatched' % data['type'], message = snatch_message, data = rls.to_dict())
|
||||
|
||||
# Mark release as snatched
|
||||
if renamer_enabled:
|
||||
self.updateStatus(rls.id, status = snatched_status)
|
||||
|
||||
# If renamer isn't used, mark media done if finished or release downloaded
|
||||
else:
|
||||
if media['status_id'] == active_status.get('id'):
|
||||
finished = next((True for profile_type in media['profile']['types'] if \
|
||||
profile_type['quality_id'] == rls.quality.id and profile_type['finish']), False)
|
||||
if finished:
|
||||
log.info('Renamer disabled, marking media as finished: %s', log_movie)
|
||||
|
||||
# Mark release done
|
||||
self.updateStatus(rls.id, status = done_status)
|
||||
|
||||
# Mark media done
|
||||
mdia = db.query(Media).filter_by(id = media['id']).first()
|
||||
mdia.status_id = done_status.get('id')
|
||||
mdia.last_edit = int(time.time())
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
|
||||
# Assume release downloaded
|
||||
self.updateStatus(rls.id, status = downloaded_status)
|
||||
|
||||
except:
|
||||
log.error('Failed storing download status: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def tryDownloadResult(self, results, media, quality_type, manual = False):
|
||||
ignored_status, failed_status = fireEvent('status.get', ['ignored', 'failed'], single = True)
|
||||
|
||||
for rel in results:
|
||||
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and rel.get('age') <= quality_type.get('wait_for', 0):
|
||||
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), rel['name']))
|
||||
continue
|
||||
|
||||
if rel['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
|
||||
log.info('Ignored: %s', rel['name'])
|
||||
continue
|
||||
|
||||
if rel['score'] <= 0:
|
||||
log.info('Ignored, score to low: %s', rel['name'])
|
||||
continue
|
||||
|
||||
downloaded = fireEvent('release.download', data = rel, media = media, manual = manual, single = True)
|
||||
if downloaded is True:
|
||||
return True
|
||||
elif downloaded != 'try_next':
|
||||
break
|
||||
|
||||
return False
|
||||
|
||||
def createFromSearch(self, search_results, media, quality_type):
|
||||
|
||||
available_status = fireEvent('status.get', ['available'], single = True)
|
||||
db = get_session()
|
||||
|
||||
found_releases = []
|
||||
|
||||
for rel in search_results:
|
||||
|
||||
rel_identifier = md5(rel['url'])
|
||||
found_releases.append(rel_identifier)
|
||||
|
||||
rls = db.query(Relea).filter_by(identifier = rel_identifier).first()
|
||||
if not rls:
|
||||
rls = Relea(
|
||||
identifier = rel_identifier,
|
||||
media_id = media.get('id'),
|
||||
quality_id = quality_type.get('quality_id'),
|
||||
status_id = available_status.get('id')
|
||||
)
|
||||
db.add(rls)
|
||||
else:
|
||||
[db.delete(old_info) for old_info in rls.info]
|
||||
rls.last_edit = int(time.time())
|
||||
|
||||
db.commit()
|
||||
|
||||
for info in rel:
|
||||
try:
|
||||
if not isinstance(rel[info], (str, unicode, int, long, float)):
|
||||
continue
|
||||
|
||||
rls_info = ReleaseInfo(
|
||||
identifier = info,
|
||||
value = toUnicode(rel[info])
|
||||
)
|
||||
rls.info.append(rls_info)
|
||||
except InterfaceError:
|
||||
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
||||
|
||||
db.commit()
|
||||
|
||||
rel['status_id'] = rls.status_id
|
||||
|
||||
return found_releases
|
||||
|
||||
def forMovie(self, id = None):
|
||||
|
||||
db = get_session()
|
||||
@@ -419,7 +224,7 @@ class Release(Plugin):
|
||||
releases_raw = db.query(Relea) \
|
||||
.options(joinedload_all('info')) \
|
||||
.options(joinedload_all('files')) \
|
||||
.filter(Relea.media_id == id) \
|
||||
.filter(Relea.movie_id == id) \
|
||||
.all()
|
||||
|
||||
releases = [r.to_dict({'info':{}, 'files':{}}) for r in releases_raw]
|
||||
@@ -436,32 +241,3 @@ class Release(Plugin):
|
||||
'success': True
|
||||
}
|
||||
|
||||
def updateStatus(self, id, status = None):
|
||||
if not status: return False
|
||||
|
||||
db = get_session()
|
||||
|
||||
rel = db.query(Relea).filter_by(id = id).first()
|
||||
if rel and status and rel.status_id != status.get('id'):
|
||||
|
||||
item = {}
|
||||
for info in rel.info:
|
||||
item[info.identifier] = info.value
|
||||
|
||||
if rel.files:
|
||||
for file_item in rel.files:
|
||||
if file_item.type.identifier == 'movie':
|
||||
release_name = os.path.basename(file_item.path)
|
||||
break
|
||||
else:
|
||||
release_name = item['name']
|
||||
#update status in Db
|
||||
log.debug('Marking release %s as %s', (release_name, status.get("label")))
|
||||
rel.status_id = status.get('id')
|
||||
rel.last_edit = int(time.time())
|
||||
db.commit()
|
||||
|
||||
#Update all movie info as there is no release update function
|
||||
fireEvent('notify.frontend', type = 'release.update_status', data = rel.to_dict())
|
||||
|
||||
return True
|
||||
|
||||
@@ -28,7 +28,6 @@ rename_options = {
|
||||
'cd': 'CD number (cd1)',
|
||||
'cd_nr': 'Just the cd nr. (1)',
|
||||
'mpaa': 'MPAA Rating',
|
||||
'category': 'Category label',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -93,7 +92,7 @@ config = [{
|
||||
'default': 1,
|
||||
'type': 'int',
|
||||
'unit': 'min(s)',
|
||||
'description': ('Detect movie status every X minutes.', 'Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled'),
|
||||
'description': 'Detect movie status every X minutes. Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled',
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
@@ -122,13 +121,13 @@ config = [{
|
||||
'advanced': True,
|
||||
'name': 'separator',
|
||||
'label': 'File-Separator',
|
||||
'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'),
|
||||
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
'name': 'foldersep',
|
||||
'label': 'Folder-Separator',
|
||||
'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'),
|
||||
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
|
||||
},
|
||||
{
|
||||
'name': 'file_action',
|
||||
@@ -136,7 +135,7 @@ config = [{
|
||||
'default': 'link',
|
||||
'type': 'dropdown',
|
||||
'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
|
||||
'description': ('<strong>Link</strong>, <strong>Copy</strong> or <strong>Move</strong> after download completed.', 'Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy. It is perfered to use link when downloading torrents as it will save you space, while still beeing able to seed.'),
|
||||
'description': '<strong>Link</strong> or <strong>Copy</strong> after downloading completed (and allow for seeding), or <strong>Move</strong> after seeding completed. Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy.',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user