Compare commits
235 Commits
build/2.5.
...
tv_old
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
796aff4514 | ||
|
|
2a2fe448e7 | ||
|
|
23967d11dd | ||
|
|
7a3251f649 | ||
|
|
9ba8910281 | ||
|
|
e83a3cf263 | ||
|
|
b3c2945d9b | ||
|
|
fc3cf08675 | ||
|
|
8162cd31b7 | ||
|
|
1ea6fdc9a7 | ||
|
|
1b0c9f40cc | ||
|
|
c0111a467b | ||
|
|
64175151f8 | ||
|
|
586957e840 | ||
|
|
f2fc775963 | ||
|
|
b8bce948c8 | ||
|
|
0a996857dd | ||
|
|
26509f614c | ||
|
|
3e28d5a936 | ||
|
|
95ff427873 | ||
|
|
8ed10037df | ||
|
|
7a090dd4a2 | ||
|
|
49f34cb48d | ||
|
|
2a76de50dd | ||
|
|
8adf7fc600 | ||
|
|
f4c053f56f | ||
|
|
5cb5a1677d | ||
|
|
9fb9f0ef5b | ||
|
|
242d69a981 | ||
|
|
eb151a4c5d | ||
|
|
2520b19798 | ||
|
|
319c9e979a | ||
|
|
93aa5b1920 | ||
|
|
f648af66a6 | ||
|
|
7c4185e1fa | ||
|
|
0fb06a3fd3 | ||
|
|
1e39d643a8 | ||
|
|
69d58663ef | ||
|
|
e59b53fab2 | ||
|
|
a66f6f0166 | ||
|
|
1344f03b16 | ||
|
|
a23c409939 | ||
|
|
a6b1cc833f | ||
|
|
d2c7e3ef56 | ||
|
|
6c87008d7b | ||
|
|
6b3af21e45 | ||
|
|
5a5cc0005c | ||
|
|
d65117c0e3 | ||
|
|
d8884bb655 | ||
|
|
afe9aed2eb | ||
|
|
01e64e989e | ||
|
|
9496df9e9d | ||
|
|
8b4c67b977 | ||
|
|
f77a8f5573 | ||
|
|
de8aefebb7 | ||
|
|
8f0d22a6f2 | ||
|
|
721190028b | ||
|
|
50e565142e | ||
|
|
bead3e2b07 | ||
|
|
71aa0cbb9a | ||
|
|
8de19cbd52 | ||
|
|
8573832ff7 | ||
|
|
7c1d3f8762 | ||
|
|
9cd1adcdee | ||
|
|
f017ac9dca | ||
|
|
907704e45f | ||
|
|
b17f937389 | ||
|
|
f591c56dd4 | ||
|
|
2fd54901e7 | ||
|
|
1bf6c5a82e | ||
|
|
45484461b5 | ||
|
|
aa394f59ae | ||
|
|
717111f5d2 | ||
|
|
e3461dc35f | ||
|
|
9b834f62a9 | ||
|
|
935938474c | ||
|
|
6573196186 | ||
|
|
9a07f2ed65 | ||
|
|
613ff3b729 | ||
|
|
def62fc865 | ||
|
|
037c355836 | ||
|
|
180b2bbffe | ||
|
|
143dcad4f3 | ||
|
|
b0e352ab6d | ||
|
|
5ea7dc5920 | ||
|
|
3c675b5b8a | ||
|
|
11ea9b4e91 | ||
|
|
e8a2139ecf | ||
|
|
dc57d7b6d1 | ||
|
|
0925f1312d | ||
|
|
efc02f66f5 | ||
|
|
9ce8ffc14b | ||
|
|
bab07a05e7 | ||
|
|
1df9f7c83f | ||
|
|
efdf77ef6c | ||
|
|
a989c93505 | ||
|
|
d122bd1b43 | ||
|
|
ab81824f4c | ||
|
|
4eb73e3609 | ||
|
|
6bcb279f0e | ||
|
|
f446c8ed33 | ||
|
|
10a34f2b69 | ||
|
|
cc3ebd79e8 | ||
|
|
3e035f84b1 | ||
|
|
611c159373 | ||
|
|
db65980ba4 | ||
|
|
180576f2b7 | ||
|
|
46d4d34da7 | ||
|
|
3fa21560be | ||
|
|
b902186389 | ||
|
|
da87e68fad | ||
|
|
f23412ea7e | ||
|
|
07abf7c83d | ||
|
|
6259684487 | ||
|
|
0a0935d635 | ||
|
|
fb5b17005f | ||
|
|
e3745b5d74 | ||
|
|
8d24d96804 | ||
|
|
529b535d9f | ||
|
|
0793668e5c | ||
|
|
8d368ecf29 | ||
|
|
2d2b0c9048 | ||
|
|
fb0719d677 | ||
|
|
7ffa5dc7b6 | ||
|
|
32c289fd3d | ||
|
|
ff63b8a1c5 | ||
|
|
60d8934444 | ||
|
|
e0aba01866 | ||
|
|
1ae498e3c8 | ||
|
|
d1db099f71 | ||
|
|
f4ef64290d | ||
|
|
026151d1a1 | ||
|
|
70dada8ef6 | ||
|
|
9ef752f8a3 | ||
|
|
d265a5bddd | ||
|
|
b2b6e3eb33 | ||
|
|
2b6c7a8f94 | ||
|
|
6070209d33 | ||
|
|
fa78d18890 | ||
|
|
40eaf2a96b | ||
|
|
73dd0916c0 | ||
|
|
77d32fe16b | ||
|
|
7def0944a6 | ||
|
|
8782cd77d5 | ||
|
|
1b59fd9af0 | ||
|
|
9dca8a03be | ||
|
|
132f4882e5 | ||
|
|
9e32a38288 | ||
|
|
c1b13cd076 | ||
|
|
820588aa5f | ||
|
|
8fbf050510 | ||
|
|
dd5ae3c4ee | ||
|
|
ab51707607 | ||
|
|
8acdc56df1 | ||
|
|
d345a05b3c | ||
|
|
5f427ec6ea | ||
|
|
a95c030885 | ||
|
|
bef6a74dfe | ||
|
|
01da470c21 | ||
|
|
5fdf4d9085 | ||
|
|
bc51e263e1 | ||
|
|
4c527f0931 | ||
|
|
e9fc528a0f | ||
|
|
c9ba3c804e | ||
|
|
ee9fe347c7 | ||
|
|
515aafe112 | ||
|
|
314016e1fa | ||
|
|
906a54ef09 | ||
|
|
ec2facd056 | ||
|
|
ddbfef575f | ||
|
|
8dd7a4771c | ||
|
|
49ba1f1acd | ||
|
|
c4d661535c | ||
|
|
bd52ab7ab1 | ||
|
|
cce0a8ec62 | ||
|
|
d02e62f89f | ||
|
|
e180addc3c | ||
|
|
a37a4a8cd4 | ||
|
|
8328c18728 | ||
|
|
7ae07d6c15 | ||
|
|
770bcf5bc6 | ||
|
|
7bd6a295d8 | ||
|
|
4063761313 | ||
|
|
d62b346a74 | ||
|
|
155732ab1a | ||
|
|
b3713b7ae5 | ||
|
|
19d026756c | ||
|
|
3cddd29425 | ||
|
|
23bde0b866 | ||
|
|
6f895c1805 | ||
|
|
96089074ce | ||
|
|
2ed53df008 | ||
|
|
060859483a | ||
|
|
eced476eaf | ||
|
|
8d5b55a753 | ||
|
|
7296dc54d0 | ||
|
|
e5e9cf7d5f | ||
|
|
b106229a78 | ||
|
|
73efd5549f | ||
|
|
8139016636 | ||
|
|
59c0d0416e | ||
|
|
cd559ece04 | ||
|
|
120a4ad1ed | ||
|
|
3363e164fd | ||
|
|
6d6d5caeb6 | ||
|
|
21030e7cb4 | ||
|
|
9b238ba712 | ||
|
|
b3d2d5349b | ||
|
|
f9bad281de | ||
|
|
72ce919989 | ||
|
|
ff782669f6 | ||
|
|
36950993f1 | ||
|
|
7df93dc1b4 | ||
|
|
a45913eee7 | ||
|
|
a25eac6c4e | ||
|
|
dd0fcf0bc1 | ||
|
|
2267235eca | ||
|
|
029cf9ecac | ||
|
|
f4217ecd3d | ||
|
|
31cd993506 | ||
|
|
fb579561de | ||
|
|
37eb424827 | ||
|
|
4348451692 | ||
|
|
e93e55a0f7 | ||
|
|
bc11f90529 | ||
|
|
8fcc246f25 | ||
|
|
4d7c38d6db | ||
|
|
c8d79cde21 | ||
|
|
f4d792079b | ||
|
|
78ab419cd8 | ||
|
|
3e93983f6e | ||
|
|
6a4822cc26 | ||
|
|
92b08bb5d5 | ||
|
|
e270e09969 | ||
|
|
40cd5218db |
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import print_function
|
||||
from logging import handlers
|
||||
from os.path import dirname
|
||||
import logging
|
||||
@@ -19,12 +18,7 @@ base_path = dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.join(base_path, 'libs'))
|
||||
|
||||
from couchpotato.environment import Env
|
||||
from couchpotato.core.helpers.variable import getDataDir, removePyc
|
||||
|
||||
|
||||
# Remove pyc files before dynamic load (sees .pyc files regular .py modules)
|
||||
removePyc(base_path)
|
||||
|
||||
from couchpotato.core.helpers.variable import getDataDir
|
||||
|
||||
class Loader(object):
|
||||
|
||||
@@ -34,7 +28,7 @@ class Loader(object):
|
||||
|
||||
# Get options via arg
|
||||
from couchpotato.runner import getOptions
|
||||
self.options = getOptions(sys.argv[1:])
|
||||
self.options = getOptions(base_path, sys.argv[1:])
|
||||
|
||||
# Load settings
|
||||
settings = Env.get('settings')
|
||||
@@ -55,7 +49,7 @@ class Loader(object):
|
||||
# Create logging dir
|
||||
self.log_dir = os.path.join(self.data_dir, 'logs');
|
||||
if not os.path.isdir(self.log_dir):
|
||||
os.makedirs(self.log_dir)
|
||||
os.mkdir(self.log_dir)
|
||||
|
||||
# Logging
|
||||
from couchpotato.core.logger import CPLog
|
||||
@@ -72,11 +66,10 @@ class Loader(object):
|
||||
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
||||
|
||||
from couchpotato.core.event import addEvent
|
||||
addEvent('app.do_shutdown', self.setRestart)
|
||||
addEvent('app.after_shutdown', self.afterShutdown)
|
||||
|
||||
def setRestart(self, restart):
|
||||
def afterShutdown(self, restart):
|
||||
self.do_restart = restart
|
||||
return True
|
||||
|
||||
def onExit(self, signal, frame):
|
||||
from couchpotato.core.event import fireEvent
|
||||
@@ -104,6 +97,7 @@ class Loader(object):
|
||||
|
||||
# Release log files and shutdown logger
|
||||
logging.shutdown()
|
||||
time.sleep(3)
|
||||
|
||||
args = [sys.executable] + [os.path.join(base_path, os.path.basename(__file__))] + sys.argv[1:]
|
||||
subprocess.Popen(args)
|
||||
@@ -138,15 +132,14 @@ if __name__ == '__main__':
|
||||
pass
|
||||
except SystemExit:
|
||||
raise
|
||||
except socket.error as e:
|
||||
except socket.error as (nr, msg):
|
||||
# log when socket receives SIGINT, but continue.
|
||||
# previous code would have skipped over other types of IO errors too.
|
||||
nr, msg = e
|
||||
if nr != 4:
|
||||
try:
|
||||
l.log.critical(traceback.format_exc())
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
print traceback.format_exc()
|
||||
raise
|
||||
except:
|
||||
try:
|
||||
@@ -155,7 +148,7 @@ if __name__ == '__main__':
|
||||
if l:
|
||||
l.log.critical(traceback.format_exc())
|
||||
else:
|
||||
print(traceback.format_exc())
|
||||
print traceback.format_exc()
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
print traceback.format_exc()
|
||||
raise
|
||||
|
||||
232
Desktop.py
232
Desktop.py
@@ -1,232 +0,0 @@
|
||||
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(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()
|
||||
self._desktop.ExitMainLoop()
|
||||
|
||||
|
||||
class CouchPotatoApp(wx.App, SoftwareUpdate):
|
||||
|
||||
settings = {}
|
||||
events = {}
|
||||
restart = False
|
||||
closing = False
|
||||
|
||||
def OnInit(self):
|
||||
|
||||
# Updater
|
||||
base_url = 'https://api.couchpota.to/updates/%s'
|
||||
self.InitUpdates(base_url % VERSION + '/', 'https://couchpota.to/updates/%s' % '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:])
|
||||
27
README.md
27
README.md
@@ -1,4 +1,4 @@
|
||||
CouchPotato
|
||||
CouchPotato Server
|
||||
=====
|
||||
|
||||
CouchPotato (CP) is an automatic NZB and torrent downloader. You can keep a "movies I want"-list and it will search for NZBs/torrents of these movies every X hours.
|
||||
@@ -7,7 +7,7 @@ Once a movie is found, it will send it to SABnzbd or download the torrent to a s
|
||||
|
||||
## Running from Source
|
||||
|
||||
CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed.
|
||||
CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed also.
|
||||
|
||||
Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for more details:
|
||||
|
||||
@@ -17,9 +17,9 @@ Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for
|
||||
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`.
|
||||
* You can now start CP via `CouchPotatoServer\CouchPotato.py` to start
|
||||
* Your browser should open up, but if it doesn't go to `http://localhost:5050/`
|
||||
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
|
||||
|
||||
OS X:
|
||||
OSx:
|
||||
|
||||
* If you're on Leopard (10.5) install Python 2.6+: [Python 2.6.5](http://www.python.org/download/releases/2.6.5/)
|
||||
* Install [GIT](http://git-scm.com/)
|
||||
@@ -27,21 +27,20 @@ OS X:
|
||||
* Go to your App folder `cd /Applications`
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||
* Then do `python CouchPotatoServer/CouchPotato.py`
|
||||
* Your browser should open up, but if it doesn't go to `http://localhost:5050/`
|
||||
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
|
||||
|
||||
Linux (Ubuntu / Debian):
|
||||
Linux (ubuntu / debian):
|
||||
|
||||
* Install [GIT](http://git-scm.com/) with `apt-get install git-core`
|
||||
* 'cd' to the folder of your choosing.
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||
* Then do `python CouchPotatoServer/CouchPotato.py` to start
|
||||
* To run on boot copy the init script `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
|
||||
* Copy the default paths file `sudo cp CouchPotatoServer/init/ubuntu.default /etc/default/couchpotato`
|
||||
* Change the paths inside the default file `sudo nano /etc/default/couchpotato`
|
||||
* Make it executable `sudo chmod +x /etc/init.d/couchpotato`
|
||||
* Add it to defaults `sudo update-rc.d couchpotato defaults`
|
||||
* Open your browser and go to `http://localhost:5050/`
|
||||
|
||||
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
|
||||
* Change the paths inside the init script. `sudo nano /etc/init.d/couchpotato`
|
||||
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
||||
* Open your browser and go to: `http://localhost:5050/`
|
||||
|
||||
|
||||
FreeBSD :
|
||||
|
||||
@@ -57,7 +56,7 @@ FreeBSD :
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time
|
||||
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato`
|
||||
* Change the paths inside the init script. `sudo vim /etc/rc.d/couchpotato`
|
||||
* Change the paths inside the init script. `sudo vim /etc/init.d/couchpotato`
|
||||
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato`
|
||||
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
|
||||
* Open your browser and go to: `http://server:5050/`
|
||||
|
||||
@@ -1,36 +1,25 @@
|
||||
# Contributing to CouchPotatoServer
|
||||
## Got a issue/feature request or submitting a pull request?
|
||||
|
||||
1. [Contributing](#contributing)
|
||||
2. [Submitting an Issue](#issues)
|
||||
3. [Submitting a Pull Request](#pull-requests)
|
||||
Make sure you think of the following things:
|
||||
|
||||
## Contributing
|
||||
Thank you for your interest in contributing to CouchPotato. There are several ways to help out, even if you've never worked on an open source project before.
|
||||
If you've found a bug or want to request a feature, you can report it by [posting an issue](https://github.com/RuudBurger/CouchPotatoServer/issues/new) - be sure to read the [guidelines](#issues) first!
|
||||
If you want to contribute your own work, please read the [guidelines](#pull-requests) for submitting a pull request.
|
||||
Lastly, for anything related to CouchPotato, feel free to stop by the [forum](http://couchpota.to/forum/) or the [#couchpotato](http://webchat.freenode.net/?channels=couchpotato) IRC channel at irc.freenode.net.
|
||||
|
||||
## Issues
|
||||
Issues are intended for reporting bugs and weird behaviour or suggesting improvements to CouchPotatoServer.
|
||||
Before you submit an issue, please go through the following checklist:
|
||||
* Search through existing issues (*including closed issues!*) first: you might be able to get your answer there.
|
||||
* Double check your issue manually, because it could be an external issue.
|
||||
* Post logs with your issue: Without seeing what is going on, the developers can't reproduce the error.
|
||||
* Check the logs yourself before submitting them. Obvious errors like permission or HTTP errors are often not related to CouchPotato.
|
||||
* What movie and quality are you searching for?
|
||||
* What are your settings for the specific problem?
|
||||
* What providers are you using? (While your logs include these, scanning through hundreds of lines of logs isn't our hobby)
|
||||
* Post the logs from the *config* directory, please do not copy paste the UI. Use pastebin to store these logs!
|
||||
## Issue
|
||||
* 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 hardware / OS are you using and what are its limitations? For example: NAS can be slow and maybe have a different version of python installed than when you use CP on OS X or Windows.
|
||||
* Your issue might be marked with the "can't reproduce" tag. Don't ask why your issue was closed if it says so in the tag.
|
||||
* If you're running on a NAS (QNAP, Austor, Synology etc.) with pre-made packages, make sure these are set up to use our source repository (RuudBurger/CouchPotatoServer) and nothing else!
|
||||
* 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!!
|
||||
|
||||
The more relevant information you provide, the more likely that your issue will be resolved.
|
||||
|
||||
## Pull Requests
|
||||
Pull requests are intended for contributing code or documentation to the project. Before you submit a pull request, consider the following:
|
||||
* Make sure your pull request is made for the *develop* branch (or relevant feature branch).
|
||||
## 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?
|
||||
* Does your PR have any limitations we should know of?
|
||||
* Is your PR up-to-date with the branch you're trying to push into?
|
||||
* Are there any limitations of your PR we should know of?
|
||||
* Make sure to keep you're PR up-to-date with the branch you're trying to push into.
|
||||
|
||||
**If we don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from couchpotato.api import api_docs, api_docs_missing, api
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.variable import md5, tryInt
|
||||
@@ -9,10 +5,13 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from tornado import template
|
||||
from tornado.web import RequestHandler, authenticated
|
||||
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
views = {}
|
||||
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
|
||||
|
||||
@@ -25,10 +24,9 @@ class BaseHandler(RequestHandler):
|
||||
|
||||
if username and password:
|
||||
return self.get_secure_cookie('user')
|
||||
else: # Login when no username or password are set
|
||||
else: # Login when no username or password are set
|
||||
return True
|
||||
|
||||
|
||||
# Main web handler
|
||||
class WebHandler(BaseHandler):
|
||||
|
||||
@@ -45,13 +43,11 @@ class WebHandler(BaseHandler):
|
||||
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
|
||||
self.write({'success': False, 'error': 'Failed returning results'})
|
||||
|
||||
|
||||
def addView(route, func):
|
||||
def addView(route, func, static = False):
|
||||
views[route] = func
|
||||
|
||||
|
||||
def get_db():
|
||||
return Env.get('db')
|
||||
def get_session(engine = None):
|
||||
return Env.getSession(engine)
|
||||
|
||||
|
||||
# Web view
|
||||
@@ -59,10 +55,12 @@ def index():
|
||||
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
|
||||
addView('', index)
|
||||
|
||||
|
||||
# API docs
|
||||
def apiDocs():
|
||||
routes = list(api.keys())
|
||||
routes = []
|
||||
|
||||
for route in api.iterkeys():
|
||||
routes.append(route)
|
||||
|
||||
if api_docs.get(''):
|
||||
del api_docs['']
|
||||
@@ -72,30 +70,21 @@ def apiDocs():
|
||||
|
||||
addView('docs', apiDocs)
|
||||
|
||||
|
||||
# Database debug manager
|
||||
def databaseManage():
|
||||
return template_loader.load('database.html').generate(fireEvent = fireEvent, Env = Env)
|
||||
|
||||
addView('database', databaseManage)
|
||||
|
||||
|
||||
# Make non basic auth option to get api key
|
||||
class KeyHandler(RequestHandler):
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
api_key = None
|
||||
api = None
|
||||
|
||||
try:
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
|
||||
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
|
||||
api_key = Env.setting('api_key')
|
||||
api = Env.setting('api_key')
|
||||
|
||||
self.write({
|
||||
'success': api_key is not None,
|
||||
'api_key': api_key
|
||||
'success': api is not None,
|
||||
'api_key': api
|
||||
})
|
||||
except:
|
||||
log.error('Failed doing key request: %s', (traceback.format_exc()))
|
||||
@@ -113,21 +102,20 @@ class LoginHandler(BaseHandler):
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
|
||||
api_key = None
|
||||
api = None
|
||||
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
|
||||
if (self.get_argument('username') == username or not username) and (md5(self.get_argument('password')) == password or not password):
|
||||
api_key = Env.setting('api_key')
|
||||
api = Env.setting('api_key')
|
||||
|
||||
if api_key:
|
||||
if api:
|
||||
remember_me = tryInt(self.get_argument('remember_me', default = 0))
|
||||
self.set_secure_cookie('user', api_key, expires_days = 30 if remember_me > 0 else None)
|
||||
self.set_secure_cookie('user', api, expires_days = 30 if remember_me > 0 else None)
|
||||
|
||||
self.redirect(Env.get('web_base'))
|
||||
|
||||
|
||||
class LogoutHandler(BaseHandler):
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
@@ -148,3 +136,4 @@ def page_not_found(rh):
|
||||
|
||||
rh.set_status(404)
|
||||
rh.write('Wrong API key used')
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from functools import wraps
|
||||
from threading import Thread
|
||||
import json
|
||||
import threading
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
from couchpotato.core.helpers.request import getParams
|
||||
from couchpotato.core.logger import CPLog
|
||||
from functools import wraps
|
||||
from threading import Thread
|
||||
from tornado.gen import coroutine
|
||||
from tornado.web import RequestHandler, asynchronous
|
||||
|
||||
import json
|
||||
import threading
|
||||
import tornado
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -20,25 +20,15 @@ api_nonblock = {}
|
||||
api_docs = {}
|
||||
api_docs_missing = []
|
||||
|
||||
|
||||
def run_async(func):
|
||||
@wraps(func)
|
||||
def async_func(*args, **kwargs):
|
||||
func_hl = Thread(target = func, args = args, kwargs = kwargs)
|
||||
func_hl.start()
|
||||
return func_hl
|
||||
|
||||
return async_func
|
||||
|
||||
@run_async
|
||||
def run_handler(route, kwargs, callback = None):
|
||||
try:
|
||||
res = api[route](**kwargs)
|
||||
callback(res, route)
|
||||
except:
|
||||
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
|
||||
callback({'success': False, 'error': 'Failed returning results'}, route)
|
||||
|
||||
|
||||
# NonBlock API handler
|
||||
class NonBlockHandler(RequestHandler):
|
||||
|
||||
@@ -71,7 +61,6 @@ class NonBlockHandler(RequestHandler):
|
||||
|
||||
self.stopper = None
|
||||
|
||||
|
||||
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
api_nonblock[route] = func_tuple
|
||||
|
||||
@@ -80,22 +69,16 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
else:
|
||||
api_docs_missing.append(route)
|
||||
|
||||
|
||||
# Blocking API handler
|
||||
class ApiHandler(RequestHandler):
|
||||
|
||||
@asynchronous
|
||||
@coroutine
|
||||
def get(self, route, *args, **kwargs):
|
||||
route = route.strip('/')
|
||||
if not api.get(route):
|
||||
self.write('API call doesn\'t seem to exist')
|
||||
self.finish()
|
||||
return
|
||||
|
||||
# Create lock if it doesn't exist
|
||||
if route in api_locks and not api_locks.get(route):
|
||||
api_locks[route] = threading.Lock()
|
||||
|
||||
api_locks[route].acquire()
|
||||
|
||||
try:
|
||||
@@ -106,51 +89,39 @@ class ApiHandler(RequestHandler):
|
||||
|
||||
# Split array arguments
|
||||
kwargs = getParams(kwargs)
|
||||
kwargs['_request'] = self
|
||||
|
||||
# Remove t random string
|
||||
try: del kwargs['t']
|
||||
except: pass
|
||||
|
||||
# Add async callback handler
|
||||
run_handler(route, kwargs, callback = self.taskFinished)
|
||||
@run_async
|
||||
def run_handler(callback):
|
||||
try:
|
||||
result = api[route](**kwargs)
|
||||
callback(result)
|
||||
except:
|
||||
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
|
||||
callback({'success': False, 'error': 'Failed returning results'})
|
||||
result = yield tornado.gen.Task(run_handler)
|
||||
|
||||
# Check JSONP callback
|
||||
jsonp_callback = self.get_argument('callback_func', default = None)
|
||||
|
||||
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:
|
||||
self.write(result)
|
||||
|
||||
except:
|
||||
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
|
||||
try:
|
||||
self.write({'success': False, 'error': 'Failed returning results'})
|
||||
self.finish()
|
||||
except:
|
||||
log.error('Failed write error "%s": %s', (route, traceback.format_exc()))
|
||||
|
||||
api_locks[route].release()
|
||||
|
||||
post = get
|
||||
|
||||
def taskFinished(self, result, route):
|
||||
|
||||
if not self.request.connection.stream.closed():
|
||||
try:
|
||||
# Check JSONP callback
|
||||
jsonp_callback = self.get_argument('callback_func', default = None)
|
||||
|
||||
if jsonp_callback:
|
||||
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
||||
self.set_header("Content-Type", "text/javascript")
|
||||
self.finish()
|
||||
elif isinstance(result, tuple) and result[0] == 'redirect':
|
||||
self.redirect(result[1])
|
||||
else:
|
||||
self.write(result)
|
||||
self.finish()
|
||||
except:
|
||||
log.debug('Failed doing request, probably already closed: %s', (traceback.format_exc()))
|
||||
try: self.finish({'success': False, 'error': 'Failed returning results'})
|
||||
except: pass
|
||||
self.write({'success': False, 'error': 'Failed returning results'})
|
||||
|
||||
api_locks[route].release()
|
||||
|
||||
|
||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||
|
||||
if static: func(route)
|
||||
|
||||
100
couchpotato/core/_base/_core/__init__.py
Normal file
100
couchpotato/core/_base/_core/__init__.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from .main import Core
|
||||
from uuid import uuid4
|
||||
|
||||
def start():
|
||||
return Core()
|
||||
|
||||
config = [{
|
||||
'name': 'core',
|
||||
'order': 1,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'general',
|
||||
'name': 'basics',
|
||||
'description': 'Needs restart before changes take effect.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'port',
|
||||
'default': 5050,
|
||||
'type': 'int',
|
||||
'description': 'The port I should listen to.',
|
||||
},
|
||||
{
|
||||
'name': 'ssl_cert',
|
||||
'description': 'Path to SSL server.crt',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'ssl_key',
|
||||
'description': 'Path to SSL server.key',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'launch_browser',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'description': 'Launch the browser when I start.',
|
||||
'wizard': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'tab': 'general',
|
||||
'name': 'advanced',
|
||||
'description': "For those who know what they're doing",
|
||||
'advanced': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'api_key',
|
||||
'default': uuid4().hex,
|
||||
'readonly': 1,
|
||||
'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>',
|
||||
},
|
||||
{
|
||||
'name': 'debug',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'description': 'Enable debugging.',
|
||||
},
|
||||
{
|
||||
'name': 'development',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
|
||||
},
|
||||
{
|
||||
'name': 'data_dir',
|
||||
'type': 'directory',
|
||||
'description': 'Where cache/logs/etc are stored. Keep empty for defaults.',
|
||||
},
|
||||
{
|
||||
'name': 'url_base',
|
||||
'default': '',
|
||||
'description': 'When using mod_proxy use this to append the url with this.',
|
||||
},
|
||||
{
|
||||
'name': 'permission_folder',
|
||||
'default': '0755',
|
||||
'label': 'Folder CHMOD',
|
||||
'description': 'Can be either decimal (493) or octal (leading zero: 0755)',
|
||||
},
|
||||
{
|
||||
'name': 'permission_file',
|
||||
'default': '0755',
|
||||
'label': 'File CHMOD',
|
||||
'description': 'Same as Folder CHMOD but for files',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,3 +1,10 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.variable import cleanHost, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from tornado.ioloop import IOLoop
|
||||
from uuid import uuid4
|
||||
import os
|
||||
import platform
|
||||
@@ -6,19 +13,8 @@ import time
|
||||
import traceback
|
||||
import webbrowser
|
||||
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.variable import cleanHost, md5, isSubFolder
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from tornado.ioloop import IOLoop
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Core'
|
||||
|
||||
|
||||
class Core(Plugin):
|
||||
|
||||
@@ -51,7 +47,6 @@ class Core(Plugin):
|
||||
addEvent('app.api_url', self.createApiUrl)
|
||||
addEvent('app.version', self.version)
|
||||
addEvent('app.load', self.checkDataDir)
|
||||
addEvent('app.load', self.cleanUpFolders)
|
||||
|
||||
addEvent('setting.save.core.password', self.md5Password)
|
||||
addEvent('setting.save.core.api_key', self.checkApikey)
|
||||
@@ -71,15 +66,11 @@ class Core(Plugin):
|
||||
return value if value and len(value) > 3 else uuid4().hex
|
||||
|
||||
def checkDataDir(self):
|
||||
if isSubFolder(Env.get('data_dir'), Env.get('app_dir')):
|
||||
if Env.get('app_dir') in Env.get('data_dir'):
|
||||
log.error('You should NOT use your CouchPotato directory to save your settings in. Files will get overwritten or be deleted.')
|
||||
|
||||
return True
|
||||
|
||||
def cleanUpFolders(self):
|
||||
only_clean = ['couchpotato', 'libs', 'init']
|
||||
self.deleteEmptyFolder(Env.get('app_dir'), show_error = False, only_clean = only_clean)
|
||||
|
||||
def available(self, **kwargs):
|
||||
return {
|
||||
'success': True
|
||||
@@ -91,11 +82,7 @@ class Core(Plugin):
|
||||
|
||||
def shutdown():
|
||||
self.initShutdown()
|
||||
|
||||
if IOLoop.current()._closing:
|
||||
shutdown()
|
||||
else:
|
||||
IOLoop.current().add_callback(shutdown)
|
||||
IOLoop.current().add_callback(shutdown)
|
||||
|
||||
return 'shutdown'
|
||||
|
||||
@@ -118,7 +105,7 @@ class Core(Plugin):
|
||||
|
||||
self.shutdown_started = True
|
||||
|
||||
fireEvent('app.do_shutdown', restart = restart)
|
||||
fireEvent('app.do_shutdown')
|
||||
log.debug('Every plugin got shutdown event')
|
||||
|
||||
loop = True
|
||||
@@ -130,7 +117,7 @@ class Core(Plugin):
|
||||
|
||||
if len(still_running) == 0:
|
||||
break
|
||||
elif starttime < time.time() - 30: # Always force break after 30s wait
|
||||
elif starttime < time.time() - 30: # Always force break after 30s wait
|
||||
break
|
||||
|
||||
running = list(set(still_running) - set(self.ignore_restart))
|
||||
@@ -143,11 +130,8 @@ class Core(Plugin):
|
||||
|
||||
log.debug('Safe to shutdown/restart')
|
||||
|
||||
loop = IOLoop.current()
|
||||
|
||||
try:
|
||||
if not loop._closing:
|
||||
loop.stop()
|
||||
IOLoop.current().stop()
|
||||
except RuntimeError:
|
||||
pass
|
||||
except:
|
||||
@@ -197,104 +181,8 @@ class Core(Plugin):
|
||||
def signalHandler(self):
|
||||
if Env.get('daemonized'): return
|
||||
|
||||
def signal_handler(*args, **kwargs):
|
||||
def signal_handler(signal, frame):
|
||||
fireEvent('app.shutdown', single = True)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'core',
|
||||
'order': 1,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'general',
|
||||
'name': 'basics',
|
||||
'description': 'Needs restart before changes take effect.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'port',
|
||||
'default': 5050,
|
||||
'type': 'int',
|
||||
'description': 'The port I should listen to.',
|
||||
},
|
||||
{
|
||||
'name': 'ssl_cert',
|
||||
'description': 'Path to SSL server.crt',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'ssl_key',
|
||||
'description': 'Path to SSL server.key',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'launch_browser',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'description': 'Launch the browser when I start.',
|
||||
'wizard': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'tab': 'general',
|
||||
'name': 'advanced',
|
||||
'description': "For those who know what they're doing",
|
||||
'advanced': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'api_key',
|
||||
'default': uuid4().hex,
|
||||
'readonly': 1,
|
||||
'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>',
|
||||
},
|
||||
{
|
||||
'name': 'debug',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'description': 'Enable debugging.',
|
||||
},
|
||||
{
|
||||
'name': 'development',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
|
||||
},
|
||||
{
|
||||
'name': 'data_dir',
|
||||
'type': 'directory',
|
||||
'description': 'Where cache/logs/etc are stored. Keep empty for defaults.',
|
||||
},
|
||||
{
|
||||
'name': 'url_base',
|
||||
'default': '',
|
||||
'description': 'When using mod_proxy use this to append the url with this.',
|
||||
},
|
||||
{
|
||||
'name': 'permission_folder',
|
||||
'default': '0755',
|
||||
'label': 'Folder CHMOD',
|
||||
'description': 'Can be either decimal (493) or octal (leading zero: 0755)',
|
||||
},
|
||||
{
|
||||
'name': 'permission_file',
|
||||
'default': '0755',
|
||||
'label': 'File CHMOD',
|
||||
'description': 'Same as Folder CHMOD but for files',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
6
couchpotato/core/_base/clientscript/__init__.py
Normal file
6
couchpotato/core/_base/clientscript/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import ClientScript
|
||||
|
||||
def start():
|
||||
return ClientScript()
|
||||
|
||||
config = []
|
||||
@@ -1,7 +1,3 @@
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
@@ -11,12 +7,12 @@ from couchpotato.environment import Env
|
||||
from minify.cssmin import cssmin
|
||||
from minify.jsmin import jsmin
|
||||
from tornado.web import StaticFileHandler
|
||||
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'ClientScript'
|
||||
|
||||
|
||||
class ClientScript(Plugin):
|
||||
|
||||
@@ -49,17 +45,20 @@ class ClientScript(Plugin):
|
||||
'scripts/block/footer.js',
|
||||
'scripts/block/menu.js',
|
||||
'scripts/page/home.js',
|
||||
'scripts/page/wanted.js',
|
||||
'scripts/page/settings.js',
|
||||
'scripts/page/about.js',
|
||||
'scripts/page/manage.js',
|
||||
],
|
||||
}
|
||||
|
||||
urls = {'style': {}, 'script': {}}
|
||||
minified = {'style': {}, 'script': {}}
|
||||
paths = {'style': {}, 'script': {}}
|
||||
|
||||
urls = {'style': {}, 'script': {}, }
|
||||
minified = {'style': {}, 'script': {}, }
|
||||
paths = {'style': {}, 'script': {}, }
|
||||
comment = {
|
||||
'style': '/*** %s:%d ***/\n',
|
||||
'script': '// %s:%d\n'
|
||||
'style': '/*** %s:%d ***/\n',
|
||||
'script': '// %s:%d\n'
|
||||
}
|
||||
|
||||
html = {
|
||||
@@ -91,6 +90,7 @@ class ClientScript(Plugin):
|
||||
else:
|
||||
self.registerStyle(core_url, file_path, position = 'front')
|
||||
|
||||
|
||||
def minify(self):
|
||||
|
||||
# Create cache dir
|
||||
@@ -124,7 +124,7 @@ class ClientScript(Plugin):
|
||||
data = cssmin(data)
|
||||
data = data.replace('../images/', '../static/images/')
|
||||
data = data.replace('../fonts/', '../static/fonts/')
|
||||
data = data.replace('../../static/', '../static/') # Replace inside plugins
|
||||
data = data.replace('../../static/', '../static/') # Replace inside plugins
|
||||
|
||||
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
|
||||
|
||||
@@ -187,7 +187,6 @@ class ClientScript(Plugin):
|
||||
|
||||
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
|
||||
prefix_tags = ['ms', 'moz', 'webkit']
|
||||
|
||||
def prefix(self, data):
|
||||
|
||||
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
|
||||
6
couchpotato/core/_base/desktop/__init__.py
Normal file
6
couchpotato/core/_base/desktop/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import Desktop
|
||||
|
||||
def start():
|
||||
return Desktop()
|
||||
|
||||
config = []
|
||||
@@ -5,9 +5,6 @@ from couchpotato.environment import Env
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Desktop'
|
||||
|
||||
|
||||
if Env.get('desktop'):
|
||||
|
||||
class Desktop(Plugin):
|
||||
@@ -1,20 +0,0 @@
|
||||
from .main import Downloader
|
||||
|
||||
|
||||
def autoload():
|
||||
return Downloader()
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'download_providers',
|
||||
'groups': [
|
||||
{
|
||||
'label': 'Downloaders',
|
||||
'description': 'You can select different downloaders for each type (usenet / torrent)',
|
||||
'type': 'list',
|
||||
'name': 'download_providers',
|
||||
'tab': 'downloaders',
|
||||
'options': [],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,76 +0,0 @@
|
||||
var DownloadersBase = new Class({
|
||||
|
||||
Implements: [Events],
|
||||
|
||||
initialize: function(){
|
||||
var self = this;
|
||||
|
||||
// Add test buttons to settings page
|
||||
App.addEvent('loadSettings', self.addTestButtons.bind(self));
|
||||
|
||||
},
|
||||
|
||||
// Downloaders setting tests
|
||||
addTestButtons: function(){
|
||||
var self = this;
|
||||
|
||||
var setting_page = App.getPage('Settings');
|
||||
setting_page.addEvent('create', function(){
|
||||
Object.each(setting_page.tabs.downloaders.groups, self.addTestButton.bind(self))
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
addTestButton: function(fieldset, plugin_name){
|
||||
var self = this,
|
||||
button_name = self.testButtonName(fieldset);
|
||||
|
||||
if(button_name.contains('Downloaders')) return;
|
||||
|
||||
new Element('.ctrlHolder.test_button').adopt(
|
||||
new Element('a.button', {
|
||||
'text': button_name,
|
||||
'events': {
|
||||
'click': function(){
|
||||
var button = fieldset.getElement('.test_button .button');
|
||||
button.set('text', 'Connecting...');
|
||||
|
||||
Api.request('download.'+plugin_name+'.test', {
|
||||
'onComplete': function(json){
|
||||
|
||||
button.set('text', button_name);
|
||||
|
||||
var message;
|
||||
if(json.success){
|
||||
message = new Element('span.success', {
|
||||
'text': 'Connection successful'
|
||||
}).inject(button, 'after')
|
||||
}
|
||||
else {
|
||||
var msg_text = 'Connection failed. Check logs for details.';
|
||||
if(json.hasOwnProperty('msg')) msg_text = json.msg;
|
||||
message = new Element('span.failed', {
|
||||
'text': msg_text
|
||||
}).inject(button, 'after')
|
||||
}
|
||||
|
||||
(function(){
|
||||
message.destroy();
|
||||
}).delay(3000)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(fieldset);
|
||||
|
||||
},
|
||||
|
||||
testButtonName: function(fieldset){
|
||||
var name = String(fieldset.getElement('h2').innerHTML).substring(0,String(fieldset.getElement('h2').innerHTML).indexOf("<span"));
|
||||
return 'Test '+name;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var Downloaders = new DownloadersBase();
|
||||
6
couchpotato/core/_base/scheduler/__init__.py
Normal file
6
couchpotato/core/_base/scheduler/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import Scheduler
|
||||
|
||||
def start():
|
||||
return Scheduler()
|
||||
|
||||
config = []
|
||||
@@ -5,8 +5,6 @@ from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Scheduler'
|
||||
|
||||
|
||||
class Scheduler(Plugin):
|
||||
|
||||
@@ -19,7 +17,6 @@ class Scheduler(Plugin):
|
||||
addEvent('schedule.cron', self.cron)
|
||||
addEvent('schedule.interval', self.interval)
|
||||
addEvent('schedule.remove', self.remove)
|
||||
addEvent('schedule.queue', self.queue)
|
||||
|
||||
self.sched = Sched(misfire_grace_time = 60)
|
||||
self.sched.start()
|
||||
@@ -33,9 +30,9 @@ class Scheduler(Plugin):
|
||||
except:
|
||||
pass
|
||||
|
||||
def doShutdown(self, *args, **kwargs):
|
||||
def doShutdown(self):
|
||||
self.stop()
|
||||
return super(Scheduler, self).doShutdown(*args, **kwargs)
|
||||
return super(Scheduler, self).doShutdown()
|
||||
|
||||
def stop(self):
|
||||
if self.started:
|
||||
@@ -67,16 +64,3 @@ class Scheduler(Plugin):
|
||||
'seconds': seconds,
|
||||
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
|
||||
}
|
||||
|
||||
return True
|
||||
|
||||
def queue(self, handlers = None):
|
||||
if not handlers: handlers = []
|
||||
|
||||
for h in handlers:
|
||||
h()
|
||||
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
return True
|
||||
@@ -1,10 +1,8 @@
|
||||
import os
|
||||
|
||||
from .main import Updater
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
|
||||
|
||||
def autoload():
|
||||
def start():
|
||||
return Updater()
|
||||
|
||||
config = [{
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from datetime import datetime
|
||||
from dateutil.parser import parse
|
||||
from git.repository import LocalRepository
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tarfile
|
||||
import time
|
||||
import traceback
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from threading import RLock
|
||||
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.helpers.variable import removePyc
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
from git.repository import LocalRepository
|
||||
import version
|
||||
from six.moves import filter
|
||||
|
||||
import zipfile
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -27,7 +22,6 @@ log = CPLog(__name__)
|
||||
class Updater(Plugin):
|
||||
|
||||
available_notified = False
|
||||
_lock = RLock()
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -38,7 +32,6 @@ class Updater(Plugin):
|
||||
else:
|
||||
self.updater = SourceUpdater()
|
||||
|
||||
addEvent('app.load', self.logVersion, priority = 10000)
|
||||
addEvent('app.load', self.setCrons)
|
||||
addEvent('updater.info', self.info)
|
||||
|
||||
@@ -60,16 +53,12 @@ class Updater(Plugin):
|
||||
|
||||
addEvent('setting.save.updater.enabled.after', self.setCrons)
|
||||
|
||||
def logVersion(self):
|
||||
info = self.info()
|
||||
log.info('=== VERSION %s, using %s ===', (info.get('version', {}).get('repr', 'UNKNOWN'), self.updater.getName()))
|
||||
|
||||
def setCrons(self):
|
||||
|
||||
fireEvent('schedule.remove', 'updater.check', single = True)
|
||||
if self.isEnabled():
|
||||
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
|
||||
self.autoUpdate() # Check after enabling
|
||||
self.autoUpdate() # Check after enabling
|
||||
|
||||
def autoUpdate(self):
|
||||
if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||
@@ -105,17 +94,7 @@ class Updater(Plugin):
|
||||
return False
|
||||
|
||||
def info(self, **kwargs):
|
||||
self._lock.acquire()
|
||||
|
||||
info = {}
|
||||
try:
|
||||
info = self.updater.info()
|
||||
except:
|
||||
log.error('Failed getting updater info: %s', traceback.format_exc())
|
||||
|
||||
self._lock.release()
|
||||
|
||||
return info
|
||||
return self.updater.info()
|
||||
|
||||
def checkView(self, **kwargs):
|
||||
return {
|
||||
@@ -142,12 +121,6 @@ class Updater(Plugin):
|
||||
'success': success
|
||||
}
|
||||
|
||||
def doShutdown(self, *args, **kwargs):
|
||||
if not Env.get('dev') and not Env.get('desktop'):
|
||||
removePyc(Env.get('app_dir'), show_logs = False)
|
||||
|
||||
return super(Updater, self).doShutdown(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseUpdater(Plugin):
|
||||
|
||||
@@ -165,23 +138,42 @@ class BaseUpdater(Plugin):
|
||||
pass
|
||||
|
||||
def info(self):
|
||||
|
||||
current_version = self.getVersion()
|
||||
|
||||
return {
|
||||
'last_check': self.last_check,
|
||||
'update_version': self.update_version,
|
||||
'version': current_version,
|
||||
'version': self.getVersion(),
|
||||
'repo_name': '%s/%s' % (self.repo_user, self.repo_name),
|
||||
'branch': current_version.get('branch', self.branch),
|
||||
'branch': self.branch,
|
||||
}
|
||||
|
||||
def getVersion(self):
|
||||
pass
|
||||
|
||||
def check(self):
|
||||
pass
|
||||
|
||||
def deletePyc(self, only_excess = True):
|
||||
|
||||
for root, dirs, files in os.walk(ss(Env.get('app_dir'))):
|
||||
|
||||
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
|
||||
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
|
||||
excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files) if only_excess else pyc_files
|
||||
|
||||
for excess_pyc_file in excess_pyc_files:
|
||||
full_path = os.path.join(root, excess_pyc_file)
|
||||
log.debug('Removing old PYC file: %s', full_path)
|
||||
try:
|
||||
os.remove(full_path)
|
||||
except:
|
||||
log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc()))
|
||||
|
||||
for dir_name in dirs:
|
||||
full_path = os.path.join(root, dir_name)
|
||||
if len(os.listdir(full_path)) == 0:
|
||||
try:
|
||||
os.rmdir(full_path)
|
||||
except:
|
||||
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
|
||||
|
||||
|
||||
|
||||
class GitUpdater(BaseUpdater):
|
||||
|
||||
@@ -194,6 +186,9 @@ class GitUpdater(BaseUpdater):
|
||||
log.info('Updating to latest version')
|
||||
self.repo.pull()
|
||||
|
||||
# Delete leftover .pyc files
|
||||
self.deletePyc()
|
||||
|
||||
return True
|
||||
except:
|
||||
log.error('Failed updating via GIT: %s', traceback.format_exc())
|
||||
@@ -206,16 +201,14 @@ class GitUpdater(BaseUpdater):
|
||||
|
||||
if not self.version:
|
||||
try:
|
||||
output = self.repo.getHead() # Yes, please
|
||||
output = self.repo.getHead() # Yes, please
|
||||
log.debug('Git version output: %s', output.hash)
|
||||
self.version = {
|
||||
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.repo.getCurrentBranch().name or self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())),
|
||||
'hash': output.hash[:8],
|
||||
'date': output.getDate(),
|
||||
'type': 'git',
|
||||
'branch': self.repo.getCurrentBranch().name
|
||||
}
|
||||
except Exception as e:
|
||||
except Exception, e:
|
||||
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
|
||||
return 'No GIT'
|
||||
|
||||
@@ -238,7 +231,7 @@ class GitUpdater(BaseUpdater):
|
||||
local = self.repo.getHead()
|
||||
remote = branch.getHead()
|
||||
|
||||
log.debug('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8]))
|
||||
log.info('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8]))
|
||||
|
||||
if local.getDate() < remote.getDate():
|
||||
self.update_version = {
|
||||
@@ -251,6 +244,7 @@ class GitUpdater(BaseUpdater):
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class SourceUpdater(BaseUpdater):
|
||||
|
||||
def __init__(self):
|
||||
@@ -276,9 +270,9 @@ class SourceUpdater(BaseUpdater):
|
||||
|
||||
# Extract
|
||||
if download_data.get('type') == 'zip':
|
||||
zip_file = zipfile.ZipFile(destination)
|
||||
zip_file.extractall(extracted_path)
|
||||
zip_file.close()
|
||||
zip = zipfile.ZipFile(destination)
|
||||
zip.extractall(extracted_path)
|
||||
zip.close()
|
||||
else:
|
||||
tar = tarfile.open(destination)
|
||||
tar.extractall(path = extracted_path)
|
||||
@@ -300,12 +294,11 @@ class SourceUpdater(BaseUpdater):
|
||||
return False
|
||||
|
||||
def replaceWith(self, path):
|
||||
path = sp(path)
|
||||
app_dir = Env.get('app_dir')
|
||||
data_dir = Env.get('data_dir')
|
||||
app_dir = ss(Env.get('app_dir'))
|
||||
data_dir = ss(Env.get('data_dir'))
|
||||
|
||||
# Get list of files we want to overwrite
|
||||
removePyc(app_dir)
|
||||
self.deletePyc()
|
||||
existing_files = []
|
||||
for root, subfiles, filenames in os.walk(app_dir):
|
||||
for filename in filenames:
|
||||
@@ -346,12 +339,13 @@ class SourceUpdater(BaseUpdater):
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def removeDir(self, path):
|
||||
try:
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
except OSError as inst:
|
||||
os.chmod(inst.filename, 0o777)
|
||||
except OSError, inst:
|
||||
os.chmod(inst.filename, 0777)
|
||||
self.removeDir(path)
|
||||
|
||||
def getVersion(self):
|
||||
@@ -365,8 +359,7 @@ class SourceUpdater(BaseUpdater):
|
||||
log.debug('Source version output: %s', output)
|
||||
self.version = output
|
||||
self.version['type'] = 'source'
|
||||
self.version['repr'] = 'source:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.get('hash', '')[:8], datetime.fromtimestamp(output.get('date', 0)))
|
||||
except Exception as e:
|
||||
except Exception, e:
|
||||
log.error('Failed using source updater. %s', e)
|
||||
return {}
|
||||
|
||||
@@ -396,7 +389,7 @@ class SourceUpdater(BaseUpdater):
|
||||
|
||||
return {
|
||||
'hash': commit['sha'],
|
||||
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
|
||||
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
|
||||
}
|
||||
except:
|
||||
log.error('Failed getting latest request from github: %s', traceback.format_exc())
|
||||
@@ -441,7 +434,7 @@ class DesktopUpdater(BaseUpdater):
|
||||
if latest and latest != current_version.get('hash'):
|
||||
self.update_version = {
|
||||
'hash': latest,
|
||||
'date': None,
|
||||
'date': None,
|
||||
'changelog': self.desktop._changelogURL,
|
||||
}
|
||||
|
||||
@@ -453,7 +446,6 @@ class DesktopUpdater(BaseUpdater):
|
||||
|
||||
def getVersion(self):
|
||||
return {
|
||||
'repr': 'desktop: %s' % self.desktop._esky.active_version,
|
||||
'hash': self.desktop._esky.active_version,
|
||||
'date': None,
|
||||
'type': 'desktop',
|
||||
|
||||
@@ -5,7 +5,7 @@ var UpdaterBase = new Class({
|
||||
initialize: function(){
|
||||
var self = this;
|
||||
|
||||
App.addEvent('load', self.info.bind(self, 2000));
|
||||
App.addEvent('load', self.info.bind(self, 2000))
|
||||
App.addEvent('unload', function(){
|
||||
if(self.timer)
|
||||
clearTimeout(self.timer);
|
||||
@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
|
||||
self.doUpdate();
|
||||
else {
|
||||
App.unBlockPage();
|
||||
App.trigger('message', ['No updates available']);
|
||||
App.on('message', 'No updates available');
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -66,7 +66,7 @@ var UpdaterBase = new Class({
|
||||
|
||||
var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.branch;
|
||||
if(data.update_version.changelog)
|
||||
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash;
|
||||
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash
|
||||
|
||||
self.message = new Element('div.message.update').adopt(
|
||||
new Element('span', {
|
||||
|
||||
@@ -1,576 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from CodernityDB.index import IndexException, IndexNotFoundException, IndexConflict
|
||||
from couchpotato import CPLog
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, sp
|
||||
from couchpotato.core.helpers.variable import getImdb, tryInt
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Database(object):
|
||||
|
||||
indexes = None
|
||||
db = None
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.indexes = {}
|
||||
|
||||
addApiView('database.list_documents', self.listDocuments)
|
||||
addApiView('database.reindex', self.reindex)
|
||||
addApiView('database.compact', self.compact)
|
||||
addApiView('database.document.update', self.updateDocument)
|
||||
addApiView('database.document.delete', self.deleteDocument)
|
||||
|
||||
addEvent('database.setup.after', self.startup_compact)
|
||||
addEvent('database.setup_index', self.setupIndex)
|
||||
|
||||
addEvent('app.migrate', self.migrate)
|
||||
addEvent('app.after_shutdown', self.close)
|
||||
|
||||
def getDB(self):
|
||||
|
||||
if not self.db:
|
||||
from couchpotato import get_db
|
||||
self.db = get_db()
|
||||
|
||||
return self.db
|
||||
|
||||
def close(self, **kwargs):
|
||||
self.getDB().close()
|
||||
|
||||
def setupIndex(self, index_name, klass):
|
||||
|
||||
self.indexes[index_name] = klass
|
||||
|
||||
db = self.getDB()
|
||||
|
||||
# Category index
|
||||
index_instance = klass(db.path, index_name)
|
||||
try:
|
||||
|
||||
# Make sure store and bucket don't exist
|
||||
exists = []
|
||||
for x in ['buck', 'stor']:
|
||||
full_path = os.path.join(db.path, '%s_%s' % (index_name, x))
|
||||
if os.path.exists(full_path):
|
||||
exists.append(full_path)
|
||||
|
||||
if index_name not in db.indexes_names:
|
||||
|
||||
# Remove existing buckets if index isn't there
|
||||
for x in exists:
|
||||
os.unlink(x)
|
||||
|
||||
# Add index (will restore buckets)
|
||||
db.add_index(index_instance)
|
||||
db.reindex_index(index_name)
|
||||
else:
|
||||
# Previous info
|
||||
previous = db.indexes_names[index_name]
|
||||
previous_version = previous._version
|
||||
current_version = klass._version
|
||||
|
||||
# Only edit index if versions are different
|
||||
if previous_version < current_version:
|
||||
log.debug('Index "%s" already exists, updating and reindexing', index_name)
|
||||
db.destroy_index(previous)
|
||||
db.add_index(index_instance)
|
||||
db.reindex_index(index_name)
|
||||
|
||||
except:
|
||||
log.error('Failed adding index %s: %s', (index_name, traceback.format_exc()))
|
||||
|
||||
def deleteDocument(self, **kwargs):
|
||||
|
||||
db = self.getDB()
|
||||
|
||||
try:
|
||||
|
||||
document_id = kwargs.get('_request').get_argument('id')
|
||||
document = db.get('id', document_id)
|
||||
db.delete(document)
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
||||
except:
|
||||
return {
|
||||
'success': False,
|
||||
'error': traceback.format_exc()
|
||||
}
|
||||
|
||||
def updateDocument(self, **kwargs):
|
||||
|
||||
db = self.getDB()
|
||||
|
||||
try:
|
||||
|
||||
document = json.loads(kwargs.get('_request').get_argument('document'))
|
||||
d = db.update(document)
|
||||
document.update(d)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'document': document
|
||||
}
|
||||
except:
|
||||
return {
|
||||
'success': False,
|
||||
'error': traceback.format_exc()
|
||||
}
|
||||
|
||||
def listDocuments(self, **kwargs):
|
||||
db = self.getDB()
|
||||
|
||||
results = {
|
||||
'unknown': []
|
||||
}
|
||||
|
||||
for document in db.all('id'):
|
||||
key = document.get('_t', 'unknown')
|
||||
|
||||
if kwargs.get('show') and key != kwargs.get('show'):
|
||||
continue
|
||||
|
||||
if not results.get(key):
|
||||
results[key] = []
|
||||
results[key].append(document)
|
||||
|
||||
return results
|
||||
|
||||
def reindex(self, **kwargs):
|
||||
|
||||
success = True
|
||||
try:
|
||||
db = self.getDB()
|
||||
db.reindex()
|
||||
except:
|
||||
log.error('Failed index: %s', traceback.format_exc())
|
||||
success = False
|
||||
|
||||
return {
|
||||
'success': success
|
||||
}
|
||||
|
||||
def compact(self, try_repair = True, **kwargs):
|
||||
|
||||
success = False
|
||||
db = self.getDB()
|
||||
|
||||
# Removing left over compact files
|
||||
db_path = sp(db.path)
|
||||
for f in os.listdir(sp(db.path)):
|
||||
for x in ['_compact_buck', '_compact_stor']:
|
||||
if f[-len(x):] == x:
|
||||
os.unlink(os.path.join(db_path, f))
|
||||
|
||||
try:
|
||||
start = time.time()
|
||||
size = float(db.get_db_details().get('size', 0))
|
||||
log.debug('Compacting database, current size: %sMB', round(size/1048576, 2))
|
||||
|
||||
db.compact()
|
||||
new_size = float(db.get_db_details().get('size', 0))
|
||||
log.debug('Done compacting database in %ss, new size: %sMB, saved: %sMB', (round(time.time()-start, 2), round(new_size/1048576, 2), round((size-new_size)/1048576, 2)))
|
||||
success = True
|
||||
except (IndexException, AttributeError):
|
||||
if try_repair:
|
||||
log.error('Something wrong with indexes, trying repair')
|
||||
|
||||
# Remove all indexes
|
||||
old_indexes = self.indexes.keys()
|
||||
for index_name in old_indexes:
|
||||
try:
|
||||
db.destroy_index(index_name)
|
||||
except IndexNotFoundException:
|
||||
pass
|
||||
except:
|
||||
log.error('Failed removing old index %s', index_name)
|
||||
|
||||
# Add them again
|
||||
for index_name in self.indexes:
|
||||
klass = self.indexes[index_name]
|
||||
|
||||
# Category index
|
||||
index_instance = klass(db.path, index_name)
|
||||
try:
|
||||
db.add_index(index_instance)
|
||||
db.reindex_index(index_name)
|
||||
except IndexConflict:
|
||||
pass
|
||||
except:
|
||||
log.error('Failed adding index %s', index_name)
|
||||
raise
|
||||
|
||||
self.compact(try_repair = False)
|
||||
else:
|
||||
log.error('Failed compact: %s', traceback.format_exc())
|
||||
|
||||
except:
|
||||
log.error('Failed compact: %s', traceback.format_exc())
|
||||
|
||||
return {
|
||||
'success': success
|
||||
}
|
||||
|
||||
# Compact on start
|
||||
def startup_compact(self):
|
||||
from couchpotato import Env
|
||||
|
||||
db = self.getDB()
|
||||
size = db.get_db_details().get('size')
|
||||
prop_name = 'last_db_compact'
|
||||
last_check = int(Env.prop(prop_name, default = 0))
|
||||
|
||||
if size > 26214400 and last_check < time.time()-604800: # 25MB / 7 days
|
||||
self.compact()
|
||||
Env.prop(prop_name, value = int(time.time()))
|
||||
|
||||
def migrate(self):
|
||||
|
||||
from couchpotato import Env
|
||||
old_db = os.path.join(Env.get('data_dir'), 'couchpotato.db')
|
||||
if not os.path.isfile(old_db): return
|
||||
|
||||
log.info('=' * 30)
|
||||
log.info('Migrating database, hold on..')
|
||||
time.sleep(1)
|
||||
|
||||
if os.path.isfile(old_db):
|
||||
|
||||
migrate_start = time.time()
|
||||
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(old_db)
|
||||
|
||||
migrate_list = {
|
||||
'category': ['id', 'label', 'order', 'required', 'preferred', 'ignored', 'destination'],
|
||||
'profile': ['id', 'label', 'order', 'core', 'hide'],
|
||||
'profiletype': ['id', 'order', 'finish', 'wait_for', 'quality_id', 'profile_id'],
|
||||
'quality': ['id', 'identifier', 'order', 'size_min', 'size_max'],
|
||||
'movie': ['id', 'last_edit', 'library_id', 'status_id', 'profile_id', 'category_id'],
|
||||
'library': ['id', 'identifier', 'info'],
|
||||
'librarytitle': ['id', 'title', 'default', 'libraries_id'],
|
||||
'library_files__file_library': ['library_id', 'file_id'],
|
||||
'release': ['id', 'identifier', 'movie_id', 'status_id', 'quality_id', 'last_edit'],
|
||||
'releaseinfo': ['id', 'identifier', 'value', 'release_id'],
|
||||
'release_files__file_release': ['release_id', 'file_id'],
|
||||
'status': ['id', 'identifier'],
|
||||
'properties': ['id', 'identifier', 'value'],
|
||||
'file': ['id', 'path', 'type_id'],
|
||||
'filetype': ['identifier', 'id']
|
||||
}
|
||||
|
||||
migrate_data = {}
|
||||
|
||||
c = conn.cursor()
|
||||
|
||||
for ml in migrate_list:
|
||||
migrate_data[ml] = {}
|
||||
rows = migrate_list[ml]
|
||||
|
||||
try:
|
||||
c.execute('SELECT %s FROM `%s`' % ('`' + '`,`'.join(rows) + '`', ml))
|
||||
except:
|
||||
# ignore faulty destination_id database
|
||||
if ml == 'category':
|
||||
migrate_data[ml] = {}
|
||||
else:
|
||||
raise
|
||||
|
||||
for p in c.fetchall():
|
||||
columns = {}
|
||||
for row in migrate_list[ml]:
|
||||
columns[row] = p[rows.index(row)]
|
||||
|
||||
if not migrate_data[ml].get(p[0]):
|
||||
migrate_data[ml][p[0]] = columns
|
||||
else:
|
||||
if not isinstance(migrate_data[ml][p[0]], list):
|
||||
migrate_data[ml][p[0]] = [migrate_data[ml][p[0]]]
|
||||
migrate_data[ml][p[0]].append(columns)
|
||||
|
||||
conn.close()
|
||||
|
||||
log.info('Getting data took %s', time.time() - migrate_start)
|
||||
|
||||
db = self.getDB()
|
||||
|
||||
# Use properties
|
||||
properties = migrate_data['properties']
|
||||
log.info('Importing %s properties', len(properties))
|
||||
for x in properties:
|
||||
property = properties[x]
|
||||
Env.prop(property.get('identifier'), property.get('value'))
|
||||
|
||||
# Categories
|
||||
categories = migrate_data.get('category', [])
|
||||
log.info('Importing %s categories', len(categories))
|
||||
category_link = {}
|
||||
for x in categories:
|
||||
c = categories[x]
|
||||
|
||||
new_c = db.insert({
|
||||
'_t': 'category',
|
||||
'order': c.get('order', 999),
|
||||
'label': toUnicode(c.get('label', '')),
|
||||
'ignored': toUnicode(c.get('ignored', '')),
|
||||
'preferred': toUnicode(c.get('preferred', '')),
|
||||
'required': toUnicode(c.get('required', '')),
|
||||
'destination': toUnicode(c.get('destination', '')),
|
||||
})
|
||||
|
||||
category_link[x] = new_c.get('_id')
|
||||
|
||||
# Profiles
|
||||
log.info('Importing profiles')
|
||||
new_profiles = db.all('profile', with_doc = True)
|
||||
new_profiles_by_label = {}
|
||||
for x in new_profiles:
|
||||
|
||||
# Remove default non core profiles
|
||||
if not x['doc'].get('core'):
|
||||
db.delete(x['doc'])
|
||||
else:
|
||||
new_profiles_by_label[x['doc']['label']] = x['_id']
|
||||
|
||||
profiles = migrate_data['profile']
|
||||
profile_link = {}
|
||||
for x in profiles:
|
||||
p = profiles[x]
|
||||
|
||||
exists = new_profiles_by_label.get(p.get('label'))
|
||||
|
||||
# Update existing with order only
|
||||
if exists and p.get('core'):
|
||||
profile = db.get('id', exists)
|
||||
profile['order'] = tryInt(p.get('order'))
|
||||
profile['hide'] = p.get('hide') in [1, True, 'true', 'True']
|
||||
db.update(profile)
|
||||
|
||||
profile_link[x] = profile.get('_id')
|
||||
else:
|
||||
|
||||
new_profile = {
|
||||
'_t': 'profile',
|
||||
'label': p.get('label'),
|
||||
'order': int(p.get('order', 999)),
|
||||
'core': p.get('core', False),
|
||||
'qualities': [],
|
||||
'wait_for': [],
|
||||
'finish': []
|
||||
}
|
||||
|
||||
types = migrate_data['profiletype']
|
||||
for profile_type in types:
|
||||
p_type = types[profile_type]
|
||||
if types[profile_type]['profile_id'] == p['id']:
|
||||
if p_type['quality_id']:
|
||||
new_profile['finish'].append(p_type['finish'])
|
||||
new_profile['wait_for'].append(p_type['wait_for'])
|
||||
new_profile['qualities'].append(migrate_data['quality'][p_type['quality_id']]['identifier'])
|
||||
|
||||
if len(new_profile['qualities']) > 0:
|
||||
new_profile.update(db.insert(new_profile))
|
||||
profile_link[x] = new_profile.get('_id')
|
||||
else:
|
||||
log.error('Corrupt profile list for "%s", using default.', p.get('label'))
|
||||
|
||||
# Qualities
|
||||
log.info('Importing quality sizes')
|
||||
new_qualities = db.all('quality', with_doc = True)
|
||||
new_qualities_by_identifier = {}
|
||||
for x in new_qualities:
|
||||
new_qualities_by_identifier[x['doc']['identifier']] = x['_id']
|
||||
|
||||
qualities = migrate_data['quality']
|
||||
quality_link = {}
|
||||
for x in qualities:
|
||||
q = qualities[x]
|
||||
q_id = new_qualities_by_identifier[q.get('identifier')]
|
||||
|
||||
quality = db.get('id', q_id)
|
||||
quality['order'] = q.get('order')
|
||||
quality['size_min'] = tryInt(q.get('size_min'))
|
||||
quality['size_max'] = tryInt(q.get('size_max'))
|
||||
db.update(quality)
|
||||
|
||||
quality_link[x] = quality
|
||||
|
||||
# Titles
|
||||
titles = migrate_data['librarytitle']
|
||||
titles_by_library = {}
|
||||
for x in titles:
|
||||
title = titles[x]
|
||||
if title.get('default'):
|
||||
titles_by_library[title.get('libraries_id')] = title.get('title')
|
||||
|
||||
# Releases
|
||||
releaseinfos = migrate_data['releaseinfo']
|
||||
for x in releaseinfos:
|
||||
info = releaseinfos[x]
|
||||
|
||||
# Skip if release doesn't exist for this info
|
||||
if not migrate_data['release'].get(info.get('release_id')):
|
||||
continue
|
||||
|
||||
if not migrate_data['release'][info.get('release_id')].get('info'):
|
||||
migrate_data['release'][info.get('release_id')]['info'] = {}
|
||||
|
||||
migrate_data['release'][info.get('release_id')]['info'][info.get('identifier')] = info.get('value')
|
||||
|
||||
releases = migrate_data['release']
|
||||
releases_by_media = {}
|
||||
for x in releases:
|
||||
release = releases[x]
|
||||
if not releases_by_media.get(release.get('movie_id')):
|
||||
releases_by_media[release.get('movie_id')] = []
|
||||
|
||||
releases_by_media[release.get('movie_id')].append(release)
|
||||
|
||||
# Type ids
|
||||
types = migrate_data['filetype']
|
||||
type_by_id = {}
|
||||
for t in types:
|
||||
type = types[t]
|
||||
type_by_id[type.get('id')] = type
|
||||
|
||||
# Media
|
||||
log.info('Importing %s media items', len(migrate_data['movie']))
|
||||
statuses = migrate_data['status']
|
||||
libraries = migrate_data['library']
|
||||
library_files = migrate_data['library_files__file_library']
|
||||
releases_files = migrate_data['release_files__file_release']
|
||||
all_files = migrate_data['file']
|
||||
poster_type = migrate_data['filetype']['poster']
|
||||
medias = migrate_data['movie']
|
||||
for x in medias:
|
||||
m = medias[x]
|
||||
|
||||
status = statuses.get(m['status_id']).get('identifier')
|
||||
l = libraries.get(m['library_id'])
|
||||
|
||||
# Only migrate wanted movies, Skip if no identifier present
|
||||
if not l or not getImdb(l.get('identifier')): continue
|
||||
|
||||
profile_id = profile_link.get(m['profile_id'])
|
||||
category_id = category_link.get(m['category_id'])
|
||||
title = titles_by_library.get(m['library_id'])
|
||||
releases = releases_by_media.get(x, [])
|
||||
info = json.loads(l.get('info', ''))
|
||||
|
||||
files = library_files.get(m['library_id'], [])
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
|
||||
added_media = fireEvent('movie.add', {
|
||||
'info': info,
|
||||
'identifier': l.get('identifier'),
|
||||
'profile_id': profile_id,
|
||||
'category_id': category_id,
|
||||
'title': title
|
||||
}, force_readd = False, search_after = False, update_after = False, notify_after = False, status = status, single = True)
|
||||
|
||||
if not added_media:
|
||||
log.error('Failed adding media %s: %s', (l.get('identifier'), info))
|
||||
continue
|
||||
|
||||
added_media['files'] = added_media.get('files', {})
|
||||
for f in files:
|
||||
ffile = all_files[f.get('file_id')]
|
||||
|
||||
# Only migrate posters
|
||||
if ffile.get('type_id') == poster_type.get('id'):
|
||||
if ffile.get('path') not in added_media['files'].get('image_poster', []) and os.path.isfile(ffile.get('path')):
|
||||
added_media['files']['image_poster'] = [ffile.get('path')]
|
||||
break
|
||||
|
||||
if 'image_poster' in added_media['files']:
|
||||
db.update(added_media)
|
||||
|
||||
for rel in releases:
|
||||
|
||||
empty_info = False
|
||||
if not rel.get('info'):
|
||||
empty_info = True
|
||||
rel['info'] = {}
|
||||
|
||||
quality = quality_link.get(rel.get('quality_id'))
|
||||
if not quality:
|
||||
continue
|
||||
|
||||
release_status = statuses.get(rel.get('status_id')).get('identifier')
|
||||
|
||||
if rel['info'].get('download_id'):
|
||||
status_support = rel['info'].get('download_status_support', False) in [True, 'true', 'True']
|
||||
rel['info']['download_info'] = {
|
||||
'id': rel['info'].get('download_id'),
|
||||
'downloader': rel['info'].get('download_downloader'),
|
||||
'status_support': status_support,
|
||||
}
|
||||
|
||||
# Add status to keys
|
||||
rel['info']['status'] = release_status
|
||||
if not empty_info:
|
||||
fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True)
|
||||
else:
|
||||
release = {
|
||||
'_t': 'release',
|
||||
'identifier': rel.get('identifier'),
|
||||
'media_id': added_media.get('_id'),
|
||||
'quality': quality.get('identifier'),
|
||||
'status': release_status,
|
||||
'last_edit': int(time.time()),
|
||||
'files': {}
|
||||
}
|
||||
|
||||
# Add downloader info if provided
|
||||
try:
|
||||
release['download_info'] = rel['info']['download_info']
|
||||
del rel['download_info']
|
||||
except:
|
||||
pass
|
||||
|
||||
# Add files
|
||||
release_files = releases_files.get(rel.get('id'), [])
|
||||
if not isinstance(release_files, list):
|
||||
release_files = [release_files]
|
||||
|
||||
if len(release_files) == 0:
|
||||
continue
|
||||
|
||||
for f in release_files:
|
||||
rfile = all_files[f.get('file_id')]
|
||||
file_type = type_by_id.get(rfile.get('type_id')).get('identifier')
|
||||
|
||||
if not release['files'].get(file_type):
|
||||
release['files'][file_type] = []
|
||||
|
||||
release['files'][file_type].append(rfile.get('path'))
|
||||
|
||||
try:
|
||||
rls = db.get('release_identifier', rel.get('identifier'), with_doc = True)['doc']
|
||||
rls.update(release)
|
||||
db.update(rls)
|
||||
except:
|
||||
db.insert(release)
|
||||
|
||||
log.info('Total migration took %s', time.time() - migrate_start)
|
||||
log.info('=' * 30)
|
||||
|
||||
# rename old database
|
||||
log.info('Renaming old database to %s ', old_db + '.old')
|
||||
os.rename(old_db, old_db + '.old')
|
||||
|
||||
if os.path.isfile(old_db + '-wal'):
|
||||
os.rename(old_db + '-wal', old_db + '-wal.old')
|
||||
if os.path.isfile(old_db + '-shm'):
|
||||
os.rename(old_db + '-shm', old_db + '-shm.old')
|
||||
@@ -0,0 +1,13 @@
|
||||
config = [{
|
||||
'name': 'download_providers',
|
||||
'groups': [
|
||||
{
|
||||
'label': 'Downloaders',
|
||||
'description': 'You can select different downloaders for each type (usenet / torrent)',
|
||||
'type': 'list',
|
||||
'name': 'download_providers',
|
||||
'tab': 'downloaders',
|
||||
'options': [],
|
||||
},
|
||||
],
|
||||
}]
|
||||
|
||||
@@ -1,51 +1,37 @@
|
||||
from base64 import b32decode, b16encode
|
||||
import random
|
||||
import re
|
||||
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.base import Provider
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
from couchpotato.core.providers.base import Provider
|
||||
import random
|
||||
import re
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
## This is here to load the static files
|
||||
class Downloader(Plugin):
|
||||
pass
|
||||
|
||||
|
||||
class DownloaderBase(Provider):
|
||||
class Downloader(Provider):
|
||||
|
||||
protocol = []
|
||||
http_time_between_calls = 0
|
||||
status_support = True
|
||||
|
||||
torrent_sources = [
|
||||
'https://zoink.it/torrent/%s.torrent',
|
||||
'http://torrage.com/torrent/%s.torrent',
|
||||
'https://torcache.net/torrent/%s.torrent',
|
||||
]
|
||||
|
||||
torrent_trackers = [
|
||||
'udp://tracker.istole.it:80/announce',
|
||||
'http://tracker.istole.it/announce',
|
||||
'udp://fr33domtracker.h33t.com:3310/announce',
|
||||
'http://tracker.publicbt.com/announce',
|
||||
'udp://tracker.publicbt.com:80/announce',
|
||||
'udp://tracker.istole.it:80/announce',
|
||||
'udp://fr33domtracker.h33t.com:3310/announce',
|
||||
'http://tracker.istole.it/announce',
|
||||
'http://tracker.ccc.de/announce',
|
||||
'udp://tracker.publicbt.com:80/announce',
|
||||
'udp://tracker.ccc.de:80/announce',
|
||||
'http://exodus.desync.com/announce',
|
||||
'http://exodus.desync.com:6969/announce',
|
||||
'http://tracker.publichd.eu/announce',
|
||||
'udp://tracker.publichd.eu:80/announce',
|
||||
'http://tracker.openbittorrent.com/announce',
|
||||
'udp://tracker.openbittorrent.com/announce',
|
||||
'udp://tracker.openbittorrent.com:80/announce',
|
||||
'udp://open.demonii.com:1337/announce',
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
@@ -56,7 +42,6 @@ class DownloaderBase(Provider):
|
||||
addEvent('download.remove_failed', self._removeFailed)
|
||||
addEvent('download.pause', self._pause)
|
||||
addEvent('download.process_complete', self._processComplete)
|
||||
addApiView('download.%s.test' % self.getName().lower(), self._test)
|
||||
|
||||
def getEnabledProtocol(self):
|
||||
for download_protocol in self.protocol:
|
||||
@@ -73,9 +58,6 @@ class DownloaderBase(Provider):
|
||||
return
|
||||
return self.download(data = data, media = media, filedata = filedata)
|
||||
|
||||
def download(self, *args, **kwargs):
|
||||
return False
|
||||
|
||||
def _getAllDownloadStatus(self, download_ids):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
@@ -172,19 +154,10 @@ class DownloaderBase(Provider):
|
||||
if not data: data = {}
|
||||
|
||||
d_manual = self.conf('manual', default = False)
|
||||
return super(DownloaderBase, self).isEnabled() and \
|
||||
return super(Downloader, self).isEnabled() and \
|
||||
(d_manual and manual or d_manual is False) and \
|
||||
(not data or self.isCorrectProtocol(data.get('protocol')))
|
||||
|
||||
def _test(self, **kwargs):
|
||||
t = self.test()
|
||||
if isinstance(t, tuple):
|
||||
return {'success': t[0], 'msg': t[1]}
|
||||
return {'success': t}
|
||||
|
||||
def test(self):
|
||||
return False
|
||||
|
||||
def _pause(self, release_download, pause = True):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
@@ -198,7 +171,6 @@ class DownloaderBase(Provider):
|
||||
def pause(self, release_download, pause):
|
||||
return
|
||||
|
||||
|
||||
class ReleaseDownloadList(list):
|
||||
|
||||
provider = None
|
||||
@@ -225,7 +197,7 @@ class ReleaseDownloadList(list):
|
||||
'status': 'busy',
|
||||
'downloader': self.provider.getName(),
|
||||
'folder': '',
|
||||
'files': [],
|
||||
'files': '',
|
||||
}
|
||||
|
||||
return mergeDicts(defaults, result)
|
||||
55
couchpotato/core/downloaders/blackhole/__init__.py
Normal file
55
couchpotato/core/downloaders/blackhole/__init__.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from .main import Blackhole
|
||||
from couchpotato.core.helpers.variable import getDownloadDir
|
||||
|
||||
def start():
|
||||
return Blackhole()
|
||||
|
||||
config = [{
|
||||
'name': 'blackhole',
|
||||
'order': 30,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'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>.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': True,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb,torrent',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
|
||||
'default': getDownloadDir()
|
||||
},
|
||||
{
|
||||
'name': 'use_for',
|
||||
'label': 'Use for',
|
||||
'default': 'both',
|
||||
'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,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,20 +1,14 @@
|
||||
from __future__ import with_statement
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.helpers.variable import getDownloadDir
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Blackhole'
|
||||
|
||||
|
||||
class Blackhole(DownloaderBase):
|
||||
class Blackhole(Downloader):
|
||||
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
status_support = False
|
||||
@@ -73,20 +67,6 @@ class Blackhole(DownloaderBase):
|
||||
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
directory = self.conf('directory')
|
||||
if directory and os.path.isdir(directory):
|
||||
|
||||
test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
|
||||
|
||||
# Check if folder is writable
|
||||
self.createFile(test_file, 'This is a test file')
|
||||
if os.path.isfile(test_file):
|
||||
os.remove(test_file)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def getEnabledProtocol(self):
|
||||
if self.conf('use_for') == 'both':
|
||||
return super(Blackhole, self).getEnabledProtocol()
|
||||
@@ -105,54 +85,3 @@ class Blackhole(DownloaderBase):
|
||||
|
||||
return super(Blackhole, self).isEnabled(manual, data) and \
|
||||
((self.conf('use_for') in for_protocol))
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'blackhole',
|
||||
'order': 30,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'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>.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': True,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb,torrent',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
|
||||
'default': getDownloadDir()
|
||||
},
|
||||
{
|
||||
'name': 'use_for',
|
||||
'label': 'Use for',
|
||||
'default': 'both',
|
||||
'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,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
90
couchpotato/core/downloaders/deluge/__init__.py
Normal file
90
couchpotato/core/downloaders/deluge/__init__.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from .main import Deluge
|
||||
|
||||
def start():
|
||||
return Deluge()
|
||||
|
||||
config = [{
|
||||
'name': 'deluge',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'deluge',
|
||||
'label': 'Deluge',
|
||||
'description': 'Use <a href="http://www.deluge-torrent.org/" target="_blank">Deluge</a> to download torrents.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:58846',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:58846</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Download to this directory. Keep empty for default Deluge download directory.',
|
||||
},
|
||||
{
|
||||
'name': 'completed_directory',
|
||||
'type': 'directory',
|
||||
'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'label',
|
||||
'description': 'Label to add to torrents in the Deluge UI.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'description': 'Remove the torrent from Deluge after it has finished seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,37 +1,33 @@
|
||||
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 couchpotato.core.helpers.variable import tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from synchronousdeluge import DelugeClient
|
||||
import os.path
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from bencode import bencode as benc, bdecode
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import isInt, sp
|
||||
from couchpotato.core.helpers.variable import tryFloat, cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from synchronousdeluge import DelugeClient
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Deluge'
|
||||
|
||||
|
||||
class Deluge(DownloaderBase):
|
||||
class Deluge(Downloader):
|
||||
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
drpc = None
|
||||
|
||||
def connect(self, reconnect = False):
|
||||
def connect(self):
|
||||
# Load host from config and split out port.
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
host = self.conf('host').split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
if not self.drpc or reconnect:
|
||||
if not self.drpc:
|
||||
self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
|
||||
return self.drpc
|
||||
@@ -90,11 +86,6 @@ class Deluge(DownloaderBase):
|
||||
log.info('Torrent sent to Deluge successfully.')
|
||||
return self.downloadReturnId(remote_torrent)
|
||||
|
||||
def test(self):
|
||||
if self.connect(True) and self.drpc.test():
|
||||
return True
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking Deluge download status.')
|
||||
@@ -112,13 +103,8 @@ class Deluge(DownloaderBase):
|
||||
|
||||
for torrent_id in queue:
|
||||
torrent = queue[torrent_id]
|
||||
|
||||
if not 'hash' in torrent:
|
||||
# When given a list of ids, deluge will return an empty item for a non-existant torrent.
|
||||
continue
|
||||
|
||||
log.debug('name=%s / id=%s / save_path=%s / move_on_completed=%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_on_completed'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
|
||||
|
||||
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
|
||||
|
||||
# Deluge has no easy way to work out if a torrent is stalled or failing.
|
||||
#status = 'failed'
|
||||
status = 'busy'
|
||||
@@ -134,11 +120,11 @@ class Deluge(DownloaderBase):
|
||||
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'],
|
||||
@@ -147,7 +133,7 @@ class Deluge(DownloaderBase):
|
||||
'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': torrent_files,
|
||||
'files': '|'.join(torrent_files),
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
@@ -166,7 +152,6 @@ class Deluge(DownloaderBase):
|
||||
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)
|
||||
|
||||
|
||||
class DelugeRPC(object):
|
||||
|
||||
host = 'localhost'
|
||||
@@ -187,13 +172,6 @@ class DelugeRPC(object):
|
||||
self.client = DelugeClient()
|
||||
self.client.connect(self.host, int(self.port), self.username, self.password)
|
||||
|
||||
def test(self):
|
||||
try:
|
||||
self.connect()
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
|
||||
def add_torrent_magnet(self, torrent, options):
|
||||
torrent_id = False
|
||||
try:
|
||||
@@ -204,7 +182,7 @@ class DelugeRPC(object):
|
||||
|
||||
if torrent_id and options['label']:
|
||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||
except Exception as err:
|
||||
except Exception, err:
|
||||
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
@@ -222,7 +200,7 @@ class DelugeRPC(object):
|
||||
|
||||
if torrent_id and options['label']:
|
||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||
except Exception as err:
|
||||
except Exception, err:
|
||||
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
@@ -234,8 +212,8 @@ class DelugeRPC(object):
|
||||
ret = False
|
||||
try:
|
||||
self.connect()
|
||||
ret = self.client.core.get_torrents_status({'id': ids}, ('name', 'hash', 'save_path', 'move_completed_path', 'progress', 'state', 'eta', 'ratio', 'stop_ratio', 'is_seed', 'is_finished', 'paused', 'move_on_completed', 'files')).get()
|
||||
except Exception as err:
|
||||
ret = self.client.core.get_torrents_status({'id': ids}, {}).get()
|
||||
except Exception, err:
|
||||
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
@@ -246,7 +224,7 @@ class DelugeRPC(object):
|
||||
try:
|
||||
self.connect()
|
||||
self.client.core.pause_torrent(torrent_ids).get()
|
||||
except Exception as err:
|
||||
except Exception, err:
|
||||
log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
@@ -256,7 +234,7 @@ class DelugeRPC(object):
|
||||
try:
|
||||
self.connect()
|
||||
self.client.core.resume_torrent(torrent_ids).get()
|
||||
except Exception as err:
|
||||
except Exception, err:
|
||||
log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
@@ -267,7 +245,7 @@ class DelugeRPC(object):
|
||||
try:
|
||||
self.connect()
|
||||
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
|
||||
except Exception as err:
|
||||
except Exception, err:
|
||||
log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
@@ -295,90 +273,3 @@ class DelugeRPC(object):
|
||||
return torrent_hash
|
||||
|
||||
return False
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'deluge',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'deluge',
|
||||
'label': 'Deluge',
|
||||
'description': 'Use <a href="http://www.deluge-torrent.org/" target="_blank">Deluge</a> to download torrents.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:58846',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:58846</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Download to this directory. Keep empty for default Deluge download directory.',
|
||||
},
|
||||
{
|
||||
'name': 'completed_directory',
|
||||
'type': 'directory',
|
||||
'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'label',
|
||||
'description': 'Label to add to torrents in the Deluge UI.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'description': 'Remove the torrent from Deluge after it has finished seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
76
couchpotato/core/downloaders/nzbget/__init__.py
Normal file
76
couchpotato/core/downloaders/nzbget/__init__.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from .main import NZBGet
|
||||
|
||||
def start():
|
||||
return NZBGet()
|
||||
|
||||
config = [{
|
||||
'name': 'nzbget',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'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',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'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',
|
||||
'advanced': True,
|
||||
'description': 'Set a different username to connect. Default: nzbget',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
|
||||
},
|
||||
{
|
||||
'name': 'category',
|
||||
'default': 'Movies',
|
||||
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'priority',
|
||||
'advanced': True,
|
||||
'default': '0',
|
||||
'type': 'dropdown',
|
||||
'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)],
|
||||
'description': 'Only change this if you are using NZBget 9.0 or higher',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,4 +1,8 @@
|
||||
from base64 import standard_b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import ss, sp
|
||||
from couchpotato.core.helpers.variable import tryInt, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import shutil
|
||||
@@ -6,21 +10,14 @@ import socket
|
||||
import traceback
|
||||
import xmlrpclib
|
||||
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import ss, sp
|
||||
from couchpotato.core.helpers.variable import tryInt, md5, cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'NZBGet'
|
||||
|
||||
|
||||
class NZBGet(DownloaderBase):
|
||||
class NZBGet(Downloader):
|
||||
|
||||
protocol = ['nzb']
|
||||
rpc = 'xmlrpc'
|
||||
|
||||
url = '%(protocol)s://%(username)s:%(password)s@%(host)s/xmlrpc'
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
@@ -32,10 +29,10 @@ class NZBGet(DownloaderBase):
|
||||
|
||||
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))
|
||||
|
||||
rpc = self.getRPC()
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name):
|
||||
log.debug('Successfully connected to NZBGet')
|
||||
@@ -44,7 +41,7 @@ class NZBGet(DownloaderBase):
|
||||
except socket.error:
|
||||
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
|
||||
return False
|
||||
except xmlrpclib.ProtocolError as e:
|
||||
except xmlrpclib.ProtocolError, e:
|
||||
if e.errcode == 401:
|
||||
log.error('Password is incorrect.')
|
||||
else:
|
||||
@@ -58,7 +55,7 @@ class NZBGet(DownloaderBase):
|
||||
|
||||
if xml_response:
|
||||
log.info('NZB sent successfully to NZBGet')
|
||||
nzb_id = md5(data['url']) # about as unique as they come ;)
|
||||
nzb_id = md5(data['url']) # about as unique as they come ;)
|
||||
couchpotato_id = "couchpotato=" + nzb_id
|
||||
groups = rpc.listgroups()
|
||||
file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name]
|
||||
@@ -70,32 +67,13 @@ class NZBGet(DownloaderBase):
|
||||
log.error('NZBGet could not add %s to the queue.', nzb_name)
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
rpc = self.getRPC()
|
||||
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to test connection'):
|
||||
log.debug('Successfully connected to NZBGet')
|
||||
else:
|
||||
log.info('Successfully connected to NZBGet, but unable to send a message')
|
||||
except socket.error:
|
||||
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
|
||||
return False
|
||||
except xmlrpclib.ProtocolError as e:
|
||||
if e.errcode == 401:
|
||||
log.error('Password is incorrect.')
|
||||
else:
|
||||
log.error('Protocol Error: %s', e)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking NZBGet download status.')
|
||||
|
||||
rpc = self.getRPC()
|
||||
url = self.url % {'protocol': 'https' if self.conf('ssl') else 'http', 'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to check status'):
|
||||
log.debug('Successfully connected to NZBGet')
|
||||
@@ -104,7 +82,7 @@ class NZBGet(DownloaderBase):
|
||||
except socket.error:
|
||||
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
|
||||
return []
|
||||
except xmlrpclib.ProtocolError as e:
|
||||
except xmlrpclib.ProtocolError, e:
|
||||
if e.errcode == 401:
|
||||
log.error('Password is incorrect.')
|
||||
else:
|
||||
@@ -137,7 +115,7 @@ class NZBGet(DownloaderBase):
|
||||
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
@@ -146,7 +124,7 @@ class NZBGet(DownloaderBase):
|
||||
'timeleft': timeleft,
|
||||
})
|
||||
|
||||
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||
if nzb['NZBID'] in ids:
|
||||
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
|
||||
release_downloads.append({
|
||||
@@ -179,8 +157,9 @@ class NZBGet(DownloaderBase):
|
||||
|
||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||
|
||||
rpc = self.getRPC()
|
||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
|
||||
log.debug('Successfully connected to NZBGet')
|
||||
@@ -189,7 +168,7 @@ class NZBGet(DownloaderBase):
|
||||
except socket.error:
|
||||
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
|
||||
return False
|
||||
except xmlrpclib.ProtocolError as e:
|
||||
except xmlrpclib.ProtocolError, e:
|
||||
if e.errcode == 401:
|
||||
log.error('Password is incorrect.')
|
||||
else:
|
||||
@@ -214,80 +193,3 @@ class NZBGet(DownloaderBase):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def getRPC(self):
|
||||
url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc
|
||||
return xmlrpclib.ServerProxy(url)
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'nzbget',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'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',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'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',
|
||||
'advanced': True,
|
||||
'description': 'Set a different username to connect. Default: nzbget',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
|
||||
},
|
||||
{
|
||||
'name': 'category',
|
||||
'default': 'Movies',
|
||||
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'priority',
|
||||
'advanced': True,
|
||||
'default': '0',
|
||||
'type': 'dropdown',
|
||||
'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)],
|
||||
'description': 'Only change this if you are using NZBget 9.0 or higher',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
48
couchpotato/core/downloaders/nzbvortex/__init__.py
Normal file
48
couchpotato/core/downloaders/nzbvortex/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from .main import NZBVortex
|
||||
|
||||
def start():
|
||||
return NZBVortex()
|
||||
|
||||
config = [{
|
||||
'name': 'nzbvortex',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'nzbvortex',
|
||||
'label': 'NZBVortex',
|
||||
'description': 'Use <a href="http://www.nzbvortex.com/landing/" target="_blank">NZBVortex</a> to download NZBs.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'https://localhost:4321',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'Api Key',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,4 +1,8 @@
|
||||
from base64 import b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, sp
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from urllib2 import URLError
|
||||
from uuid import uuid4
|
||||
import hashlib
|
||||
@@ -12,18 +16,10 @@ import time
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, sp
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'NZBVortex'
|
||||
|
||||
|
||||
class NZBVortex(DownloaderBase):
|
||||
class NZBVortex(Downloader):
|
||||
|
||||
protocol = ['nzb']
|
||||
api_level = None
|
||||
@@ -40,20 +36,12 @@ class NZBVortex(DownloaderBase):
|
||||
|
||||
time.sleep(10)
|
||||
raw_statuses = self.call('nzb')
|
||||
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(nzb['nzbFileName']) == nzb_filename][0]
|
||||
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(item['nzbFileName']) == 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 test(self):
|
||||
try:
|
||||
login_result = self.login()
|
||||
except:
|
||||
return False
|
||||
|
||||
return login_result
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
raw_statuses = self.call('nzb')
|
||||
@@ -68,13 +56,13 @@ class NZBVortex(DownloaderBase):
|
||||
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,
|
||||
'timeleft':-1,
|
||||
'folder': sp(nzb['destinationPath']),
|
||||
})
|
||||
|
||||
@@ -114,6 +102,7 @@ class NZBVortex(DownloaderBase):
|
||||
log.error('Login failed, please check you api-key')
|
||||
return False
|
||||
|
||||
|
||||
def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs):
|
||||
|
||||
# Login first
|
||||
@@ -127,14 +116,14 @@ class NZBVortex(DownloaderBase):
|
||||
|
||||
params = tryUrlencode(parameters)
|
||||
|
||||
url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api/' + call
|
||||
url = cleanHost(self.conf('host')) + 'api/' + call
|
||||
|
||||
try:
|
||||
data = self.urlopen('%s?%s' % (url, params), *args, **kwargs)
|
||||
|
||||
if data:
|
||||
return json.loads(data)
|
||||
except URLError as e:
|
||||
except URLError, e:
|
||||
if hasattr(e, 'code') and e.code == 403:
|
||||
# Try login and do again
|
||||
if not repeat:
|
||||
@@ -156,7 +145,7 @@ class NZBVortex(DownloaderBase):
|
||||
try:
|
||||
data = self.urlopen(url, show_error = False)
|
||||
self.api_level = float(json.loads(data).get('apilevel'))
|
||||
except URLError as e:
|
||||
except URLError, e:
|
||||
if hasattr(e, 'code') and e.code == 403:
|
||||
log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher')
|
||||
else:
|
||||
@@ -186,60 +175,6 @@ class HTTPSConnection(httplib.HTTPSConnection):
|
||||
|
||||
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1)
|
||||
|
||||
|
||||
class HTTPSHandler(urllib2.HTTPSHandler):
|
||||
def https_open(self, req):
|
||||
return self.do_open(HTTPSConnection, req)
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'nzbvortex',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'nzbvortex',
|
||||
'label': 'NZBVortex',
|
||||
'description': 'Use <a href="http://www.nzbvortex.com/landing/" target="_blank">NZBVortex</a> to download NZBs.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:4321',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:4321</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'ssl',
|
||||
'default': 1,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'Api Key',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,111 +0,0 @@
|
||||
from __future__ import with_statement
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.logger import CPLog
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Pneumatic'
|
||||
|
||||
|
||||
class Pneumatic(DownloaderBase):
|
||||
|
||||
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 = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
if not directory or not os.path.isdir(directory):
|
||||
log.error('No directory set for .strm downloads.')
|
||||
else:
|
||||
try:
|
||||
if not filedata or len(filedata) < 50:
|
||||
log.error('No nzb available!')
|
||||
return False
|
||||
|
||||
full_path = os.path.join(directory, self.createFileName(data, filedata, media))
|
||||
|
||||
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:
|
||||
f.write(filedata)
|
||||
|
||||
nzb_name = self.createNzbName(data, media)
|
||||
strm_path = os.path.join(directory, nzb_name)
|
||||
|
||||
strm_file = open(strm_path + '.strm', 'wb')
|
||||
strmContent = self.strm_syntax % (full_path, nzb_name)
|
||||
strm_file.write(strmContent)
|
||||
strm_file.close()
|
||||
|
||||
return self.downloadReturnId('')
|
||||
|
||||
else:
|
||||
log.info('File %s already exists.', full_path)
|
||||
return self.downloadReturnId('')
|
||||
|
||||
except:
|
||||
log.error('Failed to download .strm: %s', traceback.format_exc())
|
||||
pass
|
||||
|
||||
except:
|
||||
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
|
||||
return False
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
directory = self.conf('directory')
|
||||
if directory and os.path.isdir(directory):
|
||||
|
||||
test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
|
||||
|
||||
# Check if folder is writable
|
||||
self.createFile(test_file, 'This is a test file')
|
||||
if os.path.isfile(test_file):
|
||||
os.remove(test_file)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'pneumatic',
|
||||
'order': 30,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'pneumatic',
|
||||
'label': 'Pneumatic',
|
||||
'description': 'Use <a href="http://forum.xbmc.org/showthread.php?tid=97657" target="_blank">Pneumatic</a> to download .strm files.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Directory where the .strm file is saved to.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
37
couchpotato/core/downloaders/pneumatic/__init__.py
Normal file
37
couchpotato/core/downloaders/pneumatic/__init__.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from .main import Pneumatic
|
||||
|
||||
def start():
|
||||
return Pneumatic()
|
||||
|
||||
config = [{
|
||||
'name': 'pneumatic',
|
||||
'order': 30,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'pneumatic',
|
||||
'label': 'Pneumatic',
|
||||
'description': 'Use <a href="http://forum.xbmc.org/showthread.php?tid=97657" target="_blank">Pneumatic</a> to download .strm files.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Directory where the .strm file is saved to.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
58
couchpotato/core/downloaders/pneumatic/main.py
Normal file
58
couchpotato/core/downloaders/pneumatic/main.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from __future__ import with_statement
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.logger import CPLog
|
||||
import os
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
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 = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
if not directory or not os.path.isdir(directory):
|
||||
log.error('No directory set for .strm downloads.')
|
||||
else:
|
||||
try:
|
||||
if not filedata or len(filedata) < 50:
|
||||
log.error('No nzb available!')
|
||||
return False
|
||||
|
||||
fullPath = os.path.join(directory, self.createFileName(data, filedata, media))
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
nzb_name = self.createNzbName(data, media)
|
||||
strm_path = os.path.join(directory, nzb_name)
|
||||
|
||||
strm_file = open(strm_path + '.strm', 'wb')
|
||||
strmContent = self.strm_syntax % (fullPath, nzb_name)
|
||||
strm_file.write(strmContent)
|
||||
strm_file.close()
|
||||
|
||||
return self.downloadReturnId('')
|
||||
|
||||
else:
|
||||
log.info('File %s already exists.', fullPath)
|
||||
return self.downloadReturnId('')
|
||||
|
||||
except:
|
||||
log.error('Failed to download .strm: %s', traceback.format_exc())
|
||||
pass
|
||||
|
||||
except:
|
||||
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
|
||||
return False
|
||||
return False
|
||||
@@ -1,245 +0,0 @@
|
||||
from base64 import b16encode, b32decode
|
||||
from hashlib import sha1
|
||||
import os
|
||||
|
||||
from bencode import bencode, bdecode
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from qbittorrent.client import QBittorrentClient
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'qBittorrent'
|
||||
|
||||
|
||||
class qBittorrent(DownloaderBase):
|
||||
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
qb = None
|
||||
|
||||
def __init__(self):
|
||||
super(qBittorrent, self).__init__()
|
||||
|
||||
def connect(self):
|
||||
if self.qb is not None:
|
||||
return self.qb
|
||||
|
||||
url = cleanHost(self.conf('host'), protocol = True, ssl = False)
|
||||
|
||||
if self.conf('username') and self.conf('password'):
|
||||
self.qb = QBittorrentClient(
|
||||
url,
|
||||
username = self.conf('username'),
|
||||
password = self.conf('password')
|
||||
)
|
||||
else:
|
||||
self.qb = QBittorrentClient(url)
|
||||
|
||||
return self.qb
|
||||
|
||||
def test(self):
|
||||
if self.connect():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.debug('Sending "%s" to qBittorrent.', (data.get('name')))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
if not filedata and data.get('protocol') == 'torrent':
|
||||
log.error('Failed sending torrent, no data')
|
||||
return False
|
||||
|
||||
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
filedata = self.magnetToTorrent(data.get('url'))
|
||||
|
||||
if filedata is False:
|
||||
return False
|
||||
|
||||
data['protocol'] = 'torrent'
|
||||
|
||||
info = bdecode(filedata)["info"]
|
||||
torrent_hash = sha1(bencode(info)).hexdigest()
|
||||
|
||||
# Convert base 32 to hex
|
||||
if len(torrent_hash) == 32:
|
||||
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||
|
||||
# Send request to qBittorrent
|
||||
try:
|
||||
self.qb.add_file(filedata)
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
except Exception as e:
|
||||
log.error('Failed to send torrent to qBittorrent: %s', e)
|
||||
return False
|
||||
|
||||
def getTorrentStatus(self, torrent):
|
||||
|
||||
if torrent.state in ('uploading', 'queuedUP', 'stalledUP'):
|
||||
return 'seeding'
|
||||
|
||||
if torrent.progress == 1:
|
||||
return 'completed'
|
||||
|
||||
return 'busy'
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
log.debug('Checking qBittorrent download status.')
|
||||
|
||||
if not self.connect():
|
||||
return []
|
||||
|
||||
try:
|
||||
torrents = self.qb.get_torrents()
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
|
||||
for torrent in torrents:
|
||||
if torrent.hash in ids:
|
||||
torrent.update_general() # get extra info
|
||||
torrent_filelist = torrent.get_files()
|
||||
|
||||
torrent_files = []
|
||||
torrent_dir = os.path.join(torrent.save_path, torrent.name)
|
||||
|
||||
if os.path.isdir(torrent_dir):
|
||||
torrent.save_path = torrent_dir
|
||||
|
||||
if len(torrent_filelist) > 1 and os.path.isdir(torrent_dir): # multi file torrent, path.isdir check makes sure we're not in the root download folder
|
||||
for root, _, files in os.walk(torrent.save_path):
|
||||
for f in files:
|
||||
torrent_files.append(sp(os.path.join(root, f)))
|
||||
|
||||
else: # multi or single file placed directly in torrent.save_path
|
||||
for f in torrent_filelist:
|
||||
file_path = os.path.join(torrent.save_path, f.name)
|
||||
if os.path.isfile(file_path):
|
||||
torrent_files.append(sp(file_path))
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent.hash,
|
||||
'name': torrent.name,
|
||||
'status': self.getTorrentStatus(torrent),
|
||||
'seed_ratio': torrent.ratio,
|
||||
'original_status': torrent.state,
|
||||
'timeleft': torrent.progress * 100 if torrent.progress else -1, # percentage
|
||||
'folder': sp(torrent.save_path),
|
||||
'files': torrent_files
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
except Exception as e:
|
||||
log.error('Failed to get status from qBittorrent: %s', e)
|
||||
return []
|
||||
|
||||
def pause(self, release_download, pause = True):
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent = self.qb.get_torrent(release_download['id'])
|
||||
if torrent is None:
|
||||
return False
|
||||
|
||||
if pause:
|
||||
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 processComplete(self, release_download, delete_files):
|
||||
log.debug('Requesting qBittorrent to remove the torrent %s%s.',
|
||||
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent = self.qb.find_torrent(release_download['id'])
|
||||
|
||||
if torrent is None:
|
||||
return False
|
||||
|
||||
if delete_files:
|
||||
torrent.delete() # deletes torrent with data
|
||||
else:
|
||||
torrent.remove() # just removes the torrent, doesn't delete data
|
||||
|
||||
return True
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'qbittorrent',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'qbittorrent',
|
||||
'label': 'qbittorrent',
|
||||
'description': '',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'http://localhost:8080/',
|
||||
'description': 'RPC Communication URI. Usually <strong>http://localhost:8080/</strong>'
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'default': False,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the torrent after it finishes seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
78
couchpotato/core/downloaders/rtorrent/__init__.py
Executable file
78
couchpotato/core/downloaders/rtorrent/__init__.py
Executable file
@@ -0,0 +1,78 @@
|
||||
from .main import rTorrent
|
||||
|
||||
def start():
|
||||
return rTorrent()
|
||||
|
||||
config = [{
|
||||
'name': 'rtorrent',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'rtorrent',
|
||||
'label': 'rTorrent',
|
||||
'description': '',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'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',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'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',
|
||||
'default': False,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the torrent after it finishes seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
229
couchpotato/core/downloaders/rtorrent/main.py
Executable file
229
couchpotato/core/downloaders/rtorrent/main.py
Executable file
@@ -0,0 +1,229 @@
|
||||
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.logger import CPLog
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from rtorrent import RTorrent
|
||||
from rtorrent.err import MethodError
|
||||
import os
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class rTorrent(Downloader):
|
||||
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
rt = None
|
||||
|
||||
def connect(self):
|
||||
# Already connected?
|
||||
if self.rt is not None:
|
||||
return self.rt
|
||||
|
||||
# Ensure url is set
|
||||
if not self.conf('url'):
|
||||
log.error('Config properties are not filled in correctly, url is missing.')
|
||||
return False
|
||||
|
||||
if self.conf('username') and self.conf('password'):
|
||||
self.rt = RTorrent(
|
||||
self.conf('url'),
|
||||
self.conf('username'),
|
||||
self.conf('password')
|
||||
)
|
||||
else:
|
||||
self.rt = RTorrent(self.conf('url'))
|
||||
|
||||
return self.rt
|
||||
|
||||
def _update_provider_group(self, name, data):
|
||||
if data.get('seed_time'):
|
||||
log.info('seeding time ignored, not supported')
|
||||
|
||||
if not name:
|
||||
return False
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
views = self.rt.get_views()
|
||||
|
||||
if name not in views:
|
||||
self.rt.create_group(name)
|
||||
|
||||
group = self.rt.get_group(name)
|
||||
|
||||
try:
|
||||
if data.get('seed_ratio'):
|
||||
ratio = int(float(data.get('seed_ratio')) * 100)
|
||||
log.debug('Updating provider ratio to %s, group name: %s', (ratio, name))
|
||||
|
||||
# Explicitly set all group options to ensure it is setup correctly
|
||||
group.set_upload('1M')
|
||||
group.set_min(ratio)
|
||||
group.set_max(ratio)
|
||||
group.set_command('d.stop')
|
||||
group.enable()
|
||||
else:
|
||||
# Reset group action and disable it
|
||||
group.set_command()
|
||||
group.disable()
|
||||
except MethodError, err:
|
||||
log.error('Unable to set group options: %s', err.msg)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
group_name = 'cp_' + data.get('provider').lower()
|
||||
if not self._update_provider_group(group_name, data):
|
||||
return False
|
||||
|
||||
torrent_params = {}
|
||||
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
|
||||
|
||||
# Try download magnet torrents
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
filedata = self.magnetToTorrent(data.get('url'))
|
||||
|
||||
if filedata is False:
|
||||
return False
|
||||
|
||||
data['protocol'] = 'torrent'
|
||||
|
||||
info = bdecode(filedata)["info"]
|
||||
torrent_hash = sha1(bencode(info)).hexdigest().upper()
|
||||
|
||||
# Convert base 32 to hex
|
||||
if len(torrent_hash) == 32:
|
||||
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||
|
||||
# Send request to rTorrent
|
||||
try:
|
||||
# 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)
|
||||
|
||||
# Start torrent
|
||||
if not self.conf('paused', default = 0):
|
||||
torrent.start()
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
except Exception, err:
|
||||
log.error('Failed to send torrent to rTorrent: %s', err)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
log.debug('Checking rTorrent download status.')
|
||||
|
||||
if not self.connect():
|
||||
return []
|
||||
|
||||
try:
|
||||
torrents = self.rt.get_torrents()
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
|
||||
for torrent in torrents:
|
||||
if torrent.info_hash in ids:
|
||||
torrent_files = []
|
||||
for file_item in torrent.get_files():
|
||||
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
|
||||
|
||||
status = 'busy'
|
||||
if torrent.complete:
|
||||
if torrent.active:
|
||||
status = 'seeding'
|
||||
else:
|
||||
status = 'completed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent.info_hash,
|
||||
'name': torrent.name,
|
||||
'status': status,
|
||||
'seed_ratio': torrent.ratio,
|
||||
'original_status': torrent.state,
|
||||
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
|
||||
'folder': sp(torrent.directory),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed to get status from rTorrent: %s', err)
|
||||
return []
|
||||
|
||||
def pause(self, release_download, pause = True):
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent = self.rt.find_torrent(release_download['id'])
|
||||
if torrent is None:
|
||||
return False
|
||||
|
||||
if pause:
|
||||
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 processComplete(self, release_download, delete_files):
|
||||
log.debug('Requesting rTorrent to remove the torrent %s%s.',
|
||||
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent = self.rt.find_torrent(release_download['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
|
||||
|
||||
return True
|
||||
@@ -1,335 +0,0 @@
|
||||
from base64 import b16encode, b32decode
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from urlparse import urlparse
|
||||
import os
|
||||
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.helpers.variable import cleanHost, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from bencode import bencode, bdecode
|
||||
from rtorrent import RTorrent
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'rTorrent'
|
||||
|
||||
|
||||
class rTorrent(DownloaderBase):
|
||||
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
rt = None
|
||||
error_msg = ''
|
||||
|
||||
# Migration url to host options
|
||||
def __init__(self):
|
||||
super(rTorrent, self).__init__()
|
||||
|
||||
addEvent('app.load', self.migrate)
|
||||
addEvent('setting.save.rtorrent.*.after', self.settingsChanged)
|
||||
|
||||
def migrate(self):
|
||||
|
||||
url = self.conf('url')
|
||||
if url:
|
||||
host_split = splitString(url.split('://')[-1], split_on = '/')
|
||||
|
||||
self.conf('ssl', value = url.startswith('https'))
|
||||
self.conf('host', value = host_split[0].strip())
|
||||
self.conf('rpc_url', value = '/'.join(host_split[1:]))
|
||||
|
||||
self.deleteConf('url')
|
||||
|
||||
def settingsChanged(self):
|
||||
# Reset active connection if settings have changed
|
||||
if self.rt:
|
||||
log.debug('Settings have changed, closing active connection')
|
||||
|
||||
self.rt = None
|
||||
return True
|
||||
|
||||
def connect(self, reconnect = False):
|
||||
# Already connected?
|
||||
if not reconnect and self.rt is not None:
|
||||
return self.rt
|
||||
|
||||
url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl'))
|
||||
|
||||
# Automatically add '+https' to 'httprpc' protocol if SSL is enabled
|
||||
if self.conf('ssl') and url.startswith('httprpc://'):
|
||||
url = url.replace('httprpc://', 'httprpc+https://')
|
||||
|
||||
parsed = urlparse(url)
|
||||
|
||||
# rpc_url is only used on http/https scgi pass-through
|
||||
if parsed.scheme in ['http', 'https']:
|
||||
url += self.conf('rpc_url')
|
||||
|
||||
self.rt = RTorrent(
|
||||
url,
|
||||
self.conf('username'),
|
||||
self.conf('password')
|
||||
)
|
||||
|
||||
self.error_msg = ''
|
||||
try:
|
||||
self.rt._verify_conn()
|
||||
except AssertionError as e:
|
||||
self.error_msg = e.message
|
||||
self.rt = None
|
||||
|
||||
return self.rt
|
||||
|
||||
def test(self):
|
||||
if self.connect(True):
|
||||
return True
|
||||
|
||||
if self.error_msg:
|
||||
return False, 'Connection failed: ' + self.error_msg
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent_params = {}
|
||||
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
|
||||
|
||||
# Try download magnet torrents
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
filedata = self.magnetToTorrent(data.get('url'))
|
||||
|
||||
if filedata is False:
|
||||
return False
|
||||
|
||||
data['protocol'] = 'torrent'
|
||||
|
||||
info = bdecode(filedata)["info"]
|
||||
torrent_hash = sha1(bencode(info)).hexdigest().upper()
|
||||
|
||||
# Convert base 32 to hex
|
||||
if len(torrent_hash) == 32:
|
||||
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||
|
||||
# Send request to rTorrent
|
||||
try:
|
||||
# Send torrent to rTorrent
|
||||
torrent = self.rt.load_torrent(filedata, verify_retries=10)
|
||||
|
||||
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'))
|
||||
|
||||
# Start torrent
|
||||
if not self.conf('paused', default = 0):
|
||||
torrent.start()
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
except Exception as err:
|
||||
log.error('Failed to send torrent to rTorrent: %s', err)
|
||||
return False
|
||||
|
||||
def getTorrentStatus(self, torrent):
|
||||
if not torrent.complete:
|
||||
return 'busy'
|
||||
|
||||
if torrent.open:
|
||||
return 'seeding'
|
||||
|
||||
return 'completed'
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
log.debug('Checking rTorrent download status.')
|
||||
|
||||
if not self.connect():
|
||||
return []
|
||||
|
||||
try:
|
||||
torrents = self.rt.get_torrents()
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
|
||||
for torrent in torrents:
|
||||
if torrent.info_hash in ids:
|
||||
torrent_directory = os.path.normpath(torrent.directory)
|
||||
torrent_files = []
|
||||
|
||||
for file in torrent.get_files():
|
||||
if not os.path.normpath(file.path).startswith(torrent_directory):
|
||||
file_path = os.path.join(torrent_directory, file.path.lstrip('/'))
|
||||
else:
|
||||
file_path = file.path
|
||||
|
||||
torrent_files.append(sp(file_path))
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent.info_hash,
|
||||
'name': torrent.name,
|
||||
'status': self.getTorrentStatus(torrent),
|
||||
'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': torrent_files
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
except Exception as err:
|
||||
log.error('Failed to get status from rTorrent: %s', err)
|
||||
return []
|
||||
|
||||
def pause(self, release_download, pause = True):
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent = self.rt.find_torrent(release_download['id'])
|
||||
if torrent is None:
|
||||
return False
|
||||
|
||||
if pause:
|
||||
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 processComplete(self, release_download, delete_files):
|
||||
log.debug('Requesting rTorrent to remove the torrent %s%s.',
|
||||
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent = self.rt.find_torrent(release_download['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(sp(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
|
||||
|
||||
return True
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'rtorrent',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'rtorrent',
|
||||
'label': 'rTorrent',
|
||||
'description': '',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:80',
|
||||
'description': 'RPC Communication URI. Usually <strong>scgi://localhost:5000</strong>, '
|
||||
'<strong>httprpc://localhost/rutorrent</strong> or <strong>localhost:80</strong>'
|
||||
},
|
||||
{
|
||||
'name': 'ssl',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'rpc_url',
|
||||
'type': 'string',
|
||||
'default': 'RPC2',
|
||||
'advanced': True,
|
||||
'description': 'Change if your RPC mount is at a different path.',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'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',
|
||||
'default': False,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the torrent after it finishes seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
71
couchpotato/core/downloaders/sabnzbd/__init__.py
Normal file
71
couchpotato/core/downloaders/sabnzbd/__init__.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from .main import Sabnzbd
|
||||
|
||||
def start():
|
||||
return Sabnzbd()
|
||||
|
||||
config = [{
|
||||
'name': 'sabnzbd',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'sabnzbd',
|
||||
'label': 'Sabnzbd',
|
||||
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:8080',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'Api Key',
|
||||
'description': 'Used for all calls to Sabnzbd.',
|
||||
},
|
||||
{
|
||||
'name': 'category',
|
||||
'label': 'Category',
|
||||
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'priority',
|
||||
'label': 'Priority',
|
||||
'type': 'dropdown',
|
||||
'default': '0',
|
||||
'advanced': True,
|
||||
'values': [('Paused', -2), ('Low', -1), ('Normal', 0), ('High', 1), ('Forced', 2)],
|
||||
'description': 'Add to the queue with this priority.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'advanced': True,
|
||||
'label': 'Remove NZB',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the NZB from history after it completed.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,22 +1,18 @@
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp
|
||||
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
|
||||
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp
|
||||
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Sabnzbd'
|
||||
|
||||
|
||||
class Sabnzbd(DownloaderBase):
|
||||
class Sabnzbd(Downloader):
|
||||
|
||||
protocol = ['nzb']
|
||||
|
||||
@@ -68,26 +64,6 @@ class Sabnzbd(DownloaderBase):
|
||||
log.error('Error getting data from SABNZBd: %s', sab_data)
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
try:
|
||||
sab_data = self.call({
|
||||
'mode': 'version',
|
||||
})
|
||||
v = sab_data.split('.')
|
||||
if int(v[0]) == 0 and int(v[1]) < 7:
|
||||
return False, 'Your Sabnzbd client is too old, please update to newest version.'
|
||||
|
||||
# the version check will work even with wrong api key, so we need the next check as well
|
||||
sab_data = self.call({
|
||||
'mode': 'qstatus',
|
||||
})
|
||||
if not sab_data:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking SABnzbd download status.')
|
||||
@@ -119,7 +95,7 @@ class Sabnzbd(DownloaderBase):
|
||||
status = 'busy'
|
||||
if 'ENCRYPTED / ' in nzb['filename']:
|
||||
status = 'failed'
|
||||
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['filename'],
|
||||
@@ -136,7 +112,7 @@ class Sabnzbd(DownloaderBase):
|
||||
status = 'failed'
|
||||
elif nzb['status'] == 'Completed':
|
||||
status = 'completed'
|
||||
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['name'],
|
||||
@@ -189,9 +165,9 @@ class Sabnzbd(DownloaderBase):
|
||||
|
||||
def call(self, request_params, use_json = True, **kwargs):
|
||||
|
||||
url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
|
||||
'apikey': self.conf('api_key'),
|
||||
'output': 'json'
|
||||
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
|
||||
'apikey': self.conf('api_key'),
|
||||
'output': 'json'
|
||||
}))
|
||||
|
||||
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs)
|
||||
@@ -205,77 +181,3 @@ class Sabnzbd(DownloaderBase):
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'sabnzbd',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'sabnzbd',
|
||||
'label': 'Sabnzbd',
|
||||
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:8080',
|
||||
},
|
||||
{
|
||||
'name': 'ssl',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'Api Key',
|
||||
'description': 'Used for all calls to Sabnzbd.',
|
||||
},
|
||||
{
|
||||
'name': 'category',
|
||||
'label': 'Category',
|
||||
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'priority',
|
||||
'label': 'Priority',
|
||||
'type': 'dropdown',
|
||||
'default': '0',
|
||||
'advanced': True,
|
||||
'values': [('Paused', -2), ('Low', -1), ('Normal', 0), ('High', 1), ('Forced', 2)],
|
||||
'description': 'Add to the queue with this priority.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'advanced': True,
|
||||
'label': 'Remove NZB',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the NZB from history after it completed.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
52
couchpotato/core/downloaders/synology/__init__.py
Normal file
52
couchpotato/core/downloaders/synology/__init__.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from .main import Synology
|
||||
|
||||
def start():
|
||||
return Synology()
|
||||
|
||||
config = [{
|
||||
'name': 'synology',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'synology',
|
||||
'label': 'Synology',
|
||||
'description': 'Use <a href="http://www.synology.com/dsm/home_home_applications_download_station.php" target="_blank">Synology Download Station</a> to download.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb,torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:5000',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:5000</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'use_for',
|
||||
'label': 'Use for',
|
||||
'default': 'both',
|
||||
'type': 'dropdown',
|
||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,19 +1,14 @@
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.encoding import isInt
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
import json
|
||||
import requests
|
||||
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Synology'
|
||||
|
||||
|
||||
class Synology(DownloaderBase):
|
||||
class Synology(Downloader):
|
||||
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
status_support = False
|
||||
@@ -26,14 +21,14 @@ class Synology(DownloaderBase):
|
||||
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['protocol']))
|
||||
|
||||
# Load host from config and split out port.
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
host = self.conf('host').split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
try:
|
||||
# Send request to Synology
|
||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'), self.conf('destination'))
|
||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||
if data['protocol'] == 'torrent_magnet':
|
||||
log.info('Adding torrent URL %s', data['url'])
|
||||
response = srpc.create_task(url = data['url'])
|
||||
@@ -49,16 +44,6 @@ class Synology(DownloaderBase):
|
||||
finally:
|
||||
return self.downloadReturnId('') if response else False
|
||||
|
||||
def test(self):
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
try:
|
||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||
test_result = srpc.test()
|
||||
except:
|
||||
return False
|
||||
|
||||
return test_result
|
||||
|
||||
def getEnabledProtocol(self):
|
||||
if self.conf('use_for') == 'both':
|
||||
return super(Synology, self).getEnabledProtocol()
|
||||
@@ -79,21 +64,18 @@ class Synology(DownloaderBase):
|
||||
return super(Synology, self).isEnabled(manual, data) and\
|
||||
((self.conf('use_for') in for_protocol))
|
||||
|
||||
|
||||
class SynologyRPC(object):
|
||||
|
||||
"""SynologyRPC lite library"""
|
||||
|
||||
def __init__(self, host = 'localhost', port = 5000, username = None, password = None, destination = None):
|
||||
def __init__(self, host = 'localhost', port = 5000, username = None, password = None):
|
||||
|
||||
super(SynologyRPC, self).__init__()
|
||||
|
||||
self.download_url = 'http://%s:%s/webapi/DownloadStation/task.cgi' % (host, port)
|
||||
self.auth_url = 'http://%s:%s/webapi/auth.cgi' % (host, port)
|
||||
self.sid = None
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.destination = destination
|
||||
self.session_name = 'DownloadStation'
|
||||
|
||||
def _login(self):
|
||||
@@ -124,11 +106,11 @@ class SynologyRPC(object):
|
||||
if response['success']:
|
||||
log.info('Synology action successfull')
|
||||
return response
|
||||
except requests.ConnectionError as err:
|
||||
except requests.ConnectionError, err:
|
||||
log.error('Synology connection error, check your config %s', err)
|
||||
except requests.HTTPError as err:
|
||||
except requests.HTTPError, err:
|
||||
log.error('SynologyRPC HTTPError: %s', err)
|
||||
except Exception as err:
|
||||
except Exception, err:
|
||||
log.error('Exception: %s', err)
|
||||
finally:
|
||||
return response
|
||||
@@ -146,10 +128,6 @@ class SynologyRPC(object):
|
||||
'version': '1',
|
||||
'method': 'create',
|
||||
'_sid': self.sid}
|
||||
|
||||
if self.destination and len(self.destination) > 0:
|
||||
args['destination'] = self.destination
|
||||
|
||||
if url:
|
||||
log.info('Login success, adding torrent URI')
|
||||
args['uri'] = url
|
||||
@@ -167,60 +145,3 @@ class SynologyRPC(object):
|
||||
self._logout()
|
||||
|
||||
return result
|
||||
|
||||
def test(self):
|
||||
return bool(self._login())
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'synology',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'synology',
|
||||
'label': 'Synology',
|
||||
'description': 'Use <a href="http://www.synology.com/dsm/home_home_applications_download_station.php" target="_blank">Synology Download Station</a> to download.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb,torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:5000',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:5000</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'destination',
|
||||
'description': 'Specify <strong>existing</strong> destination share to where your files will be downloaded, usually <strong>Downloads</strong>',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'use_for',
|
||||
'label': 'Use for',
|
||||
'default': 'both',
|
||||
'type': 'dropdown',
|
||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
94
couchpotato/core/downloaders/transmission/__init__.py
Normal file
94
couchpotato/core/downloaders/transmission/__init__.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from .main import Transmission
|
||||
|
||||
def start():
|
||||
return Transmission()
|
||||
|
||||
config = [{
|
||||
'name': 'transmission',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'transmission',
|
||||
'label': 'Transmission',
|
||||
'description': 'Use <a href="http://www.transmissionbt.com/" target="_blank">Transmission</a> to download torrents.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:9091',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:9091</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'rpc_url',
|
||||
'type': 'string',
|
||||
'default': 'transmission',
|
||||
'advanced': True,
|
||||
'description': 'Change if you don\'t run Transmission RPC at the default url.',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Download to this directory. Keep empty for default Transmission download directory.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the torrent from Transmission after it finished seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'stalled_as_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Consider a stalled torrent as failed',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,4 +1,8 @@
|
||||
from base64 import b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import isInt, sp
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
import httplib
|
||||
import json
|
||||
@@ -6,32 +10,24 @@ import os.path
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import isInt, sp
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Transmission'
|
||||
|
||||
|
||||
class Transmission(DownloaderBase):
|
||||
class Transmission(Downloader):
|
||||
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
trpc = None
|
||||
|
||||
def connect(self, reconnect = False):
|
||||
def connect(self):
|
||||
# Load host from config and split out port.
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
host = self.conf('host').split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
if not self.trpc or reconnect:
|
||||
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url').strip('/ '), username = self.conf('username'), password = self.conf('password'))
|
||||
if not self.trpc:
|
||||
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url'), username = self.conf('username'), password = self.conf('password'))
|
||||
|
||||
return self.trpc
|
||||
|
||||
@@ -87,11 +83,6 @@ class Transmission(DownloaderBase):
|
||||
log.info('Torrent sent to Transmission successfully.')
|
||||
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
|
||||
|
||||
def test(self):
|
||||
if self.connect(True) and self.trpc.get_session():
|
||||
return True
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking Transmission download status.')
|
||||
@@ -105,7 +96,6 @@ class Transmission(DownloaderBase):
|
||||
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit', 'files']
|
||||
}
|
||||
|
||||
session = self.trpc.get_session()
|
||||
queue = self.trpc.get_alltorrents(return_params)
|
||||
if not (queue and queue.get('torrents')):
|
||||
log.debug('Nothing in queue or error')
|
||||
@@ -113,9 +103,13 @@ class Transmission(DownloaderBase):
|
||||
|
||||
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 / incomplete-dir-enabled=%s / incomplete-dir=%s',
|
||||
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent.get('isStalled', 'N/A'), torrent['eta'], torrent['uploadRatio'], torrent['isFinished'], session['incomplete-dir-enabled'], session['incomplete-dir']))
|
||||
|
||||
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'
|
||||
@@ -123,16 +117,7 @@ class Transmission(DownloaderBase):
|
||||
status = 'completed'
|
||||
elif torrent['status'] in [5, 6]:
|
||||
status = 'seeding'
|
||||
|
||||
if session['incomplete-dir-enabled'] and status == 'busy':
|
||||
torrent_folder = session['incomplete-dir']
|
||||
else:
|
||||
torrent_folder = torrent['downloadDir']
|
||||
|
||||
torrent_files = []
|
||||
for file_item in torrent['files']:
|
||||
torrent_files.append(sp(os.path.join(torrent_folder, file_item['name'])))
|
||||
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent['hashString'],
|
||||
'name': torrent['name'],
|
||||
@@ -140,8 +125,8 @@ class Transmission(DownloaderBase):
|
||||
'original_status': torrent['status'],
|
||||
'seed_ratio': torrent['uploadRatio'],
|
||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||
'folder': sp(torrent_folder if len(torrent_files) == 1 else os.path.join(torrent_folder, torrent['name'])),
|
||||
'files': torrent_files
|
||||
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
@@ -160,7 +145,6 @@ class Transmission(DownloaderBase):
|
||||
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)
|
||||
|
||||
|
||||
class TransmissionRPC(object):
|
||||
|
||||
"""TransmissionRPC lite library"""
|
||||
@@ -174,8 +158,8 @@ class TransmissionRPC(object):
|
||||
self.session = {}
|
||||
if username and password:
|
||||
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_manager.add_password(realm = 'Transmission', uri = self.url, user = username, passwd = password)
|
||||
opener = urllib2.build_opener(urllib2.HTTPBasicAuthHandler(password_manager))
|
||||
password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
|
||||
opener = urllib2.build_opener(urllib2.HTTPBasicAuthHandler(password_manager), urllib2.HTTPDigestAuthHandler(password_manager))
|
||||
opener.addheaders = [('User-agent', 'couchpotato-transmission-client/1.0')]
|
||||
urllib2.install_opener(opener)
|
||||
elif username or password:
|
||||
@@ -197,10 +181,10 @@ class TransmissionRPC(object):
|
||||
else:
|
||||
log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
|
||||
return False
|
||||
except httplib.InvalidURL as err:
|
||||
except httplib.InvalidURL, err:
|
||||
log.error('Invalid Transmission host, check your config %s', err)
|
||||
return False
|
||||
except urllib2.HTTPError as err:
|
||||
except urllib2.HTTPError, err:
|
||||
if err.code == 401:
|
||||
log.error('Invalid Transmission Username or Password, check your config')
|
||||
return False
|
||||
@@ -218,7 +202,7 @@ class TransmissionRPC(object):
|
||||
log.error('Unable to get Transmission Session-Id %s', err)
|
||||
else:
|
||||
log.error('TransmissionRPC HTTPError: %s', err)
|
||||
except urllib2.URLError as err:
|
||||
except urllib2.URLError, err:
|
||||
log.error('Unable to connect to Transmission %s', err)
|
||||
|
||||
def get_session(self):
|
||||
@@ -256,93 +240,3 @@ class TransmissionRPC(object):
|
||||
post_data = {'arguments': {'ids': torrent_id, 'delete-local-data': delete_local_data}, 'method': 'torrent-remove', 'tag': self.tag}
|
||||
return self._request(post_data)
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'transmission',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'transmission',
|
||||
'label': 'Transmission',
|
||||
'description': 'Use <a href="http://www.transmissionbt.com/" target="_blank">Transmission</a> to download torrents.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:9091',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:9091</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'rpc_url',
|
||||
'type': 'string',
|
||||
'default': 'transmission',
|
||||
'advanced': True,
|
||||
'description': 'Change if you don\'t run Transmission RPC at the default url.',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Download to this directory. Keep empty for default Transmission download directory.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the torrent from Transmission after it finished seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'stalled_as_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Consider a stalled torrent as failed',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
79
couchpotato/core/downloaders/utorrent/__init__.py
Normal file
79
couchpotato/core/downloaders/utorrent/__init__.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from .main import uTorrent
|
||||
|
||||
def start():
|
||||
return uTorrent()
|
||||
|
||||
config = [{
|
||||
'name': 'utorrent',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'utorrent',
|
||||
'label': 'uTorrent',
|
||||
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> (3.0+) to download torrents.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:8000',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:8000</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'label',
|
||||
'description': 'Label to add torrent as.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the torrent from uTorrent after it finished seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,6 +1,12 @@
|
||||
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.helpers.variable import tryInt, tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from multipartpost import MultipartPostHandler
|
||||
import cookielib
|
||||
import httplib
|
||||
import json
|
||||
@@ -11,37 +17,27 @@ import time
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
from bencode import bencode as benc, bdecode
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import isInt, ss, sp
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from multipartpost import MultipartPostHandler
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'uTorrent'
|
||||
|
||||
|
||||
class uTorrent(DownloaderBase):
|
||||
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
|
||||
'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.
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
host = self.conf('host').split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
@@ -70,7 +66,7 @@ class uTorrent(DownloaderBase):
|
||||
new_settings['seed_prio_limitul_flag'] = True
|
||||
log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.')
|
||||
|
||||
if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function
|
||||
if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function
|
||||
new_settings['bt.read_only_on_complete'] = False
|
||||
log.info('Updated uTorrent settings to not set the files to read only after completing.')
|
||||
|
||||
@@ -119,17 +115,6 @@ class uTorrent(DownloaderBase):
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
|
||||
def test(self):
|
||||
if self.connect():
|
||||
build_version = self.utorrent_api.get_build()
|
||||
if not build_version:
|
||||
return False
|
||||
if build_version < 25406: # This build corresponds to version 3.0.0 stable
|
||||
return False, 'Your uTorrent client is too old, please update to newest version.'
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking uTorrent download status.')
|
||||
@@ -164,18 +149,18 @@ class uTorrent(DownloaderBase):
|
||||
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']:
|
||||
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],
|
||||
@@ -184,7 +169,7 @@ class uTorrent(DownloaderBase):
|
||||
'original_status': torrent[1],
|
||||
'timeleft': str(timedelta(seconds = torrent[10])),
|
||||
'folder': sp(torrent[26]),
|
||||
'files': torrent_files
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
@@ -229,6 +214,7 @@ class uTorrentAPI(object):
|
||||
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
|
||||
self.opener.add_handler(urllib2.HTTPBasicAuthHandler(password_manager))
|
||||
self.opener.add_handler(urllib2.HTTPDigestAuthHandler(password_manager))
|
||||
elif username or password:
|
||||
log.debug('User or password missing, not using authentication.')
|
||||
self.token = self.get_token()
|
||||
@@ -245,14 +231,14 @@ class uTorrentAPI(object):
|
||||
return response
|
||||
else:
|
||||
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
|
||||
except httplib.InvalidURL as err:
|
||||
except httplib.InvalidURL, err:
|
||||
log.error('Invalid uTorrent host, check your config %s', err)
|
||||
except urllib2.HTTPError as err:
|
||||
except urllib2.HTTPError, err:
|
||||
if err.code == 401:
|
||||
log.error('Invalid uTorrent Username or Password, check your config')
|
||||
else:
|
||||
log.error('uTorrent HTTPError: %s', err)
|
||||
except urllib2.URLError as err:
|
||||
except urllib2.URLError, err:
|
||||
log.error('Unable to connect to uTorrent %s', err)
|
||||
return False
|
||||
|
||||
@@ -275,7 +261,7 @@ class uTorrentAPI(object):
|
||||
|
||||
def set_torrent(self, hash, params):
|
||||
action = 'action=setprops&hash=%s' % hash
|
||||
for k, v in params.items():
|
||||
for k, v in params.iteritems():
|
||||
action += '&s=%s&v=%s' % (k, v)
|
||||
return self._request(action)
|
||||
|
||||
@@ -318,7 +304,7 @@ class uTorrentAPI(object):
|
||||
|
||||
#log.debug('uTorrent settings: %s', settings_dict)
|
||||
|
||||
except Exception as err:
|
||||
except Exception, err:
|
||||
log.error('Failed to get settings from uTorrent: %s', err)
|
||||
|
||||
return settings_dict
|
||||
@@ -336,86 +322,3 @@ class uTorrentAPI(object):
|
||||
def get_files(self, hash):
|
||||
action = 'action=getfiles&hash=%s' % hash
|
||||
return self._request(action)
|
||||
|
||||
def get_build(self):
|
||||
data = self._request('')
|
||||
if not data:
|
||||
return False
|
||||
response = json.loads(data)
|
||||
return int(response.get('build'))
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'utorrent',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'utorrent',
|
||||
'label': 'uTorrent',
|
||||
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> (3.0+) to download torrents.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:8000',
|
||||
'description': 'Port can be found in settings when enabling WebUI.',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'label',
|
||||
'description': 'Label to add torrent as.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the torrent from uTorrent after it finished seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -1,15 +1,12 @@
|
||||
from axl.axel import Event
|
||||
from couchpotato.core.helpers.variable import mergeDicts, natcmp
|
||||
from couchpotato.core.logger import CPLog
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from axl.axel import Event
|
||||
from couchpotato.core.helpers.variable import mergeDicts, natsortKey
|
||||
from couchpotato.core.logger import CPLog
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
events = {}
|
||||
|
||||
|
||||
def runHandler(name, handler, *args, **kwargs):
|
||||
try:
|
||||
return handler(*args, **kwargs)
|
||||
@@ -17,7 +14,6 @@ def runHandler(name, handler, *args, **kwargs):
|
||||
from couchpotato.environment import Env
|
||||
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all() if not Env.get('dev') else ''))
|
||||
|
||||
|
||||
def addEvent(name, handler, priority = 100):
|
||||
|
||||
if not events.get(name):
|
||||
@@ -31,7 +27,7 @@ def addEvent(name, handler, priority = 100):
|
||||
has_parent = hasattr(handler, 'im_self')
|
||||
parent = None
|
||||
if has_parent:
|
||||
parent = handler.__self__
|
||||
parent = handler.im_self
|
||||
bc = hasattr(parent, 'beforeCall')
|
||||
if bc: parent.beforeCall(handler)
|
||||
|
||||
@@ -52,19 +48,22 @@ def addEvent(name, handler, priority = 100):
|
||||
'priority': priority,
|
||||
})
|
||||
|
||||
def removeEvent(name, handler):
|
||||
e = events[name]
|
||||
e -= handler
|
||||
|
||||
def fireEvent(name, *args, **kwargs):
|
||||
if name not in events: return
|
||||
if not events.has_key(name): return
|
||||
|
||||
#log.debug('Firing event %s', name)
|
||||
try:
|
||||
|
||||
options = {
|
||||
'is_after_event': False, # Fire after event
|
||||
'on_complete': False, # onComplete event
|
||||
'single': False, # Return single handler
|
||||
'merge': False, # Merge items
|
||||
'in_order': False, # Fire them in specific order, waits for the other to finish
|
||||
'is_after_event': False, # Fire after event
|
||||
'on_complete': False, # onComplete event
|
||||
'single': False, # Return single handler
|
||||
'merge': False, # Merge items
|
||||
'in_order': False, # Fire them in specific order, waits for the other to finish
|
||||
}
|
||||
|
||||
# Do options
|
||||
@@ -102,14 +101,11 @@ def fireEvent(name, *args, **kwargs):
|
||||
# Fire
|
||||
result = e(*args, **kwargs)
|
||||
|
||||
result_keys = result.keys()
|
||||
result_keys.sort(key = natsortKey)
|
||||
|
||||
if options['single'] and not options['merge']:
|
||||
results = None
|
||||
|
||||
# Loop over results, stop when first not None result is found.
|
||||
for r_key in result_keys:
|
||||
for r_key in sorted(result.iterkeys(), cmp = natcmp):
|
||||
r = result[r_key]
|
||||
if r[0] is True and r[1] is not None:
|
||||
results = r[1]
|
||||
@@ -121,7 +117,7 @@ def fireEvent(name, *args, **kwargs):
|
||||
|
||||
else:
|
||||
results = []
|
||||
for r_key in result_keys:
|
||||
for r_key in sorted(result.iterkeys(), cmp = natcmp):
|
||||
r = result[r_key]
|
||||
if r[0] == True and r[1]:
|
||||
results.append(r[1])
|
||||
@@ -164,21 +160,18 @@ def fireEvent(name, *args, **kwargs):
|
||||
except Exception:
|
||||
log.error('%s: %s', (name, traceback.format_exc()))
|
||||
|
||||
|
||||
def fireEventAsync(*args, **kwargs):
|
||||
try:
|
||||
t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
return True
|
||||
except Exception as e:
|
||||
except Exception, e:
|
||||
log.error('%s: %s', (args[0], e))
|
||||
|
||||
|
||||
def errorHandler(error):
|
||||
etype, value, tb = error
|
||||
log.error(''.join(traceback.format_exception(etype, value, tb)))
|
||||
|
||||
|
||||
def getEvent(name):
|
||||
return events[name]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from string import ascii_letters, digits
|
||||
from urllib import quote_plus
|
||||
import os
|
||||
@@ -5,34 +6,28 @@ import re
|
||||
import traceback
|
||||
import unicodedata
|
||||
|
||||
from couchpotato.core.logger import CPLog
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
def toSafeString(original):
|
||||
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
|
||||
cleaned_filename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
|
||||
valid_string = ''.join(c for c in cleaned_filename if c in valid_chars)
|
||||
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
|
||||
valid_string = ''.join(c for c in cleanedFilename if c in valid_chars)
|
||||
return ' '.join(valid_string.split())
|
||||
|
||||
|
||||
def simplifyString(original):
|
||||
string = stripAccents(original.lower())
|
||||
string = toSafeString(' '.join(re.split('\W+', string)))
|
||||
split = re.split('\W+|_', string.lower())
|
||||
return toUnicode(' '.join(split))
|
||||
|
||||
|
||||
def toUnicode(original, *args):
|
||||
try:
|
||||
if isinstance(original, unicode):
|
||||
return original
|
||||
else:
|
||||
try:
|
||||
return six.text_type(original, *args)
|
||||
return unicode(original, *args)
|
||||
except:
|
||||
try:
|
||||
return ek(original, *args)
|
||||
@@ -43,18 +38,16 @@ def toUnicode(original, *args):
|
||||
ascii_text = str(original).encode('string_escape')
|
||||
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 as e:
|
||||
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 '\'
|
||||
@@ -65,7 +58,7 @@ def sp(path, *args):
|
||||
if os.path.sep == '/' and '\\' in path:
|
||||
path = '/' + path.replace(':', '').replace('\\', '/')
|
||||
|
||||
path = os.path.normpath(ss(path, *args))
|
||||
path = os.path.normcase(os.path.normpath(ss(path, *args)))
|
||||
|
||||
# Remove any trailing path separators
|
||||
if path != os.path.sep:
|
||||
@@ -80,7 +73,6 @@ def sp(path, *args):
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def ek(original, *args):
|
||||
if isinstance(original, (str, unicode)):
|
||||
try:
|
||||
@@ -91,7 +83,6 @@ def ek(original, *args):
|
||||
|
||||
return original
|
||||
|
||||
|
||||
def isInt(value):
|
||||
try:
|
||||
int(value)
|
||||
@@ -99,16 +90,14 @@ def isInt(value):
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def stripAccents(s):
|
||||
return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn'))
|
||||
|
||||
|
||||
def tryUrlencode(s):
|
||||
new = six.u('')
|
||||
new = u''
|
||||
if isinstance(s, dict):
|
||||
for key, value in s.items():
|
||||
new += six.u('&%s=%s') % (key, tryUrlencode(value))
|
||||
for key, value in s.iteritems():
|
||||
new += u'&%s=%s' % (key, tryUrlencode(value))
|
||||
|
||||
return new[1:]
|
||||
else:
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import natcmp
|
||||
from urllib import unquote
|
||||
import re
|
||||
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import natsortKey
|
||||
|
||||
|
||||
def getParams(params):
|
||||
|
||||
reg = re.compile('^[a-z0-9_\.]+$')
|
||||
|
||||
# Sort keys
|
||||
param_keys = params.keys()
|
||||
param_keys.sort(key = natsortKey)
|
||||
|
||||
temp = {}
|
||||
for param in param_keys:
|
||||
value = params[param]
|
||||
for param, value in sorted(params.iteritems()):
|
||||
|
||||
nest = re.split("([\[\]]+)", param)
|
||||
if len(nest) > 1:
|
||||
@@ -43,26 +37,13 @@ def getParams(params):
|
||||
|
||||
return dictToList(temp)
|
||||
|
||||
non_decimal = re.compile(r'[^\d.]+')
|
||||
|
||||
def dictToList(params):
|
||||
|
||||
if type(params) is dict:
|
||||
new = {}
|
||||
for x, value in params.items():
|
||||
for x, value in params.iteritems():
|
||||
try:
|
||||
convert = lambda text: int(text) if text.isdigit() else text.lower()
|
||||
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
|
||||
sorted_keys = sorted(value.keys(), key = alphanum_key)
|
||||
|
||||
all_ints = 0
|
||||
for pnr in sorted_keys:
|
||||
all_ints += 1 if non_decimal.sub('', pnr) == pnr else 0
|
||||
|
||||
if all_ints == len(sorted_keys):
|
||||
new_value = [dictToList(value[k]) for k in sorted_keys]
|
||||
else:
|
||||
new_value = value
|
||||
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys(), cmp = natcmp)]
|
||||
except:
|
||||
new_value = value
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
from couchpotato.core.logger import CPLog
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class RSS(object):
|
||||
|
||||
def getTextElements(self, xml, path):
|
||||
@@ -49,6 +46,6 @@ class RSS(object):
|
||||
def getItems(self, data, path = 'channel/item'):
|
||||
try:
|
||||
return XMLTree.parse(data).findall(path)
|
||||
except Exception as e:
|
||||
except Exception, e:
|
||||
log.error('Error parsing RSS. %s', e)
|
||||
return []
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
|
||||
from couchpotato.core.logger import CPLog
|
||||
import collections
|
||||
import ctypes
|
||||
import hashlib
|
||||
import os
|
||||
import platform
|
||||
@@ -7,37 +8,26 @@ import random
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss, sp
|
||||
from couchpotato.core.logger import CPLog
|
||||
import six
|
||||
from six.moves import map, zip, filter
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
def fnEscape(pattern):
|
||||
return pattern.replace('[', '[[').replace(']', '[]]').replace('[[', '[[]')
|
||||
|
||||
return pattern.replace('[','[[').replace(']','[]]').replace('[[','[[]')
|
||||
|
||||
def link(src, dst):
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
if ctypes.windll.kernel32.CreateHardLinkW(six.text_type(dst), six.text_type(src), 0) == 0: raise ctypes.WinError()
|
||||
if ctypes.windll.kernel32.CreateHardLinkW(unicode(dst), unicode(src), 0) == 0: raise ctypes.WinError()
|
||||
else:
|
||||
os.link(src, dst)
|
||||
|
||||
|
||||
def symlink(src, dst):
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
if ctypes.windll.kernel32.CreateSymbolicLinkW(six.text_type(dst), six.text_type(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
|
||||
if ctypes.windll.kernel32.CreateSymbolicLinkW(unicode(dst), unicode(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
|
||||
else:
|
||||
os.symlink(src, dst)
|
||||
|
||||
|
||||
def getUserDir():
|
||||
try:
|
||||
import pwd
|
||||
@@ -47,7 +37,6 @@ def getUserDir():
|
||||
|
||||
return os.path.expanduser('~')
|
||||
|
||||
|
||||
def getDownloadDir():
|
||||
user_dir = getUserDir()
|
||||
|
||||
@@ -60,7 +49,6 @@ def getDownloadDir():
|
||||
|
||||
return user_dir
|
||||
|
||||
|
||||
def getDataDir():
|
||||
|
||||
# Windows
|
||||
@@ -80,10 +68,8 @@ def getDataDir():
|
||||
# Linux
|
||||
return os.path.join(user_dir, '.couchpotato')
|
||||
|
||||
|
||||
def isDict(obj):
|
||||
return isinstance(obj, dict)
|
||||
|
||||
def isDict(object):
|
||||
return isinstance(object, dict)
|
||||
|
||||
def mergeDicts(a, b, prepend_list = False):
|
||||
assert isDict(a), isDict(b)
|
||||
@@ -105,7 +91,6 @@ def mergeDicts(a, b, prepend_list = False):
|
||||
current_dst[key] = current_src[key]
|
||||
return dst
|
||||
|
||||
|
||||
def removeListDuplicates(seq):
|
||||
checked = []
|
||||
for e in seq:
|
||||
@@ -113,73 +98,35 @@ def removeListDuplicates(seq):
|
||||
checked.append(e)
|
||||
return checked
|
||||
|
||||
|
||||
def flattenList(l):
|
||||
if isinstance(l, list):
|
||||
return sum(map(flattenList, l))
|
||||
else:
|
||||
return l
|
||||
|
||||
|
||||
def md5(text):
|
||||
return hashlib.md5(ss(text)).hexdigest()
|
||||
|
||||
|
||||
def sha1(text):
|
||||
return hashlib.sha1(text).hexdigest()
|
||||
|
||||
|
||||
def isLocalIP(ip):
|
||||
ip = ip.lstrip('htps:/')
|
||||
regex = '/(^127\.)|(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^::1)$/'
|
||||
return re.search(regex, ip) is not None or 'localhost' in ip or ip[:4] == '127.'
|
||||
|
||||
|
||||
def getExt(filename):
|
||||
return os.path.splitext(filename)[1][1:]
|
||||
|
||||
def cleanHost(host):
|
||||
if not host.startswith(('http://', 'https://')):
|
||||
host = 'http://' + host
|
||||
|
||||
def cleanHost(host, protocol = True, ssl = False, username = None, password = None):
|
||||
"""Return a cleaned up host with given url options set
|
||||
|
||||
Changes protocol to https if ssl is set to True and http if ssl is set to false.
|
||||
>>> cleanHost("localhost:80", ssl=True)
|
||||
'https://localhost:80/'
|
||||
>>> cleanHost("localhost:80", ssl=False)
|
||||
'http://localhost:80/'
|
||||
|
||||
Username and password is managed with the username and password variables
|
||||
>>> cleanHost("localhost:80", username="user", password="passwd")
|
||||
'http://user:passwd@localhost:80/'
|
||||
|
||||
Output without scheme (protocol) can be forced with protocol=False
|
||||
>>> cleanHost("localhost:80", protocol=False)
|
||||
'localhost:80'
|
||||
"""
|
||||
|
||||
if not '://' in host and protocol:
|
||||
host = ('https://' if ssl else 'http://') + host
|
||||
|
||||
if not protocol:
|
||||
host = host.split('://', 1)[-1]
|
||||
|
||||
if protocol and username and password:
|
||||
try:
|
||||
auth = re.findall('^(?:.+?//)(.+?):(.+?)@(?:.+)$', host)
|
||||
if auth:
|
||||
log.error('Cleanhost error: auth already defined in url: %s, please remove BasicAuth from url.', host)
|
||||
else:
|
||||
host = host.replace('://', '://%s:%s@' % (username, password), 1)
|
||||
except:
|
||||
pass
|
||||
|
||||
host = host.rstrip('/ ')
|
||||
if protocol:
|
||||
host += '/'
|
||||
host = host.rstrip('/')
|
||||
host += '/'
|
||||
|
||||
return host
|
||||
|
||||
|
||||
def getImdb(txt, check_inside = False, multiple = False):
|
||||
|
||||
if not check_inside:
|
||||
@@ -196,7 +143,7 @@ def getImdb(txt, check_inside = False, multiple = False):
|
||||
ids = re.findall('(tt\d{4,7})', txt)
|
||||
|
||||
if multiple:
|
||||
return removeDuplicate(['tt%07d' % tryInt(x[2:]) for x in ids]) if len(ids) > 0 else []
|
||||
return list(set(['tt%07d' % tryInt(x[2:]) for x in ids])) if len(ids) > 0 else []
|
||||
|
||||
return 'tt%07d' % tryInt(ids[0][2:])
|
||||
except IndexError:
|
||||
@@ -204,12 +151,10 @@ def getImdb(txt, check_inside = False, multiple = False):
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def tryInt(s, default = 0):
|
||||
try: return int(s)
|
||||
except: return default
|
||||
|
||||
|
||||
def tryFloat(s):
|
||||
try:
|
||||
if isinstance(s, str):
|
||||
@@ -218,42 +163,38 @@ def tryFloat(s):
|
||||
return float(s)
|
||||
except: return 0
|
||||
|
||||
def natsortKey(s):
|
||||
return map(tryInt, re.findall(r'(\d+|\D+)', s))
|
||||
|
||||
def natsortKey(string_):
|
||||
"""See http://www.codinghorror.com/blog/archives/001018.html"""
|
||||
return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)]
|
||||
|
||||
def natcmp(a, b):
|
||||
return cmp(natsortKey(a), natsortKey(b))
|
||||
|
||||
def toIterable(value):
|
||||
if isinstance(value, collections.Iterable):
|
||||
if type(value) in [list, tuple]:
|
||||
return value
|
||||
return [value]
|
||||
|
||||
|
||||
def getIdentifier(media):
|
||||
return media.get('identifier') or media.get('identifiers', {}).get('imdb')
|
||||
|
||||
|
||||
def getTitle(media_dict):
|
||||
def getTitle(library_dict):
|
||||
try:
|
||||
try:
|
||||
return media_dict['title']
|
||||
return library_dict['titles'][0]['title']
|
||||
except:
|
||||
try:
|
||||
return media_dict['titles'][0]
|
||||
for title in library_dict.titles:
|
||||
if title.default:
|
||||
return title.title
|
||||
except:
|
||||
try:
|
||||
return media_dict['info']['titles'][0]
|
||||
return library_dict['info']['titles'][0]
|
||||
except:
|
||||
try:
|
||||
return media_dict['media']['info']['titles'][0]
|
||||
except:
|
||||
log.error('Could not get title for %s', getIdentifier(media_dict))
|
||||
return None
|
||||
except:
|
||||
log.error('Could not get title for library item: %s', media_dict)
|
||||
return None
|
||||
log.error('Could not get title for %s', library_dict.identifier)
|
||||
return None
|
||||
|
||||
log.error('Could not get title for %s', library_dict['identifier'])
|
||||
return None
|
||||
except:
|
||||
log.error('Could not get title for library item: %s', library_dict)
|
||||
return None
|
||||
|
||||
def possibleTitles(raw_title):
|
||||
|
||||
@@ -267,116 +208,18 @@ def possibleTitles(raw_title):
|
||||
new_title = raw_title.replace('&', 'and')
|
||||
titles.append(simplifyString(new_title))
|
||||
|
||||
return removeDuplicate(titles)
|
||||
|
||||
return list(set(titles))
|
||||
|
||||
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
|
||||
def splitString(str, split_on = ',', clean = True):
|
||||
l = [x.strip() for x in str.split(split_on)] if str else []
|
||||
return removeEmpty(l) if clean else l
|
||||
|
||||
|
||||
def removeEmpty(l):
|
||||
return list(filter(None, l))
|
||||
|
||||
|
||||
def removeDuplicate(l):
|
||||
seen = set()
|
||||
return [x for x in l if x not in seen and not seen.add(x)]
|
||||
|
||||
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()])
|
||||
|
||||
|
||||
# Returns True if sub_folder is the same as or inside base_folder
|
||||
def isSubFolder(sub_folder, base_folder):
|
||||
if base_folder and sub_folder:
|
||||
base = sp(os.path.realpath(base_folder)) + os.path.sep
|
||||
subfolder = sp(os.path.realpath(sub_folder)) + os.path.sep
|
||||
return os.path.commonprefix([subfolder, base]) == base
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# From SABNZBD
|
||||
re_password = [re.compile(r'(.+){{([^{}]+)}}$'), re.compile(r'(.+)\s+password\s*=\s*(.+)$', re.I)]
|
||||
|
||||
|
||||
def scanForPassword(name):
|
||||
m = None
|
||||
for reg in re_password:
|
||||
m = reg.search(name)
|
||||
if m: break
|
||||
|
||||
if m:
|
||||
return m.group(1).strip('. '), m.group(2).strip()
|
||||
|
||||
|
||||
under_pat = re.compile(r'_([a-z])')
|
||||
|
||||
def underscoreToCamel(name):
|
||||
return under_pat.sub(lambda x: x.group(1).upper(), name)
|
||||
|
||||
|
||||
def removePyc(folder, only_excess = True, show_logs = True):
|
||||
|
||||
folder = sp(folder)
|
||||
|
||||
for root, dirs, files in os.walk(folder):
|
||||
|
||||
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
|
||||
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
|
||||
excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files) if only_excess else pyc_files
|
||||
|
||||
for excess_pyc_file in excess_pyc_files:
|
||||
full_path = os.path.join(root, excess_pyc_file)
|
||||
if show_logs: log.debug('Removing old PYC file: %s', full_path)
|
||||
try:
|
||||
os.remove(full_path)
|
||||
except:
|
||||
log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc()))
|
||||
|
||||
for dir_name in dirs:
|
||||
full_path = os.path.join(root, dir_name)
|
||||
if len(os.listdir(full_path)) == 0:
|
||||
try:
|
||||
os.rmdir(full_path)
|
||||
except:
|
||||
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
|
||||
|
||||
|
||||
def getFreeSpace(directories):
|
||||
|
||||
single = not isinstance(directories, (tuple, list))
|
||||
if single:
|
||||
directories = [directories]
|
||||
|
||||
free_space = {}
|
||||
for folder in directories:
|
||||
|
||||
size = None
|
||||
if os.path.isdir(folder):
|
||||
if os.name == 'nt':
|
||||
_, total, free = ctypes.c_ulonglong(), ctypes.c_ulonglong(), \
|
||||
ctypes.c_ulonglong()
|
||||
if sys.version_info >= (3,) or isinstance(folder, unicode):
|
||||
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExW #@UndefinedVariable
|
||||
else:
|
||||
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExA #@UndefinedVariable
|
||||
ret = fun(folder, ctypes.byref(_), ctypes.byref(total), ctypes.byref(free))
|
||||
if ret == 0:
|
||||
raise ctypes.WinError()
|
||||
return [total.value, free.value]
|
||||
else:
|
||||
s = os.statvfs(folder)
|
||||
size = [s.f_blocks * s.f_frsize / (1024 * 1024), (s.f_bavail * s.f_frsize) / (1024 * 1024)]
|
||||
|
||||
if single: return size
|
||||
|
||||
free_space[folder] = size
|
||||
|
||||
return free_space
|
||||
# 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,33 +1,30 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from importlib import import_module
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from importhelper import import_module
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Loader(object):
|
||||
|
||||
def __init__(self):
|
||||
self.plugins = {}
|
||||
self.providers = {}
|
||||
self.modules = {}
|
||||
self.paths = {}
|
||||
plugins = {}
|
||||
providers = {}
|
||||
modules = {}
|
||||
|
||||
def preload(self, root = ''):
|
||||
core = os.path.join(root, 'couchpotato', 'core')
|
||||
|
||||
self.paths.update({
|
||||
self.paths = {
|
||||
'core': (0, 'couchpotato.core._base', os.path.join(core, '_base')),
|
||||
'plugin': (1, 'couchpotato.core.plugins', os.path.join(core, 'plugins')),
|
||||
'notifications': (20, 'couchpotato.core.notifications', os.path.join(core, 'notifications')),
|
||||
'downloaders': (20, 'couchpotato.core.downloaders', os.path.join(core, 'downloaders')),
|
||||
})
|
||||
}
|
||||
|
||||
# Add providers to loader
|
||||
self.addPath(root, ['couchpotato', 'core', 'providers'], 25, recursive = False)
|
||||
|
||||
# Add media to loader
|
||||
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
|
||||
@@ -40,7 +37,7 @@ class Loader(object):
|
||||
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.items():
|
||||
for plugin_type, plugin_tuple in self.paths.iteritems():
|
||||
priority, module, dir_name = plugin_tuple
|
||||
self.addFromDir(plugin_type, priority, module, dir_name)
|
||||
|
||||
@@ -48,7 +45,7 @@ class Loader(object):
|
||||
did_save = 0
|
||||
|
||||
for priority in sorted(self.modules):
|
||||
for module_name, plugin in sorted(self.modules[priority].items()):
|
||||
for module_name, plugin in sorted(self.modules[priority].iteritems()):
|
||||
|
||||
# Load module
|
||||
try:
|
||||
@@ -59,10 +56,12 @@ class Loader(object):
|
||||
if m is None:
|
||||
continue
|
||||
|
||||
log.info('Loading %s: %s', (plugin['type'], plugin['name']))
|
||||
|
||||
# Save default settings for plugin/provider
|
||||
did_save += self.loadSettings(m, module_name, save = False)
|
||||
|
||||
self.loadPlugins(m, plugin.get('type'), plugin.get('name'))
|
||||
self.loadPlugins(m, plugin.get('name'))
|
||||
except ImportError as e:
|
||||
# todo:: subclass ImportError for missing requirements.
|
||||
if e.message.lower().startswith("missing"):
|
||||
@@ -82,7 +81,7 @@ class Loader(object):
|
||||
for filename in os.listdir(root_path):
|
||||
path = os.path.join(root_path, filename)
|
||||
if os.path.isdir(path) and filename[:2] != '__':
|
||||
if six.u('__init__.py') in os.listdir(path):
|
||||
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)
|
||||
|
||||
@@ -96,19 +95,14 @@ class Loader(object):
|
||||
self.addModule(priority, plugin_type, module, os.path.basename(dir_name))
|
||||
|
||||
for name in os.listdir(dir_name):
|
||||
path = os.path.join(dir_name, name)
|
||||
ext = os.path.splitext(path)[1]
|
||||
ext_length = len(ext)
|
||||
if name != 'static' and ((os.path.isdir(path) and os.path.isfile(os.path.join(path, '__init__.py')))
|
||||
or (os.path.isfile(path) and ext == '.py')):
|
||||
name = name[:-ext_length] if ext_length > 0 else 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')):
|
||||
module_name = '%s.%s' % (module, name)
|
||||
self.addModule(priority, plugin_type, module_name, name)
|
||||
|
||||
def loadSettings(self, module, name, save = True):
|
||||
|
||||
if not hasattr(module, 'config'):
|
||||
#log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__)
|
||||
log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__)
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -124,20 +118,13 @@ class Loader(object):
|
||||
log.debug('Failed loading settings for "%s": %s', (name, traceback.format_exc()))
|
||||
return False
|
||||
|
||||
def loadPlugins(self, module, type, name):
|
||||
def loadPlugins(self, module, name):
|
||||
|
||||
if not hasattr(module, 'autoload'):
|
||||
#log.debug('Skip startup for plugin %s as it has no start section' % module.__file__)
|
||||
if not hasattr(module, 'start'):
|
||||
log.debug('Skip startup for plugin %s as it has no start section' % module.__file__)
|
||||
return False
|
||||
try:
|
||||
# Load single file plugin
|
||||
if isinstance(module.autoload, (str, unicode)):
|
||||
getattr(module, module.autoload)()
|
||||
# Load folder plugin
|
||||
else:
|
||||
module.autoload()
|
||||
|
||||
log.info('Loaded %s: %s', (type, name))
|
||||
module.start()
|
||||
return True
|
||||
except:
|
||||
log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc()))
|
||||
@@ -149,9 +136,6 @@ class Loader(object):
|
||||
self.modules[priority] = {}
|
||||
|
||||
module = module.lstrip('.')
|
||||
if plugin_type.startswith('couchpotato_core'):
|
||||
plugin_type = plugin_type[17:]
|
||||
|
||||
self.modules[priority][module] = {
|
||||
'priority': priority,
|
||||
'module': module,
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
|
||||
class CPLog(object):
|
||||
|
||||
context = ''
|
||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
|
||||
|
||||
Env = None
|
||||
is_develop = False
|
||||
|
||||
def __init__(self, context = ''):
|
||||
if context.endswith('.main'):
|
||||
context = context[:-5]
|
||||
@@ -17,20 +13,6 @@ class CPLog(object):
|
||||
self.context = context
|
||||
self.logger = logging.getLogger()
|
||||
|
||||
def setup(self):
|
||||
|
||||
if not self.Env:
|
||||
from couchpotato.environment import Env
|
||||
|
||||
self.Env = Env
|
||||
self.is_develop = Env.get('dev')
|
||||
|
||||
from couchpotato.core.event import addEvent
|
||||
addEvent('app.after_shutdown', self.close)
|
||||
|
||||
def close(self, *args, **kwargs):
|
||||
logging.shutdown()
|
||||
|
||||
def info(self, msg, replace_tuple = ()):
|
||||
self.logger.info(self.addContext(msg, replace_tuple))
|
||||
|
||||
@@ -54,7 +36,8 @@ class CPLog(object):
|
||||
|
||||
def safeMessage(self, msg, replace_tuple = ()):
|
||||
|
||||
from couchpotato.core.helpers.encoding import ss, toUnicode
|
||||
from couchpotato.environment import Env
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
|
||||
msg = ss(msg)
|
||||
|
||||
@@ -66,11 +49,10 @@ class CPLog(object):
|
||||
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
|
||||
else:
|
||||
msg = msg % ss(replace_tuple)
|
||||
except Exception as e:
|
||||
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e))
|
||||
except Exception, e:
|
||||
self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e))
|
||||
|
||||
self.setup()
|
||||
if not self.is_develop:
|
||||
if not Env.get('dev'):
|
||||
|
||||
for replace in self.replace_private:
|
||||
msg = re.sub('(\?%s=)[^\&]+' % replace, '?%s=xxx' % replace, msg)
|
||||
@@ -78,10 +60,10 @@ class CPLog(object):
|
||||
|
||||
# Replace api key
|
||||
try:
|
||||
api_key = self.Env.setting('api_key')
|
||||
api_key = Env.setting('api_key')
|
||||
if api_key:
|
||||
msg = msg.replace(api_key, 'API_KEY')
|
||||
except:
|
||||
pass
|
||||
|
||||
return toUnicode(msg)
|
||||
return msg
|
||||
|
||||
@@ -1,98 +1,52 @@
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from couchpotato import CPLog
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
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, media_id):
|
||||
def createOnComplete(self, id):
|
||||
|
||||
def onComplete():
|
||||
try:
|
||||
media = fireEvent('media.get', media_id, single = True)
|
||||
event_name = '%s.searcher.single' % media.get('type')
|
||||
|
||||
fireEventAsync(event_name, media, on_complete = self.createNotifyFront(media_id), manual = True)
|
||||
except:
|
||||
log.error('Failed creating onComplete: %s', traceback.format_exc())
|
||||
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():
|
||||
try:
|
||||
media = fireEvent('media.get', media_id, single = True)
|
||||
event_name = '%s.update' % media.get('type')
|
||||
|
||||
fireEvent('notify.frontend', type = event_name, data = media)
|
||||
except:
|
||||
log.error('Failed creating onComplete: %s', traceback.format_exc())
|
||||
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
|
||||
|
||||
def getDefaultTitle(self, info, ):
|
||||
|
||||
# Set default title
|
||||
default_title = toUnicode(info.get('title'))
|
||||
titles = info.get('titles', [])
|
||||
counter = 0
|
||||
def_title = None
|
||||
for title in titles:
|
||||
if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title):
|
||||
def_title = toUnicode(title)
|
||||
break
|
||||
counter += 1
|
||||
|
||||
if not def_title:
|
||||
def_title = toUnicode(titles[0])
|
||||
|
||||
return def_title or 'UNKNOWN'
|
||||
|
||||
def getPoster(self, image_urls, existing_files):
|
||||
image_type = 'poster'
|
||||
|
||||
# Remove non-existing files
|
||||
file_type = 'image_%s' % image_type
|
||||
|
||||
# Make existing unique
|
||||
unique_files = list(set(existing_files.get(file_type, [])))
|
||||
|
||||
# Remove files that can't be found
|
||||
for ef in unique_files:
|
||||
if not os.path.isfile(ef):
|
||||
unique_files.remove(ef)
|
||||
|
||||
# Replace new files list
|
||||
existing_files[file_type] = unique_files
|
||||
if len(existing_files) == 0:
|
||||
del existing_files[file_type]
|
||||
|
||||
# Loop over type
|
||||
for image in image_urls.get(image_type, []):
|
||||
if not isinstance(image, (str, unicode)):
|
||||
continue
|
||||
|
||||
if file_type not in existing_files or len(existing_files.get(file_type, [])) == 0:
|
||||
file_path = fireEvent('file.download', url = image, single = True)
|
||||
if file_path:
|
||||
existing_files[file_type] = [file_path]
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from .main import Library
|
||||
|
||||
|
||||
def autoload():
|
||||
def start():
|
||||
return Library()
|
||||
|
||||
config = []
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from .main import Matcher
|
||||
|
||||
|
||||
def autoload():
|
||||
def start():
|
||||
return Matcher()
|
||||
|
||||
config = []
|
||||
|
||||
@@ -8,7 +8,6 @@ log = CPLog(__name__)
|
||||
|
||||
|
||||
class Matcher(MatcherBase):
|
||||
|
||||
def __init__(self):
|
||||
super(Matcher, self).__init__()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import MediaPlugin
|
||||
|
||||
|
||||
def autoload():
|
||||
def start():
|
||||
return MediaPlugin()
|
||||
|
||||
config = []
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
from string import ascii_letters
|
||||
from hashlib import md5
|
||||
|
||||
from CodernityDB.tree_index import MultiTreeBasedIndex, TreeBasedIndex
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
|
||||
|
||||
class MediaIndex(MultiTreeBasedIndex):
|
||||
_version = 3
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['key_format'] = '32s'
|
||||
super(MediaIndex, self).__init__(*args, **kwargs)
|
||||
|
||||
def make_key(self, key):
|
||||
return md5(key).hexdigest()
|
||||
|
||||
def make_key_value(self, data):
|
||||
if data.get('_t') == 'media' and (data.get('identifier') or data.get('identifiers')):
|
||||
|
||||
identifiers = data.get('identifiers', {})
|
||||
if data.get('identifier') and 'imdb' not in identifiers:
|
||||
identifiers['imdb'] = data.get('identifier')
|
||||
|
||||
ids = []
|
||||
for x in identifiers:
|
||||
ids.append(md5('%s-%s' % (x, identifiers[x])).hexdigest())
|
||||
|
||||
return ids, None
|
||||
|
||||
|
||||
class MediaStatusIndex(TreeBasedIndex):
|
||||
_version = 1
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['key_format'] = '32s'
|
||||
super(MediaStatusIndex, self).__init__(*args, **kwargs)
|
||||
|
||||
def make_key(self, key):
|
||||
return md5(key).hexdigest()
|
||||
|
||||
def make_key_value(self, data):
|
||||
if data.get('_t') == 'media' and data.get('status'):
|
||||
return md5(data.get('status')).hexdigest(), None
|
||||
|
||||
|
||||
class MediaTypeIndex(TreeBasedIndex):
|
||||
_version = 1
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['key_format'] = '32s'
|
||||
super(MediaTypeIndex, self).__init__(*args, **kwargs)
|
||||
|
||||
def make_key(self, key):
|
||||
return md5(key).hexdigest()
|
||||
|
||||
def make_key_value(self, data):
|
||||
if data.get('_t') == 'media' and data.get('type'):
|
||||
return md5(data.get('type')).hexdigest(), None
|
||||
|
||||
|
||||
class TitleSearchIndex(MultiTreeBasedIndex):
|
||||
_version = 1
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex
|
||||
from itertools import izip
|
||||
from couchpotato.core.helpers.encoding import simplifyString"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['key_format'] = '32s'
|
||||
super(TitleSearchIndex, self).__init__(*args, **kwargs)
|
||||
self.__l = kwargs.get('w_len', 2)
|
||||
|
||||
def make_key_value(self, data):
|
||||
|
||||
if data.get('_t') == 'media' and len(data.get('title', '')) > 0:
|
||||
|
||||
out = set()
|
||||
title = str(simplifyString(data.get('title').lower()))
|
||||
l = self.__l
|
||||
title_split = title.split()
|
||||
|
||||
for x in range(len(title_split)):
|
||||
combo = ' '.join(title_split[x:])[:32].strip()
|
||||
out.add(combo.rjust(32, '_'))
|
||||
combo_range = max(l, min(len(combo), 32))
|
||||
|
||||
for cx in range(1, combo_range):
|
||||
ccombo = combo[:-cx].strip()
|
||||
if len(ccombo) > l:
|
||||
out.add(ccombo.rjust(32, '_'))
|
||||
|
||||
return out, None
|
||||
|
||||
def make_key(self, key):
|
||||
return key.rjust(32, '_').lower()
|
||||
|
||||
|
||||
class TitleIndex(TreeBasedIndex):
|
||||
_version = 4
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import TreeBasedIndex
|
||||
from string import ascii_letters
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['key_format'] = '32s'
|
||||
super(TitleIndex, self).__init__(*args, **kwargs)
|
||||
|
||||
def make_key(self, key):
|
||||
return self.simplify(key)
|
||||
|
||||
def make_key_value(self, data):
|
||||
if data.get('_t') == 'media' and data.get('title') is not None and len(data.get('title')) > 0:
|
||||
return self.simplify(data['title']), None
|
||||
|
||||
def simplify(self, title):
|
||||
|
||||
title = toUnicode(title)
|
||||
|
||||
nr_prefix = '' if title and len(title) > 0 and title[0] in ascii_letters else '#'
|
||||
title = simplifyString(title)
|
||||
|
||||
for prefix in ['the ', 'an ', 'a ']:
|
||||
if prefix == title[:len(prefix)]:
|
||||
title = title[len(prefix):]
|
||||
break
|
||||
|
||||
return str(nr_prefix + title).ljust(32, ' ')[:32]
|
||||
|
||||
|
||||
class StartsWithIndex(TreeBasedIndex):
|
||||
_version = 3
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import TreeBasedIndex
|
||||
from string import ascii_letters
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['key_format'] = '1s'
|
||||
super(StartsWithIndex, self).__init__(*args, **kwargs)
|
||||
|
||||
def make_key(self, key):
|
||||
return self.first(key)
|
||||
|
||||
def make_key_value(self, data):
|
||||
if data.get('_t') == 'media' and data.get('title') is not None:
|
||||
return self.first(data['title']), None
|
||||
|
||||
def first(self, title):
|
||||
title = toUnicode(title)
|
||||
title = simplifyString(title)
|
||||
|
||||
for prefix in ['the ', 'an ', 'a ']:
|
||||
if prefix == title[:len(prefix)]:
|
||||
title = title[len(prefix):]
|
||||
break
|
||||
|
||||
return str(title[0] if title and len(title) > 0 and title[0] in ascii_letters else '#').lower()
|
||||
|
||||
|
||||
|
||||
class MediaChildrenIndex(TreeBasedIndex):
|
||||
_version = 1
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['key_format'] = '32s'
|
||||
super(MediaChildrenIndex, self).__init__(*args, **kwargs)
|
||||
|
||||
def make_key(self, key):
|
||||
return key
|
||||
|
||||
def make_key_value(self, data):
|
||||
if data.get('_t') == 'media' and data.get('parent_id'):
|
||||
return data.get('parent_id'), None
|
||||
|
||||
|
||||
class MediaTagIndex(MultiTreeBasedIndex):
|
||||
_version = 2
|
||||
|
||||
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['key_format'] = '32s'
|
||||
super(MediaTagIndex, self).__init__(*args, **kwargs)
|
||||
|
||||
def make_key_value(self, data):
|
||||
if data.get('_t') == 'media' and data.get('tags') and len(data.get('tags', [])) > 0:
|
||||
|
||||
tags = set()
|
||||
for tag in data.get('tags', []):
|
||||
tags.add(self.make_key(tag))
|
||||
|
||||
return list(tags), None
|
||||
|
||||
def make_key(self, key):
|
||||
return md5(key).hexdigest()
|
||||
@@ -1,36 +1,21 @@
|
||||
from datetime import timedelta
|
||||
from operator import itemgetter
|
||||
import time
|
||||
import traceback
|
||||
from string import ascii_lowercase
|
||||
|
||||
from CodernityDB.database import RecordNotFound
|
||||
from couchpotato import tryInt, get_db
|
||||
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, getImdb, getTitle
|
||||
from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media import MediaBase
|
||||
from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex, MediaTagIndex
|
||||
|
||||
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):
|
||||
|
||||
_database = {
|
||||
'media': MediaIndex,
|
||||
'media_search_title': TitleSearchIndex,
|
||||
'media_status': MediaStatusIndex,
|
||||
'media_tag': MediaTagIndex,
|
||||
'media_by_type': MediaTypeIndex,
|
||||
'media_title': TitleIndex,
|
||||
'media_startswith': StartsWithIndex,
|
||||
'media_children': MediaChildrenIndex,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('media.refresh', self.refresh, docs = {
|
||||
@@ -74,51 +59,37 @@ class MediaPlugin(MediaBase):
|
||||
|
||||
addApiView('media.available_chars', self.charView)
|
||||
|
||||
addEvent('app.load', self.addSingleRefreshView, priority = 100)
|
||||
addEvent('app.load', self.addSingleListView, priority = 100)
|
||||
addEvent('app.load', self.addSingleCharView, priority = 100)
|
||||
addEvent('app.load', self.addSingleDeleteView, priority = 100)
|
||||
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.with_status', self.withStatus)
|
||||
addEvent('media.with_identifiers', self.withIdentifiers)
|
||||
addEvent('media.list', self.list)
|
||||
addEvent('media.delete', self.delete)
|
||||
addEvent('media.restatus', self.restatus)
|
||||
addEvent('media.tag', self.tag)
|
||||
addEvent('media.untag', self.unTag)
|
||||
|
||||
def refresh(self, id = '', **kwargs):
|
||||
handlers = []
|
||||
ids = splitString(id)
|
||||
db = get_session()
|
||||
|
||||
for x in ids:
|
||||
for x in splitString(id):
|
||||
media = db.query(Media).filter_by(id = x).first()
|
||||
|
||||
refresh_handler = self.createRefreshHandler(x)
|
||||
if refresh_handler:
|
||||
handlers.append(refresh_handler)
|
||||
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 = 'media.busy', data = {'_id': ids})
|
||||
fireEventAsync('schedule.queue', handlers = handlers)
|
||||
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 createRefreshHandler(self, media_id):
|
||||
|
||||
try:
|
||||
media = get_db().get('id', media_id)
|
||||
event = '%s.update_info' % media.get('type')
|
||||
|
||||
def handler():
|
||||
fireEvent(event, media_id = media_id, on_complete = self.createOnComplete(media_id))
|
||||
|
||||
return handler
|
||||
|
||||
except:
|
||||
log.error('Refresh handler for non existing media: %s', traceback.format_exc())
|
||||
|
||||
def addSingleRefreshView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
@@ -126,30 +97,21 @@ class MediaPlugin(MediaBase):
|
||||
|
||||
def get(self, media_id):
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
db = get_session()
|
||||
|
||||
imdb_id = getImdb(str(media_id))
|
||||
imdb_id = getImdb(str(media_id))
|
||||
|
||||
if imdb_id:
|
||||
media = db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc']
|
||||
else:
|
||||
media = db.get('id', 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()
|
||||
|
||||
if media:
|
||||
results = None
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
# Attach category
|
||||
try: media['category'] = db.get('id', media.get('category_id'))
|
||||
except: pass
|
||||
|
||||
media['releases'] = fireEvent('release.for_media', media['_id'], single = True)
|
||||
|
||||
return media
|
||||
|
||||
except RecordNotFound:
|
||||
log.error('Media with id "%s" not found', media_id)
|
||||
except:
|
||||
raise
|
||||
db.expire_all()
|
||||
return results
|
||||
|
||||
def getView(self, id = None, **kwargs):
|
||||
|
||||
@@ -160,39 +122,9 @@ class MediaPlugin(MediaBase):
|
||||
'media': media,
|
||||
}
|
||||
|
||||
def withStatus(self, status, with_doc = True):
|
||||
def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
||||
|
||||
db = get_db()
|
||||
|
||||
status = list(status if isinstance(status, (list, tuple)) else [status])
|
||||
|
||||
for s in status:
|
||||
for ms in db.get_many('media_status', s):
|
||||
if with_doc:
|
||||
try:
|
||||
doc = db.get('id', ms['_id'])
|
||||
yield doc
|
||||
except RecordNotFound:
|
||||
log.debug('Record not found, skipping: %s', ms['_id'])
|
||||
else:
|
||||
yield ms
|
||||
|
||||
def withIdentifiers(self, identifiers, with_doc = False):
|
||||
|
||||
db = get_db()
|
||||
|
||||
for x in identifiers:
|
||||
try:
|
||||
media = db.get('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc)
|
||||
return media
|
||||
except:
|
||||
pass
|
||||
|
||||
log.debug('No media found with identifiers: %s', identifiers)
|
||||
|
||||
def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, with_tags = None, starts_with = None, search = None):
|
||||
|
||||
db = get_db()
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if status and not isinstance(status, (list, tuple)):
|
||||
@@ -201,100 +133,140 @@ class MediaPlugin(MediaBase):
|
||||
release_status = [release_status]
|
||||
if types and not isinstance(types, (list, tuple)):
|
||||
types = [types]
|
||||
if with_tags and not isinstance(with_tags, (list, tuple)):
|
||||
with_tags = [with_tags]
|
||||
|
||||
# query media ids
|
||||
if types:
|
||||
all_media_ids = set()
|
||||
for media_type in types:
|
||||
all_media_ids = all_media_ids.union(set([x['_id'] for x in db.get_many('media_by_type', media_type)]))
|
||||
else:
|
||||
all_media_ids = set([x['_id'] for x in db.all('media')])
|
||||
|
||||
media_ids = list(all_media_ids)
|
||||
filter_by = {}
|
||||
# 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:
|
||||
filter_by['media_status'] = set()
|
||||
for media_status in fireEvent('media.with_status', status, with_doc = False, single = True):
|
||||
filter_by['media_status'].add(media_status.get('_id'))
|
||||
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:
|
||||
filter_by['release_status'] = set()
|
||||
for release_status in fireEvent('release.with_status', release_status, with_doc = False, single = True):
|
||||
filter_by['release_status'].add(release_status.get('media_id'))
|
||||
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())[0]
|
||||
starts_with = starts_with if starts_with in ascii_lowercase else '#'
|
||||
filter_by['starts_with'] = [x['_id'] for x in db.get_many('media_startswith', 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)))
|
||||
|
||||
# Add tag filter
|
||||
if with_tags:
|
||||
filter_by['with_tags'] = set()
|
||||
for tag in with_tags:
|
||||
for x in db.get_many('media_tag', tag):
|
||||
filter_by['with_tags'].add(x['_id'])
|
||||
|
||||
# Filter with search query
|
||||
if search:
|
||||
filter_by['search'] = [x['_id'] for x in db.get_many('media_search_title', search)]
|
||||
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
||||
|
||||
if status_or and 'media_status' in filter_by and 'release_status' in filter_by:
|
||||
filter_by['status'] = list(filter_by['media_status']) + list(filter_by['release_status'])
|
||||
del filter_by['media_status']
|
||||
del filter_by['release_status']
|
||||
if len(filter_or) > 0:
|
||||
q = q.filter(or_(*filter_or))
|
||||
|
||||
# Filter by combining ids
|
||||
for x in filter_by:
|
||||
media_ids = [n for n in media_ids if n in filter_by[x]]
|
||||
|
||||
total_count = len(media_ids)
|
||||
total_count = q.count()
|
||||
if total_count == 0:
|
||||
return 0, []
|
||||
|
||||
offset = 0
|
||||
limit = -1
|
||||
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 = tryInt(splt[0])
|
||||
offset = tryInt(0 if len(splt) is 1 else splt[1])
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q = q.limit(limit).offset(offset)
|
||||
|
||||
# List movies based on title order
|
||||
medias = []
|
||||
for m in db.all('media_title'):
|
||||
media_id = m['_id']
|
||||
if media_id not in media_ids: continue
|
||||
if offset > 0:
|
||||
offset -= 1
|
||||
continue
|
||||
# Get all media_ids in sorted order
|
||||
media_ids = [m.id for m in q.all()]
|
||||
|
||||
media = fireEvent('media.get', media_id, single = True)
|
||||
# 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
|
||||
medias.append(media)
|
||||
movies.append(mergeDicts(movie_dict[media_id].to_dict({
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
}), {
|
||||
'releases': releases,
|
||||
'releases_count': releases_count.get(media_id),
|
||||
}))
|
||||
|
||||
# remove from media ids
|
||||
media_ids.remove(media_id)
|
||||
if len(media_ids) == 0 or len(medias) == limit: break
|
||||
|
||||
return total_count, medias
|
||||
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 = splitString(kwargs.get('type')),
|
||||
status = splitString(kwargs.get('status')),
|
||||
release_status = splitString(kwargs.get('release_status')),
|
||||
status_or = kwargs.get('status_or') is not None,
|
||||
limit_offset = kwargs.get('limit_offset'),
|
||||
with_tags = splitString(kwargs.get('with_tags')),
|
||||
starts_with = kwargs.get('starts_with'),
|
||||
search = kwargs.get('search')
|
||||
types = types,
|
||||
status = status,
|
||||
release_status = release_status,
|
||||
limit_offset = limit_offset,
|
||||
starts_with = starts_with,
|
||||
search = search,
|
||||
order = order
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -308,57 +280,68 @@ class MediaPlugin(MediaBase):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
def tempList(*args, **kwargs):
|
||||
return self.listView(types = media_type, **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):
|
||||
|
||||
db = get_db()
|
||||
types = types or []
|
||||
status = status or []
|
||||
release_status = release_status or []
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if status and not isinstance(status, (list, tuple)):
|
||||
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]
|
||||
|
||||
# query media ids
|
||||
if types:
|
||||
all_media_ids = set()
|
||||
for media_type in types:
|
||||
all_media_ids = all_media_ids.union(set([x['_id'] for x in db.get_many('media_by_type', media_type)]))
|
||||
else:
|
||||
all_media_ids = set([x['_id'] for x in db.all('media')])
|
||||
|
||||
media_ids = all_media_ids
|
||||
filter_by = {}
|
||||
q = db.query(Media)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
filter_by['media_status'] = set()
|
||||
for media_status in fireEvent('media.with_status', status, with_doc = False, single = True):
|
||||
filter_by['media_status'].add(media_status.get('_id'))
|
||||
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:
|
||||
filter_by['release_status'] = set()
|
||||
for release_status in fireEvent('release.with_status', release_status, with_doc = False, single = True):
|
||||
filter_by['release_status'].add(release_status.get('media_id'))
|
||||
|
||||
# Filter by combining ids
|
||||
for x in filter_by:
|
||||
media_ids = [n for n in media_ids if n in filter_by[x]]
|
||||
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 x in db.all('media_startswith'):
|
||||
if x['_id'] in media_ids:
|
||||
chars.add(x['key'])
|
||||
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
|
||||
|
||||
return list(chars)
|
||||
db.expire_all()
|
||||
return ''.join(sorted(chars))
|
||||
|
||||
def charView(self, **kwargs):
|
||||
|
||||
@@ -377,60 +360,55 @@ class MediaPlugin(MediaBase):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
def tempChar(*args, **kwargs):
|
||||
return self.charView(types = media_type, **kwargs)
|
||||
return self.charView(types = media_type, *args, **kwargs)
|
||||
addApiView('%s.available_chars' % media_type, tempChar)
|
||||
|
||||
def delete(self, media_id, delete_from = None):
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
db = get_session()
|
||||
|
||||
media = db.get('id', media_id)
|
||||
if media:
|
||||
deleted = False
|
||||
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)
|
||||
|
||||
media_releases = fireEvent('release.for_media', media['_id'], single = True)
|
||||
|
||||
if delete_from == 'all':
|
||||
# Delete connected releases
|
||||
for release in media_releases:
|
||||
db.delete(release)
|
||||
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)
|
||||
|
||||
total_releases = len(media_releases)
|
||||
total_deleted = 0
|
||||
new_media_status = None
|
||||
|
||||
for release in media_releases:
|
||||
if delete_from in ['wanted', 'snatched', 'late']:
|
||||
if release.get('status') != 'done':
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_media_status = 'done'
|
||||
elif delete_from == 'manage':
|
||||
if release.get('status') == 'done' or media.get('status') == 'done':
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
|
||||
if (total_releases == total_deleted and media['status'] != 'active') or (total_releases == 0 and not new_media_status) or (not new_media_status and delete_from == 'late'):
|
||||
db.delete(media)
|
||||
deleted = True
|
||||
elif new_media_status:
|
||||
media['status'] = new_media_status
|
||||
db.update(media)
|
||||
|
||||
fireEvent('media.untag', media['_id'], 'recent', single = True)
|
||||
else:
|
||||
fireEvent('media.restatus', media.get('_id'), single = True)
|
||||
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'media.deleted', data = media)
|
||||
except:
|
||||
log.error('Failed deleting media: %s', traceback.format_exc())
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict())
|
||||
|
||||
db.expire_all()
|
||||
return True
|
||||
|
||||
def deleteView(self, id = '', **kwargs):
|
||||
@@ -452,82 +430,29 @@ class MediaPlugin(MediaBase):
|
||||
|
||||
def restatus(self, media_id):
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||
|
||||
m = db.get('id', media_id)
|
||||
previous_status = m['status']
|
||||
db = get_session()
|
||||
|
||||
log.debug('Changing status for %s', getTitle(m))
|
||||
if not m['profile_id']:
|
||||
m['status'] = 'done'
|
||||
else:
|
||||
m['status'] = 'active'
|
||||
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
|
||||
|
||||
try:
|
||||
profile = db.get('id', m['profile_id'])
|
||||
media_releases = fireEvent('release.for_media', m['_id'], single = True)
|
||||
done_releases = [release for release in media_releases if release.get('status') == 'done']
|
||||
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
|
||||
|
||||
if done_releases:
|
||||
# Only look at latest added release
|
||||
release = sorted(done_releases, key = itemgetter('last_edit'), reverse = True)[0]
|
||||
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
|
||||
|
||||
# Check if we are finished with the media
|
||||
if fireEvent('quality.isfinish', {'identifier': release['quality'], 'is_3d': release.get('is_3d', False)}, profile, timedelta(seconds = time.time() - release['last_edit']).days, single = True):
|
||||
m['status'] = 'done'
|
||||
elif previous_status == 'done':
|
||||
m['status'] = 'done'
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
except RecordNotFound:
|
||||
log.debug('Failed restatus, keeping previous: %s', traceback.format_exc())
|
||||
m['status'] = previous_status
|
||||
db.commit()
|
||||
|
||||
# Only update when status has changed
|
||||
if previous_status != m['status']:
|
||||
db.update(m)
|
||||
return True
|
||||
|
||||
# Tag media as recent
|
||||
self.tag(media_id, 'recent')
|
||||
|
||||
return m['status']
|
||||
except:
|
||||
log.error('Failed restatus: %s', traceback.format_exc())
|
||||
|
||||
def tag(self, media_id, tag):
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
m = db.get('id', media_id)
|
||||
|
||||
tags = m.get('tags') or []
|
||||
if tag not in tags:
|
||||
tags.append(tag)
|
||||
m['tags'] = tags
|
||||
db.update(m)
|
||||
|
||||
return True
|
||||
except:
|
||||
log.error('Failed tagging: %s', traceback.format_exc())
|
||||
|
||||
return False
|
||||
|
||||
def unTag(self, media_id, tag):
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
m = db.get('id', media_id)
|
||||
|
||||
tags = m.get('tags') or []
|
||||
if tag in tags:
|
||||
new_tags = list(set(tags))
|
||||
new_tags.remove(tag)
|
||||
|
||||
m['tags'] = new_tags
|
||||
db.update(m)
|
||||
|
||||
return True
|
||||
except:
|
||||
log.error('Failed untagging: %s', traceback.format_exc())
|
||||
|
||||
return False
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.base import Provider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class AutomationBase(Provider):
|
||||
pass
|
||||
@@ -1,5 +0,0 @@
|
||||
from couchpotato.core.media._base.providers.base import Provider
|
||||
|
||||
|
||||
class BaseInfoProvider(Provider):
|
||||
type = 'unknown'
|
||||
@@ -1,8 +0,0 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class MetaDataBase(Plugin):
|
||||
pass
|
||||
@@ -1,120 +0,0 @@
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.variable import tryInt, simplifyString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(NZBProvider):
|
||||
|
||||
urls = {
|
||||
'download': 'https://www.binsearch.info/fcgi/nzb.fcgi?q=%s',
|
||||
'detail': 'https://www.binsearch.info%s',
|
||||
'search': 'https://www.binsearch.info/index.php?%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 4 # Seconds
|
||||
|
||||
def _search(self, media, quality, results):
|
||||
|
||||
data = self.getHTMLData(self.urls['search'] % self.buildUrl(media, quality))
|
||||
|
||||
if data:
|
||||
try:
|
||||
|
||||
html = BeautifulSoup(data)
|
||||
main_table = html.find('table', attrs = {'id': 'r2'})
|
||||
|
||||
if not main_table:
|
||||
return
|
||||
|
||||
items = main_table.find_all('tr')
|
||||
|
||||
for row in items:
|
||||
title = row.find('span', attrs = {'class': 's'})
|
||||
|
||||
if not title: continue
|
||||
|
||||
nzb_id = row.find('input', attrs = {'type': 'checkbox'})['name']
|
||||
info = row.find('span', attrs = {'class':'d'})
|
||||
size_match = re.search('size:.(?P<size>[0-9\.]+.[GMB]+)', info.text)
|
||||
|
||||
age = 0
|
||||
try: age = re.search('(?P<size>\d+d)', row.find_all('td')[-1:][0].text).group('size')[:-1]
|
||||
except: pass
|
||||
|
||||
def extra_check(item):
|
||||
parts = re.search('available:.(?P<parts>\d+)./.(?P<total>\d+)', info.text)
|
||||
total = float(tryInt(parts.group('total')))
|
||||
parts = float(tryInt(parts.group('parts')))
|
||||
|
||||
if (total / parts) < 1 and ((total / parts) < 0.95 or ((total / parts) >= 0.95 and not ('par2' in info.text.lower() or 'pa3' in info.text.lower()))):
|
||||
log.info2('Wrong: \'%s\', not complete: %s out of %s', (item['name'], parts, total))
|
||||
return False
|
||||
|
||||
if 'requires password' in info.text.lower():
|
||||
log.info2('Wrong: \'%s\', passworded', (item['name']))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'name': simplifyString(title.text),
|
||||
'age': tryInt(age),
|
||||
'size': self.parseSize(size_match.group('size')),
|
||||
'url': self.urls['download'] % nzb_id,
|
||||
'detail_url': self.urls['detail'] % info.find('a')['href'],
|
||||
'extra_check': extra_check
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parse HTML response from BinSearch: %s', traceback.format_exc())
|
||||
|
||||
def download(self, url = '', nzb_id = ''):
|
||||
|
||||
data = {
|
||||
'action': 'nzb',
|
||||
nzb_id: 'on'
|
||||
}
|
||||
|
||||
try:
|
||||
return self.urlopen(url, data = data, show_error = False)
|
||||
except:
|
||||
log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return 'try_next'
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'binsearch',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'nzb_providers',
|
||||
'name': 'binsearch',
|
||||
'description': 'Free provider, less accurate. See <a href="https://www.binsearch.info/">BinSearch</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAATklEQVQY02NwQAMMWAXOnz+PKvD//3/CAvM//z+fgiwAAs+RBab4PP//vwbFjPlAffgEChzOo2r5fBuIfRAC5w8D+QUofkkp8MHjOWQAAM3Sbogztg2wAAAAAElFTkSuQmCC',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,103 +0,0 @@
|
||||
from urlparse import urlparse, parse_qs
|
||||
import time
|
||||
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
|
||||
from dateutil.parser import parse
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(NZBProvider, RSS):
|
||||
|
||||
urls = {
|
||||
'search': 'https://rss.omgwtfnzbs.org/rss-search.php?%s',
|
||||
'detail_url': 'https://omgwtfnzbs.org/details.php?id=%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
cat_ids = [
|
||||
([15], ['dvdrip']),
|
||||
([15, 16], ['brrip']),
|
||||
([16], ['720p', '1080p', 'bd50']),
|
||||
([17], ['dvdr']),
|
||||
]
|
||||
cat_backup_id = 'movie'
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
if quality['identifier'] in fireEvent('quality.pre_releases', single = True):
|
||||
return []
|
||||
|
||||
return super(Base, self).search(movie, quality)
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
q = '%s %s' % (title, movie['info']['year'])
|
||||
params = tryUrlencode({
|
||||
'search': q,
|
||||
'catid': ','.join([str(x) for x in self.getCatId(quality)]),
|
||||
'user': self.conf('username', default = ''),
|
||||
'api': self.conf('api_key', default = ''),
|
||||
})
|
||||
|
||||
nzbs = self.getRSSData(self.urls['search'] % params)
|
||||
|
||||
for nzb in nzbs:
|
||||
|
||||
enclosure = self.getElement(nzb, 'enclosure').attrib
|
||||
nzb_id = parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0]
|
||||
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'name': toUnicode(self.getTextElement(nzb, 'title')),
|
||||
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, 'pubDate')).timetuple()))),
|
||||
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
||||
'url': enclosure['url'],
|
||||
'detail_url': self.urls['detail_url'] % nzb_id,
|
||||
'description': self.getTextElement(nzb, 'description')
|
||||
})
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'omgwtfnzbs',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'nzb_providers',
|
||||
'name': 'OMGWTFNZBs',
|
||||
'description': 'See <a href="http://omgwtfnzbs.org/">OMGWTFNZBs</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAIAAADAAbR1AAADbElEQVR4AZ2UW0ybZRiAy/OvdHaLYvB0YTRIFi7GkM44zRLmIfNixkWdiRMyYoxRE8/TC7MYvXCGEBmr3mxLwVMwY0wYA7e6Wso4lB6h/U9taSlMGIfBXLYlJMyo0S///2dJI5lxN8/F2/f9nu9737e/jYmXr6KTbN9BGG9HE/NotQ76UWziNzrXFiETk/5ARUNH+7+0kW7fSgTl0VKGOLZzidOkmuuIo7q2oTArNLPIzhdIkqXkerFOm2CaD/5bcKrjIL2c3fkhPxOq93Kcb91v46fV9TQKF4TgV/TbUsQtzfCaK6jMOd5DJrguSIIhexmqqVxN0FXbRR8/ND/LYTTj6J7nl2gnL47OkDW4KJhnQHCa6JpKVNJGA3OC58nwBJoZ//ebbIyKpBxjrr0o1q1FMRkrKXZnHWF85VvxMrJxibwhGyd0f5bLnKzqJs1k0Sfo+EU8hdAUvkbcwKEgs2D0OiV4jmmD1zb+Tp6er0JMMvDxPo5xev9zTBF683NS+N56n1YiB95B5crr93KRuKhKI0tb0Kw2mgLLqTjLEWO8424i9IvURaYeOckwf3+/yCC9e3bQQ/MuD+Monk0k+XFXMUfx7z5EEP+XlXi5tLlMxH8zLppw7idJrugcus30kC86gc7UrQqjLIukM8zWHOACeU+TiMxXN6ExVOkgz4lvPEzice1GIVhxhG4CrZvpl6TH55giKWqXGLy9hZh5aUtgDSew/msSyCKpl+DDNfxJc8NBIsxUxUnz14O/oONu+IIIvso9TLBQ1SY5rUhuSzUhAqJ2mRXBLDOCeUtgUZXsaObT8BffhUJPqWgiV+3zKKzYH0ClvTRLhD77HIqVkyh5jThnivehoG+qJctIRSPn6bxvO4FCgTl9c1DmbpjLajbQFE8aW5SU3rg+zOPGUjTUF9NFpLEbH2c/KmGYlY69/GQJVtGMSUcEp9eCbB1nctbxHTLRdTUkGDf+B02uGWRG3OvpJ/zSMwzif+oxVBID3cQKBavLCiPmB2PM2UuSCUPgrX4VDb97AwEG67bh4+KTOlncvu3M31BwA5rLHbCfEjwkNDky9e/SSbSxnD46Pg0RJtpXRvhmBSZHpRjWtKwFybjuQeXaKxto4WjLZZZvVmC17pZLJFkwxm5++PS2Mrwc7nyIMYZe/IzoP5d6QgEybqTXAAAAAElFTkSuQmCC',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'Api Key',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'default': 20,
|
||||
'type': 'int',
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,141 +0,0 @@
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.variable import tryInt, getIdentifier
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://awesome-hd.net/',
|
||||
'detail': 'https://awesome-hd.net/torrents.php?torrentid=%s',
|
||||
'search': 'https://awesome-hd.net/searchapi.php?action=imdbsearch&passkey=%s&imdb=%s&internal=%s',
|
||||
'download': 'https://awesome-hd.net/torrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s',
|
||||
}
|
||||
http_time_between_calls = 1
|
||||
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), getIdentifier(movie), self.conf('only_internal')))
|
||||
|
||||
if data:
|
||||
try:
|
||||
soup = BeautifulSoup(data)
|
||||
|
||||
if soup.find('error'):
|
||||
log.error(soup.find('error').get_text())
|
||||
return
|
||||
|
||||
authkey = soup.find('authkey').get_text()
|
||||
entries = soup.find_all('torrent')
|
||||
|
||||
for entry in entries:
|
||||
|
||||
torrentscore = 0
|
||||
torrent_id = entry.find('id').get_text()
|
||||
name = entry.find('name').get_text()
|
||||
year = entry.find('year').get_text()
|
||||
releasegroup = entry.find('releasegroup').get_text()
|
||||
resolution = entry.find('resolution').get_text()
|
||||
encoding = entry.find('encoding').get_text()
|
||||
freeleech = entry.find('freeleech').get_text()
|
||||
torrent_desc = '/ %s / %s / %s ' % (releasegroup, resolution, encoding)
|
||||
|
||||
if freeleech == '0.25' and self.conf('prefer_internal'):
|
||||
torrent_desc += '/ Internal'
|
||||
torrentscore += 200
|
||||
|
||||
if encoding == 'x264' and self.conf('favor') in ['encode', 'both']:
|
||||
torrentscore += 300
|
||||
if re.search('Remux', encoding) and self.conf('favor') in ['remux', 'both']:
|
||||
torrentscore += 200
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
|
||||
'url': self.urls['download'] % (torrent_id, authkey, self.conf('passkey')),
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'size': self.parseSize(entry.find('size').get_text()),
|
||||
'seeders': tryInt(entry.find('seeders').get_text()),
|
||||
'leechers': tryInt(entry.find('leechers').get_text()),
|
||||
'score': torrentscore
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'awesomehd',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'Awesome-HD',
|
||||
'description': '<a href="https://awesome-hd.net">AHD</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC+UlEQVR4AV1SO0y6dxQ9H4g8CoIoohZ5NA0aR2UgkYpNB5uocTSaLlrDblMH09Gt8d90r3YpJkanxjA4GGkbO7RNxSABq8jDGnkpD+UD5NV7Bxvbk9wvv+/3uPece66A/yEWi42FQqHVfD7/cbPZtIEglUpjOp3uZHR0dBvAn3gDIRqNgjE4OKj0+Xzf3NzcfD4wMCCjf5TLZbTbbajVatzf3+Pu7q5uNpt35ufnvwBQAScQRREEldfr9RWLxan+/n5YrVa+jFarhVfQQyQSCU4EhULhX15engEgSrjC0dHRVqlUmjQYDBgaGgKtuTqz4mTgIoVCASaTCX19fajVapOHh4dbFJBks9mxcDi8qtFoJEajkfVyJWi1WkxMTMDhcIAT8x6D7/Dd6+vr1fHx8TGp2+3+iqo5+YCzBwIBToK5ubl/mQwPDyMSibAs2Gw2UHNRrValz8/PDUk8Hv9EqVRCr9fj4uICTNflcqFer+Pg4AB7e3uoVCq8x9Rxfn6O7u5uqFQq8FspZXxHTekggByA3W4Hr9PpNDeRL3I1cMhkMrBrnZ2dyGQyvNYIs7OzVbJNPjIyAraLwYdcjR8wXl5eIJfLwRIFQQDLYkm3t7c1CdGPPT4+cpOImp4PODMeaK+n10As2jBbrHifHOjS6qAguVFimkqlwAMmIQnHV1dX4NDQhVwuhyZTV6pgIktzDzkkk0lEwhEEzs7ASQr5Ai4vL1nuccfCwsLO/v6+p9FoyJhF6ekJro/cPCzIZLNQa7rQoK77/SdgWWpKkCaJ5EB9aWnpe6nH40nRMBnJV4f5gw+FX3/5GX/8/htXRZdOzzqhJWn6nl6YbTZqqhrhULD16fT0d8FgcFtYW1vD5uamfGVl5cd4IjldKhZACdkJvKfWUANrxEaJV4hiGVaL1b+7653hXzwRZQr2X76xsfG1xWIRaZzbNPv/CdrjEL9cX/+WXFBSgEPgzxuwG3Yans9OT0+naBZMIJDNfzudzp8WFxd/APAX3uAf9WOTxOPLdosAAAAASUVORK5CYII=',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'passkey',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'only_internal',
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'default': 1,
|
||||
'description': 'Only search for internal releases.'
|
||||
},
|
||||
{
|
||||
'name': 'prefer_internal',
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'default': 1,
|
||||
'description': 'Favors internal releases over non-internal releases.'
|
||||
},
|
||||
{
|
||||
'name': 'favor',
|
||||
'advanced': True,
|
||||
'default': 'both',
|
||||
'type': 'dropdown',
|
||||
'values': [('Encodes & Remuxes', 'both'), ('Encodes', 'encode'), ('Remuxes', 'remux'), ('None', 'none')],
|
||||
'description': 'Give extra scoring to encodes or remuxes.'
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'type': 'int',
|
||||
'default': 20,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://www.bit-hdtv.com/',
|
||||
'login': 'http://www.bit-hdtv.com/takelogin.php',
|
||||
'login_check': 'http://www.bit-hdtv.com/messages.php',
|
||||
'detail': 'http://www.bit-hdtv.com/details.php?id=%s',
|
||||
'search': 'http://www.bit-hdtv.com/torrents.php?',
|
||||
}
|
||||
|
||||
# Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
def _search(self, media, quality, results):
|
||||
|
||||
query = self.buildUrl(media, quality)
|
||||
|
||||
url = "%s&%s" % (self.urls['search'], query)
|
||||
|
||||
data = self.getHTMLData(url)
|
||||
|
||||
if data:
|
||||
# Remove BiT-HDTV's output garbage so outdated BS4 versions successfully parse the HTML
|
||||
split_data = data.partition('-->')
|
||||
if '## SELECT COUNT(' in split_data[0]:
|
||||
data = split_data[2]
|
||||
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'width': '750', 'class': ''})
|
||||
if result_table is None:
|
||||
return
|
||||
|
||||
entries = result_table.find_all('tr')
|
||||
for result in entries[1:]:
|
||||
|
||||
cells = result.find_all('td')
|
||||
link = cells[2].find('a')
|
||||
torrent_id = link['href'].replace('/details.php?id=', '')
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': link.contents[0].get_text(),
|
||||
'url': cells[0].find('a')['href'],
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'size': self.parseSize(cells[6].get_text()),
|
||||
'seeders': tryInt(cells[8].string),
|
||||
'leechers': tryInt(cells[9].string),
|
||||
'get_more_info': self.getMoreInfo,
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return {
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
}
|
||||
|
||||
def getMoreInfo(self, item):
|
||||
full_description = self.getCache('bithdtv.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||
html = BeautifulSoup(full_description)
|
||||
nfo_pre = html.find('table', attrs = {'class': 'detail'})
|
||||
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||
|
||||
item['description'] = description
|
||||
return item
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'logout.php' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'bithdtv',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'BiT-HDTV',
|
||||
'description': '<a href="http://bit-hdtv.com">BiT-HDTV</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABMklEQVR4AZ3Qu0ojcQCF8W9MJcQbJNgEEQUbQVIqWgnaWfkIvoCgggixEAmIhRtY2GV3w7KwU61B0EYIxmiw0YCik84ipaCuc0nmP5dcjIUgOjqDvxf4OAdf9mnMLcUJyPyGSCP+YRdC+Kp8iagJKhuS+InYRhTGgDbeV2uEMand4ZRxizjXHQEimxhraAnUr73BNqQxMiNeV2SwcjTLEVtb4Zl10mXutvOWm2otw5Sxz6TGTbdd6ncuYvVLXAXrvM+ruyBpy1S3JLGDfUQ1O6jn5vTsrJXvqSt4UNfj6vxTRPxBHER5QeSirhLGk/5rWN+ffB1XZuxjnDy1q87m7TS+xOGA+Iv4gfkbaw+nOMXHDHnITGEk0VfRFnn4Po4vNYm6RGukmggR0L08+l+e4HMeASo/i6AJUjLgAAAAAElFTkSuQmCC',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 20,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,134 +0,0 @@
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup, SoupStrainer
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://www.bitsoup.me/',
|
||||
'login': 'https://www.bitsoup.me/takelogin.php',
|
||||
'login_check': 'https://www.bitsoup.me/my.php',
|
||||
'search': 'https://www.bitsoup.me/browse.php?%s',
|
||||
'baseurl': 'https://www.bitsoup.me/%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
only_tables_tags = SoupStrainer('table')
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
url = self.urls['search'] % self.buildUrl(title, movie, quality)
|
||||
data = self.getHTMLData(url)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data, 'html.parser', parse_only = self.only_tables_tags)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'class': 'koptekst'})
|
||||
if not result_table or 'nothing found!' in data.lower():
|
||||
return
|
||||
|
||||
entries = result_table.find_all('tr')
|
||||
for result in entries[1:]:
|
||||
|
||||
all_cells = result.find_all('td')
|
||||
|
||||
torrent = all_cells[1].find('a')
|
||||
download = all_cells[3].find('a')
|
||||
|
||||
torrent_id = torrent['href']
|
||||
torrent_id = torrent_id.replace('details.php?id=', '')
|
||||
torrent_id = torrent_id.replace('&hit=1', '')
|
||||
|
||||
torrent_name = torrent.getText()
|
||||
|
||||
torrent_size = self.parseSize(all_cells[7].getText())
|
||||
torrent_seeders = tryInt(all_cells[9].getText())
|
||||
torrent_leechers = tryInt(all_cells[10].getText())
|
||||
torrent_url = self.urls['baseurl'] % download['href']
|
||||
torrent_detail_url = self.urls['baseurl'] % torrent['href']
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': torrent_name,
|
||||
'size': torrent_size,
|
||||
'seeders': torrent_seeders,
|
||||
'leechers': torrent_leechers,
|
||||
'url': torrent_url,
|
||||
'detail_url': torrent_detail_url,
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return {
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'ssl': 'yes',
|
||||
}
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'logout.php' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'bitsoup',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'Bitsoup',
|
||||
'description': '<a href="https://bitsoup.me">Bitsoup</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAB8ElEQVR4AbWSS2sTURiGz3euk0mswaE37HhNhIrajQheFgF3rgR/lAt/gOBCXNZlo6AbqfUWRVCxi04wqUnTRibpJLaJzdzOOZ6WUumyC5/VHOb9eN/FA91uFx0FjI4IPfgiGLTWH73tn348GKmN7ijD0d2b41fO5qJEaX24AWNIUrVQCTTJ3Llx6vbV6Vtzk7Gi9+ebi996guFDDYAQAVj4FExP5qdOZB49W62t/zH3hECcwsPnbWeMXz6Xi2K1f0ApeK3hMCHHbP5gvvoriBgFAAQJEAxhjJ4u+YWTNsVI6b1JgtPWZkoIefKy4fcii2OTw2BABs7wj3bYDlLL4rvjGWOdTser1j5Xf7c3Q/MbHQYApxItvnm31mhQQ71eX2vUB76/vsWB2hg0QuogrMwLIG8P3InM2/eVGXeDViqVwWB79vRU2lgJYmdHcgXCTAXQFJTN5HguvDCR2Hxsxe8EvT54nlcul5vNpqDIEgwRQanAhAAABgRIyiQcjpIkkTOuWyqVoN/vSylX67XXH74uV1vHRUyxxFqbLBCSmBpiXSq6xcL5QrGYzWZ3XQIAwdlOJB+/aL764ucdmncYs0WsCI7kvTnn+qyDMEnTVCn1Tz5KsBFg6fvWcmsUAcnYNC/g2hnromvvqbHvxv+39S+MX+bWkFXwAgAAAABJRU5ErkJggg==',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 20,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,116 +0,0 @@
|
||||
import re
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from couchpotato.core.helpers.variable import tryInt, getIdentifier
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://hdbits.org/',
|
||||
'detail': 'https://hdbits.org/details.php?id=%s',
|
||||
'download': 'https://hdbits.org/download.php?id=%s&passkey=%s',
|
||||
'api': 'https://hdbits.org/api/torrents'
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
def _post_query(self, **params):
|
||||
|
||||
post_data = {
|
||||
'username': self.conf('username'),
|
||||
'passkey': self.conf('passkey')
|
||||
}
|
||||
post_data.update(params)
|
||||
|
||||
try:
|
||||
result = self.getJsonData(self.urls['api'], data = json.dumps(post_data))
|
||||
|
||||
if result:
|
||||
if result['status'] != 0:
|
||||
log.error('Error searching hdbits: %s' % result['message'])
|
||||
else:
|
||||
return result['data']
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
match = re.match(r'tt(\d{7})', getIdentifier(movie))
|
||||
|
||||
data = self._post_query(imdb = {'id': match.group(1)})
|
||||
|
||||
if data:
|
||||
try:
|
||||
for result in data:
|
||||
results.append({
|
||||
'id': result['id'],
|
||||
'name': result['name'],
|
||||
'url': self.urls['download'] % (result['id'], self.conf('passkey')),
|
||||
'detail_url': self.urls['detail'] % result['id'],
|
||||
'size': tryInt(result['size']) / 1024 / 1024,
|
||||
'seeders': tryInt(result['seeders']),
|
||||
'leechers': tryInt(result['leechers'])
|
||||
})
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'hdbits',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'HDBits',
|
||||
'wizard': True,
|
||||
'description': '<a href="http://hdbits.org">HDBits</a>',
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABi0lEQVR4AZWSzUsbQRjGdyabTcvSNPTSHlpQQeMHJApC8CJRvHgQQU969+LJP8G7f4N3DwpeFRQvRr0EKaUl0ATSpkigUNFsMl/r9NmZLCEHA/nNO5PfvMPDm0DI6fV3ZxiolEICe1oZCBVCCmBPKwOh2ErKBHGE4KYEXBpSLkUlqO4LcM7f+6nVhRnOhSkOz/hexk+tL+YL0yPF2YmN4tynD++4gTLGkNNac9YFLoREBR1+cnF3dFY6v/m6PD+FaXiNJtgA4xYbABxiGrz6+6HWaI5/+Qh37YS0/3Znc8UxwNGBIIBX22z+/ZdJ+4wzyjpR4PEpODg8tgUXBv2iWUzSpa12B0IR6n6lvt8Aek2lZHb084+fdRNgrwY8z81PjhVy2d2ttUrtV/lbBa+JXGEpDMPnoF2tN1QYRqVUtf6nFbThb7wk7le395elcqhASLb39okDiHY00VCtCTEHwSiH4AI0lkOiT1dwMeSfT3SRxiQWNO7Zwj1egkoVIQFMKvSiC3bcjXq9Jf8DcDIRT3hh10kAAAAASUVORK5CYII=',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'passkey',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,195 +0,0 @@
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'download': 'https://www.ilovetorrents.me/%s',
|
||||
'detail': 'https://www.ilovetorrents.me/%s',
|
||||
'search': 'https://www.ilovetorrents.me/browse.php?search=%s&page=%s&cat=%s',
|
||||
'test': 'https://www.ilovetorrents.me/',
|
||||
'login': 'https://www.ilovetorrents.me/takelogin.php',
|
||||
'login_check': 'https://www.ilovetorrents.me'
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
(['41'], ['720p', '1080p', 'brrip']),
|
||||
(['19'], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
|
||||
(['20'], ['dvdr'])
|
||||
]
|
||||
|
||||
cat_backup_id = 200
|
||||
disable_provider = False
|
||||
http_time_between_calls = 1
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
page = 0
|
||||
total_pages = 1
|
||||
cats = self.getCatId(quality)
|
||||
|
||||
while page < total_pages:
|
||||
|
||||
movieTitle = tryUrlencode('"%s" %s' % (title, movie['info']['year']))
|
||||
search_url = self.urls['search'] % (movieTitle, page, cats[0])
|
||||
page += 1
|
||||
|
||||
data = self.getHTMLData(search_url)
|
||||
if data:
|
||||
try:
|
||||
|
||||
results_table = None
|
||||
|
||||
data_split = splitString(data, '<table')
|
||||
soup = None
|
||||
for x in data_split:
|
||||
soup = BeautifulSoup(x)
|
||||
results_table = soup.find('table', attrs = {'class': 'koptekst'})
|
||||
if results_table:
|
||||
break
|
||||
|
||||
if not results_table:
|
||||
return
|
||||
|
||||
try:
|
||||
pagelinks = soup.findAll(href = re.compile('page'))
|
||||
page_numbers = [int(re.search('page=(?P<page_number>.+'')', i['href']).group('page_number')) for i in pagelinks]
|
||||
total_pages = max(page_numbers)
|
||||
except:
|
||||
pass
|
||||
|
||||
entries = results_table.find_all('tr')
|
||||
|
||||
for result in entries[1:]:
|
||||
prelink = result.find(href = re.compile('details.php'))
|
||||
link = prelink['href']
|
||||
download = result.find('a', href = re.compile('download.php'))['href']
|
||||
|
||||
if link and download:
|
||||
|
||||
def extra_score(item):
|
||||
trusted = (0, 10)[result.find('img', alt = re.compile('Trusted')) is not None]
|
||||
vip = (0, 20)[result.find('img', alt = re.compile('VIP')) is not None]
|
||||
confirmed = (0, 30)[result.find('img', alt = re.compile('Helpers')) is not None]
|
||||
moderated = (0, 50)[result.find('img', alt = re.compile('Moderator')) is not None]
|
||||
|
||||
return confirmed + trusted + vip + moderated
|
||||
|
||||
id = re.search('id=(?P<id>\d+)&', link).group('id')
|
||||
url = self.urls['download'] % download
|
||||
|
||||
fileSize = self.parseSize(result.select('td.rowhead')[5].text)
|
||||
results.append({
|
||||
'id': id,
|
||||
'name': toUnicode(prelink.find('b').text),
|
||||
'url': url,
|
||||
'detail_url': self.urls['detail'] % link,
|
||||
'size': fileSize,
|
||||
'seeders': tryInt(result.find_all('td')[2].string),
|
||||
'leechers': tryInt(result.find_all('td')[3].string),
|
||||
'extra_score': extra_score,
|
||||
'get_more_info': self.getMoreInfo
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return {
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'submit': 'Welcome to ILT',
|
||||
}
|
||||
|
||||
def getMoreInfo(self, item):
|
||||
cache_key = 'ilt.%s' % item['id']
|
||||
description = self.getCache(cache_key)
|
||||
|
||||
if not description:
|
||||
|
||||
try:
|
||||
full_description = self.getHTMLData(item['detail_url'])
|
||||
html = BeautifulSoup(full_description)
|
||||
nfo_pre = html.find('td', attrs = {'class': 'main'}).findAll('table')[1]
|
||||
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||
except:
|
||||
log.error('Failed getting more info for %s', item['name'])
|
||||
description = ''
|
||||
|
||||
self.setCache(cache_key, description, timeout = 25920000)
|
||||
|
||||
item['description'] = description
|
||||
return item
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'logout.php' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'ilovetorrents',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'ILoveTorrents',
|
||||
'description': 'Where the Love of Torrents is Born',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACPUlEQVR4AYWM0U9SbxjH3+v266I/oNvWZTfd2J1d0ZqbZEFwWrUImOKs4YwtumFKZvvlJJADR2TCQQlMPKg5NmpREgaekAPnBATKgmK1LqQlx6awHnZWF1Tr2Xfvvs+7z+dB0mlO7StpAh+M4S/2jbo3w8+xvJvlnSneEt+10zwer5ujNUOoChjALWFw5XOwdCAk/P57cGvPl+Oht0W7VJHN5NC1uW1BON4hGjXbwpVWMZhsy9v7sEIXAsDNYBXgdkEoIKyWD2CF8ut/aOXTZc/fBSgLWw1BgA4BDHOV0GkT90cBQpXahU5TFomsb38XhJC5/Tbh1P8c6rJlBeGfAeyMhUFwNVcs9lxV9Ot0dwmyd+mrNvRtbJ2fSPC6Z3Vsvub2z3sDFACAAYzk0+kUyxEkyfN7PopqNBro55A+P6yPKIrL5zF1HwjdeBJJCObIsZO79bo3sHhWhglo5WMV3mazuVPb4fLvSL8/FAkB1hK6rXQPwYhMyROK8VK5LAiH/jsMt0HQjxiN4/ePdoilllcqDyt3Mkg8mRBNbIhMb8RERkowQA/p76g0/UDDdCoNmDminM0qSK5vlpE5kugCHhNPxntwWmJPYTMZtYcFR6ABHQsVRlYLukVORaaULvqKI46keFSCv77kSPS6kxrPptLNDHgz16fWBtyxe6v5h08LUy+KI8ushqTPWWIX8Sg6b45IrGtyW6zXFb/hpQf9m3oqfWuB0fpSw0uZ4WB69En69uOk2rmO2V52PXj+A/mI4ESKpb2HAAAAAElFTkSuQmCC',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'label': 'Username',
|
||||
'type': 'string',
|
||||
'default': '',
|
||||
'description': 'The user name for your ILT account',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'label': 'Password',
|
||||
'type': 'password',
|
||||
'default': '',
|
||||
'description': 'The password for your ILT account.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -1,182 +0,0 @@
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.variable import tryInt, getIdentifier
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentMagnetProvider):
|
||||
|
||||
urls = {
|
||||
'detail': '%s/%s',
|
||||
'search': '%s/%s-i%s/',
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
(['cam'], ['cam']),
|
||||
(['telesync'], ['ts', 'tc']),
|
||||
(['screener', 'tvrip'], ['screener']),
|
||||
(['x264', '720p', '1080p', 'blu-ray', 'hdrip'], ['bd50', '1080p', '720p', 'brrip']),
|
||||
(['dvdrip'], ['dvdrip']),
|
||||
(['dvd'], ['dvdr']),
|
||||
]
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
cat_backup_id = None
|
||||
|
||||
proxy_list = [
|
||||
'https://kickass.to',
|
||||
'http://kickass.pw',
|
||||
'http://kickassto.come.in',
|
||||
'http://katproxy.ws',
|
||||
'http://www.kickassunblock.info',
|
||||
'http://www.kickassproxy.info',
|
||||
'http://katph.eu',
|
||||
'http://kickassto.come.in',
|
||||
]
|
||||
|
||||
def _search(self, media, quality, results):
|
||||
|
||||
data = self.getHTMLData(self.urls['search'] % (self.getDomain(), 'm', getIdentifier(media).replace('tt', '')))
|
||||
|
||||
if data:
|
||||
|
||||
cat_ids = self.getCatId(quality)
|
||||
table_order = ['name', 'size', None, 'age', 'seeds', 'leechers']
|
||||
|
||||
try:
|
||||
html = BeautifulSoup(data)
|
||||
resultdiv = html.find('div', attrs = {'class': 'tabs'})
|
||||
for result in resultdiv.find_all('div', recursive = False):
|
||||
if result.get('id').lower().strip('tab-') not in cat_ids:
|
||||
continue
|
||||
|
||||
try:
|
||||
for temp in result.find_all('tr'):
|
||||
if temp['class'] is 'firstr' or not temp.get('id'):
|
||||
continue
|
||||
|
||||
new = {}
|
||||
|
||||
nr = 0
|
||||
for td in temp.find_all('td'):
|
||||
column_name = table_order[nr]
|
||||
if column_name:
|
||||
|
||||
if column_name == 'name':
|
||||
link = td.find('div', {'class': 'torrentname'}).find_all('a')[2]
|
||||
new['id'] = temp.get('id')[-7:]
|
||||
new['name'] = link.text
|
||||
new['url'] = td.find('a', 'imagnet')['href']
|
||||
new['detail_url'] = self.urls['detail'] % (self.getDomain(), link['href'][1:])
|
||||
new['verified'] = True if td.find('a', 'iverify') else False
|
||||
new['score'] = 100 if new['verified'] else 0
|
||||
elif column_name is 'size':
|
||||
new['size'] = self.parseSize(td.text)
|
||||
elif column_name is 'age':
|
||||
new['age'] = self.ageToDays(td.text)
|
||||
elif column_name is 'seeds':
|
||||
new['seeders'] = tryInt(td.text)
|
||||
elif column_name is 'leechers':
|
||||
new['leechers'] = tryInt(td.text)
|
||||
|
||||
nr += 1
|
||||
|
||||
# Only store verified torrents
|
||||
if self.conf('only_verified') and not new['verified']:
|
||||
continue
|
||||
|
||||
results.append(new)
|
||||
except:
|
||||
log.error('Failed parsing KickAssTorrents: %s', traceback.format_exc())
|
||||
|
||||
except AttributeError:
|
||||
log.debug('No search results found.')
|
||||
|
||||
def ageToDays(self, age_str):
|
||||
age = 0
|
||||
age_str = age_str.replace(' ', ' ')
|
||||
|
||||
regex = '(\d*.?\d+).(sec|hour|day|week|month|year)+'
|
||||
matches = re.findall(regex, age_str)
|
||||
for match in matches:
|
||||
nr, size = match
|
||||
mult = 1
|
||||
if size == 'week':
|
||||
mult = 7
|
||||
elif size == 'month':
|
||||
mult = 30.5
|
||||
elif size == 'year':
|
||||
mult = 365
|
||||
|
||||
age += tryInt(nr) * mult
|
||||
|
||||
return tryInt(age)
|
||||
|
||||
def isEnabled(self):
|
||||
return super(Base, self).isEnabled() and self.getDomain()
|
||||
|
||||
def correctProxy(self, data):
|
||||
return 'search query' in data.lower()
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'kickasstorrents',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'KickAssTorrents',
|
||||
'description': '<a href="https://kat.ph/">KickAssTorrents</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACD0lEQVR42pXK20uTcRjA8d/fsJsuap0orBuFlm3hir3JJvQOVmuwllN20Lb2isI2nVHKjBqrCWYaNnNuBrkSWxglhDVJOkBdSWUOq5FgoiOrMdRJ2xPPxW+8OUf1ge/FcyCUSVe2qedK5U/OxNTTXRNXEQ52Glb4O6dNEfK1auJkvRY7+/zxnQbA/D596laXcY3OWOiaIX2393SGznUmxkUo/YkDgqHemuzobQ7+NV+reo5Q1mqp68GABdY3+/EloO+JeN4tEqiFU8f3CwhyWo9E7wfMgI0ELTDx0AvjIxcgvZoC9P7NMN7yMmrFeoKa68rfDfmrARsNN0Ihr55cx59ctZWSiwS5bLKpwW4dYJH+M/B6/CYszE0BFZ+egG+Ln+HRoBN/cpl1pV6COIMkOnBVA/w+fXgGKJVM4LxhumMleoL06hJ3wKcCfl+/TAKKx17gnFePRwkqxR4BQSpFkbCrrQJueI7mWpyfATQ9OQY43+uv/+PutBycJ3y2qn2x7jY50GJvnwLKZjOwspyE5I8F4N+1yr1uwqcs3ym63Hwo29EiAyzUWQVr6WVAS4lZCPutQG/2GtES2YiW3d3XflYKtL72kzAcdEDHeSa3czeIMyyz/TApRKvcFfE0isHbJMnrHCf6xTLb1ORvWNlWo91cvHrJUQo0o6ZoRi7dIiT/g2WEDi27Iyov21xMCvgNfXvtwIACfHwAAAAASUVORK5CYII=',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': True,
|
||||
},
|
||||
{
|
||||
'name': 'domain',
|
||||
'advanced': True,
|
||||
'label': 'Proxy server',
|
||||
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'only_verified',
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'description': 'Only search for verified releases.'
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,135 +0,0 @@
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://www.sceneaccess.eu/',
|
||||
'login': 'https://www.sceneaccess.eu/login',
|
||||
'login_check': 'https://www.sceneaccess.eu/inbox',
|
||||
'detail': 'https://www.sceneaccess.eu/details?id=%s',
|
||||
'search': 'https://www.sceneaccess.eu/browse?c%d=%d',
|
||||
'archive': 'https://www.sceneaccess.eu/archive?&c%d=%d',
|
||||
'download': 'https://www.sceneaccess.eu/%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
def _searchOnTitle(self, title, media, quality, results):
|
||||
|
||||
url = self.buildUrl(title, media, quality)
|
||||
data = self.getHTMLData(url)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
try:
|
||||
resultsTable = html.find('table', attrs = {'id': 'torrents-table'})
|
||||
if resultsTable is None:
|
||||
return
|
||||
|
||||
entries = resultsTable.find_all('tr', attrs = {'class': 'tt_row'})
|
||||
for result in entries:
|
||||
|
||||
link = result.find('td', attrs = {'class': 'ttr_name'}).find('a')
|
||||
url = result.find('td', attrs = {'class': 'td_dl'}).find('a')
|
||||
leechers = result.find('td', attrs = {'class': 'ttr_leechers'}).find('a')
|
||||
torrent_id = link['href'].replace('details?id=', '')
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': link['title'],
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'size': self.parseSize(result.find('td', attrs = {'class': 'ttr_size'}).contents[0]),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class': 'ttr_seeders'}).find('a').string),
|
||||
'leechers': tryInt(leechers.string) if leechers else 0,
|
||||
'get_more_info': self.getMoreInfo,
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getMoreInfo(self, item):
|
||||
full_description = self.getCache('sceneaccess.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||
html = BeautifulSoup(full_description)
|
||||
nfo_pre = html.find('div', attrs = {'id': 'details_table'})
|
||||
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||
|
||||
item['description'] = description
|
||||
return item
|
||||
|
||||
# Login
|
||||
def getLoginParams(self):
|
||||
return {
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'submit': 'come on in',
|
||||
}
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/inbox' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'sceneaccess',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'SceneAccess',
|
||||
'description': '<a href="https://sceneaccess.eu/">SceneAccess</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAACT0lEQVR4AYVQS0sbURidO3OTmajJ5FElTTOkPmZ01GhHrIq0aoWAj1Vc+A/cuRMXbl24V9SlCGqrLhVFCrooEhCp2BAx0mobTY2kaR7qmOm87EXL1EWxh29xL+c7nPMdgGHYO5bF/gdbefnr6WlbWRnxluMwAB4Z0uEgXa7nwaDL7+/RNPzxbYvb/XJ0FBYVfd/ayh0fQ4qCGEHcm0KLRZUk7Pb2YRJPRwcsKMidnKD3t9VVT3s7BDh+z5FOZ3Vfn3h+Hltfx00mRRSRWFcUmmVNhYVqPn8dj3va2oh+txvcQRVF9ebm1fi4k+dRFbosY5rm4Hk7xxULQnJnx93S4g0EIEEQRoDLo6PrWEw8Pc0eHLwYGopMTDirqlJ7eyhYYGHhfgfHCcKYksZGVB/NcXI2mw6HhZERqrjYTNPHi4tFPh8aJIYIhgPlcCRDoZLW1s75+Z/7+59nZ/OJhLWigqAoKZX6Mjf3dXkZ3pydGYLc4aEoCCkInzQ1fRobS2xuvllaonkedfArnY5OTdGVldBkOADgqq2Nr6z8CIWaJietDHOhKB+HhwFKC6Gnq4ukKJvP9zcSbjYDXbeVlkKzuZBhnnV3e3t6UOmaJO0ODibW1hB1GYkg8R/gup7Z3TVZLJ5AILW9LcZiVpYtYBhw16O3t7cauckyeF9Tgz0ATpL2+nopmWycmbnY2LiKRjFk6/d7+/vRJfl4HGzV1T0UIM43MGBvaIBWK/YvwM5w+IMgGH8tkyEgvIpE7M3Nt6qqZrNyOq1kMmouh455Ggz+BhKY4GEc2CfwAAAAAElFTkSuQmCC',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 20,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,136 +0,0 @@
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://www.torrentbytes.net/',
|
||||
'login': 'https://www.torrentbytes.net/takelogin.php',
|
||||
'login_check': 'https://www.torrentbytes.net/inbox.php',
|
||||
'detail': 'https://www.torrentbytes.net/details.php?id=%s',
|
||||
'search': 'https://www.torrentbytes.net/browse.php?search=%s&cat=%d',
|
||||
'download': 'https://www.torrentbytes.net/download.php?id=%s&name=%s',
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
([5], ['720p', '1080p', 'bd50']),
|
||||
([19], ['cam']),
|
||||
([19], ['ts', 'tc']),
|
||||
([19], ['r5', 'scr']),
|
||||
([19], ['dvdrip']),
|
||||
([19], ['brrip']),
|
||||
([20], ['dvdr']),
|
||||
]
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
cat_backup_id = None
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), self.getCatId(quality)[0])
|
||||
data = self.getHTMLData(url)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'border': '1'})
|
||||
if not result_table:
|
||||
return
|
||||
|
||||
entries = result_table.find_all('tr')
|
||||
|
||||
for result in entries[1:]:
|
||||
cells = result.find_all('td')
|
||||
|
||||
link = cells[1].find('a', attrs = {'class': 'index'})
|
||||
|
||||
full_id = link['href'].replace('details.php?id=', '')
|
||||
torrent_id = full_id[:6]
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': link.contents[0],
|
||||
'url': self.urls['download'] % (torrent_id, link.contents[0]),
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]),
|
||||
'seeders': tryInt(cells[8].find('span').contents[0]),
|
||||
'leechers': tryInt(cells[9].find('span').contents[0]),
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return {
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'login': 'submit',
|
||||
}
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'logout.php' in output.lower() or 'Welcome' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'torrentbytes',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentBytes',
|
||||
'description': '<a href="http://torrentbytes.net">TorrentBytes</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEUAAAAAAEQAA1QAEmEAKnQALHYAMoEAOokAQpIASYsASZgAS5UATZwATosATpgAVJ0AWZwAYZ4AZKAAaZ8Ab7IAcbMAfccAgcQAgcsAhM4AiscAjMkAmt0AoOIApecAp/EAqvQAs+kAt+wA3P8A4f8A//8VAAAfDbiaAl08AAAAjUlEQVQYGQXBO04DQRAFwHqz7Z8sECIl5f73ISRD5GBs7UxTlWfg9vYXnvJRQJqOL88D6BAwJtMMumHUVCl60aa6H93IrIv0b+157f1lpk+fm87lMWrZH0vncKbXdRUQrRmrh9C6Iwkq6rg4PXZcyXmbizzeV/g+rDra0rGve8jPKLSOJNi2AQAwAGjwD7ApPkEHdtPQAAAAAElFTkSuQmCC',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 20,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,114 +0,0 @@
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://www.td.af/',
|
||||
'login': 'http://www.td.af/torrents/',
|
||||
'login_check': 'http://www.torrentday.com/userdetails.php',
|
||||
'detail': 'http://www.td.af/details.php?id=%s',
|
||||
'search': 'http://www.td.af/V3/API/API.php',
|
||||
'download': 'http://www.td.af/download.php/%s/%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
def _searchOnTitle(self, title, media, quality, results):
|
||||
|
||||
query = '"%s" %s' % (title, media['info']['year'])
|
||||
|
||||
data = {
|
||||
'/browse.php?': None,
|
||||
'cata': 'yes',
|
||||
'jxt': 8,
|
||||
'jxw': 'b',
|
||||
'search': query,
|
||||
}
|
||||
|
||||
data = self.getJsonData(self.urls['search'], data = data)
|
||||
try: torrents = data.get('Fs', [])[0].get('Cn', {}).get('torrents', [])
|
||||
except: return
|
||||
|
||||
for torrent in torrents:
|
||||
results.append({
|
||||
'id': torrent['id'],
|
||||
'name': torrent['name'],
|
||||
'url': self.urls['download'] % (torrent['id'], torrent['fname']),
|
||||
'detail_url': self.urls['detail'] % torrent['id'],
|
||||
'size': self.parseSize(torrent.get('size')),
|
||||
'seeders': tryInt(torrent.get('seed')),
|
||||
'leechers': tryInt(torrent.get('leech')),
|
||||
})
|
||||
|
||||
def getLoginParams(self):
|
||||
return {
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'submit.x': 18,
|
||||
'submit.y': 11,
|
||||
'submit': 'submit',
|
||||
}
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'Password not correct' not in output
|
||||
|
||||
def loginCheckSuccess(self, output):
|
||||
return 'logout.php' in output.lower()
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'torrentday',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentDay',
|
||||
'description': '<a href="http://www.td.af/">TorrentDay</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC5ElEQVQ4y12TXUgUURTH//fO7Di7foeQJH6gEEEIZZllVohfSG/6UA+RSFAQQj74VA8+Bj30lmAlRVSEvZRfhNhaka5ZUG1paKaW39tq5O6Ou+PM3M4o6m6X+XPPzD3zm/+dcy574r515WfIW8CZBM4YAA5Gc/aQC3yd7oXYEONcsISE5dTDh91HS0t7FEWhBUAeN9ynV/d9qJAgE4AECURAcVsGlCCnly26LMA0IQwTa52dje3d3e3hcPi8qqrrMjcVYI3EHCQZlkFOHBwR2QHh2ASAAIJxWGAQEDxjePhs3527XjJwnb37OHBq0T+Tyyjh+9KnEzNJ7nouc1Q/3A3HGsOvnJy+PSUlj81w2Lny9WuJ6+3AmTjD4HOcrdR2dWXLRQePvyaSLfQOPMPC8mC9iHCsOxSyzJCelzdSXlNzD5ujpb25Wbfc/XXJemTXF4+nnCNq+AMLe50uFfEJTiw4GXSFtiHL0SnIq66+p0kSArqO+eH3RdsAv9+f5vW7L7GICq6rmM8XBCAXlBw90rOyxibn5yzfkg/L09M52/jxqdESaIrBXHYZZbB1GX8cEpySxKIB8S5XcOnvqpli1zuwmrTtoLjw5LOK/eeuWsE4JH5IRPaPZKiKigmPp+5pa+u1aEjIMhEgrRkmi9mgxGUhM7LNJSzOzsE3+cOeExovXOjdytE0LV4zqNZUtV0uZzAGoGkhDH/2YHZiErmv4uyWQnZZWc+hoqL3WzlTExN5hhA8IEwkZWZOxwB++30YG/9GkYCPvqAaHAW5uWPROW86OmqCprUR7z1yZDAGQNuCvkoB/baIKUBWMTYymv+gra3eJNvjXu+B562tFyXqTJ6YuHK8rKwvBmC3vR7cOCPQLWFz8LnfXWUrJo9U19BwMyUlJRjTSMJ2ENxUiGxq9KXQfwqYlnWstvbR5aamG9g0uzM8Q4OFt++3NNixQ2NgYmeN03FOTUv7XVpV9aKisvLl1vN/WVhNc/Fi1NEAAAAASUVORK5CYII=',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,126 +0,0 @@
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://www.torrentleech.org/',
|
||||
'login': 'http://www.torrentleech.org/user/account/login/',
|
||||
'login_check': 'http://torrentleech.org/user/messages',
|
||||
'detail': 'http://www.torrentleech.org/torrent/%s',
|
||||
'search': 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
|
||||
'download': 'http://www.torrentleech.org%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
cat_backup_id = None
|
||||
|
||||
def _searchOnTitle(self, title, media, quality, results):
|
||||
|
||||
url = self.urls['search'] % self.buildUrl(title, media, quality)
|
||||
|
||||
data = self.getHTMLData(url)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'id': 'torrenttable'})
|
||||
if not result_table:
|
||||
return
|
||||
|
||||
entries = result_table.find_all('tr')
|
||||
|
||||
for result in entries[1:]:
|
||||
|
||||
link = result.find('td', attrs = {'class': 'name'}).find('a')
|
||||
url = result.find('td', attrs = {'class': 'quickdownload'}).find('a')
|
||||
details = result.find('td', attrs = {'class': 'name'}).find('a')
|
||||
|
||||
results.append({
|
||||
'id': link['href'].replace('/torrent/', ''),
|
||||
'name': six.text_type(link.string),
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % details['href'],
|
||||
'size': self.parseSize(result.find_all('td')[4].string),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class': 'seeders'}).string),
|
||||
'leechers': tryInt(result.find('td', attrs = {'class': 'leechers'}).string),
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return {
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'remember_me': 'on',
|
||||
'login': 'submit',
|
||||
}
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'torrentleech',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentLeech',
|
||||
'description': '<a href="http://torrentleech.org">TorrentLeech</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACHUlEQVR4AZVSO48SYRSdGTCBEMKzILLAWiybkKAGMZRUUJEoDZX7B9zsbuQPYEEjNLTQkYgJDwsoSaxspEBsCITXjjNAIKi8AkzceXgmbHQ1NJ5iMufmO9/9zrmXlCSJ+B8o75J8Pp/NZj0eTzweBy0Wi4PBYD6f12o1r9ebTCZx+22HcrnMsuxms7m6urTZ7LPZDMVYLBZ8ZV3yo8aq9Pq0wzCMTqe77dDv9y8uLyAWBH6xWOyL0K/56fcb+rrPgPZ6PZfLRe1fsl6vCUmGKIqoqNXqdDr9Dbjps9znUV0uTqdTjuPkDoVCIfcuJ4gizjMMm8u9vW+1nr04czqdK56c37CbKY9j2+1WEARZ0Gq1RFHAz2q1qlQqXxoN69HRcDjUarW8ZD6QUigUOnY8uKYH8N1sNkul9yiGw+F6vS4Rxn8EsodEIqHRaOSnq9T7ajQazWQycEIR1AEBYDabSZJyHDucJyegwWBQr9ebTCaKvHd4cCQANUU9evwQ1Ofz4YvUKUI43GE8HouSiFiNRhOowWBIpVLyHITJkuW3PwgAEf3pgIwxF5r+OplMEsk3CPT5szCMnY7EwUdhwUh/CXiej0Qi3idPz89fdrpdbsfBzH7S3Q9K5pP4c0sAKpVKoVAQGO1ut+t0OoFAQHkH2Da/3/+but3uarWK0ZMQoNdyucRutdttmqZxMTzY7XaYxsrgtUjEZrNhkSwWyy/0NCatZumrNQAAAABJRU5ErkJggg==',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 20,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,132 +0,0 @@
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://torrentshack.net/',
|
||||
'login': 'https://torrentshack.net/login.php',
|
||||
'login_check': 'https://torrentshack.net/inbox.php',
|
||||
'detail': 'https://torrentshack.net/torrent/%s',
|
||||
'search': 'https://torrentshack.net/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1',
|
||||
'download': 'https://torrentshack.net/%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
def _search(self, media, quality, results):
|
||||
|
||||
url = self.urls['search'] % self.buildUrl(media, quality)
|
||||
data = self.getHTMLData(url)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'id': 'torrent_table'})
|
||||
if not result_table:
|
||||
return
|
||||
|
||||
entries = result_table.find_all('tr', attrs = {'class': 'torrent'})
|
||||
|
||||
for result in entries:
|
||||
|
||||
link = result.find('span', attrs = {'class': 'torrent_name_link'}).parent
|
||||
url = result.find('td', attrs = {'class': 'torrent_td'}).find('a')
|
||||
|
||||
results.append({
|
||||
'id': link['href'].replace('torrents.php?torrentid=', ''),
|
||||
'name': six.text_type(link.span.string).translate({ord(six.u('\xad')): None}),
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % link['href'],
|
||||
'size': self.parseSize(result.find_all('td')[5].string),
|
||||
'seeders': tryInt(result.find_all('td')[7].string),
|
||||
'leechers': tryInt(result.find_all('td')[8].string),
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return {
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'keeplogged': '1',
|
||||
'login': 'Login',
|
||||
}
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'logout.php' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
def getSceneOnly(self):
|
||||
return '1' if self.conf('scene_only') else ''
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'torrentshack',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentShack',
|
||||
'description': '<a href="https://www.torrentshack.net/">TorrentShack</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'scene_only',
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'description': 'Only allow scene releases.'
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,129 +0,0 @@
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentMagnetProvider, RSS):
|
||||
|
||||
urls = {
|
||||
'detail': 'https://torrentz.eu/%s',
|
||||
'search': 'https://torrentz.eu/feed?q=%s',
|
||||
'verified_search': 'https://torrentz.eu/feed_verified?q=%s'
|
||||
}
|
||||
|
||||
http_time_between_calls = 0
|
||||
|
||||
def _search(self, media, quality, results):
|
||||
|
||||
search_url = self.urls['verified_search'] if self.conf('verified_only') else self.urls['search']
|
||||
|
||||
# Create search parameters
|
||||
search_params = self.buildUrl(media)
|
||||
|
||||
smin = quality.get('size_min')
|
||||
smax = quality.get('size_max')
|
||||
if smin and smax:
|
||||
search_params += ' size %sm - %sm' % (smin, smax)
|
||||
|
||||
min_seeds = tryInt(self.conf('minimal_seeds'))
|
||||
if min_seeds:
|
||||
search_params += ' seed > %s' % (min_seeds - 1)
|
||||
|
||||
rss_data = self.getRSSData(search_url % search_params)
|
||||
|
||||
if rss_data:
|
||||
try:
|
||||
|
||||
for result in rss_data:
|
||||
|
||||
name = self.getTextElement(result, 'title')
|
||||
detail_url = self.getTextElement(result, 'link')
|
||||
description = self.getTextElement(result, 'description')
|
||||
|
||||
magnet = splitString(detail_url, '/')[-1]
|
||||
magnet_url = 'magnet:?xt=urn:btih:%s&dn=%s&tr=%s' % (magnet.upper(), tryUrlencode(name), tryUrlencode('udp://tracker.openbittorrent.com/announce'))
|
||||
|
||||
reg = re.search('Size: (?P<size>\d+) MB Seeds: (?P<seeds>[\d,]+) Peers: (?P<peers>[\d,]+)', six.text_type(description))
|
||||
size = reg.group('size')
|
||||
seeds = reg.group('seeds').replace(',', '')
|
||||
peers = reg.group('peers').replace(',', '')
|
||||
|
||||
results.append({
|
||||
'id': magnet,
|
||||
'name': six.text_type(name),
|
||||
'url': magnet_url,
|
||||
'detail_url': detail_url,
|
||||
'size': tryInt(size),
|
||||
'seeders': tryInt(seeds),
|
||||
'leechers': tryInt(peers),
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'torrentz',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'Torrentz',
|
||||
'description': 'Torrentz is a free, fast and powerful meta-search engine. <a href="https://torrentz.eu/">Torrentz</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAQklEQVQ4y2NgAALjtJn/ycEMlGiGG0IVAxiwAKzOxaKGARcgxgC8YNSAwWoAzuRMjgsIugqfAUR5CZcBRIcHsWEAADSA96Ig020yAAAAAElFTkSuQmCC',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': True
|
||||
},
|
||||
{
|
||||
'name': 'verified_only',
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'description': 'Only search verified releases',
|
||||
},
|
||||
{
|
||||
'name': 'minimal_seeds',
|
||||
'type': 'int',
|
||||
'default': 1,
|
||||
'advanced': True,
|
||||
'description': 'Only return releases with minimal X seeds',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -1,120 +0,0 @@
|
||||
import traceback
|
||||
|
||||
from couchpotato.core.helpers.variable import tryInt, getIdentifier
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentMagnetProvider):
|
||||
|
||||
urls = {
|
||||
'test': '%s/api',
|
||||
'search': '%s/api/list.json?keywords=%s&quality=%s',
|
||||
'detail': '%s/api/movie.json?id=%s'
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # seconds
|
||||
|
||||
proxy_list = [
|
||||
'http://yify.unlocktorrent.com',
|
||||
'http://yify-torrents.com.come.in',
|
||||
'http://yts.re',
|
||||
'http://yts.im'
|
||||
'http://yify-torrents.im',
|
||||
]
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
if not quality.get('hd', False):
|
||||
return []
|
||||
|
||||
return super(Base, self).search(movie, quality)
|
||||
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
domain = self.getDomain()
|
||||
if not domain:
|
||||
return
|
||||
|
||||
search_url = self.urls['search'] % (domain, getIdentifier(movie), quality['identifier'])
|
||||
|
||||
data = self.getJsonData(search_url)
|
||||
|
||||
if data and data.get('MovieList'):
|
||||
try:
|
||||
for result in data.get('MovieList'):
|
||||
|
||||
if result['Quality'] and result['Quality'] not in result['MovieTitle']:
|
||||
title = result['MovieTitle'] + ' BrRip ' + result['Quality']
|
||||
else:
|
||||
title = result['MovieTitle'] + ' BrRip'
|
||||
|
||||
results.append({
|
||||
'id': result['MovieID'],
|
||||
'name': title,
|
||||
'url': result['TorrentMagnetUrl'],
|
||||
'detail_url': self.urls['detail'] % (domain, result['MovieID']),
|
||||
'size': self.parseSize(result['Size']),
|
||||
'seeders': tryInt(result['TorrentSeeds']),
|
||||
'leechers': tryInt(result['TorrentPeers']),
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def correctProxy(self, data):
|
||||
data = data.lower()
|
||||
return 'yify' in data and 'yts' in data
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'yify',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'Yify',
|
||||
'description': 'Free provider, less accurate. Small HD movies, encoded by <a href="https://yify-torrents.com/">Yify</a>.',
|
||||
'wizard': False,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACL0lEQVR4AS1SPW/UQBAd23fxne/Ld2dvzvHuzPocEBAKokCBqGiQ6IgACYmvUKRBFEQgKKGg4BAlUoggggYUEQpSHOI7CIEoQs/fYcbLaU/efTvvvZlnA1qydoxU5kcxX0CkgmQZtPy0hCUjvK+WgEByOZ5dns1O5bzna8fRVkgsxH8B0YouIvBhdD5T11NiVOoKrsttyUcpRW0InUrFnwe9HzuP2uaQZYhF2LQ76TTXw2RVMTK8mYYbjfh+zNquMVCrqn93aArLSixPxnafdGDLaz1tjY5rmNa8z5BczEQOxQfCl1GyoqoWxYRN1bkh7ELw3q/vhP6HIL4TG9KumpjgvwuyM7OsjSj98E/vszMfZ7xvPtMaWxGO5crwIumKCR5HxDtJ0AWKGG204RfUd/3smJYqwem/Q7BTS1ZGfM4LNpVwuKAz6cMeROst0S2EwNE7GjTehO2H3dxqIpdkydat15G3F8SXBi4GlpBNlSz012L/k2+W0CLLk/jbcf13rf41yJeMQ8QWUZiHCfCA9ad+81nEKPtoS9mJOf9v0NmMJHgUT6xayheK9EIK7JJeU/AF4scDF7Y5SPlJrRcxJ+um4ibNEdObxLiIwJim+eT2AL5D9CIcnZ5zvSJi9eIlNHVVtZ831dk5svPgvjPWTq+ktWkd/kD0qtm71x+sDQe3kt6DXnM7Ct+GajmTxKlkAokWljyAKSm5oWa2w+BH4P2UuVub7eTyiGOQYapY/wEztHduSDYz5gAAAABJRU5ErkJggg==',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False
|
||||
},
|
||||
{
|
||||
'name': 'domain',
|
||||
'advanced': True,
|
||||
'label': 'Proxy server',
|
||||
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Search
|
||||
|
||||
|
||||
def autoload():
|
||||
def start():
|
||||
return Search()
|
||||
|
||||
config = []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts, getImdb
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
@@ -35,21 +35,12 @@ class Search(Plugin):
|
||||
elif isinstance(types, (list, tuple, set)):
|
||||
types = list(types)
|
||||
|
||||
imdb_identifier = getImdb(q)
|
||||
|
||||
if not types:
|
||||
if imdb_identifier:
|
||||
result = fireEvent('movie.info', identifier = imdb_identifier, merge = True)
|
||||
result = {result['type']: [result]}
|
||||
else:
|
||||
result = fireEvent('info.search', q = q, merge = True)
|
||||
result = fireEvent('info.search', q = q, merge = True)
|
||||
else:
|
||||
result = {}
|
||||
for media_type in types:
|
||||
if imdb_identifier:
|
||||
result[media_type] = fireEvent('%s.info' % media_type, identifier = imdb_identifier)
|
||||
else:
|
||||
result[media_type] = fireEvent('%s.search' % media_type, q = q)
|
||||
result[media_type] = fireEvent('%s.search' % media_type)
|
||||
|
||||
return mergeDicts({
|
||||
'success': True,
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
top: 0;
|
||||
text-align: right;
|
||||
height: 100%;
|
||||
border-bottom: 4px solid transparent;
|
||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
border: 0 solid transparent;
|
||||
border-bottom-width: 4px;
|
||||
border: 1px solid transparent;
|
||||
border-width: 0 0 4px;
|
||||
}
|
||||
.search_form:hover {
|
||||
border-color: #047792;
|
||||
@@ -20,19 +22,19 @@
|
||||
right: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.search_form.focused,
|
||||
.search_form.shown {
|
||||
border-color: #04bce6;
|
||||
}
|
||||
|
||||
|
||||
.search_form .input {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 45px;
|
||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||
}
|
||||
|
||||
|
||||
.search_form.focused .input,
|
||||
.search_form.shown .input {
|
||||
width: 380px;
|
||||
@@ -47,6 +49,7 @@
|
||||
color: #FFF;
|
||||
font-size: 25px;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
padding: 0 40px 0 10px;
|
||||
@@ -56,23 +59,23 @@
|
||||
.search_form.shown .input input {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.search_form input::-ms-clear {
|
||||
width : 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.search_form .input input {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
|
||||
.search_form.focused .input,
|
||||
.search_form.shown .input {
|
||||
width: 277px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.search_form .input a {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -86,7 +89,7 @@
|
||||
font-size: 15px;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
|
||||
.search_form .input a:after {
|
||||
content: "\e03e";
|
||||
}
|
||||
@@ -94,7 +97,7 @@
|
||||
.search_form.shown.filled .input a:after {
|
||||
content: "\e04e";
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.search_form .input a {
|
||||
line-height: 44px;
|
||||
@@ -164,13 +167,13 @@
|
||||
.media_result .options select[name=title] { width: 170px; }
|
||||
.media_result .options select[name=profile] { width: 90px; }
|
||||
.media_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; }
|
||||
|
||||
|
||||
}
|
||||
|
||||
.media_result .options .button {
|
||||
@@ -224,14 +227,14 @@
|
||||
right: 7px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
.media_result .info h2 {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
font-size: 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
.search_form .info h2 {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@@ -244,12 +247,12 @@
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.search_form .info h2 .title {
|
||||
position: absolute;
|
||||
width: 88%;
|
||||
}
|
||||
|
||||
|
||||
.media_result .info h2 .year {
|
||||
padding: 0 5px;
|
||||
text-align: center;
|
||||
@@ -257,14 +260,14 @@
|
||||
width: 12%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
|
||||
|
||||
.search_form .info h2 .year {
|
||||
font-size: 12px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.search_form .mask,
|
||||
@@ -274,4 +277,4 @@
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,10 @@ Block.Search = new Class({
|
||||
self.input = new Element('input', {
|
||||
'placeholder': 'Search & add a new media',
|
||||
'events': {
|
||||
'input': self.keyup.bind(self),
|
||||
'paste': self.keyup.bind(self),
|
||||
'change': self.keyup.bind(self),
|
||||
'keyup': self.keyup.bind(self),
|
||||
'focus': function(){
|
||||
if(focus_timer) clearTimeout(focus_timer);
|
||||
self.el.addClass('focused');
|
||||
self.el.addClass('focused')
|
||||
if(this.get('value'))
|
||||
self.hideResults(false)
|
||||
},
|
||||
@@ -60,17 +57,17 @@ Block.Search = new Class({
|
||||
(e).preventDefault();
|
||||
|
||||
if(self.last_q === ''){
|
||||
self.input.blur();
|
||||
self.input.blur()
|
||||
self.last_q = null;
|
||||
}
|
||||
else {
|
||||
|
||||
self.last_q = '';
|
||||
self.input.set('value', '');
|
||||
self.input.focus();
|
||||
self.input.focus()
|
||||
|
||||
self.media = {};
|
||||
self.results.empty();
|
||||
self.media = {}
|
||||
self.results.empty()
|
||||
self.el.removeClass('filled')
|
||||
|
||||
}
|
||||
@@ -95,16 +92,16 @@ Block.Search = new Class({
|
||||
self.hidden = bool;
|
||||
},
|
||||
|
||||
keyup: function(){
|
||||
keyup: function(e){
|
||||
var self = this;
|
||||
|
||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled');
|
||||
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);
|
||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
||||
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
||||
}
|
||||
|
||||
@@ -114,7 +111,7 @@ Block.Search = new Class({
|
||||
var self = this;
|
||||
|
||||
if(!self.q()){
|
||||
self.hideResults(true);
|
||||
self.hideResults(true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -142,7 +139,7 @@ Block.Search = new Class({
|
||||
})
|
||||
}
|
||||
else
|
||||
self.fill(q, cache);
|
||||
self.fill(q, cache)
|
||||
|
||||
self.last_q = q;
|
||||
|
||||
@@ -151,31 +148,31 @@ Block.Search = new Class({
|
||||
fill: function(q, json){
|
||||
var self = this;
|
||||
|
||||
self.cache[q] = json;
|
||||
self.cache[q] = json
|
||||
|
||||
self.media = {};
|
||||
self.results.empty();
|
||||
|
||||
Object.each(json, function(media){
|
||||
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;
|
||||
|
||||
$(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.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
|
||||
self.mask.fade('out')
|
||||
|
||||
},
|
||||
@@ -188,4 +185,4 @@ Block.Search = new Class({
|
||||
return this.input.get('value').trim();
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
from .main import Searcher
|
||||
|
||||
|
||||
def autoload():
|
||||
def start():
|
||||
return Searcher()
|
||||
|
||||
config = [{
|
||||
|
||||
@@ -12,6 +12,7 @@ class SearcherBase(Plugin):
|
||||
def __init__(self):
|
||||
super(SearcherBase, self).__init__()
|
||||
|
||||
|
||||
addEvent('searcher.progress', self.getProgress)
|
||||
addEvent('%s.searcher.progress' % self.getType(), self.getProgress)
|
||||
|
||||
@@ -25,8 +26,9 @@ class SearcherBase(Plugin):
|
||||
_type = self.getType()
|
||||
|
||||
def setCrons():
|
||||
|
||||
fireEvent('schedule.cron', '%s.searcher.all' % _type, self.searchAll,
|
||||
day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
||||
day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
||||
|
||||
addEvent('app.load', setCrons)
|
||||
addEvent('setting.save.%s_searcher.cron_day.after' % _type, setCrons)
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
from couchpotato.core.helpers.variable import splitString, removeEmpty, removeDuplicate
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.helpers.variable import md5, getTitle, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||
|
||||
from couchpotato.core.settings.model import Media, Release, ReleaseInfo
|
||||
from couchpotato.environment import Env
|
||||
from inspect import ismethod, isfunction
|
||||
import datetime
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Searcher(SearcherBase):
|
||||
|
||||
# noinspection PyMissingConstructor
|
||||
def __init__(self):
|
||||
addEvent('searcher.protocols', self.getSearchProtocols)
|
||||
addEvent('searcher.contains_other_quality', self.containsOtherQuality)
|
||||
addEvent('searcher.correct_3d', self.correct3D)
|
||||
addEvent('searcher.correct_year', self.correctYear)
|
||||
addEvent('searcher.correct_name', self.correctName)
|
||||
addEvent('searcher.correct_words', self.correctWords)
|
||||
@@ -52,7 +54,7 @@ class Searcher(SearcherBase):
|
||||
results = []
|
||||
|
||||
for search_protocol in protocols:
|
||||
protocol_results = fireEvent('provider.search.%s.%s' % (search_protocol, media.get('type')), media, quality, merge = True)
|
||||
protocol_results = fireEvent('provider.search.%s.%s' % (search_protocol, media['type']), media, quality, merge = True)
|
||||
if protocol_results:
|
||||
results += protocol_results
|
||||
|
||||
@@ -87,26 +89,34 @@ class Searcher(SearcherBase):
|
||||
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = None):
|
||||
if not preferred_quality: preferred_quality = {}
|
||||
|
||||
name = nzb['name']
|
||||
size = nzb.get('size', 0)
|
||||
nzb_words = re.split('\W+', simplifyString(name))
|
||||
|
||||
qualities = fireEvent('quality.all', single = True)
|
||||
|
||||
found = {}
|
||||
for quality in qualities:
|
||||
# Main in words
|
||||
if quality['identifier'] in nzb_words:
|
||||
found[quality['identifier']] = True
|
||||
|
||||
# Alt in words
|
||||
if list(set(nzb_words) & set(quality['alternative'])):
|
||||
found[quality['identifier']] = True
|
||||
|
||||
# Try guessing via quality tags
|
||||
guess = fireEvent('quality.guess', files = [nzb.get('name')], size = nzb.get('size', None), single = True)
|
||||
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
|
||||
if guess:
|
||||
found[guess['identifier']] = True
|
||||
|
||||
# Hack for older movies that don't contain quality tag
|
||||
name = nzb['name']
|
||||
size = nzb.get('size', 0)
|
||||
|
||||
year_name = fireEvent('scanner.name_year', name, single = True)
|
||||
if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
|
||||
if size > 20000: # Assume bd50
|
||||
log.info('Quality was missing in name, assuming it\'s a BR-Disk based on the size: %s', size)
|
||||
found['bd50'] = True
|
||||
elif size > 3000: # Assume dvdr
|
||||
if size > 3000: # Assume dvdr
|
||||
log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', size)
|
||||
found['dvdr'] = True
|
||||
else: # Assume dvdrip
|
||||
else: # Assume dvdrip
|
||||
log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', size)
|
||||
found['dvdrip'] = True
|
||||
|
||||
@@ -115,21 +125,7 @@ class Searcher(SearcherBase):
|
||||
if found.get(allowed):
|
||||
del found[allowed]
|
||||
|
||||
if found.get(preferred_quality['identifier']) and len(found) == 1:
|
||||
return False
|
||||
|
||||
return found
|
||||
|
||||
def correct3D(self, nzb, preferred_quality = None):
|
||||
if not preferred_quality: preferred_quality = {}
|
||||
if not preferred_quality.get('custom'): return
|
||||
|
||||
threed = preferred_quality['custom'].get('3d')
|
||||
|
||||
# Try guessing via quality tags
|
||||
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
|
||||
|
||||
return threed == guess.get('is_3d')
|
||||
return not (found.get(preferred_quality['identifier']) and len(found) == 1)
|
||||
|
||||
def correctYear(self, haystack, year, year_range):
|
||||
|
||||
@@ -160,12 +156,12 @@ class Searcher(SearcherBase):
|
||||
try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', check_name), key = len).strip())
|
||||
except: pass
|
||||
|
||||
for check_name in removeDuplicate(check_names):
|
||||
for check_name in list(set(check_names)):
|
||||
check_movie = fireEvent('scanner.name_year', check_name, single = True)
|
||||
|
||||
try:
|
||||
check_words = removeEmpty(re.split('\W+', check_movie.get('name', '')))
|
||||
movie_words = removeEmpty(re.split('\W+', simplifyString(movie_name)))
|
||||
check_words = filter(None, re.split('\W+', check_movie.get('name', '')))
|
||||
movie_words = filter(None, re.split('\W+', simplifyString(movie_name)))
|
||||
|
||||
if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0:
|
||||
return True
|
||||
@@ -175,7 +171,7 @@ class Searcher(SearcherBase):
|
||||
return False
|
||||
|
||||
def correctWords(self, rel_name, media):
|
||||
media_title = fireEvent('searcher.get_search_title', media, single = True)
|
||||
media_title = fireEvent('library.title', media['library'], single = True)
|
||||
media_words = re.split('\W+', simplifyString(media_title))
|
||||
|
||||
rel_name = simplifyString(rel_name)
|
||||
@@ -183,7 +179,7 @@ class Searcher(SearcherBase):
|
||||
|
||||
# Make sure it has required words
|
||||
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
|
||||
try: required_words = removeDuplicate(required_words + splitString(media['category']['required'].lower()))
|
||||
try: required_words = list(set(required_words + splitString(media['category']['required'].lower())))
|
||||
except: pass
|
||||
|
||||
req_match = 0
|
||||
@@ -191,13 +187,13 @@ class Searcher(SearcherBase):
|
||||
req = splitString(req_set, '&')
|
||||
req_match += len(list(set(rel_words) & set(req))) == len(req)
|
||||
|
||||
if len(required_words) > 0 and req_match == 0:
|
||||
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 = removeDuplicate(ignored_words + splitString(media['category']['ignored'].lower()))
|
||||
try: ignored_words = list(set(ignored_words + splitString(media['category']['ignored'].lower())))
|
||||
except: pass
|
||||
|
||||
ignored_match = 0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import MovieBase
|
||||
|
||||
|
||||
def autoload():
|
||||
def start():
|
||||
return MovieBase()
|
||||
|
||||
config = []
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import os
|
||||
import traceback
|
||||
import time
|
||||
|
||||
from CodernityDB.database import RecordNotFound
|
||||
from couchpotato import get_db
|
||||
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, getTitle, getImdb, getIdentifier
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.movie import MovieTypeBase
|
||||
import six
|
||||
|
||||
from couchpotato.core.settings.model import Media
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -46,21 +41,16 @@ class MovieBase(MovieTypeBase):
|
||||
})
|
||||
|
||||
addEvent('movie.add', self.add)
|
||||
addEvent('movie.update_info', self.updateInfo)
|
||||
addEvent('movie.update_release_dates', self.updateReleaseDate)
|
||||
|
||||
def add(self, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None):
|
||||
def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||
if not params: params = {}
|
||||
|
||||
# Make sure it's a correct zero filled imdb id
|
||||
params['identifier'] = getImdb(params.get('identifier', ''))
|
||||
|
||||
if not params.get('identifier'):
|
||||
msg = 'Can\'t add movie without imdb identifier.'
|
||||
log.error(msg)
|
||||
fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
|
||||
return False
|
||||
elif not params.get('info'):
|
||||
else:
|
||||
try:
|
||||
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True)
|
||||
if not is_movie:
|
||||
@@ -71,135 +61,78 @@ class MovieBase(MovieTypeBase):
|
||||
except:
|
||||
pass
|
||||
|
||||
info = params.get('info')
|
||||
if not info or (info and len(info.get('titles', [])) == 0):
|
||||
info = fireEvent('movie.info', merge = True, extended = False, identifier = params.get('identifier'))
|
||||
|
||||
# Set default title
|
||||
default_title = toUnicode(info.get('title'))
|
||||
titles = info.get('titles', [])
|
||||
counter = 0
|
||||
def_title = None
|
||||
for title in titles:
|
||||
if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title):
|
||||
def_title = toUnicode(title)
|
||||
break
|
||||
counter += 1
|
||||
library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library)
|
||||
|
||||
if not def_title:
|
||||
def_title = toUnicode(titles[0])
|
||||
# Status
|
||||
status_active, snatched_status, ignored_status, done_status, downloaded_status = \
|
||||
fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True)
|
||||
|
||||
# Default profile and category
|
||||
default_profile = {}
|
||||
if (not params.get('profile_id') and status != 'done') or params.get('ignore_previous', False):
|
||||
default_profile = fireEvent('profile.default', single = True)
|
||||
default_profile = fireEvent('profile.default', single = True)
|
||||
cat_id = params.get('category_id')
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
db = get_session()
|
||||
m = db.query(Media).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(
|
||||
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()
|
||||
|
||||
media = {
|
||||
'_t': 'media',
|
||||
'type': 'movie',
|
||||
'title': def_title,
|
||||
'identifiers': {
|
||||
'imdb': params.get('identifier')
|
||||
},
|
||||
'status': status if status else 'active',
|
||||
'profile_id': params.get('profile_id') or default_profile.get('_id'),
|
||||
'category_id': cat_id if cat_id is not None and len(cat_id) > 0 and cat_id != '-1' else None,
|
||||
}
|
||||
|
||||
# Update movie info
|
||||
try: del info['in_wanted']
|
||||
except: pass
|
||||
try: del info['in_library']
|
||||
except: pass
|
||||
media['info'] = info
|
||||
|
||||
new = False
|
||||
previous_profile = None
|
||||
try:
|
||||
m = db.get('media', 'imdb-%s' % params.get('identifier'), with_doc = True)['doc']
|
||||
|
||||
try:
|
||||
db.get('id', m.get('profile_id'))
|
||||
previous_profile = m.get('profile_id')
|
||||
except RecordNotFound:
|
||||
pass
|
||||
except:
|
||||
log.error('Failed getting previous profile: %s', traceback.format_exc())
|
||||
except:
|
||||
new = True
|
||||
m = db.insert(media)
|
||||
|
||||
# Update dict to be usable
|
||||
m.update(media)
|
||||
|
||||
added = True
|
||||
do_search = False
|
||||
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
|
||||
onComplete = None
|
||||
if search_after:
|
||||
onComplete = self.createOnComplete(m.id)
|
||||
|
||||
if new:
|
||||
if search_after:
|
||||
onComplete = self.createOnComplete(m['_id'])
|
||||
search_after = False
|
||||
elif force_readd:
|
||||
fireEventAsync('library.update.movie', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
|
||||
search_after = False
|
||||
elif force_readd:
|
||||
|
||||
# Clean snatched history
|
||||
for release in fireEvent('release.for_media', m['_id'], single = True):
|
||||
if release.get('status') in ['downloaded', 'snatched', 'seeding', 'done']:
|
||||
if params.get('ignore_previous', False):
|
||||
release['status'] = 'ignored'
|
||||
db.update(release)
|
||||
else:
|
||||
fireEvent('release.delete', release['_id'], single = True)
|
||||
|
||||
m['profile_id'] = (params.get('profile_id') or default_profile.get('_id')) if not previous_profile else previous_profile
|
||||
m['category_id'] = cat_id if cat_id is not None and len(cat_id) > 0 else (m.get('category_id') or None)
|
||||
m['last_edit'] = int(time.time())
|
||||
m['tags'] = []
|
||||
|
||||
do_search = True
|
||||
db.update(m)
|
||||
else:
|
||||
try: del params['info']
|
||||
except: pass
|
||||
log.debug('Movie already exists, not updating: %s', params)
|
||||
added = False
|
||||
|
||||
# Trigger update info
|
||||
if added and update_after:
|
||||
# Do full update to get images etc
|
||||
fireEventAsync('movie.update_info', m['_id'], default_title = params.get('title'), on_complete = onComplete)
|
||||
|
||||
# Remove releases
|
||||
for rel in fireEvent('release.for_media', m['_id'], single = True):
|
||||
if rel['status'] is 'available':
|
||||
db.delete(rel)
|
||||
|
||||
movie_dict = fireEvent('media.get', m['_id'], single = True)
|
||||
|
||||
if do_search and search_after:
|
||||
onComplete = self.createOnComplete(m['_id'])
|
||||
onComplete()
|
||||
|
||||
if added and notify_after:
|
||||
|
||||
if params.get('title'):
|
||||
message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')
|
||||
else:
|
||||
title = getTitle(m)
|
||||
if title:
|
||||
message = 'Successfully added "%s" to your wanted list.' % title
|
||||
# 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:
|
||||
message = 'Successfully added to your wanted list.'
|
||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = message)
|
||||
fireEvent('release.delete', release.id, single = True)
|
||||
|
||||
return movie_dict
|
||||
except:
|
||||
log.error('Failed adding media: %s', traceback.format_exc())
|
||||
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 (m.category_id or None)
|
||||
else:
|
||||
log.debug('Movie 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()
|
||||
|
||||
movie_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 = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
|
||||
|
||||
db.expire_all()
|
||||
return movie_dict
|
||||
|
||||
def addView(self, **kwargs):
|
||||
add_dict = self.add(params = kwargs)
|
||||
@@ -211,170 +144,42 @@ class MovieBase(MovieTypeBase):
|
||||
|
||||
def edit(self, id = '', **kwargs):
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
db = get_session()
|
||||
|
||||
ids = splitString(id)
|
||||
for media_id in ids:
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
|
||||
try:
|
||||
m = db.get('id', media_id)
|
||||
m['profile_id'] = kwargs.get('profile_id')
|
||||
ids = splitString(id)
|
||||
for media_id in ids:
|
||||
|
||||
cat_id = kwargs.get('category_id')
|
||||
if cat_id is not None:
|
||||
m['category_id'] = cat_id if len(cat_id) > 0 else None
|
||||
m = db.query(Media).filter_by(id = media_id).first()
|
||||
if not m:
|
||||
continue
|
||||
|
||||
# Remove releases
|
||||
for rel in fireEvent('release.for_media', m['_id'], single = True):
|
||||
if rel['status'] is 'available':
|
||||
db.delete(rel)
|
||||
m.profile_id = kwargs.get('profile_id')
|
||||
|
||||
# Default title
|
||||
if kwargs.get('default_title'):
|
||||
m['title'] = kwargs.get('default_title')
|
||||
cat_id = kwargs.get('category_id')
|
||||
if cat_id is not None:
|
||||
m.category_id = tryInt(cat_id) if tryInt(cat_id) > 0 else None
|
||||
|
||||
db.update(m)
|
||||
# Remove releases
|
||||
for rel in m.releases:
|
||||
if rel.status_id is available_status.get('id'):
|
||||
db.delete(rel)
|
||||
db.commit()
|
||||
|
||||
fireEvent('media.restatus', m['_id'], single = True)
|
||||
# Default title
|
||||
if kwargs.get('default_title'):
|
||||
for title in m.library.titles:
|
||||
title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower()
|
||||
|
||||
m = db.get('id', media_id)
|
||||
db.commit()
|
||||
|
||||
movie_dict = fireEvent('media.get', m['_id'], single = True)
|
||||
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id))
|
||||
fireEvent('media.restatus', m.id)
|
||||
|
||||
except:
|
||||
log.error('Can\'t edit non-existing media')
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
except:
|
||||
log.error('Failed editing media: %s', traceback.format_exc())
|
||||
movie_dict = m.to_dict(self.search_dict)
|
||||
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id))
|
||||
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': False,
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def updateInfo(self, media_id = None, identifier = None, default_title = None, extended = False):
|
||||
"""
|
||||
Update movie information inside media['doc']['info']
|
||||
|
||||
@param media_id: document id
|
||||
@param default_title: default title, if empty, use first one or existing one
|
||||
@param extended: update with extended info (parses more info, actors, images from some info providers)
|
||||
@return: dict, with media
|
||||
"""
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
|
||||
if media_id:
|
||||
media = db.get('id', media_id)
|
||||
else:
|
||||
media = db.get('media', 'imdb-%s' % identifier, with_doc = True)['doc']
|
||||
|
||||
info = fireEvent('movie.info', merge = True, extended = extended, identifier = getIdentifier(media))
|
||||
|
||||
# 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
|
||||
|
||||
# Update basic info
|
||||
media['info'] = info
|
||||
|
||||
titles = info.get('titles', [])
|
||||
log.debug('Adding titles: %s', titles)
|
||||
|
||||
# Define default title
|
||||
if default_title:
|
||||
def_title = None
|
||||
if default_title:
|
||||
counter = 0
|
||||
for title in titles:
|
||||
if title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title):
|
||||
def_title = toUnicode(title)
|
||||
break
|
||||
counter += 1
|
||||
|
||||
if not def_title:
|
||||
def_title = toUnicode(titles[0])
|
||||
|
||||
media['title'] = def_title
|
||||
|
||||
# Files
|
||||
images = info.get('images', [])
|
||||
media['files'] = media.get('files', {})
|
||||
for image_type in ['poster']:
|
||||
|
||||
# Remove non-existing files
|
||||
file_type = 'image_%s' % image_type
|
||||
existing_files = list(set(media['files'].get(file_type, [])))
|
||||
for ef in media['files'].get(file_type, []):
|
||||
if not os.path.isfile(ef):
|
||||
existing_files.remove(ef)
|
||||
|
||||
# Replace new files list
|
||||
media['files'][file_type] = existing_files
|
||||
if len(existing_files) == 0:
|
||||
del media['files'][file_type]
|
||||
|
||||
# Loop over type
|
||||
for image in images.get(image_type, []):
|
||||
if not isinstance(image, (str, unicode)):
|
||||
continue
|
||||
|
||||
if file_type not in media['files'] or len(media['files'].get(file_type, [])) == 0:
|
||||
file_path = fireEvent('file.download', url = image, single = True)
|
||||
if file_path:
|
||||
media['files'][file_type] = [file_path]
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
db.update(media)
|
||||
|
||||
return media
|
||||
except:
|
||||
log.error('Failed update media: %s', traceback.format_exc())
|
||||
|
||||
return {}
|
||||
|
||||
def updateReleaseDate(self, media_id):
|
||||
"""
|
||||
Update release_date (eta) info only
|
||||
|
||||
@param media_id: document id
|
||||
@return: dict, with dates dvd, theater, bluray, expires
|
||||
"""
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
|
||||
media = db.get('id', media_id)
|
||||
|
||||
if not media.get('info'):
|
||||
media = self.updateInfo(media_id)
|
||||
dates = media.get('info', {}).get('release_date')
|
||||
else:
|
||||
dates = media.get('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:
|
||||
dates = fireEvent('movie.info.release_date', identifier = getIdentifier(media), merge = True)
|
||||
media['info'].update({'release_date': dates})
|
||||
db.update(media)
|
||||
|
||||
return dates
|
||||
except:
|
||||
log.error('Failed updating release dates: %s', traceback.format_exc())
|
||||
|
||||
return {}
|
||||
|
||||
@@ -26,7 +26,7 @@ var MovieList = new Class({
|
||||
self.filter = self.options.filter || {
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
};
|
||||
}
|
||||
|
||||
self.el = new Element('div.movies').adopt(
|
||||
self.title = self.options.title ? new Element('h2', {
|
||||
@@ -52,18 +52,18 @@ var MovieList = new Class({
|
||||
|
||||
self.getMovies();
|
||||
|
||||
App.on('movie.added', self.movieAdded.bind(self));
|
||||
App.on('movie.added', self.movieAdded.bind(self))
|
||||
App.on('movie.deleted', self.movieDeleted.bind(self))
|
||||
},
|
||||
|
||||
movieDeleted: function(notification){
|
||||
var self = this;
|
||||
|
||||
if(self.movies_added[notification.data._id]){
|
||||
if(self.movies_added[notification.data.id]){
|
||||
self.movies.each(function(movie){
|
||||
if(movie.get('_id') == notification.data._id){
|
||||
if(movie.get('id') == notification.data.id){
|
||||
movie.destroy();
|
||||
delete self.movies_added[notification.data._id];
|
||||
delete self.movies_added[notification.data.id];
|
||||
self.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
}
|
||||
@@ -77,7 +77,7 @@ var MovieList = new Class({
|
||||
var self = this;
|
||||
|
||||
self.fireEvent('movieAdded', notification);
|
||||
if(self.options.add_new && !self.movies_added[notification.data._id] && notification.data.status == self.options.status){
|
||||
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
|
||||
window.scroll(0,0);
|
||||
self.createMovie(notification.data, 'top');
|
||||
self.setCounter(self.counter_count+1);
|
||||
@@ -96,7 +96,7 @@ var MovieList = new Class({
|
||||
if(self.options.load_more)
|
||||
self.scrollspy = new ScrollSpy({
|
||||
min: function(){
|
||||
var c = self.load_more.getCoordinates();
|
||||
var c = self.load_more.getCoordinates()
|
||||
return c.top - window.document.getSize().y - 300
|
||||
},
|
||||
onEnter: self.loadMore.bind(self)
|
||||
@@ -179,15 +179,15 @@ var MovieList = new Class({
|
||||
|
||||
m.fireEvent('injected');
|
||||
|
||||
self.movies.include(m);
|
||||
self.movies_added[movie._id] = true;
|
||||
self.movies.include(m)
|
||||
self.movies_added[movie.id] = true;
|
||||
},
|
||||
|
||||
createNavigation: function(){
|
||||
var self = this;
|
||||
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
self.el.addClass('with_navigation');
|
||||
self.el.addClass('with_navigation')
|
||||
|
||||
self.navigation = new Element('div.alph_nav').adopt(
|
||||
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
|
||||
@@ -242,7 +242,7 @@ var MovieList = new Class({
|
||||
this.addClass(a);
|
||||
|
||||
el.inject(el.getParent(), 'top');
|
||||
el.getSiblings().hide();
|
||||
el.getSiblings().hide()
|
||||
setTimeout(function(){
|
||||
el.getSiblings().setStyle('display', null);
|
||||
}, 100)
|
||||
@@ -259,8 +259,8 @@ var MovieList = new Class({
|
||||
self.mass_edit_select_class = new Form.Check(self.mass_edit_select);
|
||||
Quality.getActiveProfiles().each(function(profile){
|
||||
new Element('option', {
|
||||
'value': profile.get('_id'),
|
||||
'text': profile.get('label')
|
||||
'value': profile.id ? profile.id : profile.data.id,
|
||||
'text': profile.label ? profile.label : profile.data.label
|
||||
}).inject(self.mass_edit_quality)
|
||||
});
|
||||
|
||||
@@ -286,9 +286,9 @@ var MovieList = new Class({
|
||||
'status': self.options.status
|
||||
}, self.filter),
|
||||
'onSuccess': function(json){
|
||||
available_chars = json.chars;
|
||||
available_chars = json.chars
|
||||
|
||||
available_chars.each(function(c){
|
||||
json.chars.split('').each(function(c){
|
||||
self.letters[c.capitalize()].addClass('available')
|
||||
})
|
||||
|
||||
@@ -300,7 +300,7 @@ var MovieList = new Class({
|
||||
self.navigation_alpha = new Element('ul.numbers', {
|
||||
'events': {
|
||||
'click:relay(li.available)': function(e, el){
|
||||
self.activateLetter(el.get('data-letter'));
|
||||
self.activateLetter(el.get('data-letter'))
|
||||
self.getMovies(true)
|
||||
}
|
||||
}
|
||||
@@ -318,7 +318,7 @@ var MovieList = new Class({
|
||||
|
||||
// All
|
||||
self.letters['all'] = new Element('li.letter_all.available.active', {
|
||||
'text': 'ALL'
|
||||
'text': 'ALL',
|
||||
}).inject(self.navigation_alpha);
|
||||
|
||||
// Chars
|
||||
@@ -334,7 +334,7 @@ var MovieList = new Class({
|
||||
if (self.options.menu.length > 0)
|
||||
self.options.menu.each(function(menu_item){
|
||||
self.navigation_menu.addLink(menu_item);
|
||||
});
|
||||
})
|
||||
else
|
||||
self.navigation_menu.hide();
|
||||
|
||||
@@ -347,15 +347,15 @@ var MovieList = new Class({
|
||||
movies = self.movies.length;
|
||||
self.movies.each(function(movie){
|
||||
selected += movie.isSelected() ? 1 : 0
|
||||
});
|
||||
})
|
||||
|
||||
var indeterminate = selected > 0 && selected < movies,
|
||||
checked = selected == movies && selected > 0;
|
||||
|
||||
self.mass_edit_select.set('indeterminate', indeterminate);
|
||||
self.mass_edit_select.set('indeterminate', indeterminate)
|
||||
|
||||
self.mass_edit_select_class[checked ? 'check' : 'uncheck']();
|
||||
self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate');
|
||||
self.mass_edit_select_class[checked ? 'check' : 'uncheck']()
|
||||
self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate')
|
||||
|
||||
self.mass_edit_selected.set('text', selected);
|
||||
},
|
||||
@@ -371,9 +371,8 @@ var MovieList = new Class({
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
this.set('text', 'Deleting..');
|
||||
this.set('text', 'Deleting..')
|
||||
Api.request('media.delete', {
|
||||
'method': 'post',
|
||||
'data': {
|
||||
'id': ids.join(','),
|
||||
'delete_from': self.options.identifier
|
||||
@@ -384,7 +383,7 @@ var MovieList = new Class({
|
||||
var erase_movies = [];
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected()){
|
||||
$(movie).destroy();
|
||||
$(movie).destroy()
|
||||
erase_movies.include(movie);
|
||||
}
|
||||
});
|
||||
@@ -411,10 +410,9 @@ var MovieList = new Class({
|
||||
|
||||
changeQualitySelected: function(){
|
||||
var self = this;
|
||||
var ids = self.getSelectedMovies();
|
||||
var ids = self.getSelectedMovies()
|
||||
|
||||
Api.request('movie.edit', {
|
||||
'method': 'post',
|
||||
'data': {
|
||||
'id': ids.join(','),
|
||||
'profile_id': self.mass_edit_quality.get('value')
|
||||
@@ -425,12 +423,11 @@ var MovieList = new Class({
|
||||
|
||||
refreshSelected: function(){
|
||||
var self = this;
|
||||
var ids = self.getSelectedMovies();
|
||||
var ids = self.getSelectedMovies()
|
||||
|
||||
Api.request('media.refresh', {
|
||||
'method': 'post',
|
||||
'data': {
|
||||
'id': ids.join(',')
|
||||
'id': ids.join(','),
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -438,10 +435,10 @@ var MovieList = new Class({
|
||||
getSelectedMovies: function(){
|
||||
var self = this;
|
||||
|
||||
var ids = [];
|
||||
var ids = []
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected())
|
||||
ids.include(movie.get('_id'))
|
||||
ids.include(movie.get('id'))
|
||||
});
|
||||
|
||||
return ids
|
||||
@@ -462,15 +459,15 @@ var MovieList = new Class({
|
||||
reset: function(){
|
||||
var self = this;
|
||||
|
||||
self.movies = [];
|
||||
self.movies = []
|
||||
if(self.mass_edit_select)
|
||||
self.calculateSelected();
|
||||
self.calculateSelected()
|
||||
if(self.navigation_alpha)
|
||||
self.navigation_alpha.getElements('.active').removeClass('active');
|
||||
self.navigation_alpha.getElements('.active').removeClass('active')
|
||||
|
||||
self.offset = 0;
|
||||
if(self.scrollspy){
|
||||
//self.load_more.show();
|
||||
self.load_more.show();
|
||||
self.scrollspy.start();
|
||||
}
|
||||
},
|
||||
@@ -478,7 +475,7 @@ var MovieList = new Class({
|
||||
activateLetter: function(letter){
|
||||
var self = this;
|
||||
|
||||
self.reset();
|
||||
self.reset()
|
||||
|
||||
self.letters[letter || 'all'].addClass('active');
|
||||
self.filter.starts_with = letter;
|
||||
@@ -490,7 +487,7 @@ var MovieList = new Class({
|
||||
|
||||
self.el
|
||||
.removeClass(self.current_view+'_list')
|
||||
.addClass(new_view+'_list');
|
||||
.addClass(new_view+'_list')
|
||||
|
||||
self.current_view = new_view;
|
||||
Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000});
|
||||
@@ -507,9 +504,9 @@ var MovieList = new Class({
|
||||
if(self.search_timer) clearTimeout(self.search_timer);
|
||||
self.search_timer = (function(){
|
||||
var search_value = self.navigation_search_input.get('value');
|
||||
if (search_value == self.last_search_value) return;
|
||||
if (search_value == self.last_search_value) return
|
||||
|
||||
self.reset();
|
||||
self.reset()
|
||||
|
||||
self.activateLetter();
|
||||
self.filter.search = search_value;
|
||||
@@ -555,7 +552,7 @@ var MovieList = new Class({
|
||||
|
||||
Api.request(self.options.api_call || 'media.list', {
|
||||
'data': Object.merge({
|
||||
'type': self.options.type || 'movie',
|
||||
'type': 'movie',
|
||||
'status': self.options.status,
|
||||
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
|
||||
}, self.filter),
|
||||
@@ -566,7 +563,7 @@ var MovieList = new Class({
|
||||
|
||||
if(self.loader_first){
|
||||
var lf = self.loader_first;
|
||||
self.loader_first.addClass('hide');
|
||||
self.loader_first.addClass('hide')
|
||||
self.loader_first = null;
|
||||
setTimeout(function(){
|
||||
lf.destroy();
|
||||
@@ -606,10 +603,10 @@ var MovieList = new Class({
|
||||
var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
|
||||
|
||||
if(self.title)
|
||||
self.title[is_empty ? 'hide' : 'show']();
|
||||
self.title[is_empty ? 'hide' : 'show']()
|
||||
|
||||
if(self.description)
|
||||
self.description.setStyle('display', [is_empty ? 'none' : '']);
|
||||
self.description.setStyle('display', [is_empty ? 'none' : ''])
|
||||
|
||||
if(is_empty && self.options.on_empty_element){
|
||||
self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after');
|
||||
@@ -632,4 +629,4 @@ var MovieList = new Class({
|
||||
return this.el;
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
@@ -60,6 +60,22 @@ var MovieAction = new Class({
|
||||
'z-index': '1'
|
||||
}
|
||||
}).inject(self.movie, 'top').fade('hide');
|
||||
//self.positionMask();
|
||||
},
|
||||
|
||||
positionMask: function(){
|
||||
var self = this,
|
||||
movie = $(self.movie),
|
||||
s = movie.getSize()
|
||||
|
||||
return;
|
||||
|
||||
return self.mask.setStyles({
|
||||
'width': s.x,
|
||||
'height': s.y
|
||||
}).position({
|
||||
'relativeTo': movie
|
||||
})
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
@@ -78,7 +94,7 @@ MA.IMDB = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.id = self.movie.getIdentifier ? self.movie.getIdentifier() : self.get('imdb');
|
||||
self.id = self.movie.get('imdb') || self.movie.get('identifier');
|
||||
|
||||
self.el = new Element('a.imdb', {
|
||||
'title': 'Go to the IMDB page of ' + self.getTitle(),
|
||||
@@ -105,13 +121,13 @@ MA.Release = new Class({
|
||||
}
|
||||
});
|
||||
|
||||
if(!self.movie.data.releases || self.movie.data.releases.length == 0)
|
||||
self.el.hide();
|
||||
if(self.movie.data.releases.length == 0)
|
||||
self.el.hide()
|
||||
else
|
||||
self.showHelper();
|
||||
|
||||
App.on('movie.searcher.ended', function(notification){
|
||||
if(self.movie.data._id != notification.data._id) return;
|
||||
if(self.movie.data.id != notification.data.id) return;
|
||||
|
||||
self.releases = null;
|
||||
if(self.options_container){
|
||||
@@ -127,7 +143,30 @@ MA.Release = new Class({
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.createReleases();
|
||||
if(self.releases)
|
||||
self.createReleases();
|
||||
else {
|
||||
|
||||
self.movie.busy(true);
|
||||
|
||||
Api.request('release.for_movie', {
|
||||
'data': {
|
||||
'id': self.movie.data.id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.movie.busy(false, 1);
|
||||
|
||||
if(json && json.releases){
|
||||
self.releases = json.releases;
|
||||
self.createReleases();
|
||||
}
|
||||
else
|
||||
alert('Something went wrong, check the logs.');
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
@@ -148,102 +187,106 @@ MA.Release = new Class({
|
||||
new Element('span.age', {'text': 'Age'}),
|
||||
new Element('span.score', {'text': 'Score'}),
|
||||
new Element('span.provider', {'text': 'Provider'})
|
||||
).inject(self.release_container);
|
||||
).inject(self.release_container)
|
||||
|
||||
if(self.movie.data.releases)
|
||||
self.movie.data.releases.each(function(release){
|
||||
self.releases.each(function(release){
|
||||
|
||||
var quality = Quality.getQuality(release.quality) || {},
|
||||
info = release.info || {},
|
||||
provider = self.get(release, 'provider') + (info['provider_extra'] ? self.get(release, 'provider_extra') : '');
|
||||
var status = Status.get(release.status_id),
|
||||
quality = Quality.getProfile(release.quality_id) || {},
|
||||
info = release.info,
|
||||
provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : '');
|
||||
release.status = status;
|
||||
|
||||
var release_name = self.get(release, 'name');
|
||||
if(release.files && release.files.length > 0){
|
||||
try {
|
||||
var movie_file = release.files.filter(function(file){
|
||||
var type = File.Type.get(file.type_id);
|
||||
return type && type.identifier == 'movie'
|
||||
}).pick();
|
||||
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
|
||||
}
|
||||
catch(e){}
|
||||
var release_name = self.get(release, 'name');
|
||||
if(release.files && release.files.length > 0){
|
||||
try {
|
||||
var movie_file = release.files.filter(function(file){
|
||||
var type = File.Type.get(file.type_id);
|
||||
return type && type.identifier == 'movie'
|
||||
}).pick();
|
||||
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
|
||||
}
|
||||
catch(e){}
|
||||
}
|
||||
|
||||
// Create release
|
||||
release['el'] = new Element('div', {
|
||||
'class': 'item '+release.status,
|
||||
'id': 'release_'+release._id
|
||||
}).adopt(
|
||||
new Element('span.name', {'text': release_name, 'title': release_name}),
|
||||
new Element('span.status', {'text': release.status, 'class': 'release_status '+release.status}),
|
||||
new Element('span.quality', {'text': quality.label + (release.is_3d ? ' 3D' : '') || 'n/a'}),
|
||||
new Element('span.size', {'text': info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
|
||||
new Element('span.age', {'text': self.get(release, 'age')}),
|
||||
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||
new Element('span.provider', { 'text': provider, 'title': provider }),
|
||||
info['detail_url'] ? new Element('a.info.icon2', {
|
||||
'href': info['detail_url'],
|
||||
'target': '_blank'
|
||||
}) : new Element('a'),
|
||||
new Element('a.download.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
if(!this.hasClass('completed'))
|
||||
self.download(release);
|
||||
}
|
||||
// Create release
|
||||
var item = new Element('div', {
|
||||
'class': 'item '+status.identifier,
|
||||
'id': 'release_'+release.id
|
||||
}).adopt(
|
||||
new Element('span.name', {'text': release_name, 'title': release_name}),
|
||||
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
|
||||
new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
|
||||
new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
|
||||
new Element('span.age', {'text': self.get(release, 'age')}),
|
||||
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||
new Element('span.provider', { 'text': provider, 'title': provider }),
|
||||
release.info['detail_url'] ? new Element('a.info.icon2', {
|
||||
'href': release.info['detail_url'],
|
||||
'target': '_blank'
|
||||
}) : new Element('a'),
|
||||
new Element('a.download.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
if(!this.hasClass('completed'))
|
||||
self.download(release);
|
||||
}
|
||||
}),
|
||||
new Element('a.delete.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
self.ignore(release);
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.release_container);
|
||||
|
||||
if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){
|
||||
if(!self.last_release || (self.last_release && self.last_release.status != 'snatched' && release.status == 'snatched'))
|
||||
self.last_release = release;
|
||||
}
|
||||
else if(!self.next_release && release.status == 'available'){
|
||||
self.next_release = release;
|
||||
}
|
||||
|
||||
var update_handle = function(notification) {
|
||||
if(notification.data._id != release._id) return;
|
||||
|
||||
var q = self.movie.quality.getElement('.q_' + release.quality),
|
||||
new_status = notification.data.status;
|
||||
|
||||
release.el.set('class', 'item ' + new_status);
|
||||
|
||||
var status_el = release.el.getElement('.release_status');
|
||||
status_el.set('class', 'release_status ' + new_status);
|
||||
status_el.set('text', new_status);
|
||||
|
||||
if(!q && (new_status == 'snatched' || new_status == 'seeding' || new_status == 'done'))
|
||||
q = self.addQuality(release.quality_id);
|
||||
|
||||
if(q && !q.hasClass(new_status)) {
|
||||
q.removeClass(release.status).addClass(new_status);
|
||||
q.set('title', q.get('title').replace(release.status, new_status));
|
||||
}
|
||||
};
|
||||
}),
|
||||
new Element('a.delete.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
self.ignore(release);
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.release_container);
|
||||
release['el'] = item;
|
||||
|
||||
App.on('release.update_status', update_handle);
|
||||
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
|
||||
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
|
||||
self.last_release = release;
|
||||
}
|
||||
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.getElements('#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.getElements('#release_'+self.next_release.id).addClass('next_release');
|
||||
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status) === false)){
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
|
||||
|
||||
@@ -252,7 +295,7 @@ MA.Release = new Class({
|
||||
|
||||
self.trynext_container.adopt(
|
||||
new Element('span.or', {
|
||||
'text': 'If anything went wrong, download'
|
||||
'text': 'This movie is snatched, if anything went wrong, download'
|
||||
}),
|
||||
lr ? new Element('a.button.orange', {
|
||||
'text': 'the same release again',
|
||||
@@ -298,17 +341,18 @@ MA.Release = new Class({
|
||||
var has_available = false,
|
||||
has_snatched = false;
|
||||
|
||||
if(self.movie.data.releases)
|
||||
self.movie.data.releases.each(function(release){
|
||||
if(has_available && has_snatched) return;
|
||||
self.movie.data.releases.each(function(release){
|
||||
if(has_available && has_snatched) return;
|
||||
|
||||
if(['snatched', 'downloaded', 'seeding', 'done'].contains(release.status))
|
||||
has_snatched = true;
|
||||
var status = Status.get(release.status_id);
|
||||
|
||||
if(['available'].contains(release.status))
|
||||
has_available = true;
|
||||
if(['snatched', 'downloaded', 'seeding'].contains(status.identifier))
|
||||
has_snatched = true;
|
||||
|
||||
});
|
||||
if(['available'].contains(status.identifier))
|
||||
has_available = true;
|
||||
|
||||
});
|
||||
|
||||
if(has_available || has_snatched){
|
||||
|
||||
@@ -341,13 +385,13 @@ MA.Release = new Class({
|
||||
},
|
||||
|
||||
get: function(release, type){
|
||||
return (release.info && release.info[type] !== undefined) ? release.info[type] : 'n/a'
|
||||
return release.info[type] !== undefined ? release.info[type] : 'n/a'
|
||||
},
|
||||
|
||||
download: function(release){
|
||||
var self = this;
|
||||
|
||||
var release_el = self.release_container.getElement('#release_'+release._id),
|
||||
var release_el = self.release_container.getElement('#release_'+release.id),
|
||||
icon = release_el.getElement('.download.icon2');
|
||||
|
||||
if(icon)
|
||||
@@ -355,7 +399,7 @@ MA.Release = new Class({
|
||||
|
||||
Api.request('release.manual_download', {
|
||||
'data': {
|
||||
'id': release._id
|
||||
'id': release.id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
if(icon)
|
||||
@@ -374,11 +418,12 @@ MA.Release = new Class({
|
||||
},
|
||||
|
||||
ignore: function(release){
|
||||
var self = this;
|
||||
|
||||
Api.request('release.ignore', {
|
||||
'data': {
|
||||
'id': release._id
|
||||
}
|
||||
'id': release.id
|
||||
},
|
||||
})
|
||||
|
||||
},
|
||||
@@ -388,7 +433,7 @@ MA.Release = new Class({
|
||||
|
||||
Api.request('media.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('_id'),
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': 'wanted'
|
||||
},
|
||||
'onComplete': function(){
|
||||
@@ -410,7 +455,7 @@ MA.Release = new Class({
|
||||
|
||||
Api.request('movie.searcher.try_next', {
|
||||
'data': {
|
||||
'media_id': self.movie.get('_id')
|
||||
'id': self.movie.get('id')
|
||||
}
|
||||
});
|
||||
|
||||
@@ -438,7 +483,7 @@ MA.Trailer = new Class({
|
||||
watch: function(offset){
|
||||
var self = this;
|
||||
|
||||
var data_url = 'https://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18';
|
||||
var data_url = 'https://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
|
||||
var url = data_url.substitute({
|
||||
'title': encodeURI(self.getTitle()),
|
||||
'year': self.get('year'),
|
||||
@@ -497,7 +542,7 @@ MA.Trailer = new Class({
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
self.player.addEventListener('onStateChange', change_quality);
|
||||
|
||||
}
|
||||
@@ -514,7 +559,7 @@ MA.Trailer = new Class({
|
||||
$(self.movie).setStyle('height', null);
|
||||
|
||||
setTimeout(function(){
|
||||
self.container.destroy();
|
||||
self.container.destroy()
|
||||
self.close_button.destroy();
|
||||
}, 1800)
|
||||
}
|
||||
@@ -565,13 +610,13 @@ MA.Edit = new Class({
|
||||
)
|
||||
).inject(self.movie, 'top');
|
||||
|
||||
Array.each(self.movie.data.info.titles, function(title){
|
||||
Array.each(self.movie.data.library.titles, function(alt){
|
||||
new Element('option', {
|
||||
'text': title
|
||||
'text': alt.title
|
||||
}).inject(self.title_select);
|
||||
|
||||
if(title == self.movie.data.title)
|
||||
self.title_select.set('value', title);
|
||||
if(alt['default'])
|
||||
self.title_select.set('value', alt.title);
|
||||
});
|
||||
|
||||
|
||||
@@ -584,14 +629,14 @@ MA.Edit = new Class({
|
||||
self.category_select.show();
|
||||
categories.each(function(category){
|
||||
|
||||
var category_id = category.data._id;
|
||||
var category_id = category.data.id;
|
||||
|
||||
new Element('option', {
|
||||
'value': category_id,
|
||||
'text': category.data.label
|
||||
}).inject(self.category_select);
|
||||
|
||||
if(self.movie.category && self.movie.category.data && self.movie.category.data._id == category_id)
|
||||
if(self.movie.category && self.movie.category.data && self.movie.category.data.id == category_id)
|
||||
self.category_select.set('value', category_id);
|
||||
|
||||
});
|
||||
@@ -604,7 +649,7 @@ MA.Edit = new Class({
|
||||
|
||||
profiles.each(function(profile){
|
||||
|
||||
var profile_id = profile.get('_id');
|
||||
var profile_id = profile.id ? profile.id : profile.data.id;
|
||||
|
||||
new Element('option', {
|
||||
'value': profile_id,
|
||||
@@ -627,7 +672,7 @@ MA.Edit = new Class({
|
||||
|
||||
Api.request('movie.edit', {
|
||||
'data': {
|
||||
'id': self.movie.get('_id'),
|
||||
'id': self.movie.get('id'),
|
||||
'default_title': self.title_select.get('value'),
|
||||
'profile_id': self.profile_select.get('value'),
|
||||
'category_id': self.category_select.get('value')
|
||||
@@ -643,7 +688,7 @@ MA.Edit = new Class({
|
||||
self.movie.slide('out');
|
||||
}
|
||||
|
||||
});
|
||||
})
|
||||
|
||||
MA.Refresh = new Class({
|
||||
|
||||
@@ -667,7 +712,7 @@ MA.Refresh = new Class({
|
||||
|
||||
Api.request('media.refresh', {
|
||||
'data': {
|
||||
'id': self.movie.get('_id')
|
||||
'id': self.movie.get('id')
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -681,10 +726,10 @@ MA.Readd = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var movie_done = self.movie.data.status == 'done';
|
||||
if(self.movie.data.releases && !movie_done)
|
||||
var movie_done = Status.get(self.movie.data.status_id).identifier == 'done';
|
||||
if(!movie_done)
|
||||
var snatched = self.movie.data.releases.filter(function(release){
|
||||
return release.status && (release.status == 'snatched' || release.status == 'seeding' || release.status == 'downloaded' || release.status == 'done');
|
||||
return release.status && (release.status.identifier == 'snatched' || release.status.identifier == 'downloaded' || release.status.identifier == 'done');
|
||||
}).length;
|
||||
|
||||
if(movie_done || snatched && snatched > 0)
|
||||
@@ -703,7 +748,7 @@ MA.Readd = new Class({
|
||||
|
||||
Api.request('movie.add', {
|
||||
'data': {
|
||||
'identifier': self.movie.getIdentifier(),
|
||||
'identifier': self.movie.get('identifier'),
|
||||
'ignore_previous': 1
|
||||
}
|
||||
});
|
||||
@@ -778,7 +823,7 @@ MA.Delete = new Class({
|
||||
function(){
|
||||
Api.request('media.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('_id'),
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': self.movie.list.options.identifier
|
||||
},
|
||||
'onComplete': function(){
|
||||
@@ -807,17 +852,46 @@ MA.Files = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.movie.data.releases && self.movie.data.releases.length > 0)
|
||||
self.el = new Element('a.directory', {
|
||||
'title': 'Available files',
|
||||
'events': {
|
||||
'click': self.show.bind(self)
|
||||
}
|
||||
});
|
||||
self.el = new Element('a.directory', {
|
||||
'title': 'Available files',
|
||||
'events': {
|
||||
'click': self.show.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
show: function(){
|
||||
show: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
if(self.releases)
|
||||
self.showFiles();
|
||||
else {
|
||||
|
||||
self.movie.busy(true);
|
||||
|
||||
Api.request('release.for_movie', {
|
||||
'data': {
|
||||
'id': self.movie.data.id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.movie.busy(false, 1);
|
||||
|
||||
if(json && json.releases){
|
||||
self.releases = json.releases;
|
||||
self.showFiles();
|
||||
}
|
||||
else
|
||||
alert('Something went wrong, check the logs.');
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
showFiles: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.options_container){
|
||||
@@ -828,26 +902,26 @@ MA.Files = new Class({
|
||||
// Header
|
||||
new Element('div.item.head').adopt(
|
||||
new Element('span.name', {'text': 'File'}),
|
||||
new Element('span.type', {'text': 'Type'})
|
||||
).inject(self.files_container);
|
||||
new Element('span.type', {'text': 'Type'}),
|
||||
new Element('span.is_available', {'text': 'Available'})
|
||||
).inject(self.files_container)
|
||||
|
||||
if(self.movie.data.releases)
|
||||
Array.each(self.movie.data.releases, function(release){
|
||||
var rel = new Element('div.release').inject(self.files_container);
|
||||
Array.each(self.releases, function(release){
|
||||
|
||||
Object.each(release.files, function(files, type){
|
||||
Array.each(files, function(file){
|
||||
new Element('div.file.item').adopt(
|
||||
new Element('span.name', {'text': file}),
|
||||
new Element('span.type', {'text': type})
|
||||
).inject(rel)
|
||||
});
|
||||
});
|
||||
var rel = new Element('div.release').inject(self.files_container);
|
||||
|
||||
Array.each(release.files, function(file){
|
||||
new Element('div.file.item').adopt(
|
||||
new Element('span.name', {'text': file.path}),
|
||||
new Element('span.type', {'text': File.Type.get(file.type_id).name}),
|
||||
new Element('span.available', {'text': file.available})
|
||||
).inject(rel)
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
self.movie.slide('in', self.options_container);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
@@ -123,18 +123,15 @@
|
||||
.movies.thumbs_list .movie {
|
||||
width: 16.66667%;
|
||||
height: auto;
|
||||
min-height: 200px;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
.movies.thumbs_list .movie {
|
||||
width: 25%;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +343,6 @@
|
||||
top: auto;
|
||||
right: auto;
|
||||
color: #FFF;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.touch_enabled .movies.list_list .movie .info .year {
|
||||
@@ -357,40 +353,18 @@
|
||||
top: 30px;
|
||||
clear: both;
|
||||
bottom: 30px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
.movies .data:hover .description {
|
||||
overflow: auto;
|
||||
}
|
||||
.movies.list_list .movie:not(.details_view) .info .description,
|
||||
.movies.mass_edit_list .info .description,
|
||||
.movies.thumbs_list .info .description {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies .data .eta {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies.details_list .data .eta {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: block;
|
||||
min-height: 20px;
|
||||
text-align: right;
|
||||
font-style: italic;
|
||||
opacity: .8;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.movies.details_list .movie:hover .data .eta {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .data .eta {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
}
|
||||
|
||||
.movies .data .quality {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
@@ -422,6 +396,7 @@
|
||||
|
||||
.movies .data .quality span {
|
||||
padding: 2px 3px;
|
||||
font-weight: bold;
|
||||
opacity: 0.5;
|
||||
font-size: 10px;
|
||||
height: 16px;
|
||||
@@ -474,6 +449,7 @@
|
||||
right: 20px;
|
||||
line-height: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: auto;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
@@ -855,6 +831,7 @@
|
||||
}
|
||||
|
||||
.movies .alph_nav .search input {
|
||||
padding: 6px 5px;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: inline-block;
|
||||
@@ -862,6 +839,7 @@
|
||||
background: none;
|
||||
color: #444;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
padding: 0 10px 0 30px;
|
||||
border-bottom: 1px solid rgba(0,0,0,.08);
|
||||
}
|
||||
@@ -1063,6 +1041,7 @@
|
||||
}
|
||||
|
||||
.movies .progress > div .percentage {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
font-weight: normal;
|
||||
|
||||
@@ -23,46 +23,46 @@ var Movie = new Class({
|
||||
addEvents: function(){
|
||||
var self = this;
|
||||
|
||||
self.global_events = {};
|
||||
self.global_events = {}
|
||||
|
||||
// Do refresh with new data
|
||||
self.global_events['movie.update'] = function(notification){
|
||||
if(self.data._id != notification.data._id) return;
|
||||
if(self.data.id != notification.data.id) return;
|
||||
|
||||
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
|
||||
['media.busy', 'movie.searcher.started'].each(function(listener){
|
||||
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||
self.global_events[listener] = function(notification){
|
||||
if(notification.data && (self.data._id == notification.data._id || (typeOf(notification.data._id) == 'array' && notification.data._id.indexOf(self.data._id) > -1)))
|
||||
self.busy(true);
|
||||
};
|
||||
if(notification.data && self.data.id == notification.data.id)
|
||||
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)
|
||||
if(notification.data && self.data.id == notification.data.id)
|
||||
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.movie_id){
|
||||
var data = notification.data
|
||||
if(data && self.data.id == data.media_id){
|
||||
|
||||
if(!self.data.releases)
|
||||
self.data.releases = [];
|
||||
|
||||
self.data.releases.push({'quality': data.quality, 'status': data.status});
|
||||
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']);
|
||||
|
||||
@@ -73,7 +73,7 @@ var Movie = new Class({
|
||||
|
||||
self.el.destroy();
|
||||
delete self.list.movies_added[self.get('id')];
|
||||
self.list.movies.erase(self);
|
||||
self.list.movies.erase(self)
|
||||
|
||||
self.list.checkIfEmpty();
|
||||
|
||||
@@ -117,6 +117,18 @@ var Movie = new Class({
|
||||
}).inject(self.el, 'top').fade('hide');
|
||||
},
|
||||
|
||||
positionMask: function(){
|
||||
var self = this,
|
||||
s = self.el.getSize()
|
||||
|
||||
return self.mask.setStyles({
|
||||
'width': s.x,
|
||||
'height': s.y
|
||||
}).position({
|
||||
'relativeTo': self.el
|
||||
})
|
||||
},
|
||||
|
||||
update: function(notification){
|
||||
var self = this;
|
||||
|
||||
@@ -134,22 +146,8 @@ var Movie = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el.addClass('status_'+self.get('status'));
|
||||
|
||||
var eta = null,
|
||||
eta_date = null,
|
||||
now = Math.round(+new Date()/1000);
|
||||
|
||||
if(self.data.info.release_date)
|
||||
[self.data.info.release_date.dvd, self.data.info.release_date.theater].each(function(timestamp){
|
||||
if (timestamp > 0 && (eta == null || Math.abs(timestamp - now) < Math.abs(eta - now)))
|
||||
eta = timestamp;
|
||||
});
|
||||
|
||||
if(eta){
|
||||
eta_date = new Date(eta * 1000);
|
||||
eta_date = eta_date.toLocaleString('en-us', { month: "long" }) + ' ' + eta_date.getFullYear();
|
||||
}
|
||||
var s = Status.get(self.get('status_id'));
|
||||
self.el.addClass('status_'+s.identifier);
|
||||
|
||||
self.el.adopt(
|
||||
self.select_checkbox = new Element('input[type=checkbox].inlay', {
|
||||
@@ -159,10 +157,7 @@ var Movie = new Class({
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.thumbnail = (self.data.files && self.data.files.image_poster) ? new Element('img', {
|
||||
'class': 'type_image poster',
|
||||
'src': Api.createUrl('file.cache') + self.data.files.image_poster[0].split(Api.getOption('path_sep')).pop()
|
||||
}): null,
|
||||
self.thumbnail = File.Select.single('poster', self.data.library.files),
|
||||
self.data_container = new Element('div.data.inlay.light').adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
new Element('div.title').adopt(
|
||||
@@ -170,16 +165,12 @@ var Movie = new Class({
|
||||
'text': self.getTitle() || 'n/a'
|
||||
}),
|
||||
self.year = new Element('div.year', {
|
||||
'text': self.data.info.year || 'n/a'
|
||||
'text': self.data.library.year || 'n/a'
|
||||
})
|
||||
),
|
||||
self.description = new Element('div.description.tiny_scroll', {
|
||||
'text': self.data.info.plot
|
||||
self.description = new Element('div.description', {
|
||||
'text': self.data.library.plot
|
||||
}),
|
||||
self.eta = eta_date && (now+8035200 > eta) ? new Element('div.eta', {
|
||||
'text': eta_date,
|
||||
'title': 'ETA'
|
||||
}) : null,
|
||||
self.quality = new Element('div.quality', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
@@ -194,7 +185,7 @@ var Movie = new Class({
|
||||
)
|
||||
);
|
||||
|
||||
if(!self.thumbnail)
|
||||
if(self.thumbnail.empty)
|
||||
self.el.addClass('no_thumbnail');
|
||||
|
||||
//self.changeView(self.view);
|
||||
@@ -204,7 +195,7 @@ var Movie = new Class({
|
||||
if(self.profile.data)
|
||||
self.profile.getTypes().each(function(type){
|
||||
|
||||
var q = self.addQuality(type.get('quality'), type.get('3d'));
|
||||
var q = self.addQuality(type.quality_id || type.get('quality_id'));
|
||||
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
|
||||
q.addClass('finish');
|
||||
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
|
||||
@@ -216,7 +207,7 @@ var Movie = new Class({
|
||||
self.updateReleases();
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self);
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
|
||||
if(action.el)
|
||||
self.actions.adopt(action)
|
||||
});
|
||||
@@ -229,27 +220,27 @@ var Movie = new Class({
|
||||
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')),
|
||||
status = release.status;
|
||||
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||
status = Status.get(release.status_id);
|
||||
|
||||
if(!q && (status == 'snatched' || status == 'seeding' || status == 'done'))
|
||||
q = self.addQuality(release.quality, release.is_3d || false);
|
||||
if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done'))
|
||||
var q = self.addQuality(release.quality_id)
|
||||
|
||||
if (q && !q.hasClass(status)){
|
||||
q.addClass(status);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status)
|
||||
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, is_3d){
|
||||
addQuality: function(quality_id){
|
||||
var self = this;
|
||||
|
||||
var q = Quality.getQuality(quality);
|
||||
var q = Quality.getQuality(quality_id);
|
||||
return new Element('span', {
|
||||
'text': q.label + (is_3d ? ' 3D' : ''),
|
||||
'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''),
|
||||
'text': q.label,
|
||||
'class': 'q_'+q.identifier + ' q_id' + q.id,
|
||||
'title': ''
|
||||
}).inject(self.quality);
|
||||
|
||||
@@ -258,10 +249,16 @@ var Movie = new Class({
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.data.title)
|
||||
return self.getUnprefixedTitle(self.data.title);
|
||||
else if(self.data.info.titles.length > 0)
|
||||
return self.getUnprefixedTitle(self.data.info.titles[0]);
|
||||
var titles = self.data.library.titles;
|
||||
|
||||
var title = titles.filter(function(title){
|
||||
return title['default']
|
||||
}).pop()
|
||||
|
||||
if(title)
|
||||
return self.getUnprefixedTitle(title.title)
|
||||
else if(titles.length > 0)
|
||||
return self.getUnprefixedTitle(titles[0].title)
|
||||
|
||||
return 'Unknown movie'
|
||||
},
|
||||
@@ -269,10 +266,6 @@ var Movie = new Class({
|
||||
getUnprefixedTitle: function(t){
|
||||
if(t.substr(0, 4).toLowerCase() == 'the ')
|
||||
t = t.substr(4) + ', The';
|
||||
else if(t.substr(0, 3).toLowerCase() == 'an ')
|
||||
t = t.substr(3) + ', An';
|
||||
else if(t.substr(0, 2).toLowerCase() == 'a ')
|
||||
t = t.substr(2) + ', A';
|
||||
return t;
|
||||
},
|
||||
|
||||
@@ -286,12 +279,12 @@ var Movie = new Class({
|
||||
self.el.addEvent('outerClick', function(){
|
||||
self.removeView();
|
||||
self.slide('out')
|
||||
});
|
||||
})
|
||||
el.show();
|
||||
self.data_container.addClass('hide_right');
|
||||
}
|
||||
else {
|
||||
self.el.removeEvents('outerClick');
|
||||
self.el.removeEvents('outerClick')
|
||||
|
||||
setTimeout(function(){
|
||||
if(self.el)
|
||||
@@ -308,7 +301,7 @@ var Movie = new Class({
|
||||
if(self.el)
|
||||
self.el
|
||||
.removeClass(self.view+'_view')
|
||||
.addClass(new_view+'_view');
|
||||
.addClass(new_view+'_view')
|
||||
|
||||
self.view = new_view;
|
||||
},
|
||||
@@ -319,19 +312,8 @@ var Movie = new Class({
|
||||
self.el.removeClass(self.view+'_view')
|
||||
},
|
||||
|
||||
getIdentifier: function(){
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
return self.get('identifiers').imdb;
|
||||
}
|
||||
catch (e){ }
|
||||
|
||||
return self.get('imdb');
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr] || this.data.info[attr]
|
||||
return this.data[attr] || this.data.library[attr]
|
||||
},
|
||||
|
||||
select: function(bool){
|
||||
@@ -347,4 +329,4 @@ var Movie = new Class({
|
||||
return this.el;
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,7 @@ Block.Search.MovieItem = new Class({
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
if(info.titles)
|
||||
info.titles.each(function(title){
|
||||
@@ -132,19 +132,19 @@ Block.Search.MovieItem = new Class({
|
||||
|
||||
if(!self.options_el.hasClass('set')){
|
||||
|
||||
if(info.in_library){
|
||||
if(self.info.in_library){
|
||||
var in_library = [];
|
||||
(info.in_library.releases || []).each(function(release){
|
||||
in_library.include(release.quality)
|
||||
self.info.in_library.releases.each(function(release){
|
||||
in_library.include(release.quality.label)
|
||||
});
|
||||
}
|
||||
|
||||
self.options_el.grab(
|
||||
new Element('div', {
|
||||
'class': info.in_wanted && info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
|
||||
'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
|
||||
}).adopt(
|
||||
info.in_wanted && info.in_wanted.profile_id ? new Element('span.in_wanted', {
|
||||
'text': 'Already in wanted list: ' + Quality.getProfile(info.in_wanted.profile_id).get('label')
|
||||
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),
|
||||
@@ -172,7 +172,7 @@ Block.Search.MovieItem = new Class({
|
||||
new Element('option', {
|
||||
'text': alt.title
|
||||
}).inject(self.title_select)
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
// Fill categories
|
||||
@@ -181,10 +181,10 @@ Block.Search.MovieItem = new Class({
|
||||
if(categories.length == 0)
|
||||
self.category_select.hide();
|
||||
else {
|
||||
self.category_select.show();
|
||||
self.category_select.movie();
|
||||
categories.each(function(category){
|
||||
new Element('option', {
|
||||
'value': category.data._id,
|
||||
'value': category.data.id,
|
||||
'text': category.data.label
|
||||
}).inject(self.category_select);
|
||||
});
|
||||
@@ -197,8 +197,8 @@ Block.Search.MovieItem = new Class({
|
||||
|
||||
profiles.each(function(profile){
|
||||
new Element('option', {
|
||||
'value': profile.get('_id'),
|
||||
'text': profile.get('label')
|
||||
'value': profile.id ? profile.id : profile.data.id,
|
||||
'text': profile.label ? profile.label : profile.data.label
|
||||
}).inject(self.profile_select)
|
||||
});
|
||||
|
||||
@@ -215,9 +215,9 @@ Block.Search.MovieItem = new Class({
|
||||
loadingMask: function(){
|
||||
var self = this;
|
||||
|
||||
self.mask = new Element('div.mask').inject(self.el).fade('hide');
|
||||
self.mask = new Element('div.mask').inject(self.el).fade('hide')
|
||||
|
||||
createSpinner(self.mask);
|
||||
createSpinner(self.mask)
|
||||
self.mask.fade('in')
|
||||
|
||||
},
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
from .main import Charts
|
||||
|
||||
|
||||
def autoload():
|
||||
return Charts()
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'charts',
|
||||
'groups': [
|
||||
{
|
||||
'label': 'Charts',
|
||||
'description': 'Displays selected charts on the home page',
|
||||
'type': 'list',
|
||||
'name': 'charts_providers',
|
||||
'tab': 'display',
|
||||
'options': [
|
||||
{
|
||||
'name': 'max_items',
|
||||
'default': 5,
|
||||
'type': 'int',
|
||||
'description': 'Maximum number of items displayed from each chart.',
|
||||
},
|
||||
{
|
||||
'name': 'update_interval',
|
||||
'default': 12,
|
||||
'type': 'int',
|
||||
'advanced': True,
|
||||
'description': '(hours)',
|
||||
},
|
||||
{
|
||||
'name': 'hide_wanted',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Hide the chart movies that are already in your wanted list.',
|
||||
},
|
||||
{
|
||||
'name': 'hide_library',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Hide the chart movies that are already in your library.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,61 +0,0 @@
|
||||
import time
|
||||
|
||||
from couchpotato import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent,fireEvent
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Charts(Plugin):
|
||||
|
||||
update_in_progress = False
|
||||
|
||||
def __init__(self):
|
||||
addApiView('charts.view', self.automationView)
|
||||
addEvent('app.load', self.setCrons)
|
||||
|
||||
def setCrons(self):
|
||||
fireEvent('schedule.interval', 'charts.update_cache', self.updateViewCache, hours = self.conf('update_interval', default = 12))
|
||||
|
||||
def automationView(self, force_update = False, **kwargs):
|
||||
|
||||
if force_update:
|
||||
charts = self.updateViewCache()
|
||||
else:
|
||||
charts = self.getCache('charts_cached')
|
||||
if not charts:
|
||||
charts = self.updateViewCache()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'count': len(charts),
|
||||
'charts': charts
|
||||
}
|
||||
|
||||
def updateViewCache(self):
|
||||
|
||||
if self.update_in_progress:
|
||||
while self.update_in_progress:
|
||||
time.sleep(1)
|
||||
catched_charts = self.getCache('charts_cached')
|
||||
if catched_charts:
|
||||
return catched_charts
|
||||
|
||||
charts = []
|
||||
try:
|
||||
self.update_in_progress = True
|
||||
charts = fireEvent('automation.get_chart_list', merge = True)
|
||||
for chart in charts:
|
||||
chart['hide_wanted'] = self.conf('hide_wanted')
|
||||
chart['hide_library'] = self.conf('hide_library')
|
||||
self.setCache('charts_cached', charts, timeout = 7200 * tryInt(self.conf('update_interval', default = 12)))
|
||||
except:
|
||||
log.error('Failed refreshing charts')
|
||||
|
||||
self.update_in_progress = False
|
||||
|
||||
return charts
|
||||
@@ -1,266 +0,0 @@
|
||||
.charts {
|
||||
clear: both;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.charts > h2 {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.charts .chart {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
vertical-align: top;
|
||||
max-height: 510px;
|
||||
scrollbar-base-color: #4e5969;
|
||||
}
|
||||
|
||||
.charts .chart .media_result.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.charts .refresh {
|
||||
clear:both;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.charts .refresh .refreshing {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
font-size: 20px;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.charts .refresh a {
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
display: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: -40px;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.charts .refresh a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.charts p.no_charts_enabled {
|
||||
padding: 0.7em 1em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.charts .chart h3 a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.charts .chart .media_result {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 960px) {
|
||||
.charts .chart {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 600px) {
|
||||
.charts .chart {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.charts .chart .media_result .data {
|
||||
left: 150px;
|
||||
background: #4e5969;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.charts .chart .media_result .data .info {
|
||||
top: 10px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
bottom: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.charts .chart .media_result .data .info h2 {
|
||||
white-space: normal;
|
||||
max-height: 120px;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.charts .chart .media_result .data .info .rating,
|
||||
.charts .chart .media_result .data .info .genres,
|
||||
.charts .chart .media_result .data .info .year {
|
||||
position: static;
|
||||
display: block;
|
||||
padding: 0;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.charts .chart .media_result .data .info .year {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.charts .chart .media_result .data .info .rating {
|
||||
font-size: 20px;
|
||||
float: right;
|
||||
margin-top: -20px;
|
||||
}
|
||||
.charts .chart .media_result .data .info .rating:before {
|
||||
content: "\e031";
|
||||
font-family: 'Elusive-Icons';
|
||||
font-size: 14px;
|
||||
margin: 0 5px 0 0;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.charts .chart .media_result .data .info .genres {
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.charts .chart .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;
|
||||
}
|
||||
.charts .chart .media_result .data:before {
|
||||
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;
|
||||
}
|
||||
|
||||
.charts .chart .media_result .data .info .plot.full {
|
||||
top: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.charts .chart .media_result .data {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.charts .chart .media_result .options {
|
||||
left: 150px;
|
||||
}
|
||||
.charts .chart .media_result .options select[name=title] { width: 100%; }
|
||||
.charts .chart .media_result .options select[name=profile] { width: 100%; }
|
||||
.charts .chart .media_result .options select[name=category] { width: 100%; }
|
||||
|
||||
.charts .chart .media_result .button {
|
||||
position: absolute;
|
||||
margin: 2px 0 0 0;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
}
|
||||
|
||||
|
||||
.charts .chart .media_result .thumbnail {
|
||||
width: 100px;
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
}
|
||||
|
||||
.charts .chart .media_result .chart_number {
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
font: bold 2em/1em Helvetica, Sans-Serif;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
border-left: 8px solid transparent;
|
||||
}
|
||||
|
||||
.charts .chart .media_result.chart_in_wanted .chart_number {
|
||||
border-color: rgba(0, 255, 40, 0.3);
|
||||
}
|
||||
|
||||
.charts .chart .media_result.chart_in_library .chart_number {
|
||||
border-color: rgba(0, 202, 32, 0.3);
|
||||
}
|
||||
|
||||
|
||||
.charts .chart .media_result .actions {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: none;
|
||||
width: 90px;
|
||||
}
|
||||
.charts .chart .media_result:hover .actions {
|
||||
display: block;
|
||||
}
|
||||
.charts .chart .media_result:hover h2 .title {
|
||||
opacity: 0;
|
||||
}
|
||||
.charts .chart .media_result .data.open .actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.charts .chart .media_result .actions a {
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
.toggle_menu {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.toggle_menu a {
|
||||
display: block;
|
||||
width: 50%;
|
||||
float: left;
|
||||
color: rgba(255,255,255,.6);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.0666667);
|
||||
}
|
||||
|
||||
.toggle_menu a:hover {
|
||||
border-color: #047792;
|
||||
border-width: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.toggle_menu a.active {
|
||||
border-bottom: 4px solid #04bce6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.toggle_menu a:last-child {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.toggle_menu h2 {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
var Charts = 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.charts').adopt(
|
||||
self.el_no_charts_enabled = new Element('p.no_charts_enabled', {
|
||||
'html': 'Hey, it looks like you have no charts enabled at the moment. If you\'d like some great movie suggestions you can go to <a href="' + App.createUrl('settings/display') + '">settings</a> and turn on some charts of your choice.'
|
||||
}),
|
||||
self.el_refresh_container = new Element('div.refresh').adopt(
|
||||
self.el_refresh_link = new Element('a.refresh.icon2', {
|
||||
'href': '#',
|
||||
'events': {
|
||||
'click': function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
self.el.getElements('.chart').destroy();
|
||||
self.el_refreshing_text.show();
|
||||
self.el_refresh_link.hide();
|
||||
|
||||
self.api_request = Api.request('charts.view', {
|
||||
'data': { 'force_update': 1 },
|
||||
'onComplete': self.fill.bind(self)
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.el_refreshing_text = new Element('span.refreshing', {
|
||||
'text': 'Refreshing charts...'
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if( Cookie.read('suggestions_charts_menu_selected') === 'charts')
|
||||
self.el.show();
|
||||
else
|
||||
self.el.hide();
|
||||
|
||||
self.api_request = Api.request('charts.view', {
|
||||
'onComplete': self.fill.bind(self)
|
||||
});
|
||||
|
||||
self.fireEvent.delay(0, self, 'created');
|
||||
|
||||
},
|
||||
|
||||
fill: function(json){
|
||||
|
||||
var self = this;
|
||||
|
||||
self.el_refreshing_text.hide();
|
||||
self.el_refresh_link.show();
|
||||
|
||||
if(!json || json.count == 0){
|
||||
self.el_no_charts_enabled.show();
|
||||
self.el_refresh_link.show();
|
||||
self.el_refreshing_text.hide();
|
||||
}
|
||||
else {
|
||||
self.el_no_charts_enabled.hide();
|
||||
|
||||
json.charts.sort(function(a, b) {
|
||||
return a.order - b.order;
|
||||
});
|
||||
|
||||
Object.each(json.charts, function(chart){
|
||||
|
||||
var c = new Element('div.chart.tiny_scroll').grab(
|
||||
new Element('h3').grab( new Element('a', {
|
||||
'text': chart.name,
|
||||
'href': chart.url
|
||||
}))
|
||||
);
|
||||
|
||||
var it = 1;
|
||||
|
||||
Object.each(chart.list, function(movie){
|
||||
|
||||
var m = new Block.Search.MovieItem(movie, {
|
||||
'onAdded': function(){
|
||||
self.afterAdded(m, movie)
|
||||
}
|
||||
});
|
||||
|
||||
var in_database_class = (chart.hide_wanted && movie.in_wanted) ? 'hidden' : (movie.in_wanted ? 'chart_in_wanted' : ((chart.hide_library && movie.in_library) ? 'hidden': (movie.in_library ? 'chart_in_library' : ''))),
|
||||
in_database_title = movie.in_wanted ? 'Movie in wanted list' : (movie.in_library ? 'Movie in library' : '');
|
||||
|
||||
m.el
|
||||
.addClass(in_database_class)
|
||||
.grab(
|
||||
new Element('div.chart_number', {
|
||||
'text': it++,
|
||||
'title': in_database_title
|
||||
})
|
||||
);
|
||||
|
||||
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
|
||||
}))
|
||||
)
|
||||
);
|
||||
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 && 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(c);
|
||||
|
||||
});
|
||||
|
||||
c.inject(self.el);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
self.fireEvent('loaded');
|
||||
|
||||
},
|
||||
|
||||
afterAdded: function(m){
|
||||
|
||||
$(m).getElement('div.chart_number')
|
||||
.addClass('chart_in_wanted')
|
||||
.set('title', 'Movie in wanted list');
|
||||
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.variable import getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.library.base import LibraryBase
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'MovieLibraryPlugin'
|
||||
|
||||
|
||||
class MovieLibraryPlugin(LibraryBase):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('library.query', self.query)
|
||||
|
||||
def query(self, media, first = True, include_year = True, **kwargs):
|
||||
if media.get('type') != 'movie':
|
||||
return
|
||||
|
||||
default_title = getTitle(media)
|
||||
titles = media['info'].get('titles', [])
|
||||
titles.insert(0, default_title)
|
||||
|
||||
# Add year identifier to titles
|
||||
if include_year:
|
||||
titles = [title + (' %s' % str(media['info']['year'])) for title in titles]
|
||||
|
||||
if first:
|
||||
return titles[0] if titles else None
|
||||
|
||||
return titles
|
||||
6
couchpotato/core/media/movie/library/movie/__init__.py
Normal file
6
couchpotato/core/media/movie/library/movie/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import MovieLibraryPlugin
|
||||
|
||||
def start():
|
||||
return MovieLibraryPlugin()
|
||||
|
||||
config = []
|
||||
194
couchpotato/core/media/movie/library/movie/main.py
Normal file
194
couchpotato/core/media/movie/library/movie/main.py
Normal file
@@ -0,0 +1,194 @@
|
||||
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.settings.model import Library, LibraryTitle, File
|
||||
from string import ascii_letters
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
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
|
||||
)
|
||||
|
||||
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.movie', 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(Library).filter_by(identifier = identifier).first()
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
library_dict = None
|
||||
if library:
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
|
||||
do_update = True
|
||||
|
||||
info = fireEvent('movie.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 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.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):
|
||||
|
||||
db = get_session()
|
||||
library = db.query(Library).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 dates.get('expires', 0) > time.time() + (604800 * 4)) 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
|
||||
|
||||
|
||||
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,158 +0,0 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato import fireEvent
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.movie.providers.automation.base import Automation
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Bluray'
|
||||
|
||||
|
||||
class Bluray(Automation, RSS):
|
||||
|
||||
interval = 1800
|
||||
rss_url = 'http://www.blu-ray.com/rss/newreleasesfeed.xml'
|
||||
backlog_url = 'http://www.blu-ray.com/movies/movies.php?show=newreleases&page=%s'
|
||||
display_url = 'http://www.blu-ray.com/movies/movies.php?show=newreleases'
|
||||
chart_order = 1
|
||||
|
||||
def getIMDBids(self):
|
||||
|
||||
movies = []
|
||||
|
||||
if self.conf('backlog'):
|
||||
|
||||
page = 0
|
||||
while True:
|
||||
page += 1
|
||||
|
||||
url = self.backlog_url % page
|
||||
data = self.getHTMLData(url)
|
||||
soup = BeautifulSoup(data)
|
||||
|
||||
try:
|
||||
# Stop if the release year is before the minimal year
|
||||
page_year = soup.body.find_all('center')[3].table.tr.find_all('td', recursive = False)[3].h3.get_text().split(', ')[1]
|
||||
if tryInt(page_year) < self.getMinimal('year'):
|
||||
break
|
||||
|
||||
for table in soup.body.find_all('center')[3].table.tr.find_all('td', recursive = False)[3].find_all('table')[1:20]:
|
||||
name = table.h3.get_text().lower().split('blu-ray')[0].strip()
|
||||
year = table.small.get_text().split('|')[1].strip()
|
||||
|
||||
if not name.find('/') == -1: # make sure it is not a double movie release
|
||||
continue
|
||||
|
||||
if tryInt(year) < self.getMinimal('year'):
|
||||
continue
|
||||
|
||||
imdb = self.search(name, year)
|
||||
|
||||
if imdb:
|
||||
if self.isMinimalMovie(imdb):
|
||||
movies.append(imdb['imdb'])
|
||||
except:
|
||||
log.debug('Error loading page: %s', page)
|
||||
break
|
||||
|
||||
self.conf('backlog', value = False)
|
||||
|
||||
rss_movies = self.getRSSData(self.rss_url)
|
||||
|
||||
for movie in rss_movies:
|
||||
name = self.getTextElement(movie, 'title').lower().split('blu-ray')[0].strip('(').rstrip()
|
||||
year = self.getTextElement(movie, 'description').split('|')[1].strip('(').strip()
|
||||
|
||||
if not name.find('/') == -1: # make sure it is not a double movie release
|
||||
continue
|
||||
|
||||
if tryInt(year) < self.getMinimal('year'):
|
||||
continue
|
||||
|
||||
imdb = self.search(name, year)
|
||||
|
||||
if imdb:
|
||||
if self.isMinimalMovie(imdb):
|
||||
movies.append(imdb['imdb'])
|
||||
|
||||
return movies
|
||||
|
||||
|
||||
def getChartList(self):
|
||||
# Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id)
|
||||
movie_list = {'name': 'Blu-ray.com - New Releases', 'url': self.display_url, 'order': self.chart_order, 'list': []}
|
||||
movie_ids = []
|
||||
max_items = int(self.conf('max_items', section='charts', default=5))
|
||||
rss_movies = self.getRSSData(self.rss_url)
|
||||
|
||||
for movie in rss_movies:
|
||||
name = self.getTextElement(movie, 'title').lower().split('blu-ray')[0].strip('(').rstrip()
|
||||
year = self.getTextElement(movie, 'description').split('|')[1].strip('(').strip()
|
||||
|
||||
if not name.find('/') == -1: # make sure it is not a double movie release
|
||||
continue
|
||||
|
||||
movie = self.search(name, year)
|
||||
|
||||
if movie:
|
||||
|
||||
if movie.get('imdb') in movie_ids:
|
||||
continue
|
||||
|
||||
is_movie = fireEvent('movie.is_movie', identifier = movie.get('imdb'), single = True)
|
||||
if not is_movie:
|
||||
continue
|
||||
|
||||
movie_ids.append(movie.get('imdb'))
|
||||
movie_list['list'].append( movie )
|
||||
if len(movie_list['list']) >= max_items:
|
||||
break
|
||||
|
||||
if not movie_list['list']:
|
||||
return
|
||||
|
||||
return [ movie_list ]
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'bluray',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'automation',
|
||||
'list': 'automation_providers',
|
||||
'name': 'bluray_automation',
|
||||
'label': 'Blu-ray.com',
|
||||
'description': 'Imports movies from blu-ray.com.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'automation_enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'backlog',
|
||||
'advanced': True,
|
||||
'description': 'Parses the history until the minimum movie year is reached. (Will be disabled once it has completed)',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'tab': 'display',
|
||||
'list': 'charts_providers',
|
||||
'name': 'bluray_charts_display',
|
||||
'label': 'Blu-ray.com',
|
||||
'description': 'Display <a href="http://www.blu-ray.com/movies/movies.php?show=newreleases">new releases</a> from Blu-ray.com',
|
||||
'options': [
|
||||
{
|
||||
'name': 'chart_display_enabled',
|
||||
'default': True,
|
||||
'type': 'enabler',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user