Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96472a9a8f | ||
|
|
27252561e2 | ||
|
|
c9e732651f | ||
|
|
7849e7170d | ||
|
|
087894eb4e | ||
|
|
25f1b8c7a7 | ||
|
|
e71da1f14d | ||
|
|
938b14ba18 | ||
|
|
d6522d8f38 | ||
|
|
78eab890e7 | ||
|
|
1a56191f83 | ||
|
|
41c0f34d95 | ||
|
|
37bf205d7a | ||
|
|
aa1fa3eb9a | ||
|
|
0e2f8a612c | ||
|
|
465e7b2abc | ||
|
|
578fb45785 | ||
|
|
96995bbbe5 | ||
|
|
4cfdafebbc | ||
|
|
b97acb8ef5 | ||
|
|
d68d2dfdb6 | ||
|
|
39b269a454 | ||
|
|
ac081d3e10 | ||
|
|
5d4efb60cf | ||
|
|
cc408b980c | ||
|
|
59590b3ac9 | ||
|
|
ff759dacf3 | ||
|
|
a328e44130 | ||
|
|
7924cac5f9 | ||
|
|
1cef3b0c93 | ||
|
|
3cd59edc8b | ||
|
|
0d624af01d | ||
|
|
a09132570c | ||
|
|
ee3fc38432 | ||
|
|
dbf0192c8e | ||
|
|
6962cfc3f5 | ||
|
|
e096ec3b5b | ||
|
|
b30a74ae0c | ||
|
|
978eeb16c9 | ||
|
|
e5c9d91657 | ||
|
|
fa81c3a07a | ||
|
|
9cdd520d41 | ||
|
|
55d7898771 | ||
|
|
b8256bef97 | ||
|
|
5be9dc0b4a | ||
|
|
7d0be0cefb | ||
|
|
f7ce1edb13 | ||
|
|
5ad9280b60 | ||
|
|
2b353f1b20 | ||
|
|
75ab90b87b | ||
|
|
0219296120 | ||
|
|
20032b3a31 | ||
|
|
ea9e9a8c90 | ||
|
|
f7b0ee145b | ||
|
|
cc866738ee | ||
|
|
eadccf6e33 | ||
|
|
b70b66e567 | ||
|
|
5b6792dc20 | ||
|
|
f498e7343a | ||
|
|
6962f441e6 | ||
|
|
1def62b1b1 | ||
|
|
a4a4a6a185 | ||
|
|
d4c9469c1a | ||
|
|
3e2d4c5d7b | ||
|
|
d03f711d69 | ||
|
|
44dd8d9b96 | ||
|
|
549a3be0d8 | ||
|
|
1bb2edf8ec | ||
|
|
84c6f36315 |
+6
-7
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
from __future__ import print_function
|
|
||||||
from logging import handlers
|
from logging import handlers
|
||||||
from os.path import dirname
|
from os.path import dirname
|
||||||
import logging
|
import logging
|
||||||
@@ -63,6 +62,7 @@ class Loader(object):
|
|||||||
self.log.logger.addHandler(hdlr)
|
self.log.logger.addHandler(hdlr)
|
||||||
|
|
||||||
def addSignals(self):
|
def addSignals(self):
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, self.onExit)
|
signal.signal(signal.SIGINT, self.onExit)
|
||||||
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ class Loader(object):
|
|||||||
|
|
||||||
def onExit(self, signal, frame):
|
def onExit(self, signal, frame):
|
||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
fireEvent('app.shutdown', single = True)
|
fireEvent('app.crappy_shutdown', single = True)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|
||||||
@@ -133,15 +133,14 @@ if __name__ == '__main__':
|
|||||||
pass
|
pass
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
raise
|
raise
|
||||||
except socket.error as e:
|
except socket.error as (nr, msg):
|
||||||
# log when socket receives SIGINT, but continue.
|
# log when socket receives SIGINT, but continue.
|
||||||
# previous code would have skipped over other types of IO errors too.
|
# previous code would have skipped over other types of IO errors too.
|
||||||
nr, msg = e
|
|
||||||
if nr != 4:
|
if nr != 4:
|
||||||
try:
|
try:
|
||||||
l.log.critical(traceback.format_exc())
|
l.log.critical(traceback.format_exc())
|
||||||
except:
|
except:
|
||||||
print(traceback.format_exc())
|
print traceback.format_exc()
|
||||||
raise
|
raise
|
||||||
except:
|
except:
|
||||||
try:
|
try:
|
||||||
@@ -150,7 +149,7 @@ if __name__ == '__main__':
|
|||||||
if l:
|
if l:
|
||||||
l.log.critical(traceback.format_exc())
|
l.log.critical(traceback.format_exc())
|
||||||
else:
|
else:
|
||||||
print(traceback.format_exc())
|
print traceback.format_exc()
|
||||||
except:
|
except:
|
||||||
print(traceback.format_exc())
|
print traceback.format_exc()
|
||||||
raise
|
raise
|
||||||
|
|||||||
+230
@@ -0,0 +1,230 @@
|
|||||||
|
from esky.util import appdir_from_executable #@UnresolvedImport
|
||||||
|
from threading import Thread
|
||||||
|
from wx.lib.softwareupdate import SoftwareUpdate
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import webbrowser
|
||||||
|
import wx
|
||||||
|
|
||||||
|
# Include proper dirs
|
||||||
|
if hasattr(sys, 'frozen'):
|
||||||
|
import libs
|
||||||
|
base_path = os.path.dirname(os.path.dirname(os.path.abspath(libs.__file__)))
|
||||||
|
else:
|
||||||
|
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
lib_dir = os.path.join(base_path, 'libs')
|
||||||
|
|
||||||
|
sys.path.insert(0, base_path)
|
||||||
|
sys.path.insert(0, lib_dir)
|
||||||
|
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
|
||||||
|
class TaskBarIcon(wx.TaskBarIcon):
|
||||||
|
|
||||||
|
TBMENU_OPEN = wx.NewId()
|
||||||
|
TBMENU_SETTINGS = wx.NewId()
|
||||||
|
TBMENU_EXIT = wx.ID_EXIT
|
||||||
|
|
||||||
|
closed = False
|
||||||
|
menu = False
|
||||||
|
enabled = False
|
||||||
|
|
||||||
|
def __init__(self, frame):
|
||||||
|
wx.TaskBarIcon.__init__(self)
|
||||||
|
self.frame = frame
|
||||||
|
|
||||||
|
icon = wx.Icon('icon.png', wx.BITMAP_TYPE_PNG)
|
||||||
|
self.SetIcon(icon)
|
||||||
|
|
||||||
|
self.Bind(wx.EVT_TASKBAR_LEFT_UP, self.OnTaskBarClick)
|
||||||
|
self.Bind(wx.EVT_TASKBAR_RIGHT_UP, self.OnTaskBarClick)
|
||||||
|
|
||||||
|
self.Bind(wx.EVT_MENU, self.onOpen, id = self.TBMENU_OPEN)
|
||||||
|
self.Bind(wx.EVT_MENU, self.onSettings, id = self.TBMENU_SETTINGS)
|
||||||
|
self.Bind(wx.EVT_MENU, self.onTaskBarClose, id = self.TBMENU_EXIT)
|
||||||
|
|
||||||
|
def OnTaskBarClick(self, evt):
|
||||||
|
menu = self.CreatePopupMenu()
|
||||||
|
self.PopupMenu(menu)
|
||||||
|
menu.Destroy()
|
||||||
|
|
||||||
|
def enable(self):
|
||||||
|
self.enabled = True
|
||||||
|
|
||||||
|
if self.menu:
|
||||||
|
self.open_menu.Enable(True)
|
||||||
|
self.setting_menu.Enable(True)
|
||||||
|
|
||||||
|
self.open_menu.SetText('Open')
|
||||||
|
|
||||||
|
def CreatePopupMenu(self):
|
||||||
|
|
||||||
|
if not self.menu:
|
||||||
|
self.menu = wx.Menu()
|
||||||
|
self.open_menu = self.menu.Append(self.TBMENU_OPEN, 'Open')
|
||||||
|
self.setting_menu = self.menu.Append(self.TBMENU_SETTINGS, 'About')
|
||||||
|
self.exit_menu = self.menu.Append(self.TBMENU_EXIT, 'Quit')
|
||||||
|
|
||||||
|
if not self.enabled:
|
||||||
|
self.open_menu.Enable(False)
|
||||||
|
self.setting_menu.Enable(False)
|
||||||
|
|
||||||
|
self.open_menu.SetText('Loading...')
|
||||||
|
|
||||||
|
return self.menu
|
||||||
|
|
||||||
|
def onOpen(self, event):
|
||||||
|
url = self.frame.parent.getSetting('base_url')
|
||||||
|
webbrowser.open(url)
|
||||||
|
|
||||||
|
def onSettings(self, event):
|
||||||
|
url = self.frame.parent.getSetting('base_url') + '/settings/'
|
||||||
|
webbrowser.open(url)
|
||||||
|
|
||||||
|
def onTaskBarClose(self, evt):
|
||||||
|
if self.closed:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.closed = True
|
||||||
|
|
||||||
|
self.RemoveIcon()
|
||||||
|
wx.CallAfter(self.frame.Close)
|
||||||
|
|
||||||
|
|
||||||
|
def makeIcon(self, img):
|
||||||
|
if "wxMSW" in wx.PlatformInfo:
|
||||||
|
img = img.Scale(16, 16)
|
||||||
|
elif "wxGTK" in wx.PlatformInfo:
|
||||||
|
img = img.Scale(22, 22)
|
||||||
|
|
||||||
|
icon = wx.IconFromBitmap(img.CopyFromBitmap())
|
||||||
|
return icon
|
||||||
|
|
||||||
|
|
||||||
|
class MainFrame(wx.Frame):
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
wx.Frame.__init__(self, None, style = wx.FRAME_NO_TASKBAR)
|
||||||
|
|
||||||
|
self.parent = parent
|
||||||
|
self.tbicon = TaskBarIcon(self)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerThread(Thread):
|
||||||
|
|
||||||
|
def __init__(self, desktop):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.daemon = True
|
||||||
|
self._desktop = desktop
|
||||||
|
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
# Get options via arg
|
||||||
|
from couchpotato.runner import getOptions
|
||||||
|
args = ['--quiet']
|
||||||
|
self.options = getOptions(base_path, args)
|
||||||
|
|
||||||
|
# Load settings
|
||||||
|
settings = Env.get('settings')
|
||||||
|
settings.setFile(self.options.config_file)
|
||||||
|
|
||||||
|
# Create data dir if needed
|
||||||
|
self.data_dir = os.path.expanduser(Env.setting('data_dir'))
|
||||||
|
if self.data_dir == '':
|
||||||
|
from couchpotato.core.helpers.variable import getDataDir
|
||||||
|
self.data_dir = getDataDir()
|
||||||
|
|
||||||
|
if not os.path.isdir(self.data_dir):
|
||||||
|
os.makedirs(self.data_dir)
|
||||||
|
|
||||||
|
# Create logging dir
|
||||||
|
self.log_dir = os.path.join(self.data_dir, 'logs');
|
||||||
|
if not os.path.isdir(self.log_dir):
|
||||||
|
os.mkdir(self.log_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from couchpotato.runner import runCouchPotato
|
||||||
|
runCouchPotato(self.options, base_path, args, data_dir = self.data_dir, log_dir = self.log_dir, Env = Env, desktop = self._desktop)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._desktop.frame.Close()
|
||||||
|
|
||||||
|
|
||||||
|
class CouchPotatoApp(wx.App, SoftwareUpdate):
|
||||||
|
|
||||||
|
settings = {}
|
||||||
|
events = {}
|
||||||
|
restart = False
|
||||||
|
closing = False
|
||||||
|
|
||||||
|
def OnInit(self):
|
||||||
|
|
||||||
|
# Updater
|
||||||
|
base_url = 'http://couchpota.to/updates/'
|
||||||
|
self.InitUpdates(base_url, base_url + 'changelog.html',
|
||||||
|
icon = wx.Icon('icon.png'))
|
||||||
|
|
||||||
|
self.frame = MainFrame(self)
|
||||||
|
self.frame.Bind(wx.EVT_CLOSE, self.onClose)
|
||||||
|
|
||||||
|
# CouchPotato thread
|
||||||
|
self.worker = WorkerThread(self)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def onAppLoad(self):
|
||||||
|
self.frame.tbicon.enable()
|
||||||
|
|
||||||
|
def setSettings(self, settings = {}):
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
|
def getSetting(self, name):
|
||||||
|
return self.settings.get(name)
|
||||||
|
|
||||||
|
def addEvents(self, events = {}):
|
||||||
|
for name in events.iterkeys():
|
||||||
|
self.events[name] = events[name]
|
||||||
|
|
||||||
|
def onClose(self, event):
|
||||||
|
|
||||||
|
if not self.closing:
|
||||||
|
self.closing = True
|
||||||
|
self.frame.tbicon.onTaskBarClose(event)
|
||||||
|
|
||||||
|
onClose = self.events.get('onClose')
|
||||||
|
onClose(event)
|
||||||
|
|
||||||
|
def afterShutdown(self, restart = False):
|
||||||
|
self.frame.Destroy()
|
||||||
|
self.restart = restart
|
||||||
|
self.ExitMainLoop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
app = CouchPotatoApp(redirect = False)
|
||||||
|
app.MainLoop()
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if app.restart:
|
||||||
|
|
||||||
|
def appexe_from_executable(exepath):
|
||||||
|
appdir = appdir_from_executable(exepath)
|
||||||
|
exename = os.path.basename(exepath)
|
||||||
|
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
if os.path.isdir(os.path.join(appdir, "Contents", "MacOS")):
|
||||||
|
return os.path.join(appdir, "Contents", "MacOS", exename)
|
||||||
|
|
||||||
|
return os.path.join(appdir, exename)
|
||||||
|
|
||||||
|
exe = appexe_from_executable(sys.executable)
|
||||||
|
os.chdir(os.path.dirname(exe))
|
||||||
|
|
||||||
|
os.execv(exe, [exe] + sys.argv[1:])
|
||||||
@@ -1,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.
|
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
|
## 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:
|
Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for more details:
|
||||||
|
|
||||||
@@ -40,23 +40,3 @@ Linux (ubuntu / debian):
|
|||||||
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
||||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
||||||
* Open your browser and go to: `http://localhost:5050/`
|
* Open your browser and go to: `http://localhost:5050/`
|
||||||
|
|
||||||
|
|
||||||
FreeBSD :
|
|
||||||
|
|
||||||
* Update your ports tree `sudo portsnap fetch update`
|
|
||||||
* Install Python 2.6+ [lang/python](http://www.freshports.org/lang/python) with `cd /usr/ports/lang/python; sudo make install clean`
|
|
||||||
* Install port [databases/py-sqlite3](http://www.freshports.org/databases/py-sqlite3) with `cd /usr/ports/databases/py-sqlite3; sudo make install clean`
|
|
||||||
* Add a symlink to 'python2' `sudo ln -s /usr/local/bin/python /usr/local/bin/python2`
|
|
||||||
* Install port [ftp/libcurl](http://www.freshports.org/ftp/libcurl) with `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
|
|
||||||
* Install port [ftp/curl](http://www.freshports.org/ftp/bcurl), deselect 'Asynchronous DNS resolution via c-ares' when prompted as part of config `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
|
|
||||||
* Install port [textproc/docbook-xml-450](http://www.freshports.org/textproc/docbook-xml-450) with `cd /usr/ports/textproc/docbook-xml-450; sudo make install clean`
|
|
||||||
* Install port [GIT](http://git-scm.com/) with `cd /usr/ports/devel/git; sudo make install clean`
|
|
||||||
* 'cd' to the folder of your choosing.
|
|
||||||
* 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`
|
|
||||||
* 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/`
|
|
||||||
|
|||||||
+11
-33
@@ -1,36 +1,14 @@
|
|||||||
# Contributing to CouchPotatoServer
|
#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem!
|
||||||
|
|
||||||
1. [Contributing](#contributing)
|
##Just make sure you think of the following things:
|
||||||
2. [Submitting an Issue](#issues)
|
|
||||||
3. [Submitting a Pull Request](#pull-requests)
|
|
||||||
|
|
||||||
## Contributing
|
* Search through the existing (and closed) issues first. See if you can get your answer there.
|
||||||
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.
|
* Double check the result manually, because it could be an external issue.
|
||||||
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!
|
* Post logs! Without seeing what is going on, I can't reproduce the error.
|
||||||
If you want to contribute your own work, please read the [guidelines](#pull-requests) for submitting a pull request.
|
* What are you settings for the specific problem
|
||||||
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.
|
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby)
|
||||||
|
* Give me a short step by step of how to reproduce
|
||||||
|
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
|
||||||
|
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
|
||||||
|
|
||||||
## Issues
|
**If I don't get enough info, the change of the issue getting closed is a lot bigger ;)**
|
||||||
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!
|
|
||||||
* 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 then when you use CP on OSX 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 etc..) with pre-made packages, make sure these are set up to use our source repository (RuudBurger/CouchPotatoServer) and nothing else!!
|
|
||||||
|
|
||||||
The more relevant information you can provide, the more likely it is the issue will be resolved rather than closed.
|
|
||||||
|
|
||||||
## 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).
|
|
||||||
* 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?
|
|
||||||
+54
-120
@@ -1,149 +1,83 @@
|
|||||||
from couchpotato.api import api_docs, api_docs_missing, api
|
from couchpotato.api import api_docs, api_docs_missing
|
||||||
|
from couchpotato.core.auth import requires_auth
|
||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
from couchpotato.core.helpers.variable import md5, tryInt
|
from couchpotato.core.helpers.request import getParams, jsonified
|
||||||
|
from couchpotato.core.helpers.variable import md5
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from tornado import template
|
from flask.app import Flask
|
||||||
from tornado.web import RequestHandler, authenticated
|
from flask.blueprints import Blueprint
|
||||||
|
from flask.globals import request
|
||||||
|
from flask.helpers import url_for
|
||||||
|
from flask.templating import render_template
|
||||||
|
from sqlalchemy.engine import create_engine
|
||||||
|
from sqlalchemy.orm import scoped_session
|
||||||
|
from sqlalchemy.orm.session import sessionmaker
|
||||||
|
from werkzeug.utils import redirect
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
views = {}
|
app = Flask(__name__, static_folder = 'nope')
|
||||||
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
|
web = Blueprint('web', __name__)
|
||||||
|
|
||||||
|
|
||||||
class BaseHandler(RequestHandler):
|
def get_session(engine = None):
|
||||||
|
return Env.getSession(engine)
|
||||||
def get_current_user(self):
|
|
||||||
username = Env.setting('username')
|
|
||||||
password = Env.setting('password')
|
|
||||||
|
|
||||||
if username and password:
|
|
||||||
return self.get_secure_cookie('user')
|
|
||||||
else: # Login when no username or password are set
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# Main web handler
|
|
||||||
class WebHandler(BaseHandler):
|
|
||||||
|
|
||||||
@authenticated
|
|
||||||
def get(self, route, *args, **kwargs):
|
|
||||||
route = route.strip('/')
|
|
||||||
if not views.get(route):
|
|
||||||
page_not_found(self)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.write(views[route]())
|
|
||||||
except:
|
|
||||||
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
|
|
||||||
self.write({'success': False, 'error': 'Failed returning results'})
|
|
||||||
|
|
||||||
|
|
||||||
def addView(route, func, static = False):
|
def addView(route, func, static = False):
|
||||||
views[route] = func
|
web.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func)
|
||||||
|
|
||||||
|
""" Web view """
|
||||||
def get_db():
|
@web.route('/')
|
||||||
return Env.get('db')
|
@requires_auth
|
||||||
|
|
||||||
|
|
||||||
# Web view
|
|
||||||
def index():
|
def index():
|
||||||
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
|
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
|
||||||
addView('', index)
|
|
||||||
|
|
||||||
|
""" Api view """
|
||||||
# API docs
|
@web.route('docs/')
|
||||||
|
@requires_auth
|
||||||
def apiDocs():
|
def apiDocs():
|
||||||
routes = list(api.keys())
|
from couchpotato import app
|
||||||
|
routes = []
|
||||||
|
for route, x in sorted(app.view_functions.iteritems()):
|
||||||
|
if route[0:4] == 'api.':
|
||||||
|
routes += [route[4:].replace('::', '.')]
|
||||||
|
|
||||||
if api_docs.get(''):
|
if api_docs.get(''):
|
||||||
del api_docs['']
|
del api_docs['']
|
||||||
del api_docs_missing['']
|
del api_docs_missing['']
|
||||||
|
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing))
|
||||||
|
|
||||||
return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env)
|
@web.route('getkey/')
|
||||||
|
def getApiKey():
|
||||||
|
|
||||||
addView('docs', apiDocs)
|
api = None
|
||||||
|
params = getParams()
|
||||||
|
username = Env.setting('username')
|
||||||
|
password = Env.setting('password')
|
||||||
|
|
||||||
|
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password):
|
||||||
|
api = Env.setting('api_key')
|
||||||
|
|
||||||
# Database debug manager
|
return jsonified({
|
||||||
def databaseManage():
|
'success': api is not None,
|
||||||
return template_loader.load('database.html').generate(fireEvent = fireEvent, Env = Env)
|
'api_key': api
|
||||||
|
})
|
||||||
|
|
||||||
addView('database', databaseManage)
|
@app.errorhandler(404)
|
||||||
|
def page_not_found(error):
|
||||||
|
index_url = url_for('web.index')
|
||||||
# Make non basic auth option to get api key
|
url = request.path[len(index_url):]
|
||||||
class KeyHandler(RequestHandler):
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
api_key = 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')
|
|
||||||
|
|
||||||
self.write({
|
|
||||||
'success': api_key is not None,
|
|
||||||
'api_key': api_key
|
|
||||||
})
|
|
||||||
except:
|
|
||||||
log.error('Failed doing key request: %s', (traceback.format_exc()))
|
|
||||||
self.write({'success': False, 'error': 'Failed returning results'})
|
|
||||||
|
|
||||||
|
|
||||||
class LoginHandler(BaseHandler):
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
|
|
||||||
if self.get_current_user():
|
|
||||||
self.redirect(Env.get('web_base'))
|
|
||||||
else:
|
|
||||||
self.write(template_loader.load('login.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env))
|
|
||||||
|
|
||||||
def post(self, *args, **kwargs):
|
|
||||||
|
|
||||||
api_key = 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')
|
|
||||||
|
|
||||||
if api_key:
|
|
||||||
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.redirect(Env.get('web_base'))
|
|
||||||
|
|
||||||
|
|
||||||
class LogoutHandler(BaseHandler):
|
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
self.clear_cookie('user')
|
|
||||||
self.redirect('%slogin/' % Env.get('web_base'))
|
|
||||||
|
|
||||||
|
|
||||||
def page_not_found(rh):
|
|
||||||
index_url = Env.get('web_base')
|
|
||||||
url = rh.request.uri[len(index_url):]
|
|
||||||
|
|
||||||
if url[:3] != 'api':
|
if url[:3] != 'api':
|
||||||
r = index_url + '#' + url.lstrip('/')
|
if request.path != '/':
|
||||||
rh.redirect(r)
|
r = request.url.replace(request.path, index_url + '#' + url)
|
||||||
|
else:
|
||||||
|
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
|
||||||
|
return redirect(r)
|
||||||
else:
|
else:
|
||||||
if not Env.get('dev'):
|
time.sleep(0.1)
|
||||||
time.sleep(0.1)
|
return 'Wrong API key used', 404
|
||||||
|
|
||||||
rh.set_status(404)
|
|
||||||
rh.write('Wrong API key used')
|
|
||||||
|
|||||||
+28
-116
@@ -1,71 +1,47 @@
|
|||||||
from functools import wraps
|
from flask.blueprints import Blueprint
|
||||||
from threading import Thread
|
from flask.helpers import url_for
|
||||||
import json
|
from tornado.ioloop import IOLoop
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
import urllib
|
|
||||||
|
|
||||||
from couchpotato.core.helpers.request import getParams
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from tornado.gen import coroutine
|
|
||||||
from tornado.web import RequestHandler, asynchronous
|
from tornado.web import RequestHandler, asynchronous
|
||||||
import tornado
|
from werkzeug.utils import redirect
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
api = {}
|
|
||||||
api_locks = {}
|
|
||||||
api_nonblock = {}
|
|
||||||
|
|
||||||
|
api = Blueprint('api', __name__)
|
||||||
api_docs = {}
|
api_docs = {}
|
||||||
api_docs_missing = []
|
api_docs_missing = []
|
||||||
|
api_nonblock = {}
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# NonBlock API handler
|
|
||||||
class NonBlockHandler(RequestHandler):
|
class NonBlockHandler(RequestHandler):
|
||||||
|
stoppers = []
|
||||||
stopper = None
|
|
||||||
|
|
||||||
@asynchronous
|
@asynchronous
|
||||||
def get(self, route, *args, **kwargs):
|
def get(self, route):
|
||||||
route = route.strip('/')
|
cls = NonBlockHandler
|
||||||
start, stop = api_nonblock[route]
|
start, stop = api_nonblock[route]
|
||||||
self.stopper = stop
|
cls.stoppers.append(stop)
|
||||||
|
|
||||||
start(self.onNewMessage, last_id = self.get_argument('last_id', None))
|
start(self.onNewMessage, last_id = self.get_argument("last_id", None))
|
||||||
|
|
||||||
def onNewMessage(self, response):
|
def onNewMessage(self, response):
|
||||||
if self.request.connection.stream.closed():
|
if self.request.connection.stream.closed():
|
||||||
self.on_connection_close()
|
|
||||||
return
|
return
|
||||||
|
self.finish(response)
|
||||||
try:
|
|
||||||
self.finish(response)
|
|
||||||
except:
|
|
||||||
log.debug('Failed doing nonblock request, probably already closed: %s', (traceback.format_exc()))
|
|
||||||
try: self.finish({'success': False, 'error': 'Failed returning results'})
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
def on_connection_close(self):
|
def on_connection_close(self):
|
||||||
|
cls = NonBlockHandler
|
||||||
|
|
||||||
if self.stopper:
|
for stop in cls.stoppers:
|
||||||
self.stopper(self.onNewMessage)
|
stop(self.onNewMessage)
|
||||||
|
|
||||||
self.stopper = None
|
cls.stoppers = []
|
||||||
|
|
||||||
|
|
||||||
|
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||||
|
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
|
||||||
|
if docs:
|
||||||
|
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
||||||
|
else:
|
||||||
|
api_docs_missing.append(route)
|
||||||
|
|
||||||
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||||
api_nonblock[route] = func_tuple
|
api_nonblock[route] = func_tuple
|
||||||
|
|
||||||
@@ -74,73 +50,9 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
|||||||
else:
|
else:
|
||||||
api_docs_missing.append(route)
|
api_docs_missing.append(route)
|
||||||
|
|
||||||
|
""" Api view """
|
||||||
|
def index():
|
||||||
|
index_url = url_for('web.index')
|
||||||
|
return redirect(index_url + 'docs/')
|
||||||
|
|
||||||
# Blocking API handler
|
addApiView('', index)
|
||||||
class ApiHandler(RequestHandler):
|
|
||||||
|
|
||||||
@coroutine
|
|
||||||
def get(self, route, *args, **kwargs):
|
|
||||||
route = route.strip('/')
|
|
||||||
if not api.get(route):
|
|
||||||
self.write('API call doesn\'t seem to exist')
|
|
||||||
return
|
|
||||||
|
|
||||||
api_locks[route].acquire()
|
|
||||||
|
|
||||||
try:
|
|
||||||
|
|
||||||
kwargs = {}
|
|
||||||
for x in self.request.arguments:
|
|
||||||
kwargs[x] = urllib.unquote(self.get_argument(x))
|
|
||||||
|
|
||||||
# Split array arguments
|
|
||||||
kwargs = getParams(kwargs)
|
|
||||||
kwargs['_request'] = self
|
|
||||||
|
|
||||||
# Remove t random string
|
|
||||||
try: del kwargs['t']
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
# Add async callback handler
|
|
||||||
@run_async
|
|
||||||
def run_handler(callback):
|
|
||||||
try:
|
|
||||||
res = api[route](**kwargs)
|
|
||||||
callback(res)
|
|
||||||
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()))
|
|
||||||
self.write({'success': False, 'error': 'Failed returning results'})
|
|
||||||
|
|
||||||
api_locks[route].release()
|
|
||||||
|
|
||||||
post = get
|
|
||||||
|
|
||||||
|
|
||||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
|
||||||
|
|
||||||
if static: func(route)
|
|
||||||
else:
|
|
||||||
api[route] = func
|
|
||||||
api_locks[route] = threading.Lock()
|
|
||||||
|
|
||||||
if docs:
|
|
||||||
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
|
||||||
else:
|
|
||||||
api_docs_missing.append(route)
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
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': 'host',
|
||||||
|
'advanced': True,
|
||||||
|
'default': '0.0.0.0',
|
||||||
|
'hidden': True,
|
||||||
|
'label': 'IP',
|
||||||
|
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'port',
|
||||||
|
'default': 5050,
|
||||||
|
'type': 'int',
|
||||||
|
'description': 'The port I should listen to.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'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': 'Disables some checks/downloads for faster reloading.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'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,11 @@
|
|||||||
|
from couchpotato.api import addApiView
|
||||||
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
|
from couchpotato.core.helpers.request import jsonified
|
||||||
|
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
|
from uuid import uuid4
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
@@ -6,19 +14,8 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
autoload = 'Core'
|
|
||||||
|
|
||||||
|
|
||||||
class Core(Plugin):
|
class Core(Plugin):
|
||||||
|
|
||||||
@@ -51,21 +48,15 @@ class Core(Plugin):
|
|||||||
addEvent('app.api_url', self.createApiUrl)
|
addEvent('app.api_url', self.createApiUrl)
|
||||||
addEvent('app.version', self.version)
|
addEvent('app.version', self.version)
|
||||||
addEvent('app.load', self.checkDataDir)
|
addEvent('app.load', self.checkDataDir)
|
||||||
addEvent('app.load', self.cleanUpFolders)
|
|
||||||
|
|
||||||
addEvent('setting.save.core.password', self.md5Password)
|
addEvent('setting.save.core.password', self.md5Password)
|
||||||
addEvent('setting.save.core.api_key', self.checkApikey)
|
addEvent('setting.save.core.api_key', self.checkApikey)
|
||||||
|
|
||||||
# Make sure we can close-down with ctrl+c properly
|
# Make sure we can close-down with ctrl+c properly
|
||||||
if not Env.get('desktop'):
|
self.signalHandler()
|
||||||
self.signalHandler()
|
|
||||||
|
|
||||||
# Set default urlopen timeout
|
|
||||||
import socket
|
|
||||||
socket.setdefaulttimeout(30)
|
|
||||||
|
|
||||||
def md5Password(self, value):
|
def md5Password(self, value):
|
||||||
return md5(value) if value else ''
|
return md5(value.encode(Env.get('encoding'))) if value else ''
|
||||||
|
|
||||||
def checkApikey(self, value):
|
def checkApikey(self, value):
|
||||||
return value if value and len(value) > 3 else uuid4().hex
|
return value if value and len(value) > 3 else uuid4().hex
|
||||||
@@ -76,31 +67,28 @@ class Core(Plugin):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def cleanUpFolders(self):
|
def available(self):
|
||||||
self.deleteEmptyFolder(Env.get('app_dir'), show_error = False)
|
return jsonified({
|
||||||
|
|
||||||
def available(self, **kwargs):
|
|
||||||
return {
|
|
||||||
'success': True
|
'success': True
|
||||||
}
|
})
|
||||||
|
|
||||||
def shutdown(self, **kwargs):
|
def shutdown(self):
|
||||||
if self.shutdown_started:
|
if self.shutdown_started:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def shutdown():
|
def shutdown():
|
||||||
self.initShutdown()
|
self.initShutdown()
|
||||||
IOLoop.current().add_callback(shutdown)
|
IOLoop.instance().add_callback(shutdown)
|
||||||
|
|
||||||
return 'shutdown'
|
return 'shutdown'
|
||||||
|
|
||||||
def restart(self, **kwargs):
|
def restart(self):
|
||||||
if self.shutdown_started:
|
if self.shutdown_started:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def restart():
|
def restart():
|
||||||
self.initShutdown(restart = True)
|
self.initShutdown(restart = True)
|
||||||
IOLoop.current().add_callback(restart)
|
IOLoop.instance().add_callback(restart)
|
||||||
|
|
||||||
return 'restarting'
|
return 'restarting'
|
||||||
|
|
||||||
@@ -125,7 +113,7 @@ class Core(Plugin):
|
|||||||
|
|
||||||
if len(still_running) == 0:
|
if len(still_running) == 0:
|
||||||
break
|
break
|
||||||
elif starttime < time.time() - 30: # Always force break after 30s wait
|
elif starttime < time.time() - 30: # Always force break after 30s wait
|
||||||
break
|
break
|
||||||
|
|
||||||
running = list(set(still_running) - set(self.ignore_restart))
|
running = list(set(still_running) - set(self.ignore_restart))
|
||||||
@@ -136,10 +124,10 @@ class Core(Plugin):
|
|||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
log.debug('Safe to shutdown/restart')
|
log.debug('Save to shutdown/restart')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
IOLoop.current().stop()
|
IOLoop.instance().stop()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
except:
|
except:
|
||||||
@@ -163,14 +151,14 @@ class Core(Plugin):
|
|||||||
|
|
||||||
def createBaseUrl(self):
|
def createBaseUrl(self):
|
||||||
host = Env.setting('host')
|
host = Env.setting('host')
|
||||||
if host == '0.0.0.0' or host == '':
|
if host == '0.0.0.0':
|
||||||
host = 'localhost'
|
host = 'localhost'
|
||||||
port = Env.setting('port')
|
port = Env.setting('port')
|
||||||
|
|
||||||
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), Env.get('web_base'))
|
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else '')
|
||||||
|
|
||||||
def createApiUrl(self):
|
def createApiUrl(self):
|
||||||
return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
return '%s/api/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
||||||
|
|
||||||
def version(self):
|
def version(self):
|
||||||
ver = fireEvent('updater.info', single = True)
|
ver = fireEvent('updater.info', single = True)
|
||||||
@@ -181,112 +169,14 @@ class Core(Plugin):
|
|||||||
|
|
||||||
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
|
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
|
||||||
|
|
||||||
def versionView(self, **kwargs):
|
def versionView(self):
|
||||||
return {
|
return jsonified({
|
||||||
'version': self.version()
|
'version': self.version()
|
||||||
}
|
})
|
||||||
|
|
||||||
def signalHandler(self):
|
def signalHandler(self):
|
||||||
if Env.get('daemonized'): return
|
|
||||||
|
|
||||||
def signal_handler(*args, **kwargs):
|
def signal_handler(signal, frame):
|
||||||
fireEvent('app.shutdown', single = True)
|
fireEvent('app.do_shutdown')
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
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
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
from minify.cssmin import cssmin
|
|
||||||
from minify.jsmin import jsmin
|
|
||||||
from tornado.web import StaticFileHandler
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
autoload = 'ClientScript'
|
|
||||||
|
|
||||||
|
|
||||||
class ClientScript(Plugin):
|
|
||||||
|
|
||||||
core_static = {
|
|
||||||
'style': [
|
|
||||||
'style/main.css',
|
|
||||||
'style/uniform.generic.css',
|
|
||||||
'style/uniform.css',
|
|
||||||
'style/settings.css',
|
|
||||||
],
|
|
||||||
'script': [
|
|
||||||
'scripts/library/mootools.js',
|
|
||||||
'scripts/library/mootools_more.js',
|
|
||||||
'scripts/library/uniform.js',
|
|
||||||
'scripts/library/form_replacement/form_check.js',
|
|
||||||
'scripts/library/form_replacement/form_radio.js',
|
|
||||||
'scripts/library/form_replacement/form_dropdown.js',
|
|
||||||
'scripts/library/form_replacement/form_selectoption.js',
|
|
||||||
'scripts/library/question.js',
|
|
||||||
'scripts/library/scrollspy.js',
|
|
||||||
'scripts/library/spin.js',
|
|
||||||
'scripts/library/Array.stableSort.js',
|
|
||||||
'scripts/library/async.js',
|
|
||||||
'scripts/couchpotato.js',
|
|
||||||
'scripts/api.js',
|
|
||||||
'scripts/library/history.js',
|
|
||||||
'scripts/page.js',
|
|
||||||
'scripts/block.js',
|
|
||||||
'scripts/block/navigation.js',
|
|
||||||
'scripts/block/footer.js',
|
|
||||||
'scripts/block/menu.js',
|
|
||||||
'scripts/page/home.js',
|
|
||||||
'scripts/page/settings.js',
|
|
||||||
'scripts/page/about.js',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
urls = {'style': {}, 'script': {}}
|
|
||||||
minified = {'style': {}, 'script': {}}
|
|
||||||
paths = {'style': {}, 'script': {}}
|
|
||||||
comment = {
|
|
||||||
'style': '/*** %s:%d ***/\n',
|
|
||||||
'script': '// %s:%d\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
html = {
|
|
||||||
'style': '<link rel="stylesheet" href="%s" type="text/css">',
|
|
||||||
'script': '<script type="text/javascript" src="%s"></script>',
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
addEvent('register_style', self.registerStyle)
|
|
||||||
addEvent('register_script', self.registerScript)
|
|
||||||
|
|
||||||
addEvent('clientscript.get_styles', self.getStyles)
|
|
||||||
addEvent('clientscript.get_scripts', self.getScripts)
|
|
||||||
|
|
||||||
if not Env.get('dev'):
|
|
||||||
addEvent('app.load', self.minify)
|
|
||||||
|
|
||||||
self.addCore()
|
|
||||||
|
|
||||||
def addCore(self):
|
|
||||||
|
|
||||||
for static_type in self.core_static:
|
|
||||||
for rel_path in self.core_static.get(static_type):
|
|
||||||
file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path)
|
|
||||||
core_url = 'static/%s' % rel_path
|
|
||||||
|
|
||||||
if static_type == 'script':
|
|
||||||
self.registerScript(core_url, file_path, position = 'front')
|
|
||||||
else:
|
|
||||||
self.registerStyle(core_url, file_path, position = 'front')
|
|
||||||
|
|
||||||
def minify(self):
|
|
||||||
|
|
||||||
# Create cache dir
|
|
||||||
cache = Env.get('cache_dir')
|
|
||||||
parent_dir = os.path.join(cache, 'minified')
|
|
||||||
self.makeDir(parent_dir)
|
|
||||||
|
|
||||||
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + 'minified/(.*)', StaticFileHandler, {'path': parent_dir})])
|
|
||||||
|
|
||||||
for file_type in ['style', 'script']:
|
|
||||||
ext = 'js' if file_type is 'script' else 'css'
|
|
||||||
positions = self.paths.get(file_type, {})
|
|
||||||
for position in positions:
|
|
||||||
files = positions.get(position)
|
|
||||||
self._minify(file_type, files, position, position + '.' + ext)
|
|
||||||
|
|
||||||
def _minify(self, file_type, files, position, out):
|
|
||||||
|
|
||||||
cache = Env.get('cache_dir')
|
|
||||||
out_name = out
|
|
||||||
out = os.path.join(cache, 'minified', out_name)
|
|
||||||
|
|
||||||
raw = []
|
|
||||||
for file_path in files:
|
|
||||||
f = open(file_path, 'r').read()
|
|
||||||
|
|
||||||
if file_type == 'script':
|
|
||||||
data = jsmin(f)
|
|
||||||
else:
|
|
||||||
data = self.prefix(f)
|
|
||||||
data = cssmin(data)
|
|
||||||
data = data.replace('../images/', '../static/images/')
|
|
||||||
data = data.replace('../fonts/', '../static/fonts/')
|
|
||||||
data = data.replace('../../static/', '../static/') # Replace inside plugins
|
|
||||||
|
|
||||||
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
|
|
||||||
|
|
||||||
# Combine all files together with some comments
|
|
||||||
data = ''
|
|
||||||
for r in raw:
|
|
||||||
data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
|
|
||||||
data += r.get('data') + '\n\n'
|
|
||||||
|
|
||||||
self.createFile(out, data.strip())
|
|
||||||
|
|
||||||
if not self.minified.get(file_type):
|
|
||||||
self.minified[file_type] = {}
|
|
||||||
if not self.minified[file_type].get(position):
|
|
||||||
self.minified[file_type][position] = []
|
|
||||||
|
|
||||||
minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out)))
|
|
||||||
self.minified[file_type][position].append(minified_url)
|
|
||||||
|
|
||||||
def getStyles(self, *args, **kwargs):
|
|
||||||
return self.get('style', *args, **kwargs)
|
|
||||||
|
|
||||||
def getScripts(self, *args, **kwargs):
|
|
||||||
return self.get('script', *args, **kwargs)
|
|
||||||
|
|
||||||
def get(self, type, as_html = False, location = 'head'):
|
|
||||||
|
|
||||||
data = '' if as_html else []
|
|
||||||
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
if not Env.get('dev'):
|
|
||||||
return self.minified[type][location]
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return self.urls[type][location]
|
|
||||||
except:
|
|
||||||
log.error('Error getting minified %s, %s: %s', (type, location, traceback.format_exc()))
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def registerStyle(self, api_path, file_path, position = 'head'):
|
|
||||||
self.register(api_path, file_path, 'style', position)
|
|
||||||
|
|
||||||
def registerScript(self, api_path, file_path, position = 'head'):
|
|
||||||
self.register(api_path, file_path, 'script', position)
|
|
||||||
|
|
||||||
def register(self, api_path, file_path, type, location):
|
|
||||||
|
|
||||||
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
|
|
||||||
|
|
||||||
if not self.urls[type].get(location):
|
|
||||||
self.urls[type][location] = []
|
|
||||||
self.urls[type][location].append(api_path)
|
|
||||||
|
|
||||||
if not self.paths[type].get(location):
|
|
||||||
self.paths[type][location] = []
|
|
||||||
self.paths[type][location].append(file_path)
|
|
||||||
|
|
||||||
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
|
|
||||||
prefix_tags = ['ms', 'moz', 'webkit']
|
|
||||||
|
|
||||||
def prefix(self, data):
|
|
||||||
|
|
||||||
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
|
|
||||||
|
|
||||||
new_data = ''
|
|
||||||
colon_split = trimmed_data.split(';')
|
|
||||||
for splt in colon_split:
|
|
||||||
curl_split = splt.strip().split('{')
|
|
||||||
for curly in curl_split:
|
|
||||||
curly = curly.strip()
|
|
||||||
for prop in self.prefix_properties:
|
|
||||||
if curly[:len(prop) + 1] == prop + ':':
|
|
||||||
for tag in self.prefix_tags:
|
|
||||||
new_data += ' -%s-%s; ' % (tag, curly)
|
|
||||||
|
|
||||||
new_data += curly + (' { ' if len(curl_split) > 1 else ' ')
|
|
||||||
|
|
||||||
new_data += '; '
|
|
||||||
|
|
||||||
new_data = new_data.replace('{ ;', '; ').replace('} ;', '} ')
|
|
||||||
|
|
||||||
return new_data
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .main import ClientScript
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return ClientScript()
|
||||||
|
|
||||||
|
config = []
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ClientScript(Plugin):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'style': {},
|
||||||
|
'script': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
html = {
|
||||||
|
'style': '<link rel="stylesheet" href="%s" type="text/css">',
|
||||||
|
'script': '<script type="text/javascript" src="%s"></script>',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
addEvent('register_style', self.registerStyle)
|
||||||
|
addEvent('register_script', self.registerScript)
|
||||||
|
|
||||||
|
addEvent('clientscript.get_styles', self.getStyles)
|
||||||
|
addEvent('clientscript.get_scripts', self.getScripts)
|
||||||
|
|
||||||
|
def getStyles(self, *args, **kwargs):
|
||||||
|
return self.get('style', *args, **kwargs)
|
||||||
|
|
||||||
|
def getScripts(self, *args, **kwargs):
|
||||||
|
return self.get('script', *args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, type, as_html = False, location = 'head'):
|
||||||
|
|
||||||
|
data = '' if as_html else []
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.urls[type][location]
|
||||||
|
except Exception, e:
|
||||||
|
log.error(e)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def registerStyle(self, path, position = 'head'):
|
||||||
|
self.register(path, 'style', position)
|
||||||
|
|
||||||
|
def registerScript(self, path, position = 'head'):
|
||||||
|
self.register(path, 'script', position)
|
||||||
|
|
||||||
|
def register(self, filepath, type, location):
|
||||||
|
|
||||||
|
if not self.urls[type].get(location):
|
||||||
|
self.urls[type][location] = []
|
||||||
|
|
||||||
|
filePath = filepath
|
||||||
|
self.urls[type][location].append(filePath)
|
||||||
@@ -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__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
autoload = 'Desktop'
|
|
||||||
|
|
||||||
|
|
||||||
if Env.get('desktop'):
|
if Env.get('desktop'):
|
||||||
|
|
||||||
class Desktop(Plugin):
|
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,228 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
## This is here to load the static files
|
|
||||||
class Downloader(Plugin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DownloaderBase(Provider):
|
|
||||||
|
|
||||||
protocol = []
|
|
||||||
http_time_between_calls = 0
|
|
||||||
status_support = True
|
|
||||||
|
|
||||||
torrent_sources = [
|
|
||||||
'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',
|
|
||||||
'http://tracker.ccc.de/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):
|
|
||||||
addEvent('download', self._download)
|
|
||||||
addEvent('download.enabled', self._isEnabled)
|
|
||||||
addEvent('download.enabled_protocols', self.getEnabledProtocol)
|
|
||||||
addEvent('download.status', self._getAllDownloadStatus)
|
|
||||||
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:
|
|
||||||
if self.isEnabled(manual = True, data = {'protocol': download_protocol}):
|
|
||||||
return self.protocol
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _download(self, data = None, media = None, manual = False, filedata = None):
|
|
||||||
if not media: media = {}
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
if self.isDisabled(manual, data):
|
|
||||||
return
|
|
||||||
return self.download(data = data, media = media, filedata = filedata)
|
|
||||||
|
|
||||||
def _getAllDownloadStatus(self, download_ids):
|
|
||||||
if self.isDisabled(manual = True, data = {}):
|
|
||||||
return
|
|
||||||
|
|
||||||
ids = [download_id['id'] for download_id in download_ids if download_id['downloader'] == self.getName()]
|
|
||||||
|
|
||||||
if ids:
|
|
||||||
return self.getAllDownloadStatus(ids)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
def getAllDownloadStatus(self, ids):
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _removeFailed(self, release_download):
|
|
||||||
if self.isDisabled(manual = True, data = {}):
|
|
||||||
return
|
|
||||||
|
|
||||||
if release_download and release_download.get('downloader') == self.getName():
|
|
||||||
if self.conf('delete_failed'):
|
|
||||||
return self.removeFailed(release_download)
|
|
||||||
|
|
||||||
return False
|
|
||||||
return
|
|
||||||
|
|
||||||
def removeFailed(self, release_download):
|
|
||||||
return
|
|
||||||
|
|
||||||
def _processComplete(self, release_download):
|
|
||||||
if self.isDisabled(manual = True, data = {}):
|
|
||||||
return
|
|
||||||
|
|
||||||
if release_download and release_download.get('downloader') == self.getName():
|
|
||||||
if self.conf('remove_complete', default = False):
|
|
||||||
return self.processComplete(release_download = release_download, delete_files = self.conf('delete_files', default = False))
|
|
||||||
|
|
||||||
return False
|
|
||||||
return
|
|
||||||
|
|
||||||
def processComplete(self, release_download, delete_files):
|
|
||||||
return
|
|
||||||
|
|
||||||
def isCorrectProtocol(self, protocol):
|
|
||||||
is_correct = protocol in self.protocol
|
|
||||||
|
|
||||||
if not is_correct:
|
|
||||||
log.debug("Downloader doesn't support this protocol")
|
|
||||||
|
|
||||||
return is_correct
|
|
||||||
|
|
||||||
def magnetToTorrent(self, magnet_link):
|
|
||||||
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0].upper()
|
|
||||||
|
|
||||||
# Convert base 32 to hex
|
|
||||||
if len(torrent_hash) == 32:
|
|
||||||
torrent_hash = b16encode(b32decode(torrent_hash))
|
|
||||||
|
|
||||||
sources = self.torrent_sources
|
|
||||||
random.shuffle(sources)
|
|
||||||
|
|
||||||
for source in sources:
|
|
||||||
try:
|
|
||||||
filedata = self.urlopen(source % torrent_hash, headers = {'Referer': ''}, show_error = False)
|
|
||||||
if 'torcache' in filedata and 'file not found' in filedata.lower():
|
|
||||||
continue
|
|
||||||
|
|
||||||
return filedata
|
|
||||||
except:
|
|
||||||
log.debug('Torrent hash "%s" wasn\'t found on: %s', (torrent_hash, source))
|
|
||||||
|
|
||||||
log.error('Failed converting magnet url to torrent: %s', torrent_hash)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def downloadReturnId(self, download_id):
|
|
||||||
return {
|
|
||||||
'downloader': self.getName(),
|
|
||||||
'status_support': self.status_support,
|
|
||||||
'id': download_id
|
|
||||||
}
|
|
||||||
|
|
||||||
def isDisabled(self, manual = False, data = None):
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
return not self.isEnabled(manual, data)
|
|
||||||
|
|
||||||
def _isEnabled(self, manual, data = None):
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
if not self.isEnabled(manual, data):
|
|
||||||
return
|
|
||||||
return True
|
|
||||||
|
|
||||||
def isEnabled(self, manual = False, data = None):
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
d_manual = self.conf('manual', default = False)
|
|
||||||
return super(DownloaderBase, 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
|
|
||||||
|
|
||||||
if release_download and release_download.get('downloader') == self.getName():
|
|
||||||
self.pause(release_download, pause)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def pause(self, release_download, pause):
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
class ReleaseDownloadList(list):
|
|
||||||
|
|
||||||
provider = None
|
|
||||||
|
|
||||||
def __init__(self, provider, **kwargs):
|
|
||||||
|
|
||||||
self.provider = provider
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
super(ReleaseDownloadList, self).__init__()
|
|
||||||
|
|
||||||
def extend(self, results):
|
|
||||||
for r in results:
|
|
||||||
self.append(r)
|
|
||||||
|
|
||||||
def append(self, result):
|
|
||||||
new_result = self.fillResult(result)
|
|
||||||
super(ReleaseDownloadList, self).append(new_result)
|
|
||||||
|
|
||||||
def fillResult(self, result):
|
|
||||||
|
|
||||||
defaults = {
|
|
||||||
'id': 0,
|
|
||||||
'status': 'busy',
|
|
||||||
'downloader': self.provider.getName(),
|
|
||||||
'folder': '',
|
|
||||||
'files': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeDicts(defaults, result)
|
|
||||||
|
|
||||||
@@ -1,75 +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);
|
|
||||||
|
|
||||||
if(json.success){
|
|
||||||
var 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;
|
|
||||||
var 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();
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .main import Scheduler
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Scheduler()
|
||||||
|
|
||||||
|
config = []
|
||||||
@@ -2,11 +2,10 @@ from apscheduler.scheduler import Scheduler as Sched
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
import logging
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
autoload = 'Scheduler'
|
|
||||||
|
|
||||||
|
|
||||||
class Scheduler(Plugin):
|
class Scheduler(Plugin):
|
||||||
|
|
||||||
@@ -18,29 +17,60 @@ class Scheduler(Plugin):
|
|||||||
|
|
||||||
addEvent('schedule.cron', self.cron)
|
addEvent('schedule.cron', self.cron)
|
||||||
addEvent('schedule.interval', self.interval)
|
addEvent('schedule.interval', self.interval)
|
||||||
addEvent('schedule.remove', self.remove)
|
addEvent('schedule.start', self.start)
|
||||||
addEvent('schedule.queue', self.queue)
|
addEvent('schedule.restart', self.start)
|
||||||
|
|
||||||
|
addEvent('app.load', self.start)
|
||||||
|
|
||||||
self.sched = Sched(misfire_grace_time = 60)
|
self.sched = Sched(misfire_grace_time = 60)
|
||||||
self.sched.start()
|
|
||||||
self.started = True
|
|
||||||
|
|
||||||
def remove(self, identifier):
|
def remove(self, identifier):
|
||||||
for cron_type in ['intervals', 'crons']:
|
for type in ['interval', 'cron']:
|
||||||
try:
|
try:
|
||||||
self.sched.unschedule_job(getattr(self, cron_type)[identifier]['job'])
|
self.sched.unschedule_job(getattr(self, type)[identifier]['job'])
|
||||||
log.debug('%s unscheduled %s', (cron_type.capitalize(), identifier))
|
log.debug('%s unscheduled %s', (type.capitalize(), identifier))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def doShutdown(self):
|
def start(self):
|
||||||
|
|
||||||
|
# Stop all running
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
# Crons
|
||||||
|
for identifier in self.crons:
|
||||||
|
try:
|
||||||
|
self.remove(identifier)
|
||||||
|
cron = self.crons[identifier]
|
||||||
|
job = self.sched.add_cron_job(cron['handle'], day = cron['day'], hour = cron['hour'], minute = cron['minute'])
|
||||||
|
cron['job'] = job
|
||||||
|
except ValueError, e:
|
||||||
|
log.error('Failed adding cronjob: %s', e)
|
||||||
|
|
||||||
|
# Intervals
|
||||||
|
for identifier in self.intervals:
|
||||||
|
try:
|
||||||
|
self.remove(identifier)
|
||||||
|
interval = self.intervals[identifier]
|
||||||
|
job = self.sched.add_interval_job(interval['handle'], hours = interval['hours'], minutes = interval['minutes'], seconds = interval['seconds'])
|
||||||
|
interval['job'] = job
|
||||||
|
except ValueError, e:
|
||||||
|
log.error('Failed adding interval cronjob: %s', e)
|
||||||
|
|
||||||
|
# Start it
|
||||||
|
log.debug('Starting scheduler')
|
||||||
|
self.sched.start()
|
||||||
|
self.started = True
|
||||||
|
log.debug('Scheduler started')
|
||||||
|
|
||||||
|
def doShutdown(self):
|
||||||
|
super(Scheduler, self).doShutdown()
|
||||||
self.stop()
|
self.stop()
|
||||||
return super(Scheduler, self).doShutdown()
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if self.started:
|
if self.started:
|
||||||
log.debug('Stopping scheduler')
|
log.debug('Stopping scheduler')
|
||||||
self.sched.shutdown(wait = False)
|
self.sched.shutdown()
|
||||||
log.debug('Scheduler stopped')
|
log.debug('Scheduler stopped')
|
||||||
self.started = False
|
self.started = False
|
||||||
|
|
||||||
@@ -53,7 +83,6 @@ class Scheduler(Plugin):
|
|||||||
'day': day,
|
'day': day,
|
||||||
'hour': hour,
|
'hour': hour,
|
||||||
'minute': minute,
|
'minute': minute,
|
||||||
'job': self.sched.add_cron_job(handle, day = day, hour = hour, minute = minute)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
|
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
|
||||||
@@ -65,18 +94,4 @@ class Scheduler(Plugin):
|
|||||||
'hours': hours,
|
'hours': hours,
|
||||||
'minutes': minutes,
|
'minutes': minutes,
|
||||||
'seconds': seconds,
|
'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 .main import Updater
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
|
import os
|
||||||
|
|
||||||
|
def start():
|
||||||
def autoload():
|
|
||||||
return Updater()
|
return Updater()
|
||||||
|
|
||||||
config = [{
|
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.helpers.request import jsonified
|
||||||
|
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 json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tarfile
|
import tarfile
|
||||||
import time
|
import time
|
||||||
import traceback
|
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 ss
|
|
||||||
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
|
|
||||||
from scandir import scandir
|
|
||||||
import version
|
import version
|
||||||
from six.moves import filter
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -27,7 +22,6 @@ log = CPLog(__name__)
|
|||||||
class Updater(Plugin):
|
class Updater(Plugin):
|
||||||
|
|
||||||
available_notified = False
|
available_notified = False
|
||||||
_lock = RLock()
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
@@ -38,11 +32,11 @@ class Updater(Plugin):
|
|||||||
else:
|
else:
|
||||||
self.updater = SourceUpdater()
|
self.updater = SourceUpdater()
|
||||||
|
|
||||||
addEvent('app.load', self.logVersion, priority = 10000)
|
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
|
||||||
addEvent('app.load', self.setCrons)
|
addEvent('app.load', self.autoUpdate)
|
||||||
addEvent('updater.info', self.info)
|
addEvent('updater.info', self.info)
|
||||||
|
|
||||||
addApiView('updater.info', self.info, docs = {
|
addApiView('updater.info', self.getInfo, docs = {
|
||||||
'desc': 'Get updater information',
|
'desc': 'Get updater information',
|
||||||
'return': {
|
'return': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@@ -58,21 +52,8 @@ class Updater(Plugin):
|
|||||||
'return': {'type': 'see updater.info'}
|
'return': {'type': 'see updater.info'}
|
||||||
})
|
})
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def autoUpdate(self):
|
def autoUpdate(self):
|
||||||
if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
|
if self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||||
if self.updater.doUpdate():
|
if self.updater.doUpdate():
|
||||||
|
|
||||||
# Notify before restarting
|
# Notify before restarting
|
||||||
@@ -90,40 +71,31 @@ class Updater(Plugin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check(self, force = False):
|
def check(self):
|
||||||
if not force and self.isDisabled():
|
if self.isDisabled():
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.updater.check():
|
if self.updater.check():
|
||||||
if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
|
if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
|
||||||
info = self.updater.info()
|
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
|
||||||
version_date = datetime.fromtimestamp(info['update_version']['date'])
|
|
||||||
fireEvent('updater.available', message = 'A new update with hash "%s" is available, this version is from %s' % (info['update_version']['hash'], version_date), data = info)
|
|
||||||
self.available_notified = True
|
self.available_notified = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def info(self, **kwargs):
|
def info(self):
|
||||||
self._lock.acquire()
|
return self.updater.info()
|
||||||
|
|
||||||
info = {}
|
def getInfo(self):
|
||||||
try:
|
return jsonified(self.updater.info())
|
||||||
info = self.updater.info()
|
|
||||||
except:
|
|
||||||
log.error('Failed getting updater info: %s', traceback.format_exc())
|
|
||||||
|
|
||||||
self._lock.release()
|
def checkView(self):
|
||||||
|
return jsonified({
|
||||||
return info
|
'update_available': self.check(),
|
||||||
|
|
||||||
def checkView(self, **kwargs):
|
|
||||||
return {
|
|
||||||
'update_available': self.check(force = True),
|
|
||||||
'info': self.updater.info()
|
'info': self.updater.info()
|
||||||
}
|
})
|
||||||
|
|
||||||
def doUpdateView(self, **kwargs):
|
def doUpdateView(self):
|
||||||
|
|
||||||
self.check()
|
self.check()
|
||||||
if not self.updater.update_version:
|
if not self.updater.update_version:
|
||||||
@@ -138,13 +110,9 @@ class Updater(Plugin):
|
|||||||
if not success:
|
if not success:
|
||||||
success = True
|
success = True
|
||||||
|
|
||||||
return {
|
return jsonified({
|
||||||
'success': success
|
'success': success
|
||||||
}
|
})
|
||||||
|
|
||||||
def doShutdown(self):
|
|
||||||
self.updater.deletePyc(show_logs = False)
|
|
||||||
return super(Updater, self).doShutdown()
|
|
||||||
|
|
||||||
|
|
||||||
class BaseUpdater(Plugin):
|
class BaseUpdater(Plugin):
|
||||||
@@ -157,32 +125,28 @@ class BaseUpdater(Plugin):
|
|||||||
update_failed = False
|
update_failed = False
|
||||||
update_version = None
|
update_version = None
|
||||||
last_check = 0
|
last_check = 0
|
||||||
auto_register_static = False
|
|
||||||
|
|
||||||
def doUpdate(self):
|
def doUpdate(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def getInfo(self):
|
||||||
|
return jsonified(self.info())
|
||||||
|
|
||||||
def info(self):
|
def info(self):
|
||||||
|
|
||||||
current_version = self.getVersion()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'last_check': self.last_check,
|
'last_check': self.last_check,
|
||||||
'update_version': self.update_version,
|
'update_version': self.update_version,
|
||||||
'version': current_version,
|
'version': self.getVersion(),
|
||||||
'repo_name': '%s/%s' % (self.repo_user, self.repo_name),
|
'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):
|
def check(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def deletePyc(self, only_excess = True, show_logs = True):
|
def deletePyc(self, only_excess = True):
|
||||||
|
|
||||||
for root, dirs, files in scandir.walk(ss(Env.get('app_dir'))):
|
for root, dirs, files in os.walk(ss(Env.get('app_dir'))):
|
||||||
|
|
||||||
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
|
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
|
||||||
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
|
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
|
||||||
@@ -190,7 +154,7 @@ class BaseUpdater(Plugin):
|
|||||||
|
|
||||||
for excess_pyc_file in excess_pyc_files:
|
for excess_pyc_file in excess_pyc_files:
|
||||||
full_path = os.path.join(root, excess_pyc_file)
|
full_path = os.path.join(root, excess_pyc_file)
|
||||||
if show_logs: log.debug('Removing old PYC file: %s', full_path)
|
log.debug('Removing old PYC file: %s', full_path)
|
||||||
try:
|
try:
|
||||||
os.remove(full_path)
|
os.remove(full_path)
|
||||||
except:
|
except:
|
||||||
@@ -205,6 +169,7 @@ class BaseUpdater(Plugin):
|
|||||||
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
|
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class GitUpdater(BaseUpdater):
|
class GitUpdater(BaseUpdater):
|
||||||
|
|
||||||
def __init__(self, git_command):
|
def __init__(self, git_command):
|
||||||
@@ -213,9 +178,15 @@ class GitUpdater(BaseUpdater):
|
|||||||
def doUpdate(self):
|
def doUpdate(self):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
log.debug('Stashing local changes')
|
||||||
|
self.repo.saveStash()
|
||||||
|
|
||||||
log.info('Updating to latest version')
|
log.info('Updating to latest version')
|
||||||
self.repo.pull()
|
self.repo.pull()
|
||||||
|
|
||||||
|
# Delete leftover .pyc files
|
||||||
|
self.deletePyc()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
log.error('Failed updating via GIT: %s', traceback.format_exc())
|
log.error('Failed updating via GIT: %s', traceback.format_exc())
|
||||||
@@ -228,16 +199,14 @@ class GitUpdater(BaseUpdater):
|
|||||||
|
|
||||||
if not self.version:
|
if not self.version:
|
||||||
try:
|
try:
|
||||||
output = self.repo.getHead() # Yes, please
|
output = self.repo.getHead() # Yes, please
|
||||||
log.debug('Git version output: %s', output.hash)
|
log.debug('Git version output: %s', output.hash)
|
||||||
self.version = {
|
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],
|
'hash': output.hash[:8],
|
||||||
'date': output.getDate(),
|
'date': output.getDate(),
|
||||||
'type': 'git',
|
'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)
|
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
|
||||||
return 'No GIT'
|
return 'No GIT'
|
||||||
|
|
||||||
@@ -260,7 +229,7 @@ class GitUpdater(BaseUpdater):
|
|||||||
local = self.repo.getHead()
|
local = self.repo.getHead()
|
||||||
remote = branch.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():
|
if local.getDate() < remote.getDate():
|
||||||
self.update_version = {
|
self.update_version = {
|
||||||
@@ -273,6 +242,7 @@ class GitUpdater(BaseUpdater):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SourceUpdater(BaseUpdater):
|
class SourceUpdater(BaseUpdater):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -285,11 +255,11 @@ class SourceUpdater(BaseUpdater):
|
|||||||
def doUpdate(self):
|
def doUpdate(self):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
download_data = fireEvent('cp.source_url', repo = self.repo_user, repo_name = self.repo_name, branch = self.branch, single = True)
|
url = 'https://github.com/%s/%s/tarball/%s' % (self.repo_user, self.repo_name, self.branch)
|
||||||
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash')) + '.' + download_data.get('type')
|
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash') + '.tar.gz')
|
||||||
|
|
||||||
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
|
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
|
||||||
destination = fireEvent('file.download', url = download_data.get('url'), dest = destination, single = True)
|
|
||||||
|
destination = fireEvent('file.download', url = url, dest = destination, single = True)
|
||||||
|
|
||||||
# Cleanup leftover from last time
|
# Cleanup leftover from last time
|
||||||
if os.path.isdir(extracted_path):
|
if os.path.isdir(extracted_path):
|
||||||
@@ -297,15 +267,9 @@ class SourceUpdater(BaseUpdater):
|
|||||||
self.makeDir(extracted_path)
|
self.makeDir(extracted_path)
|
||||||
|
|
||||||
# Extract
|
# Extract
|
||||||
if download_data.get('type') == 'zip':
|
tar = tarfile.open(destination)
|
||||||
zip_file = zipfile.ZipFile(destination)
|
tar.extractall(path = extracted_path)
|
||||||
zip_file.extractall(extracted_path)
|
tar.close()
|
||||||
zip_file.close()
|
|
||||||
else:
|
|
||||||
tar = tarfile.open(destination)
|
|
||||||
tar.extractall(path = extracted_path)
|
|
||||||
tar.close()
|
|
||||||
|
|
||||||
os.remove(destination)
|
os.remove(destination)
|
||||||
|
|
||||||
if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):
|
if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):
|
||||||
@@ -323,16 +287,15 @@ class SourceUpdater(BaseUpdater):
|
|||||||
|
|
||||||
def replaceWith(self, path):
|
def replaceWith(self, path):
|
||||||
app_dir = ss(Env.get('app_dir'))
|
app_dir = ss(Env.get('app_dir'))
|
||||||
data_dir = ss(Env.get('data_dir'))
|
|
||||||
|
|
||||||
# Get list of files we want to overwrite
|
# Get list of files we want to overwrite
|
||||||
self.deletePyc()
|
self.deletePyc()
|
||||||
existing_files = []
|
existing_files = []
|
||||||
for root, subfiles, filenames in scandir.walk(app_dir):
|
for root, subfiles, filenames in os.walk(app_dir):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
existing_files.append(os.path.join(root, filename))
|
existing_files.append(os.path.join(root, filename))
|
||||||
|
|
||||||
for root, subfiles, filenames in scandir.walk(path):
|
for root, subfiles, filenames in os.walk(path):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
fromfile = os.path.join(root, filename)
|
fromfile = os.path.join(root, filename)
|
||||||
tofile = os.path.join(app_dir, fromfile.replace(path + os.path.sep, ''))
|
tofile = os.path.join(app_dir, fromfile.replace(path + os.path.sep, ''))
|
||||||
@@ -355,24 +318,22 @@ class SourceUpdater(BaseUpdater):
|
|||||||
log.error('Failed overwriting file "%s": %s', (tofile, traceback.format_exc()))
|
log.error('Failed overwriting file "%s": %s', (tofile, traceback.format_exc()))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for still_exists in existing_files:
|
if Env.get('app_dir') not in Env.get('data_dir'):
|
||||||
|
for still_exists in existing_files:
|
||||||
if data_dir in still_exists:
|
try:
|
||||||
continue
|
os.remove(still_exists)
|
||||||
|
except:
|
||||||
try:
|
log.error('Failed removing non-used file: %s', traceback.format_exc())
|
||||||
os.remove(still_exists)
|
|
||||||
except:
|
|
||||||
log.error('Failed removing non-used file: %s', traceback.format_exc())
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def removeDir(self, path):
|
def removeDir(self, path):
|
||||||
try:
|
try:
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
except OSError as inst:
|
except OSError, inst:
|
||||||
os.chmod(inst.filename, 0o777)
|
os.chmod(inst.filename, 0777)
|
||||||
self.removeDir(path)
|
self.removeDir(path)
|
||||||
|
|
||||||
def getVersion(self):
|
def getVersion(self):
|
||||||
@@ -386,8 +347,7 @@ class SourceUpdater(BaseUpdater):
|
|||||||
log.debug('Source version output: %s', output)
|
log.debug('Source version output: %s', output)
|
||||||
self.version = output
|
self.version = output
|
||||||
self.version['type'] = 'source'
|
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, e:
|
||||||
except Exception as e:
|
|
||||||
log.error('Failed using source updater. %s', e)
|
log.error('Failed using source updater. %s', e)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -417,7 +377,7 @@ class SourceUpdater(BaseUpdater):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'hash': commit['sha'],
|
'hash': commit['sha'],
|
||||||
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
|
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
|
||||||
}
|
}
|
||||||
except:
|
except:
|
||||||
log.error('Failed getting latest request from github: %s', traceback.format_exc())
|
log.error('Failed getting latest request from github: %s', traceback.format_exc())
|
||||||
@@ -462,7 +422,7 @@ class DesktopUpdater(BaseUpdater):
|
|||||||
if latest and latest != current_version.get('hash'):
|
if latest and latest != current_version.get('hash'):
|
||||||
self.update_version = {
|
self.update_version = {
|
||||||
'hash': latest,
|
'hash': latest,
|
||||||
'date': None,
|
'date': None,
|
||||||
'changelog': self.desktop._changelogURL,
|
'changelog': self.desktop._changelogURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,7 +434,6 @@ class DesktopUpdater(BaseUpdater):
|
|||||||
|
|
||||||
def getVersion(self):
|
def getVersion(self):
|
||||||
return {
|
return {
|
||||||
'repr': 'desktop: %s' % self.desktop._esky.active_version,
|
|
||||||
'hash': self.desktop._esky.active_version,
|
'hash': self.desktop._esky.active_version,
|
||||||
'date': None,
|
'date': None,
|
||||||
'type': 'desktop',
|
'type': 'desktop',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ var UpdaterBase = new Class({
|
|||||||
initialize: function(){
|
initialize: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
App.addEvent('load', self.info.bind(self, 2000));
|
App.addEvent('load', self.info.bind(self, 1000))
|
||||||
App.addEvent('unload', function(){
|
App.addEvent('unload', function(){
|
||||||
if(self.timer)
|
if(self.timer)
|
||||||
clearTimeout(self.timer);
|
clearTimeout(self.timer);
|
||||||
@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
|
|||||||
self.doUpdate();
|
self.doUpdate();
|
||||||
else {
|
else {
|
||||||
App.unBlockPage();
|
App.unBlockPage();
|
||||||
App.trigger('message', ['No updates available']);
|
App.fireEvent('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;
|
var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.branch;
|
||||||
if(data.update_version.changelog)
|
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(
|
self.message = new Element('div.message.update').adopt(
|
||||||
new Element('span', {
|
new Element('span', {
|
||||||
@@ -84,24 +84,23 @@ var UpdaterBase = new Class({
|
|||||||
'click': self.doUpdate.bind(self)
|
'click': self.doUpdate.bind(self)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
).inject(document.body)
|
).inject($(document.body).getElement('.header'))
|
||||||
},
|
},
|
||||||
|
|
||||||
doUpdate: function(){
|
doUpdate: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
|
|
||||||
Api.request('updater.update', {
|
Api.request('updater.update', {
|
||||||
'onComplete': function(json){
|
'onComplete': function(json){
|
||||||
if(json.success)
|
if(json.success){
|
||||||
self.updating();
|
self.updating();
|
||||||
else
|
}
|
||||||
App.unBlockPage()
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updating: function(){
|
updating: function(){
|
||||||
|
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
|
||||||
App.checkAvailable.delay(500, App, [1000, function(){
|
App.checkAvailable.delay(500, App, [1000, function(){
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from couchpotato.core.helpers.variable import md5
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from flask import request, Response
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
def check_auth(username, password):
|
||||||
|
return username == Env.setting('username') and password == Env.setting('password')
|
||||||
|
|
||||||
|
def authenticate():
|
||||||
|
return Response(
|
||||||
|
'This is not the page you are looking for. *waves hand*', 401,
|
||||||
|
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'}
|
||||||
|
)
|
||||||
|
|
||||||
|
def requires_auth(f):
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
auth = getattr(request, 'authorization')
|
||||||
|
if Env.setting('username') and Env.setting('password'):
|
||||||
|
if (not auth or not check_auth(auth.username.decode('latin1'), md5(auth.password.decode('latin1').encode(Env.get('encoding'))))):
|
||||||
|
return authenticate()
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
@@ -1,482 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from couchpotato import CPLog
|
|
||||||
from couchpotato.api import addApiView
|
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
|
||||||
from couchpotato.core.helpers.variable import getImdb, tryInt
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Database(object):
|
|
||||||
|
|
||||||
indexes = []
|
|
||||||
db = None
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
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_index', self.setupIndex)
|
|
||||||
addEvent('app.migrate', self.migrate)
|
|
||||||
|
|
||||||
def getDB(self):
|
|
||||||
|
|
||||||
if not self.db:
|
|
||||||
from couchpotato import get_db
|
|
||||||
self.db = get_db()
|
|
||||||
|
|
||||||
return self.db
|
|
||||||
|
|
||||||
def setupIndex(self, index_name, klass):
|
|
||||||
|
|
||||||
self.indexes.append(index_name)
|
|
||||||
|
|
||||||
db = self.getDB()
|
|
||||||
|
|
||||||
# Category index
|
|
||||||
index_instance = klass(db.path, index_name)
|
|
||||||
try:
|
|
||||||
db.add_index(index_instance)
|
|
||||||
db.reindex_index(index_name)
|
|
||||||
except:
|
|
||||||
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)
|
|
||||||
|
|
||||||
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, **kwargs):
|
|
||||||
|
|
||||||
success = True
|
|
||||||
try:
|
|
||||||
db = self.getDB()
|
|
||||||
db.compact()
|
|
||||||
except:
|
|
||||||
log.error('Failed compact: %s', traceback.format_exc())
|
|
||||||
success = False
|
|
||||||
|
|
||||||
return {
|
|
||||||
'success': success
|
|
||||||
}
|
|
||||||
|
|
||||||
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']:
|
|
||||||
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'])
|
|
||||||
|
|
||||||
new_profile.update(db.insert(new_profile))
|
|
||||||
|
|
||||||
profile_link[x] = new_profile.get('_id')
|
|
||||||
|
|
||||||
# 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[m['library_id']]
|
|
||||||
|
|
||||||
# Only migrate wanted movies, Skip if no identifier present
|
|
||||||
if 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[rel.get('quality_id')]
|
|
||||||
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,85 @@
|
|||||||
|
from base64 import b32decode, b16encode
|
||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Downloader(Plugin):
|
||||||
|
|
||||||
|
type = []
|
||||||
|
|
||||||
|
torrent_sources = [
|
||||||
|
'http://torrage.com/torrent/%s.torrent',
|
||||||
|
'http://torrage.ws/torrent/%s.torrent',
|
||||||
|
'http://torcache.net/torrent/%s.torrent',
|
||||||
|
]
|
||||||
|
|
||||||
|
torrent_trackers = [
|
||||||
|
'http://tracker.publicbt.com/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',
|
||||||
|
'http://tracker.openbittorrent.com/announce',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
addEvent('download', self.download)
|
||||||
|
addEvent('download.status', self.getAllDownloadStatus)
|
||||||
|
addEvent('download.remove_failed', self.removeFailed)
|
||||||
|
|
||||||
|
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def removeFailed(self, name = {}, nzo_id = {}):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isCorrectType(self, item_type):
|
||||||
|
is_correct = item_type in self.type
|
||||||
|
|
||||||
|
if not is_correct:
|
||||||
|
log.debug("Downloader doesn't support this type")
|
||||||
|
|
||||||
|
return is_correct
|
||||||
|
|
||||||
|
def magnetToTorrent(self, magnet_link):
|
||||||
|
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0].upper()
|
||||||
|
|
||||||
|
# Convert base 32 to hex
|
||||||
|
if len(torrent_hash) == 32:
|
||||||
|
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||||
|
|
||||||
|
sources = self.torrent_sources
|
||||||
|
random.shuffle(sources)
|
||||||
|
|
||||||
|
for source in sources:
|
||||||
|
try:
|
||||||
|
filedata = self.urlopen(source % torrent_hash, headers = {'Referer': ''}, show_error = False)
|
||||||
|
if 'torcache' in filedata and 'file not found' in filedata.lower():
|
||||||
|
continue
|
||||||
|
|
||||||
|
return filedata
|
||||||
|
except:
|
||||||
|
log.debug('Torrent hash "%s" wasn\'t found on: %s', (torrent_hash, source))
|
||||||
|
|
||||||
|
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isDisabled(self, manual):
|
||||||
|
return not self.isEnabled(manual)
|
||||||
|
|
||||||
|
def isEnabled(self, manual):
|
||||||
|
d_manual = self.conf('manual', default = False)
|
||||||
|
return super(Downloader, self).isEnabled() and ((d_manual and manual) or (d_manual is False))
|
||||||
@@ -1,158 +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.helpers.variable import getDownloadDir
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
autoload = 'Blackhole'
|
|
||||||
|
|
||||||
|
|
||||||
class Blackhole(DownloaderBase):
|
|
||||||
|
|
||||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
|
||||||
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 blackhole %s download.', data.get('protocol'))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
if not filedata or len(filedata) < 50:
|
|
||||||
try:
|
|
||||||
if data.get('protocol') == 'torrent_magnet':
|
|
||||||
filedata = self.magnetToTorrent(data.get('url'))
|
|
||||||
data['protocol'] = 'torrent'
|
|
||||||
except:
|
|
||||||
log.error('Failed download torrent via magnet url: %s', traceback.format_exc())
|
|
||||||
|
|
||||||
if not filedata or len(filedata) < 50:
|
|
||||||
log.error('No nzb/torrent available: %s', data.get('url'))
|
|
||||||
return False
|
|
||||||
|
|
||||||
file_name = self.createFileName(data, filedata, media)
|
|
||||||
full_path = os.path.join(directory, file_name)
|
|
||||||
|
|
||||||
if self.conf('create_subdir'):
|
|
||||||
try:
|
|
||||||
new_path = os.path.splitext(full_path)[0]
|
|
||||||
if not os.path.exists(new_path):
|
|
||||||
os.makedirs(new_path)
|
|
||||||
full_path = os.path.join(new_path, file_name)
|
|
||||||
except:
|
|
||||||
log.error('Couldnt create sub dir, reverting to old one: %s', full_path)
|
|
||||||
|
|
||||||
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)
|
|
||||||
os.chmod(full_path, Env.getPermission('file'))
|
|
||||||
return self.downloadReturnId('')
|
|
||||||
else:
|
|
||||||
log.info('File %s already exists.', full_path)
|
|
||||||
return self.downloadReturnId('')
|
|
||||||
|
|
||||||
except:
|
|
||||||
log.error('Failed to download to blackhole %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
|
|
||||||
|
|
||||||
def getEnabledProtocol(self):
|
|
||||||
if self.conf('use_for') == 'both':
|
|
||||||
return super(Blackhole, self).getEnabledProtocol()
|
|
||||||
elif self.conf('use_for') == 'torrent':
|
|
||||||
return ['torrent', 'torrent_magnet']
|
|
||||||
else:
|
|
||||||
return ['nzb']
|
|
||||||
|
|
||||||
def isEnabled(self, manual = False, data = None):
|
|
||||||
if not data: data = {}
|
|
||||||
for_protocol = ['both']
|
|
||||||
if data and 'torrent' in data.get('protocol'):
|
|
||||||
for_protocol.append('torrent')
|
|
||||||
elif data:
|
|
||||||
for_protocol.append(data.get('protocol'))
|
|
||||||
|
|
||||||
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.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from .main import Blackhole
|
||||||
|
from couchpotato.core.helpers.variable import getDownloadDir
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Blackhole()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'blackhole',
|
||||||
|
'order': 30,
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'name': 'blackhole',
|
||||||
|
'label': 'Black hole',
|
||||||
|
'description': 'Download the NZB/Torrent to a specific folder.',
|
||||||
|
'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': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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 Blackhole(Downloader):
|
||||||
|
|
||||||
|
type = ['nzb', 'torrent', 'torrent_magnet']
|
||||||
|
|
||||||
|
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||||
|
if self.isDisabled(manual) or \
|
||||||
|
(not self.isCorrectType(data.get('type')) or \
|
||||||
|
(not self.conf('use_for') in ['both', 'torrent' if 'torrent' in data.get('type') else data.get('type')])):
|
||||||
|
return
|
||||||
|
|
||||||
|
directory = self.conf('directory')
|
||||||
|
if not directory or not os.path.isdir(directory):
|
||||||
|
log.error('No directory set for blackhole %s download.', data.get('type'))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if not filedata or len(filedata) < 50:
|
||||||
|
try:
|
||||||
|
if data.get('type') == 'torrent_magnet':
|
||||||
|
filedata = self.magnetToTorrent(data.get('url'))
|
||||||
|
data['type'] = 'torrent'
|
||||||
|
except:
|
||||||
|
log.error('Failed download torrent via magnet url: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
if not filedata or len(filedata) < 50:
|
||||||
|
log.error('No nzb/torrent available: %s', data.get('url'))
|
||||||
|
return False
|
||||||
|
|
||||||
|
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not os.path.isfile(fullPath):
|
||||||
|
log.info('Downloading %s to %s.', (data.get('type'), fullPath))
|
||||||
|
with open(fullPath, 'wb') as f:
|
||||||
|
f.write(filedata)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log.info('File %s already exists.', fullPath)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed to download to blackhole %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,384 +0,0 @@
|
|||||||
from base64 import b64encode, b16encode, b32decode
|
|
||||||
from datetime import timedelta
|
|
||||||
from hashlib import sha1
|
|
||||||
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):
|
|
||||||
|
|
||||||
protocol = ['torrent', 'torrent_magnet']
|
|
||||||
log = CPLog(__name__)
|
|
||||||
drpc = None
|
|
||||||
|
|
||||||
def connect(self, reconnect = False):
|
|
||||||
# Load host from config and split out port.
|
|
||||||
host = cleanHost(self.conf('host'), protocol = False).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:
|
|
||||||
self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
|
||||||
|
|
||||||
return self.drpc
|
|
||||||
|
|
||||||
def download(self, data = None, media = None, filedata = None):
|
|
||||||
if not media: media = {}
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
|
|
||||||
|
|
||||||
if not self.connect():
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not filedata and data.get('protocol') == 'torrent':
|
|
||||||
log.error('Failed sending torrent, no data')
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Set parameters for Deluge
|
|
||||||
options = {
|
|
||||||
'add_paused': self.conf('paused', default = 0),
|
|
||||||
'label': self.conf('label')
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.conf('directory'):
|
|
||||||
if os.path.isdir(self.conf('directory')):
|
|
||||||
options['download_location'] = self.conf('directory')
|
|
||||||
else:
|
|
||||||
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
|
|
||||||
|
|
||||||
if self.conf('completed_directory'):
|
|
||||||
if os.path.isdir(self.conf('completed_directory')):
|
|
||||||
options['move_completed'] = 1
|
|
||||||
options['move_completed_path'] = self.conf('completed_directory')
|
|
||||||
else:
|
|
||||||
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
|
|
||||||
|
|
||||||
if data.get('seed_ratio'):
|
|
||||||
options['stop_at_ratio'] = 1
|
|
||||||
options['stop_ratio'] = tryFloat(data.get('seed_ratio'))
|
|
||||||
|
|
||||||
# Deluge only has seed time as a global option. Might be added in
|
|
||||||
# in a future API release.
|
|
||||||
# if data.get('seed_time'):
|
|
||||||
|
|
||||||
# Send request to Deluge
|
|
||||||
if data.get('protocol') == 'torrent_magnet':
|
|
||||||
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
|
|
||||||
else:
|
|
||||||
filename = self.createFileName(data, filedata, media)
|
|
||||||
remote_torrent = self.drpc.add_torrent_file(filename, filedata, options)
|
|
||||||
|
|
||||||
if not remote_torrent:
|
|
||||||
log.error('Failed sending torrent to Deluge')
|
|
||||||
return False
|
|
||||||
|
|
||||||
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.')
|
|
||||||
|
|
||||||
if not self.connect():
|
|
||||||
return []
|
|
||||||
|
|
||||||
release_downloads = ReleaseDownloadList(self)
|
|
||||||
|
|
||||||
queue = self.drpc.get_alltorrents(ids)
|
|
||||||
|
|
||||||
if not queue:
|
|
||||||
log.debug('Nothing in queue or error')
|
|
||||||
return []
|
|
||||||
|
|
||||||
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']))
|
|
||||||
|
|
||||||
# Deluge has no easy way to work out if a torrent is stalled or failing.
|
|
||||||
#status = 'failed'
|
|
||||||
status = 'busy'
|
|
||||||
if torrent['is_seed'] and tryFloat(torrent['ratio']) < tryFloat(torrent['stop_ratio']):
|
|
||||||
# We have torrent['seeding_time'] to work out what the seeding time is, but we do not
|
|
||||||
# have access to the downloader seed_time, as with deluge we have no way to pass it
|
|
||||||
# when the torrent is added. So Deluge will only look at the ratio.
|
|
||||||
# See above comment in download().
|
|
||||||
status = 'seeding'
|
|
||||||
elif torrent['is_seed'] and torrent['is_finished'] and torrent['paused'] and torrent['state'] == 'Paused':
|
|
||||||
status = 'completed'
|
|
||||||
|
|
||||||
download_dir = sp(torrent['save_path'])
|
|
||||||
if torrent['move_on_completed']:
|
|
||||||
download_dir = torrent['move_completed_path']
|
|
||||||
|
|
||||||
torrent_files = []
|
|
||||||
for file_item in torrent['files']:
|
|
||||||
torrent_files.append(sp(os.path.join(download_dir, file_item['path'])))
|
|
||||||
|
|
||||||
release_downloads.append({
|
|
||||||
'id': torrent['hash'],
|
|
||||||
'name': torrent['name'],
|
|
||||||
'status': status,
|
|
||||||
'original_status': torrent['state'],
|
|
||||||
'seed_ratio': torrent['ratio'],
|
|
||||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
|
||||||
'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])),
|
|
||||||
'files': torrent_files,
|
|
||||||
})
|
|
||||||
|
|
||||||
return release_downloads
|
|
||||||
|
|
||||||
def pause(self, release_download, pause = True):
|
|
||||||
if pause:
|
|
||||||
return self.drpc.pause_torrent([release_download['id']])
|
|
||||||
else:
|
|
||||||
return self.drpc.resume_torrent([release_download['id']])
|
|
||||||
|
|
||||||
def removeFailed(self, release_download):
|
|
||||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
|
||||||
return self.drpc.remove_torrent(release_download['id'], True)
|
|
||||||
|
|
||||||
def processComplete(self, release_download, delete_files = False):
|
|
||||||
log.debug('Requesting Deluge to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
|
||||||
return self.drpc.remove_torrent(release_download['id'], remove_local_data = delete_files)
|
|
||||||
|
|
||||||
|
|
||||||
class DelugeRPC(object):
|
|
||||||
|
|
||||||
host = 'localhost'
|
|
||||||
port = 58846
|
|
||||||
username = None
|
|
||||||
password = None
|
|
||||||
client = None
|
|
||||||
|
|
||||||
def __init__(self, host = 'localhost', port = 58846, username = None, password = None):
|
|
||||||
super(DelugeRPC, self).__init__()
|
|
||||||
|
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
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:
|
|
||||||
self.connect()
|
|
||||||
torrent_id = self.client.core.add_torrent_magnet(torrent, options).get()
|
|
||||||
if not torrent_id:
|
|
||||||
torrent_id = self._check_torrent(True, torrent)
|
|
||||||
|
|
||||||
if torrent_id and options['label']:
|
|
||||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
|
||||||
except Exception as err:
|
|
||||||
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
|
|
||||||
finally:
|
|
||||||
if self.client:
|
|
||||||
self.disconnect()
|
|
||||||
|
|
||||||
return torrent_id
|
|
||||||
|
|
||||||
def add_torrent_file(self, filename, torrent, options):
|
|
||||||
torrent_id = False
|
|
||||||
try:
|
|
||||||
self.connect()
|
|
||||||
torrent_id = self.client.core.add_torrent_file(filename, b64encode(torrent), options).get()
|
|
||||||
if not torrent_id:
|
|
||||||
torrent_id = self._check_torrent(False, torrent)
|
|
||||||
|
|
||||||
if torrent_id and options['label']:
|
|
||||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
|
||||||
except Exception as err:
|
|
||||||
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
|
|
||||||
finally:
|
|
||||||
if self.client:
|
|
||||||
self.disconnect()
|
|
||||||
|
|
||||||
return torrent_id
|
|
||||||
|
|
||||||
def get_alltorrents(self, ids):
|
|
||||||
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:
|
|
||||||
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
|
|
||||||
finally:
|
|
||||||
if self.client:
|
|
||||||
self.disconnect()
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def pause_torrent(self, torrent_ids):
|
|
||||||
try:
|
|
||||||
self.connect()
|
|
||||||
self.client.core.pause_torrent(torrent_ids).get()
|
|
||||||
except Exception as err:
|
|
||||||
log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc()))
|
|
||||||
finally:
|
|
||||||
if self.client:
|
|
||||||
self.disconnect()
|
|
||||||
|
|
||||||
def resume_torrent(self, torrent_ids):
|
|
||||||
try:
|
|
||||||
self.connect()
|
|
||||||
self.client.core.resume_torrent(torrent_ids).get()
|
|
||||||
except Exception as err:
|
|
||||||
log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc()))
|
|
||||||
finally:
|
|
||||||
if self.client:
|
|
||||||
self.disconnect()
|
|
||||||
|
|
||||||
def remove_torrent(self, torrent_id, remove_local_data):
|
|
||||||
ret = False
|
|
||||||
try:
|
|
||||||
self.connect()
|
|
||||||
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
|
|
||||||
except Exception as err:
|
|
||||||
log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc()))
|
|
||||||
finally:
|
|
||||||
if self.client:
|
|
||||||
self.disconnect()
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
self.client.disconnect()
|
|
||||||
|
|
||||||
def _check_torrent(self, magnet, torrent):
|
|
||||||
# Torrent not added, check if it already existed.
|
|
||||||
if magnet:
|
|
||||||
torrent_hash = re.findall('urn:btih:([\w]{32,40})', torrent)[0]
|
|
||||||
else:
|
|
||||||
info = bdecode(torrent)["info"]
|
|
||||||
torrent_hash = sha1(benc(info)).hexdigest()
|
|
||||||
|
|
||||||
# Convert base 32 to hex
|
|
||||||
if len(torrent_hash) == 32:
|
|
||||||
torrent_hash = b16encode(b32decode(torrent_hash))
|
|
||||||
|
|
||||||
torrent_hash = torrent_hash.lower()
|
|
||||||
torrent_check = self.client.core.get_torrent_status(torrent_hash, {}).get()
|
|
||||||
if torrent_check['hash']:
|
|
||||||
return torrent_hash
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
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,293 +0,0 @@
|
|||||||
from base64 import standard_b64encode
|
|
||||||
from datetime import timedelta
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
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):
|
|
||||||
|
|
||||||
protocol = ['nzb']
|
|
||||||
rpc = 'xmlrpc'
|
|
||||||
|
|
||||||
def download(self, data = None, media = None, filedata = None):
|
|
||||||
if not media: media = {}
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
if not filedata:
|
|
||||||
log.error('Unable to get NZB file: %s', traceback.format_exc())
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.info('Sending "%s" to NZBGet.', data.get('name'))
|
|
||||||
|
|
||||||
nzb_name = ss('%s.nzb' % self.createNzbName(data, media))
|
|
||||||
|
|
||||||
rpc = self.getRPC()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name):
|
|
||||||
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
|
|
||||||
|
|
||||||
if re.search(r"^0", rpc.version()):
|
|
||||||
xml_response = rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip()))
|
|
||||||
else:
|
|
||||||
xml_response = rpc.append(nzb_name, self.conf('category'), tryInt(self.conf('priority')), False, standard_b64encode(filedata.strip()))
|
|
||||||
|
|
||||||
if xml_response:
|
|
||||||
log.info('NZB sent successfully to NZBGet')
|
|
||||||
nzb_id = md5(data['url']) # about as unique as they come ;)
|
|
||||||
couchpotato_id = "couchpotato=" + nzb_id
|
|
||||||
groups = rpc.listgroups()
|
|
||||||
file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name]
|
|
||||||
confirmed = rpc.editqueue("GroupSetParameter", 0, couchpotato_id, file_id)
|
|
||||||
if confirmed:
|
|
||||||
log.debug('couchpotato parameter set in nzbget download')
|
|
||||||
return self.downloadReturnId(nzb_id)
|
|
||||||
else:
|
|
||||||
log.error('NZBGet could not add %s to the queue.', nzb_name)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def 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()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if rpc.writelog('INFO', 'CouchPotato connected to check status'):
|
|
||||||
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 []
|
|
||||||
except xmlrpclib.ProtocolError as e:
|
|
||||||
if e.errcode == 401:
|
|
||||||
log.error('Password is incorrect.')
|
|
||||||
else:
|
|
||||||
log.error('Protocol Error: %s', e)
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Get NZBGet data
|
|
||||||
try:
|
|
||||||
status = rpc.status()
|
|
||||||
groups = rpc.listgroups()
|
|
||||||
queue = rpc.postqueue(0)
|
|
||||||
history = rpc.history()
|
|
||||||
except:
|
|
||||||
log.error('Failed getting data: %s', traceback.format_exc(1))
|
|
||||||
return []
|
|
||||||
|
|
||||||
release_downloads = ReleaseDownloadList(self)
|
|
||||||
|
|
||||||
for nzb in groups:
|
|
||||||
try:
|
|
||||||
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
|
|
||||||
except:
|
|
||||||
nzb_id = nzb['NZBID']
|
|
||||||
|
|
||||||
if nzb_id in ids:
|
|
||||||
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
|
|
||||||
timeleft = -1
|
|
||||||
try:
|
|
||||||
if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
|
||||||
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
release_downloads.append({
|
|
||||||
'id': nzb_id,
|
|
||||||
'name': nzb['NZBFilename'],
|
|
||||||
'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED',
|
|
||||||
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
|
|
||||||
'timeleft': timeleft,
|
|
||||||
})
|
|
||||||
|
|
||||||
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
|
|
||||||
if nzb['NZBID'] in ids:
|
|
||||||
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
|
|
||||||
release_downloads.append({
|
|
||||||
'id': nzb['NZBID'],
|
|
||||||
'name': nzb['NZBFilename'],
|
|
||||||
'original_status': nzb['Stage'],
|
|
||||||
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
|
|
||||||
})
|
|
||||||
|
|
||||||
for nzb in history:
|
|
||||||
try:
|
|
||||||
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
|
|
||||||
except:
|
|
||||||
nzb_id = nzb['NZBID']
|
|
||||||
|
|
||||||
if nzb_id in ids:
|
|
||||||
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
|
|
||||||
release_downloads.append({
|
|
||||||
'id': nzb_id,
|
|
||||||
'name': nzb['NZBFilename'],
|
|
||||||
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
|
|
||||||
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
|
|
||||||
'timeleft': str(timedelta(seconds = 0)),
|
|
||||||
'folder': sp(nzb['DestDir'])
|
|
||||||
})
|
|
||||||
|
|
||||||
return release_downloads
|
|
||||||
|
|
||||||
def removeFailed(self, release_download):
|
|
||||||
|
|
||||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
|
||||||
|
|
||||||
rpc = self.getRPC()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
|
|
||||||
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
|
|
||||||
|
|
||||||
try:
|
|
||||||
history = rpc.history()
|
|
||||||
nzb_id = None
|
|
||||||
path = None
|
|
||||||
|
|
||||||
for hist in history:
|
|
||||||
for param in hist['Parameters']:
|
|
||||||
if param['Name'] == 'couchpotato' and param['Value'] == release_download['id']:
|
|
||||||
nzb_id = hist['ID']
|
|
||||||
path = hist['DestDir']
|
|
||||||
|
|
||||||
if nzb_id and path and rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
|
|
||||||
shutil.rmtree(path, True)
|
|
||||||
except:
|
|
||||||
log.error('Failed deleting: %s', traceback.format_exc(0))
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
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.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from .main import NZBGet
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return NZBGet()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'nzbget',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'name': 'nzbget',
|
||||||
|
'label': 'NZBGet',
|
||||||
|
'description': 'Send NZBs to your NZBGet installation.',
|
||||||
|
'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': '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': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
from base64 import standard_b64encode
|
||||||
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from inspect import isfunction
|
||||||
|
import socket
|
||||||
|
import traceback
|
||||||
|
import xmlrpclib
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
class NZBGet(Downloader):
|
||||||
|
|
||||||
|
type = ['nzb']
|
||||||
|
|
||||||
|
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
|
||||||
|
|
||||||
|
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||||
|
|
||||||
|
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not filedata:
|
||||||
|
log.error('Unable to get NZB file: %s', traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.info('Sending "%s" to NZBGet.', data.get('name'))
|
||||||
|
|
||||||
|
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
|
||||||
|
nzb_name = '%s.nzb' % self.createNzbName(data, movie)
|
||||||
|
|
||||||
|
rpc = xmlrpclib.ServerProxy(url)
|
||||||
|
try:
|
||||||
|
if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name):
|
||||||
|
log.info('Successfully connected to NZBGet')
|
||||||
|
else:
|
||||||
|
log.info('Successfully connected to NZBGet, but unable to send a message')
|
||||||
|
except socket.error:
|
||||||
|
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
|
||||||
|
return False
|
||||||
|
except xmlrpclib.ProtocolError, e:
|
||||||
|
if e.errcode == 401:
|
||||||
|
log.error('Password is incorrect.')
|
||||||
|
else:
|
||||||
|
log.error('Protocol Error: %s', e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())):
|
||||||
|
log.info('NZB sent successfully to NZBGet')
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log.error('NZBGet could not add %s to the queue.', nzb_name)
|
||||||
|
return False
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
from base64 import b64encode
|
|
||||||
from urllib2 import URLError
|
|
||||||
from uuid import uuid4
|
|
||||||
import hashlib
|
|
||||||
import httplib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import ssl
|
|
||||||
import sys
|
|
||||||
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):
|
|
||||||
|
|
||||||
protocol = ['nzb']
|
|
||||||
api_level = None
|
|
||||||
session_id = None
|
|
||||||
|
|
||||||
def download(self, data = None, media = None, filedata = None):
|
|
||||||
if not media: media = {}
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
# Send the nzb
|
|
||||||
try:
|
|
||||||
nzb_filename = self.createFileName(data, filedata, media)
|
|
||||||
self.call('nzb/add', files = {'file': (nzb_filename, filedata)})
|
|
||||||
|
|
||||||
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]
|
|
||||||
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')
|
|
||||||
|
|
||||||
release_downloads = ReleaseDownloadList(self)
|
|
||||||
for nzb in raw_statuses.get('nzbs', []):
|
|
||||||
if nzb['id'] in ids:
|
|
||||||
|
|
||||||
# Check status
|
|
||||||
status = 'busy'
|
|
||||||
if nzb['state'] == 20:
|
|
||||||
status = 'completed'
|
|
||||||
elif nzb['state'] in [21, 22, 24]:
|
|
||||||
status = 'failed'
|
|
||||||
|
|
||||||
release_downloads.append({
|
|
||||||
'id': nzb['id'],
|
|
||||||
'name': nzb['uiTitle'],
|
|
||||||
'status': status,
|
|
||||||
'original_status': nzb['state'],
|
|
||||||
'timeleft': -1,
|
|
||||||
'folder': sp(nzb['destinationPath']),
|
|
||||||
})
|
|
||||||
|
|
||||||
return release_downloads
|
|
||||||
|
|
||||||
def removeFailed(self, release_download):
|
|
||||||
|
|
||||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.call('nzb/%s/cancel' % release_download['id'])
|
|
||||||
except:
|
|
||||||
log.error('Failed deleting: %s', traceback.format_exc(0))
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def login(self):
|
|
||||||
|
|
||||||
nonce = self.call('auth/nonce', auth = False).get('authNonce')
|
|
||||||
cnonce = uuid4().hex
|
|
||||||
hashed = b64encode(hashlib.sha256('%s:%s:%s' % (nonce, cnonce, self.conf('api_key'))).digest())
|
|
||||||
|
|
||||||
params = {
|
|
||||||
'nonce': nonce,
|
|
||||||
'cnonce': cnonce,
|
|
||||||
'hash': hashed
|
|
||||||
}
|
|
||||||
|
|
||||||
login_data = self.call('auth/login', parameters = params, auth = False)
|
|
||||||
|
|
||||||
# Save for later
|
|
||||||
if login_data.get('loginResult') == 'successful':
|
|
||||||
self.session_id = login_data.get('sessionID')
|
|
||||||
return True
|
|
||||||
|
|
||||||
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
|
|
||||||
if not parameters: parameters = {}
|
|
||||||
if not self.session_id and auth:
|
|
||||||
self.login()
|
|
||||||
|
|
||||||
# Always add session id to request
|
|
||||||
if self.session_id:
|
|
||||||
parameters['sessionid'] = self.session_id
|
|
||||||
|
|
||||||
params = tryUrlencode(parameters)
|
|
||||||
|
|
||||||
url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api/' + call
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = self.urlopen('%s?%s' % (url, params), *args, **kwargs)
|
|
||||||
|
|
||||||
if data:
|
|
||||||
return json.loads(data)
|
|
||||||
except URLError as e:
|
|
||||||
if hasattr(e, 'code') and e.code == 403:
|
|
||||||
# Try login and do again
|
|
||||||
if not repeat:
|
|
||||||
self.login()
|
|
||||||
return self.call(call, parameters = parameters, repeat = True, **kwargs)
|
|
||||||
|
|
||||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
|
||||||
except:
|
|
||||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def getApiLevel(self):
|
|
||||||
|
|
||||||
if not self.api_level:
|
|
||||||
|
|
||||||
url = cleanHost(self.conf('host')) + 'api/app/apilevel'
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = self.urlopen(url, show_error = False)
|
|
||||||
self.api_level = float(json.loads(data).get('apilevel'))
|
|
||||||
except URLError as 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:
|
|
||||||
log.error('NZBVortex doesn\'t seem to be running or maybe the remote option isn\'t enabled yet: %s', traceback.format_exc(1))
|
|
||||||
|
|
||||||
return self.api_level
|
|
||||||
|
|
||||||
def isEnabled(self, manual = False, data = None):
|
|
||||||
if not data: data = {}
|
|
||||||
return super(NZBVortex, self).isEnabled(manual, data) and self.getApiLevel()
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPSConnection(httplib.HTTPSConnection):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
sock = socket.create_connection((self.host, self.port), self.timeout)
|
|
||||||
if sys.version_info < (2, 6, 7):
|
|
||||||
if hasattr(self, '_tunnel_host'):
|
|
||||||
self.sock = sock
|
|
||||||
self._tunnel()
|
|
||||||
else:
|
|
||||||
if self._tunnel_host:
|
|
||||||
self.sock = sock
|
|
||||||
self._tunnel()
|
|
||||||
|
|
||||||
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.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from .main import Pneumatic
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Pneumatic()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'pneumatic',
|
||||||
|
'order': 30,
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'name': 'pneumatic',
|
||||||
|
'label': 'Pneumatic',
|
||||||
|
'description': 'Download the .strm file to a specific folder.',
|
||||||
|
'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.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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):
|
||||||
|
|
||||||
|
type = ['nzb']
|
||||||
|
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
|
||||||
|
|
||||||
|
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||||
|
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type'))):
|
||||||
|
return
|
||||||
|
|
||||||
|
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, movie))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not os.path.isfile(fullPath):
|
||||||
|
log.info('Downloading %s to %s.', (data.get('type'), fullPath))
|
||||||
|
with open(fullPath, 'wb') as f:
|
||||||
|
f.write(filedata)
|
||||||
|
|
||||||
|
nzb_name = self.createNzbName(data, movie)
|
||||||
|
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 True
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.info('File %s already exists.', fullPath)
|
||||||
|
return True
|
||||||
|
|
||||||
|
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.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -1,343 +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
|
|
||||||
from scandir import scandir
|
|
||||||
|
|
||||||
|
|
||||||
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 torrent.hashing or torrent.hash_checking or torrent.message:
|
|
||||||
return 'busy'
|
|
||||||
|
|
||||||
if not torrent.complete:
|
|
||||||
return 'busy'
|
|
||||||
|
|
||||||
if not torrent.open:
|
|
||||||
return 'completed'
|
|
||||||
|
|
||||||
if torrent.state and torrent.active:
|
|
||||||
return 'seeding'
|
|
||||||
|
|
||||||
return 'busy'
|
|
||||||
|
|
||||||
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 scandir.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
|
|
||||||
|
|
||||||
|
|
||||||
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.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
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):
|
|
||||||
|
|
||||||
protocol = ['nzb']
|
|
||||||
|
|
||||||
def download(self, data = None, media = None, filedata = None):
|
|
||||||
if not media: media = {}
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
|
||||||
|
|
||||||
req_params = {
|
|
||||||
'cat': self.conf('category'),
|
|
||||||
'mode': 'addurl',
|
|
||||||
'nzbname': self.createNzbName(data, media),
|
|
||||||
'priority': self.conf('priority'),
|
|
||||||
}
|
|
||||||
|
|
||||||
nzb_filename = None
|
|
||||||
if filedata:
|
|
||||||
if len(filedata) < 50:
|
|
||||||
log.error('No proper nzb available: %s', filedata)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
|
||||||
nzb_filename = self.createFileName(data, filedata, media)
|
|
||||||
req_params['mode'] = 'addfile'
|
|
||||||
else:
|
|
||||||
req_params['name'] = data.get('url')
|
|
||||||
|
|
||||||
try:
|
|
||||||
if nzb_filename and req_params.get('mode') is 'addfile':
|
|
||||||
sab_data = self.call(req_params, files = {'nzbfile': (ss(nzb_filename), filedata)})
|
|
||||||
else:
|
|
||||||
sab_data = self.call(req_params)
|
|
||||||
except URLError:
|
|
||||||
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
|
|
||||||
return False
|
|
||||||
except:
|
|
||||||
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.debug('Result from SAB: %s', sab_data)
|
|
||||||
if sab_data.get('status') and not sab_data.get('error'):
|
|
||||||
log.info('NZB sent to SAB successfully.')
|
|
||||||
if filedata:
|
|
||||||
return self.downloadReturnId(sab_data.get('nzo_ids')[0])
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
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.')
|
|
||||||
|
|
||||||
# Go through Queue
|
|
||||||
try:
|
|
||||||
queue = self.call({
|
|
||||||
'mode': 'queue',
|
|
||||||
})
|
|
||||||
except:
|
|
||||||
log.error('Failed getting queue: %s', traceback.format_exc(1))
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Go through history items
|
|
||||||
try:
|
|
||||||
history = self.call({
|
|
||||||
'mode': 'history',
|
|
||||||
'limit': 15,
|
|
||||||
})
|
|
||||||
except:
|
|
||||||
log.error('Failed getting history json: %s', traceback.format_exc(1))
|
|
||||||
return []
|
|
||||||
|
|
||||||
release_downloads = ReleaseDownloadList(self)
|
|
||||||
|
|
||||||
# Get busy releases
|
|
||||||
for nzb in queue.get('slots', []):
|
|
||||||
if nzb['nzo_id'] in ids:
|
|
||||||
status = 'busy'
|
|
||||||
if 'ENCRYPTED / ' in nzb['filename']:
|
|
||||||
status = 'failed'
|
|
||||||
|
|
||||||
release_downloads.append({
|
|
||||||
'id': nzb['nzo_id'],
|
|
||||||
'name': nzb['filename'],
|
|
||||||
'status': status,
|
|
||||||
'original_status': nzb['status'],
|
|
||||||
'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Get old releases
|
|
||||||
for nzb in history.get('slots', []):
|
|
||||||
if nzb['nzo_id'] in ids:
|
|
||||||
status = 'busy'
|
|
||||||
if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()):
|
|
||||||
status = 'failed'
|
|
||||||
elif nzb['status'] == 'Completed':
|
|
||||||
status = 'completed'
|
|
||||||
|
|
||||||
release_downloads.append({
|
|
||||||
'id': nzb['nzo_id'],
|
|
||||||
'name': nzb['name'],
|
|
||||||
'status': status,
|
|
||||||
'original_status': nzb['status'],
|
|
||||||
'timeleft': str(timedelta(seconds = 0)),
|
|
||||||
'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']),
|
|
||||||
})
|
|
||||||
|
|
||||||
return release_downloads
|
|
||||||
|
|
||||||
def removeFailed(self, release_download):
|
|
||||||
|
|
||||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.call({
|
|
||||||
'mode': 'queue',
|
|
||||||
'name': 'delete',
|
|
||||||
'del_files': '1',
|
|
||||||
'value': release_download['id']
|
|
||||||
}, use_json = False)
|
|
||||||
self.call({
|
|
||||||
'mode': 'history',
|
|
||||||
'name': 'delete',
|
|
||||||
'del_files': '1',
|
|
||||||
'value': release_download['id']
|
|
||||||
}, use_json = False)
|
|
||||||
except:
|
|
||||||
log.error('Failed deleting: %s', traceback.format_exc(0))
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def processComplete(self, release_download, delete_files = False):
|
|
||||||
log.debug('Requesting SabNZBd to remove the NZB %s.', release_download['name'])
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.call({
|
|
||||||
'mode': 'history',
|
|
||||||
'name': 'delete',
|
|
||||||
'del_files': '0',
|
|
||||||
'value': release_download['id']
|
|
||||||
}, use_json = False)
|
|
||||||
except:
|
|
||||||
log.error('Failed removing: %s', traceback.format_exc(0))
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
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'
|
|
||||||
}))
|
|
||||||
|
|
||||||
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs)
|
|
||||||
if use_json:
|
|
||||||
d = json.loads(data)
|
|
||||||
if d.get('error'):
|
|
||||||
log.error('Error getting data from SABNZBd: %s', d.get('error'))
|
|
||||||
return {}
|
|
||||||
|
|
||||||
return d.get(request_params['mode']) or d
|
|
||||||
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.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
from .main import Sabnzbd
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Sabnzbd()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'sabnzbd',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'name': 'sabnzbd',
|
||||||
|
'label': 'Sabnzbd',
|
||||||
|
'description': 'Send NZBs to your Sabnzbd installation.',
|
||||||
|
'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': '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,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Delete a release after the download has failed.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
|
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from urllib2 import URLError
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
class Sabnzbd(Downloader):
|
||||||
|
|
||||||
|
type = ['nzb']
|
||||||
|
|
||||||
|
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||||
|
|
||||||
|
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
||||||
|
return
|
||||||
|
|
||||||
|
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'apikey': self.conf('api_key'),
|
||||||
|
'cat': self.conf('category'),
|
||||||
|
'mode': 'addurl',
|
||||||
|
'nzbname': self.createNzbName(data, movie),
|
||||||
|
}
|
||||||
|
|
||||||
|
if filedata:
|
||||||
|
if len(filedata) < 50:
|
||||||
|
log.error('No proper nzb available: %s', (filedata))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||||
|
nzb_filename = self.createFileName(data, filedata, movie)
|
||||||
|
params['mode'] = 'addfile'
|
||||||
|
else:
|
||||||
|
params['name'] = data.get('url')
|
||||||
|
|
||||||
|
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if params.get('mode') is 'addfile':
|
||||||
|
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (nzb_filename, filedata)}, multipart = True, show_error = False)
|
||||||
|
else:
|
||||||
|
sab = self.urlopen(url, timeout = 60, show_error = False)
|
||||||
|
except URLError:
|
||||||
|
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
|
||||||
|
return False
|
||||||
|
except:
|
||||||
|
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = sab.strip()
|
||||||
|
if not result:
|
||||||
|
log.error('SABnzbd didn\'t return anything.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.debug('Result text from SAB: %s', result[:40])
|
||||||
|
if result[:2] == 'ok':
|
||||||
|
log.info('NZB sent to SAB successfully.')
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log.error(result[:40])
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
if self.isDisabled(manual = False):
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.debug('Checking SABnzbd download status.')
|
||||||
|
|
||||||
|
# Go through Queue
|
||||||
|
try:
|
||||||
|
queue = self.call({
|
||||||
|
'mode': 'queue',
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
log.error('Failed getting queue: %s', traceback.format_exc(1))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Go through history items
|
||||||
|
try:
|
||||||
|
history = self.call({
|
||||||
|
'mode': 'history',
|
||||||
|
'limit': 15,
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
log.error('Failed getting history json: %s', traceback.format_exc(1))
|
||||||
|
return False
|
||||||
|
|
||||||
|
statuses = []
|
||||||
|
|
||||||
|
# Get busy releases
|
||||||
|
for item in queue.get('slots', []):
|
||||||
|
statuses.append({
|
||||||
|
'id': item['nzo_id'],
|
||||||
|
'name': item['filename'],
|
||||||
|
'status': 'busy',
|
||||||
|
'original_status': item['status'],
|
||||||
|
'timeleft': item['timeleft'] if not queue['paused'] else -1,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get old releases
|
||||||
|
for item in history.get('slots', []):
|
||||||
|
|
||||||
|
status = 'busy'
|
||||||
|
if item['status'] == 'Failed' or (item['status'] == 'Completed' and item['fail_message'].strip()):
|
||||||
|
status = 'failed'
|
||||||
|
elif item['status'] == 'Completed':
|
||||||
|
status = 'completed'
|
||||||
|
|
||||||
|
statuses.append({
|
||||||
|
'id': item['nzo_id'],
|
||||||
|
'name': item['name'],
|
||||||
|
'status': status,
|
||||||
|
'original_status': item['status'],
|
||||||
|
'timeleft': 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
|
||||||
|
if not self.conf('delete_failed', default = True):
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.call({
|
||||||
|
'mode': 'history',
|
||||||
|
'name': 'delete',
|
||||||
|
'del_files': '1',
|
||||||
|
'value': item['id']
|
||||||
|
}, use_json = False)
|
||||||
|
except:
|
||||||
|
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def call(self, params, use_json = True):
|
||||||
|
|
||||||
|
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
|
||||||
|
'apikey': self.conf('api_key'),
|
||||||
|
'output': 'json'
|
||||||
|
}))
|
||||||
|
|
||||||
|
data = self.urlopen(url, timeout = 60, show_error = False)
|
||||||
|
if use_json:
|
||||||
|
d = json.loads(data)
|
||||||
|
if d.get('error'):
|
||||||
|
log.error('Error getting data from SABNZBd: %s', d.get('error'))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return d[params['mode']]
|
||||||
|
else:
|
||||||
|
return data
|
||||||
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
import json
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from couchpotato.core._base.downloader.main import DownloaderBase
|
|
||||||
from couchpotato.core.helpers.encoding import isInt
|
|
||||||
from couchpotato.core.helpers.variable import cleanHost
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
import requests
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
autoload = 'Synology'
|
|
||||||
|
|
||||||
|
|
||||||
class Synology(DownloaderBase):
|
|
||||||
|
|
||||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
|
||||||
status_support = False
|
|
||||||
|
|
||||||
def download(self, data = None, media = None, filedata = None):
|
|
||||||
if not media: media = {}
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
response = False
|
|
||||||
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(':')
|
|
||||||
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'))
|
|
||||||
if data['protocol'] == 'torrent_magnet':
|
|
||||||
log.info('Adding torrent URL %s', data['url'])
|
|
||||||
response = srpc.create_task(url = data['url'])
|
|
||||||
elif data['protocol'] in ['nzb', 'torrent']:
|
|
||||||
log.info('Adding %s' % data['protocol'])
|
|
||||||
if not filedata:
|
|
||||||
log.error('No %s data found', data['protocol'])
|
|
||||||
else:
|
|
||||||
filename = data['name'] + '.' + data['protocol']
|
|
||||||
response = srpc.create_task(filename = filename, filedata = filedata)
|
|
||||||
except:
|
|
||||||
log.error('Exception while adding torrent: %s', traceback.format_exc())
|
|
||||||
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()
|
|
||||||
elif self.conf('use_for') == 'torrent':
|
|
||||||
return ['torrent', 'torrent_magnet']
|
|
||||||
else:
|
|
||||||
return ['nzb']
|
|
||||||
|
|
||||||
def isEnabled(self, manual = False, data = None):
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
for_protocol = ['both']
|
|
||||||
if data and 'torrent' in data.get('protocol'):
|
|
||||||
for_protocol.append('torrent')
|
|
||||||
elif data:
|
|
||||||
for_protocol.append(data.get('protocol'))
|
|
||||||
|
|
||||||
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):
|
|
||||||
|
|
||||||
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.username = username
|
|
||||||
self.password = password
|
|
||||||
self.destination = destination
|
|
||||||
self.session_name = 'DownloadStation'
|
|
||||||
|
|
||||||
def _login(self):
|
|
||||||
if self.username and self.password:
|
|
||||||
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
|
|
||||||
'method': 'login', 'session': self.session_name, 'format': 'sid'}
|
|
||||||
response = self._req(self.auth_url, args)
|
|
||||||
if response['success']:
|
|
||||||
self.sid = response['data']['sid']
|
|
||||||
log.debug('sid=%s', self.sid)
|
|
||||||
else:
|
|
||||||
log.error('Couldn\'t login to Synology, %s', response)
|
|
||||||
return response['success']
|
|
||||||
else:
|
|
||||||
log.error('User or password missing, not using authentication.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _logout(self):
|
|
||||||
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
|
|
||||||
return self._req(self.auth_url, args)
|
|
||||||
|
|
||||||
def _req(self, url, args, files = None):
|
|
||||||
response = {'success': False}
|
|
||||||
try:
|
|
||||||
req = requests.post(url, data = args, files = files)
|
|
||||||
req.raise_for_status()
|
|
||||||
response = json.loads(req.text)
|
|
||||||
if response['success']:
|
|
||||||
log.info('Synology action successfull')
|
|
||||||
return response
|
|
||||||
except requests.ConnectionError as err:
|
|
||||||
log.error('Synology connection error, check your config %s', err)
|
|
||||||
except requests.HTTPError as err:
|
|
||||||
log.error('SynologyRPC HTTPError: %s', err)
|
|
||||||
except Exception as err:
|
|
||||||
log.error('Exception: %s', err)
|
|
||||||
finally:
|
|
||||||
return response
|
|
||||||
|
|
||||||
def create_task(self, url = None, filename = None, filedata = None):
|
|
||||||
""" Creates new download task in Synology DownloadStation. Either specify
|
|
||||||
url or pair (filename, filedata).
|
|
||||||
|
|
||||||
Returns True if task was created, False otherwise
|
|
||||||
"""
|
|
||||||
result = False
|
|
||||||
# login
|
|
||||||
if self._login():
|
|
||||||
args = {'api': 'SYNO.DownloadStation.Task',
|
|
||||||
'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
|
|
||||||
response = self._req(self.download_url, args = args)
|
|
||||||
log.info('Response: %s', response)
|
|
||||||
result = response['success']
|
|
||||||
elif filename and filedata:
|
|
||||||
log.info('Login success, adding torrent')
|
|
||||||
files = {'file': (filename, filedata)}
|
|
||||||
response = self._req(self.download_url, args = args, files = files)
|
|
||||||
log.info('Response: %s', response)
|
|
||||||
result = response['success']
|
|
||||||
else:
|
|
||||||
log.error('Invalid use of SynologyRPC.create_task: either url or filename+filedata must be specified')
|
|
||||||
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.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from .main import Synology
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Synology()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'synology',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'name': 'synology',
|
||||||
|
'label': 'Synology',
|
||||||
|
'description': 'Send torrents to Synology\'s Download Station.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:5000',
|
||||||
|
'description': 'Hostname with port. Usually <strong>localhost:5000</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
|
from couchpotato.core.helpers.encoding import isInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
import httplib
|
||||||
|
import json
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
class Synology(Downloader):
|
||||||
|
|
||||||
|
type = ['torrent_magnet']
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
def download(self, data, movie, manual = False, filedata = None):
|
||||||
|
|
||||||
|
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
||||||
|
return
|
||||||
|
|
||||||
|
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type')))
|
||||||
|
|
||||||
|
# Load host from config and split out port.
|
||||||
|
host = self.conf('host').split(':')
|
||||||
|
if not isInt(host[1]):
|
||||||
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if data.get('type') == 'torrent':
|
||||||
|
log.error('Can\'t add binary torrent file')
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Send request to Transmission
|
||||||
|
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||||
|
remote_torrent = srpc.add_torrent_uri(data.get('url'))
|
||||||
|
log.info('Response: %s', remote_torrent)
|
||||||
|
return remote_torrent['success']
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Exception while adding torrent: %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class SynologyRPC(object):
|
||||||
|
|
||||||
|
'''SynologyRPC lite library'''
|
||||||
|
|
||||||
|
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.username = username
|
||||||
|
self.password = password
|
||||||
|
self.session_name = 'DownloadStation'
|
||||||
|
|
||||||
|
def _login(self):
|
||||||
|
if self.username and self.password:
|
||||||
|
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
|
||||||
|
'method': 'login', 'session': self.session_name, 'format': 'sid'}
|
||||||
|
response = self._req(self.auth_url, args)
|
||||||
|
if response['success'] == True:
|
||||||
|
self.sid = response['data']['sid']
|
||||||
|
log.debug('Sid=%s', self.sid)
|
||||||
|
return response
|
||||||
|
elif self.username or self.password:
|
||||||
|
log.error('User or password missing, not using authentication.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _logout(self):
|
||||||
|
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
|
||||||
|
return self._req(self.auth_url, args)
|
||||||
|
|
||||||
|
def _req(self, url, args):
|
||||||
|
req_url = url + '?' + urllib.urlencode(args)
|
||||||
|
try:
|
||||||
|
req_open = urllib2.urlopen(req_url)
|
||||||
|
response = json.loads(req_open.read())
|
||||||
|
if response['success'] == True:
|
||||||
|
log.info('Synology action successfull')
|
||||||
|
return response
|
||||||
|
except httplib.InvalidURL, err:
|
||||||
|
log.error('Invalid Transmission host, check your config %s', err)
|
||||||
|
return False
|
||||||
|
except urllib2.HTTPError, err:
|
||||||
|
log.error('SynologyRPC HTTPError: %s', err)
|
||||||
|
return False
|
||||||
|
except urllib2.URLError, err:
|
||||||
|
log.error('Unable to connect to Synology %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_torrent_uri(self, torrent):
|
||||||
|
log.info('Adding torrent URL %s', torrent)
|
||||||
|
response = {}
|
||||||
|
# login
|
||||||
|
login = self._login()
|
||||||
|
if len(login) > 0 and login['success'] == True:
|
||||||
|
log.info('Login success, adding torrent')
|
||||||
|
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid}
|
||||||
|
response = self._req(self.download_url, args)
|
||||||
|
self._logout()
|
||||||
|
else:
|
||||||
|
log.error('Couldn\'t login to Synology, %s', login)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
from base64 import b64encode
|
|
||||||
from datetime import timedelta
|
|
||||||
import httplib
|
|
||||||
import json
|
|
||||||
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):
|
|
||||||
|
|
||||||
protocol = ['torrent', 'torrent_magnet']
|
|
||||||
log = CPLog(__name__)
|
|
||||||
trpc = None
|
|
||||||
|
|
||||||
def connect(self, reconnect = False):
|
|
||||||
# Load host from config and split out port.
|
|
||||||
host = cleanHost(self.conf('host'), protocol = False).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'))
|
|
||||||
|
|
||||||
return self.trpc
|
|
||||||
|
|
||||||
def download(self, data = None, media = None, filedata = None):
|
|
||||||
if not media: media = {}
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol')))
|
|
||||||
|
|
||||||
if not self.connect():
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not filedata and data.get('protocol') == 'torrent':
|
|
||||||
log.error('Failed sending torrent, no data')
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Set parameters for adding torrent
|
|
||||||
params = {
|
|
||||||
'paused': self.conf('paused', default = False)
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.conf('directory'):
|
|
||||||
if os.path.isdir(self.conf('directory')):
|
|
||||||
params['download-dir'] = self.conf('directory')
|
|
||||||
else:
|
|
||||||
log.error('Download directory from Transmission settings: %s doesn\'t exist', self.conf('directory'))
|
|
||||||
|
|
||||||
# Change parameters of torrent
|
|
||||||
torrent_params = {}
|
|
||||||
if data.get('seed_ratio'):
|
|
||||||
torrent_params['seedRatioLimit'] = tryFloat(data.get('seed_ratio'))
|
|
||||||
torrent_params['seedRatioMode'] = 1
|
|
||||||
|
|
||||||
if data.get('seed_time'):
|
|
||||||
torrent_params['seedIdleLimit'] = tryInt(data.get('seed_time')) * 60
|
|
||||||
torrent_params['seedIdleMode'] = 1
|
|
||||||
|
|
||||||
# Send request to Transmission
|
|
||||||
if data.get('protocol') == 'torrent_magnet':
|
|
||||||
remote_torrent = self.trpc.add_torrent_uri(data.get('url'), arguments = params)
|
|
||||||
torrent_params['trackerAdd'] = self.torrent_trackers
|
|
||||||
else:
|
|
||||||
remote_torrent = self.trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
|
||||||
|
|
||||||
if not remote_torrent:
|
|
||||||
log.error('Failed sending torrent to Transmission')
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Change settings of added torrents
|
|
||||||
if torrent_params:
|
|
||||||
self.trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
|
||||||
|
|
||||||
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.')
|
|
||||||
|
|
||||||
if not self.connect():
|
|
||||||
return []
|
|
||||||
|
|
||||||
release_downloads = ReleaseDownloadList(self)
|
|
||||||
|
|
||||||
return_params = {
|
|
||||||
'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')
|
|
||||||
return []
|
|
||||||
|
|
||||||
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']))
|
|
||||||
|
|
||||||
status = 'busy'
|
|
||||||
if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'):
|
|
||||||
status = 'failed'
|
|
||||||
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
|
|
||||||
status = 'completed'
|
|
||||||
elif torrent['status'] in [5, 6]:
|
|
||||||
status = 'seeding'
|
|
||||||
|
|
||||||
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'],
|
|
||||||
'status': status,
|
|
||||||
'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
|
|
||||||
})
|
|
||||||
|
|
||||||
return release_downloads
|
|
||||||
|
|
||||||
def pause(self, release_download, pause = True):
|
|
||||||
if pause:
|
|
||||||
return self.trpc.stop_torrent(release_download['id'])
|
|
||||||
else:
|
|
||||||
return self.trpc.start_torrent(release_download['id'])
|
|
||||||
|
|
||||||
def removeFailed(self, release_download):
|
|
||||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
|
||||||
return self.trpc.remove_torrent(release_download['id'], True)
|
|
||||||
|
|
||||||
def processComplete(self, release_download, delete_files = False):
|
|
||||||
log.debug('Requesting Transmission to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
|
||||||
return self.trpc.remove_torrent(release_download['id'], delete_files)
|
|
||||||
|
|
||||||
|
|
||||||
class TransmissionRPC(object):
|
|
||||||
|
|
||||||
"""TransmissionRPC lite library"""
|
|
||||||
def __init__(self, host = 'localhost', port = 9091, rpc_url = 'transmission', username = None, password = None):
|
|
||||||
|
|
||||||
super(TransmissionRPC, self).__init__()
|
|
||||||
|
|
||||||
self.url = 'http://' + host + ':' + str(port) + '/' + rpc_url + '/rpc'
|
|
||||||
self.tag = 0
|
|
||||||
self.session_id = 0
|
|
||||||
self.session = {}
|
|
||||||
if username and password:
|
|
||||||
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
|
||||||
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:
|
|
||||||
log.debug('User or password missing, not using authentication.')
|
|
||||||
self.session = self.get_session()
|
|
||||||
|
|
||||||
def _request(self, ojson):
|
|
||||||
self.tag += 1
|
|
||||||
headers = {'x-transmission-session-id': str(self.session_id)}
|
|
||||||
request = urllib2.Request(self.url, json.dumps(ojson).encode('utf-8'), headers)
|
|
||||||
try:
|
|
||||||
open_request = urllib2.urlopen(request)
|
|
||||||
response = json.loads(open_request.read())
|
|
||||||
log.debug('request: %s', json.dumps(ojson))
|
|
||||||
log.debug('response: %s', json.dumps(response))
|
|
||||||
if response['result'] == 'success':
|
|
||||||
log.debug('Transmission action successful')
|
|
||||||
return response['arguments']
|
|
||||||
else:
|
|
||||||
log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
|
|
||||||
return False
|
|
||||||
except httplib.InvalidURL as err:
|
|
||||||
log.error('Invalid Transmission host, check your config %s', err)
|
|
||||||
return False
|
|
||||||
except urllib2.HTTPError as err:
|
|
||||||
if err.code == 401:
|
|
||||||
log.error('Invalid Transmission Username or Password, check your config')
|
|
||||||
return False
|
|
||||||
elif err.code == 409:
|
|
||||||
msg = str(err.read())
|
|
||||||
try:
|
|
||||||
self.session_id = \
|
|
||||||
re.search('X-Transmission-Session-Id:\s*(\w+)', msg).group(1)
|
|
||||||
log.debug('X-Transmission-Session-Id: %s', self.session_id)
|
|
||||||
|
|
||||||
# #resend request with the updated header
|
|
||||||
|
|
||||||
return self._request(ojson)
|
|
||||||
except:
|
|
||||||
log.error('Unable to get Transmission Session-Id %s', err)
|
|
||||||
else:
|
|
||||||
log.error('TransmissionRPC HTTPError: %s', err)
|
|
||||||
except urllib2.URLError as err:
|
|
||||||
log.error('Unable to connect to Transmission %s', err)
|
|
||||||
|
|
||||||
def get_session(self):
|
|
||||||
post_data = {'method': 'session-get', 'tag': self.tag}
|
|
||||||
return self._request(post_data)
|
|
||||||
|
|
||||||
def add_torrent_uri(self, torrent, arguments):
|
|
||||||
arguments['filename'] = torrent
|
|
||||||
post_data = {'arguments': arguments, 'method': 'torrent-add', 'tag': self.tag}
|
|
||||||
return self._request(post_data)
|
|
||||||
|
|
||||||
def add_torrent_file(self, torrent, arguments):
|
|
||||||
arguments['metainfo'] = torrent
|
|
||||||
post_data = {'arguments': arguments, 'method': 'torrent-add', 'tag': self.tag}
|
|
||||||
return self._request(post_data)
|
|
||||||
|
|
||||||
def set_torrent(self, torrent_id, arguments):
|
|
||||||
arguments['ids'] = torrent_id
|
|
||||||
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
|
|
||||||
return self._request(post_data)
|
|
||||||
|
|
||||||
def get_alltorrents(self, arguments):
|
|
||||||
post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag}
|
|
||||||
return self._request(post_data)
|
|
||||||
|
|
||||||
def stop_torrent(self, torrent_id):
|
|
||||||
post_data = {'arguments': {'ids': torrent_id}, 'method': 'torrent-stop', 'tag': self.tag}
|
|
||||||
return self._request(post_data)
|
|
||||||
|
|
||||||
def start_torrent(self, torrent_id):
|
|
||||||
post_data = {'arguments': {'ids': torrent_id}, 'method': 'torrent-start', 'tag': self.tag}
|
|
||||||
return self._request(post_data)
|
|
||||||
|
|
||||||
def remove_torrent(self, torrent_id, delete_local_data):
|
|
||||||
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.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from .main import Transmission
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Transmission()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'transmission',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'name': 'transmission',
|
||||||
|
'label': 'Transmission',
|
||||||
|
'description': 'Send torrents to Transmission.',
|
||||||
|
'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': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'paused',
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
'description': 'Add the torrent paused.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'directory',
|
||||||
|
'type': 'directory',
|
||||||
|
'description': 'Where should Transmission saved the downloaded files?',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ratio',
|
||||||
|
'default': 10,
|
||||||
|
'type': 'int',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Stop transfer when reaching ratio',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
from base64 import b64encode
|
||||||
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
|
from couchpotato.core.helpers.encoding import isInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
import httplib
|
||||||
|
import json
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Transmission(Downloader):
|
||||||
|
|
||||||
|
type = ['torrent', 'torrent_magnet']
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
def download(self, data, movie, manual = False, filedata = None):
|
||||||
|
|
||||||
|
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
||||||
|
return
|
||||||
|
|
||||||
|
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
|
||||||
|
|
||||||
|
# Load host from config and split out port.
|
||||||
|
host = self.conf('host').split(':')
|
||||||
|
if not isInt(host[1]):
|
||||||
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Set parameters for Transmission
|
||||||
|
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
|
||||||
|
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
||||||
|
|
||||||
|
# Create the empty folder to download too
|
||||||
|
self.makeDir(folder_path)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'paused': self.conf('paused', default = 0),
|
||||||
|
'download-dir': folder_path
|
||||||
|
}
|
||||||
|
|
||||||
|
torrent_params = {
|
||||||
|
'seedRatioLimit': self.conf('ratio'),
|
||||||
|
'seedRatioMode': (0 if self.conf('ratio') else 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if not filedata and data.get('type') == 'torrent':
|
||||||
|
log.error('Failed sending torrent, no data')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Send request to Transmission
|
||||||
|
try:
|
||||||
|
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||||
|
if data.get('type') == 'torrent_magnet':
|
||||||
|
remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
|
||||||
|
torrent_params['trackerAdd'] = self.torrent_trackers
|
||||||
|
else:
|
||||||
|
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
||||||
|
|
||||||
|
# Change settings of added torrents
|
||||||
|
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to change settings for transfer: %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class TransmissionRPC(object):
|
||||||
|
|
||||||
|
"""TransmissionRPC lite library"""
|
||||||
|
|
||||||
|
def __init__(self, host = 'localhost', port = 9091, username = None, password = None):
|
||||||
|
|
||||||
|
super(TransmissionRPC, self).__init__()
|
||||||
|
|
||||||
|
self.url = 'http://' + host + ':' + str(port) + '/transmission/rpc'
|
||||||
|
self.tag = 0
|
||||||
|
self.session_id = 0
|
||||||
|
self.session = {}
|
||||||
|
if username and password:
|
||||||
|
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||||
|
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:
|
||||||
|
log.debug('User or password missing, not using authentication.')
|
||||||
|
self.session = self.get_session()
|
||||||
|
|
||||||
|
def _request(self, ojson):
|
||||||
|
self.tag += 1
|
||||||
|
headers = {'x-transmission-session-id': str(self.session_id)}
|
||||||
|
request = urllib2.Request(self.url, json.dumps(ojson).encode('utf-8'), headers)
|
||||||
|
try:
|
||||||
|
open_request = urllib2.urlopen(request)
|
||||||
|
response = json.loads(open_request.read())
|
||||||
|
log.debug('response: %s', json.dumps(response))
|
||||||
|
if response['result'] == 'success':
|
||||||
|
log.debug('Transmission action successfull')
|
||||||
|
return response['arguments']
|
||||||
|
else:
|
||||||
|
log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
|
||||||
|
return False
|
||||||
|
except httplib.InvalidURL, err:
|
||||||
|
log.error('Invalid Transmission host, check your config %s', err)
|
||||||
|
return False
|
||||||
|
except urllib2.HTTPError, err:
|
||||||
|
if err.code == 401:
|
||||||
|
log.error('Invalid Transmission Username or Password, check your config')
|
||||||
|
return False
|
||||||
|
elif err.code == 409:
|
||||||
|
msg = str(err.read())
|
||||||
|
try:
|
||||||
|
self.session_id = \
|
||||||
|
re.search('X-Transmission-Session-Id:\s*(\w+)', msg).group(1)
|
||||||
|
log.debug('X-Transmission-Session-Id: %s', self.session_id)
|
||||||
|
|
||||||
|
# #resend request with the updated header
|
||||||
|
|
||||||
|
return self._request(ojson)
|
||||||
|
except:
|
||||||
|
log.error('Unable to get Transmission Session-Id %s', err)
|
||||||
|
else:
|
||||||
|
log.error('TransmissionRPC HTTPError: %s', err)
|
||||||
|
except urllib2.URLError, err:
|
||||||
|
log.error('Unable to connect to Transmission %s', err)
|
||||||
|
|
||||||
|
def get_session(self):
|
||||||
|
post_data = {'method': 'session-get', 'tag': self.tag}
|
||||||
|
return self._request(post_data)
|
||||||
|
|
||||||
|
def add_torrent_uri(self, torrent, arguments):
|
||||||
|
arguments['filename'] = torrent
|
||||||
|
post_data = {'arguments': arguments, 'method': 'torrent-add', 'tag': self.tag}
|
||||||
|
return self._request(post_data)
|
||||||
|
|
||||||
|
def add_torrent_file(self, torrent, arguments):
|
||||||
|
arguments['metainfo'] = torrent
|
||||||
|
post_data = {'arguments': arguments, 'method': 'torrent-add', 'tag': self.tag}
|
||||||
|
return self._request(post_data)
|
||||||
|
|
||||||
|
def set_torrent(self, torrent_id, arguments):
|
||||||
|
arguments['ids'] = torrent_id
|
||||||
|
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
|
||||||
|
return self._request(post_data)
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
from base64 import b16encode, b32decode
|
|
||||||
from datetime import timedelta
|
|
||||||
from hashlib import sha1
|
|
||||||
import cookielib
|
|
||||||
import httplib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import stat
|
|
||||||
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):
|
|
||||||
|
|
||||||
protocol = ['torrent', 'torrent_magnet']
|
|
||||||
utorrent_api = None
|
|
||||||
status_flags = {
|
|
||||||
'STARTED': 1,
|
|
||||||
'CHECKING': 2,
|
|
||||||
'CHECK-START': 4,
|
|
||||||
'CHECKED': 8,
|
|
||||||
'ERROR': 16,
|
|
||||||
'PAUSED': 32,
|
|
||||||
'QUEUED': 64,
|
|
||||||
'LOADED': 128
|
|
||||||
}
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
# Load host from config and split out port.
|
|
||||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
|
||||||
if not isInt(host[1]):
|
|
||||||
log.error('Config properties are not filled in correctly, port is missing.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
|
||||||
|
|
||||||
return self.utorrent_api
|
|
||||||
|
|
||||||
def download(self, data = None, media = None, filedata = None):
|
|
||||||
if not media: media = {}
|
|
||||||
if not data: data = {}
|
|
||||||
|
|
||||||
log.debug("Sending '%s' (%s) to uTorrent.", (data.get('name'), data.get('protocol')))
|
|
||||||
|
|
||||||
if not self.connect():
|
|
||||||
return False
|
|
||||||
|
|
||||||
settings = self.utorrent_api.get_settings()
|
|
||||||
if not settings:
|
|
||||||
return False
|
|
||||||
|
|
||||||
#Fix settings in case they are not set for CPS compatibility
|
|
||||||
new_settings = {}
|
|
||||||
if not (settings.get('seed_prio_limitul') == 0 and settings['seed_prio_limitul_flag']):
|
|
||||||
new_settings['seed_prio_limitul'] = 0
|
|
||||||
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
|
|
||||||
new_settings['bt.read_only_on_complete'] = False
|
|
||||||
log.info('Updated uTorrent settings to not set the files to read only after completing.')
|
|
||||||
|
|
||||||
if new_settings:
|
|
||||||
self.utorrent_api.set_settings(new_settings)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if data.get('protocol') == 'torrent_magnet':
|
|
||||||
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
|
|
||||||
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
|
|
||||||
else:
|
|
||||||
info = bdecode(filedata)['info']
|
|
||||||
torrent_hash = sha1(benc(info)).hexdigest().upper()
|
|
||||||
|
|
||||||
torrent_filename = self.createFileName(data, filedata, media)
|
|
||||||
|
|
||||||
if data.get('seed_ratio'):
|
|
||||||
torrent_params['seed_override'] = 1
|
|
||||||
torrent_params['seed_ratio'] = tryInt(tryFloat(data['seed_ratio']) * 1000)
|
|
||||||
|
|
||||||
if data.get('seed_time'):
|
|
||||||
torrent_params['seed_override'] = 1
|
|
||||||
torrent_params['seed_time'] = tryInt(data['seed_time']) * 3600
|
|
||||||
|
|
||||||
# Convert base 32 to hex
|
|
||||||
if len(torrent_hash) == 32:
|
|
||||||
torrent_hash = b16encode(b32decode(torrent_hash))
|
|
||||||
|
|
||||||
# Send request to uTorrent
|
|
||||||
if data.get('protocol') == 'torrent_magnet':
|
|
||||||
self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'))
|
|
||||||
else:
|
|
||||||
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
|
|
||||||
|
|
||||||
# Change settings of added torrent
|
|
||||||
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
|
||||||
if self.conf('paused', default = 0):
|
|
||||||
self.utorrent_api.pause_torrent(torrent_hash)
|
|
||||||
|
|
||||||
return self.downloadReturnId(torrent_hash)
|
|
||||||
|
|
||||||
def 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.')
|
|
||||||
|
|
||||||
if not self.connect():
|
|
||||||
return []
|
|
||||||
|
|
||||||
release_downloads = ReleaseDownloadList(self)
|
|
||||||
|
|
||||||
data = self.utorrent_api.get_status()
|
|
||||||
if not data:
|
|
||||||
log.error('Error getting data from uTorrent')
|
|
||||||
return []
|
|
||||||
|
|
||||||
queue = json.loads(data)
|
|
||||||
if queue.get('error'):
|
|
||||||
log.error('Error getting data from uTorrent: %s', queue.get('error'))
|
|
||||||
return []
|
|
||||||
|
|
||||||
if not queue.get('torrents'):
|
|
||||||
log.debug('Nothing in queue')
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Get torrents
|
|
||||||
for torrent in queue['torrents']:
|
|
||||||
if torrent[0] in ids:
|
|
||||||
|
|
||||||
#Get files of the torrent
|
|
||||||
torrent_files = []
|
|
||||||
try:
|
|
||||||
torrent_files = json.loads(self.utorrent_api.get_files(torrent[0]))
|
|
||||||
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]]
|
|
||||||
except:
|
|
||||||
log.debug('Failed getting files from torrent: %s', torrent[2])
|
|
||||||
|
|
||||||
status = 'busy'
|
|
||||||
if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000:
|
|
||||||
status = 'seeding'
|
|
||||||
elif (torrent[1] & self.status_flags['ERROR']):
|
|
||||||
status = 'failed'
|
|
||||||
elif torrent[4] == 1000:
|
|
||||||
status = 'completed'
|
|
||||||
|
|
||||||
if not status == 'busy':
|
|
||||||
self.removeReadOnly(torrent_files)
|
|
||||||
|
|
||||||
release_downloads.append({
|
|
||||||
'id': torrent[0],
|
|
||||||
'name': torrent[2],
|
|
||||||
'status': status,
|
|
||||||
'seed_ratio': float(torrent[7]) / 1000,
|
|
||||||
'original_status': torrent[1],
|
|
||||||
'timeleft': str(timedelta(seconds = torrent[10])),
|
|
||||||
'folder': sp(torrent[26]),
|
|
||||||
'files': torrent_files
|
|
||||||
})
|
|
||||||
|
|
||||||
return release_downloads
|
|
||||||
|
|
||||||
def pause(self, release_download, pause = True):
|
|
||||||
if not self.connect():
|
|
||||||
return False
|
|
||||||
return self.utorrent_api.pause_torrent(release_download['id'], pause)
|
|
||||||
|
|
||||||
def removeFailed(self, release_download):
|
|
||||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
|
||||||
if not self.connect():
|
|
||||||
return False
|
|
||||||
return self.utorrent_api.remove_torrent(release_download['id'], remove_data = True)
|
|
||||||
|
|
||||||
def processComplete(self, release_download, delete_files = False):
|
|
||||||
log.debug('Requesting uTorrent to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
|
||||||
if not self.connect():
|
|
||||||
return False
|
|
||||||
return self.utorrent_api.remove_torrent(release_download['id'], remove_data = delete_files)
|
|
||||||
|
|
||||||
def removeReadOnly(self, files):
|
|
||||||
#Removes all read-on ly flags in a for all files
|
|
||||||
for filepath in files:
|
|
||||||
if os.path.isfile(filepath):
|
|
||||||
#Windows only needs S_IWRITE, but we bitwise-or with current perms to preserve other permission bits on Linux
|
|
||||||
os.chmod(filepath, stat.S_IWRITE | os.stat(filepath).st_mode)
|
|
||||||
|
|
||||||
class uTorrentAPI(object):
|
|
||||||
|
|
||||||
def __init__(self, host = 'localhost', port = 8000, username = None, password = None):
|
|
||||||
|
|
||||||
super(uTorrentAPI, self).__init__()
|
|
||||||
|
|
||||||
self.url = 'http://' + str(host) + ':' + str(port) + '/gui/'
|
|
||||||
self.token = ''
|
|
||||||
self.last_time = time.time()
|
|
||||||
cookies = cookielib.CookieJar()
|
|
||||||
self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
|
|
||||||
self.opener.addheaders = [('User-agent', 'couchpotato-utorrent-client/1.0')]
|
|
||||||
if username and password:
|
|
||||||
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()
|
|
||||||
|
|
||||||
def _request(self, action, data = None):
|
|
||||||
if time.time() > self.last_time + 1800:
|
|
||||||
self.last_time = time.time()
|
|
||||||
self.token = self.get_token()
|
|
||||||
request = urllib2.Request(self.url + '?token=' + self.token + '&' + action, data)
|
|
||||||
try:
|
|
||||||
open_request = self.opener.open(request)
|
|
||||||
response = open_request.read()
|
|
||||||
if response:
|
|
||||||
return response
|
|
||||||
else:
|
|
||||||
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
|
|
||||||
except httplib.InvalidURL as err:
|
|
||||||
log.error('Invalid uTorrent host, check your config %s', err)
|
|
||||||
except urllib2.HTTPError as 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:
|
|
||||||
log.error('Unable to connect to uTorrent %s', err)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_token(self):
|
|
||||||
request = self.opener.open(self.url + 'token.html')
|
|
||||||
token = re.findall('<div.*?>(.*?)</', request.read())[0]
|
|
||||||
return token
|
|
||||||
|
|
||||||
def add_torrent_uri(self, filename, torrent, add_folder = False):
|
|
||||||
action = 'action=add-url&s=%s' % urllib.quote(torrent)
|
|
||||||
if add_folder:
|
|
||||||
action += '&path=%s' % urllib.quote(filename)
|
|
||||||
return self._request(action)
|
|
||||||
|
|
||||||
def add_torrent_file(self, filename, filedata, add_folder = False):
|
|
||||||
action = 'action=add-file'
|
|
||||||
if add_folder:
|
|
||||||
action += '&path=%s' % urllib.quote(filename)
|
|
||||||
return self._request(action, {'torrent_file': (ss(filename), filedata)})
|
|
||||||
|
|
||||||
def set_torrent(self, hash, params):
|
|
||||||
action = 'action=setprops&hash=%s' % hash
|
|
||||||
for k, v in params.items():
|
|
||||||
action += '&s=%s&v=%s' % (k, v)
|
|
||||||
return self._request(action)
|
|
||||||
|
|
||||||
def pause_torrent(self, hash, pause = True):
|
|
||||||
if pause:
|
|
||||||
action = 'action=pause&hash=%s' % hash
|
|
||||||
else:
|
|
||||||
action = 'action=unpause&hash=%s' % hash
|
|
||||||
return self._request(action)
|
|
||||||
|
|
||||||
def stop_torrent(self, hash):
|
|
||||||
action = 'action=stop&hash=%s' % hash
|
|
||||||
return self._request(action)
|
|
||||||
|
|
||||||
def remove_torrent(self, hash, remove_data = False):
|
|
||||||
if remove_data:
|
|
||||||
action = 'action=removedata&hash=%s' % hash
|
|
||||||
else:
|
|
||||||
action = 'action=remove&hash=%s' % hash
|
|
||||||
return self._request(action)
|
|
||||||
|
|
||||||
def get_status(self):
|
|
||||||
action = 'list=1'
|
|
||||||
return self._request(action)
|
|
||||||
|
|
||||||
def get_settings(self):
|
|
||||||
action = 'action=getsettings'
|
|
||||||
settings_dict = {}
|
|
||||||
try:
|
|
||||||
utorrent_settings = json.loads(self._request(action))
|
|
||||||
|
|
||||||
# Create settings dict
|
|
||||||
for setting in utorrent_settings['settings']:
|
|
||||||
if setting[1] == 0: # int
|
|
||||||
settings_dict[setting[0]] = int(setting[2] if not setting[2].strip() == '' else '0')
|
|
||||||
elif setting[1] == 1: # bool
|
|
||||||
settings_dict[setting[0]] = True if setting[2] == 'true' else False
|
|
||||||
elif setting[1] == 2: # string
|
|
||||||
settings_dict[setting[0]] = setting[2]
|
|
||||||
|
|
||||||
#log.debug('uTorrent settings: %s', settings_dict)
|
|
||||||
|
|
||||||
except Exception as err:
|
|
||||||
log.error('Failed to get settings from uTorrent: %s', err)
|
|
||||||
|
|
||||||
return settings_dict
|
|
||||||
|
|
||||||
def set_settings(self, settings_dict = None):
|
|
||||||
if not settings_dict: settings_dict = {}
|
|
||||||
|
|
||||||
for key in settings_dict:
|
|
||||||
if isinstance(settings_dict[key], bool):
|
|
||||||
settings_dict[key] = 1 if settings_dict[key] else 0
|
|
||||||
|
|
||||||
action = 'action=setsetting' + ''.join(['&s=%s&v=%s' % (key, value) for (key, value) in settings_dict.items()])
|
|
||||||
return self._request(action)
|
|
||||||
|
|
||||||
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.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from .main import uTorrent
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return uTorrent()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'utorrent',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'name': 'utorrent',
|
||||||
|
'label': 'uTorrent',
|
||||||
|
'description': 'Send torrents to uTorrent.',
|
||||||
|
'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': 'paused',
|
||||||
|
'type': 'bool',
|
||||||
|
'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.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
from bencode import bencode, bdecode
|
||||||
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
|
from couchpotato.core.helpers.encoding import isInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from hashlib import sha1
|
||||||
|
from multipartpost import MultipartPostHandler
|
||||||
|
import cookielib
|
||||||
|
import httplib
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class uTorrent(Downloader):
|
||||||
|
|
||||||
|
type = ['torrent', 'torrent_magnet']
|
||||||
|
utorrent_api = None
|
||||||
|
|
||||||
|
def download(self, data, movie, manual = False, filedata = None):
|
||||||
|
|
||||||
|
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
||||||
|
return
|
||||||
|
|
||||||
|
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type')))
|
||||||
|
|
||||||
|
# Load host from config and split out port.
|
||||||
|
host = self.conf('host').split(':')
|
||||||
|
if not isInt(host[1]):
|
||||||
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent_params = {}
|
||||||
|
if self.conf('label'):
|
||||||
|
torrent_params['label'] = self.conf('label')
|
||||||
|
|
||||||
|
if not filedata and data.get('type') == 'torrent':
|
||||||
|
log.error('Failed sending torrent, no data')
|
||||||
|
return False
|
||||||
|
if data.get('type') == 'torrent_magnet':
|
||||||
|
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
|
||||||
|
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
|
||||||
|
else:
|
||||||
|
info = bdecode(filedata)["info"]
|
||||||
|
torrent_hash = sha1(bencode(info)).hexdigest().upper()
|
||||||
|
torrent_filename = self.createFileName(data, filedata, movie)
|
||||||
|
|
||||||
|
# Send request to uTorrent
|
||||||
|
try:
|
||||||
|
if not self.utorrent_api:
|
||||||
|
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||||
|
|
||||||
|
if data.get('type') == 'torrent_magnet':
|
||||||
|
self.utorrent_api.add_torrent_uri(data.get('url'))
|
||||||
|
else:
|
||||||
|
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
|
||||||
|
|
||||||
|
# Change settings of added torrents
|
||||||
|
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
||||||
|
if self.conf('paused', default = 0):
|
||||||
|
self.utorrent_api.pause_torrent(torrent_hash)
|
||||||
|
return True
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to send torrent to uTorrent: %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class uTorrentAPI(object):
|
||||||
|
|
||||||
|
def __init__(self, host = 'localhost', port = 8000, username = None, password = None):
|
||||||
|
|
||||||
|
super(uTorrentAPI, self).__init__()
|
||||||
|
|
||||||
|
self.url = 'http://' + str(host) + ':' + str(port) + '/gui/'
|
||||||
|
self.token = ''
|
||||||
|
self.last_time = time.time()
|
||||||
|
cookies = cookielib.CookieJar()
|
||||||
|
self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
|
||||||
|
self.opener.addheaders = [('User-agent', 'couchpotato-utorrent-client/1.0')]
|
||||||
|
if username and password:
|
||||||
|
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()
|
||||||
|
|
||||||
|
def _request(self, action, data = None):
|
||||||
|
if time.time() > self.last_time + 1800:
|
||||||
|
self.last_time = time.time()
|
||||||
|
self.token = self.get_token()
|
||||||
|
request = urllib2.Request(self.url + "?token=" + self.token + "&" + action, data)
|
||||||
|
try:
|
||||||
|
open_request = self.opener.open(request)
|
||||||
|
response = open_request.read()
|
||||||
|
log.debug('response: %s', response)
|
||||||
|
if response:
|
||||||
|
log.debug('uTorrent action successfull')
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
|
||||||
|
except httplib.InvalidURL, err:
|
||||||
|
log.error('Invalid uTorrent host, check your config %s', 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, err:
|
||||||
|
log.error('Unable to connect to uTorrent %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_token(self):
|
||||||
|
request = self.opener.open(self.url + "token.html")
|
||||||
|
token = re.findall("<div.*?>(.*?)</", request.read())[0]
|
||||||
|
return token
|
||||||
|
|
||||||
|
def add_torrent_uri(self, torrent):
|
||||||
|
action = "action=add-url&s=%s" % urllib.quote(torrent)
|
||||||
|
return self._request(action)
|
||||||
|
|
||||||
|
def add_torrent_file(self, filename, filedata):
|
||||||
|
action = "action=add-file"
|
||||||
|
return self._request(action, {"torrent_file": (filename, filedata)})
|
||||||
|
|
||||||
|
def set_torrent(self, hash, params):
|
||||||
|
action = "action=setprops&hash=%s" % hash
|
||||||
|
for k, v in params.iteritems():
|
||||||
|
action += "&s=%s&v=%s" % (k, v)
|
||||||
|
return self._request(action)
|
||||||
|
|
||||||
|
def pause_torrent(self, hash):
|
||||||
|
action = "action=pause&hash=%s" % hash
|
||||||
|
return self._request(action)
|
||||||
+47
-78
@@ -1,70 +1,57 @@
|
|||||||
|
from axl.axel import Event
|
||||||
|
from couchpotato.core.helpers.variable import mergeDicts, natcmp
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from axl.axel import Event
|
|
||||||
from couchpotato.core.helpers.variable import mergeDicts, natsortKey
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
events = {}
|
events = {}
|
||||||
|
|
||||||
|
|
||||||
def runHandler(name, handler, *args, **kwargs):
|
def runHandler(name, handler, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
return handler(*args, **kwargs)
|
return handler(*args, **kwargs)
|
||||||
except:
|
except:
|
||||||
from couchpotato.environment import Env
|
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 ''))
|
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all()))
|
||||||
|
|
||||||
|
|
||||||
def addEvent(name, handler, priority = 100):
|
def addEvent(name, handler, priority = 100):
|
||||||
|
|
||||||
if not events.get(name):
|
if events.get(name):
|
||||||
events[name] = []
|
e = events[name]
|
||||||
|
else:
|
||||||
|
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
|
||||||
|
|
||||||
def createHandle(*args, **kwargs):
|
def createHandle(*args, **kwargs):
|
||||||
|
|
||||||
h = None
|
|
||||||
try:
|
try:
|
||||||
# Open handler
|
parent = handler.im_self
|
||||||
has_parent = hasattr(handler, 'im_self')
|
bc = hasattr(parent, 'beforeCall')
|
||||||
parent = None
|
if bc: parent.beforeCall(handler)
|
||||||
if has_parent:
|
|
||||||
parent = handler.__self__
|
|
||||||
bc = hasattr(parent, 'beforeCall')
|
|
||||||
if bc: parent.beforeCall(handler)
|
|
||||||
|
|
||||||
# Main event
|
|
||||||
h = runHandler(name, handler, *args, **kwargs)
|
h = runHandler(name, handler, *args, **kwargs)
|
||||||
|
ac = hasattr(parent, 'afterCall')
|
||||||
# Close handler
|
if ac: parent.afterCall(handler)
|
||||||
if parent and has_parent:
|
|
||||||
ac = hasattr(parent, 'afterCall')
|
|
||||||
if ac: parent.afterCall(handler)
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed creating handler %s %s: %s', (name, handler, traceback.format_exc()))
|
h = runHandler(name, handler, *args, **kwargs)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
|
|
||||||
events[name].append({
|
e.handle(createHandle, priority = priority)
|
||||||
'handler': createHandle,
|
|
||||||
'priority': priority,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
def removeEvent(name, handler):
|
||||||
|
e = events[name]
|
||||||
|
e -= handler
|
||||||
|
|
||||||
def fireEvent(name, *args, **kwargs):
|
def fireEvent(name, *args, **kwargs):
|
||||||
if name not in events: return
|
if not events.get(name): return
|
||||||
|
|
||||||
#log.debug('Firing event %s', name)
|
#log.debug('Firing event %s', name)
|
||||||
try:
|
try:
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
'is_after_event': False, # Fire after event
|
'is_after_event': False, # Fire after event
|
||||||
'on_complete': False, # onComplete event
|
'on_complete': False, # onComplete event
|
||||||
'single': False, # Return single handler
|
'single': False, # Return single handler
|
||||||
'merge': False, # Merge items
|
'merge': False, # Merge items
|
||||||
'in_order': False, # Fire them in specific order, waits for the other to finish
|
'in_order': False, # Fire them in specific order, waits for the other to finish
|
||||||
}
|
}
|
||||||
|
|
||||||
# Do options
|
# Do options
|
||||||
@@ -75,41 +62,28 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
options[x] = val
|
options[x] = val
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
if len(events[name]) == 1:
|
e = events[name]
|
||||||
|
|
||||||
single = None
|
# Lock this event
|
||||||
try:
|
e.lock.acquire()
|
||||||
single = events[name][0]['handler'](*args, **kwargs)
|
|
||||||
except:
|
|
||||||
log.error('Failed running single event: %s', traceback.format_exc())
|
|
||||||
|
|
||||||
# Don't load thread for single event
|
e.asynchronous = False
|
||||||
result = {
|
|
||||||
'single': (single is not None, single),
|
|
||||||
}
|
|
||||||
|
|
||||||
else:
|
# Make sure only 1 event is fired at a time when order is wanted
|
||||||
|
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
|
||||||
|
kwargs['event_return_on_result'] = options['single']
|
||||||
|
|
||||||
e = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
|
# Fire
|
||||||
|
result = e(*args, **kwargs)
|
||||||
|
|
||||||
for event in events[name]:
|
# Release lock for this event
|
||||||
e.handle(event['handler'], priority = event['priority'])
|
e.lock.release()
|
||||||
|
|
||||||
# Make sure only 1 event is fired at a time when order is wanted
|
|
||||||
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
|
|
||||||
kwargs['event_return_on_result'] = options['single']
|
|
||||||
|
|
||||||
# Fire
|
|
||||||
result = e(*args, **kwargs)
|
|
||||||
|
|
||||||
result_keys = result.keys()
|
|
||||||
result_keys.sort(key = natsortKey)
|
|
||||||
|
|
||||||
if options['single'] and not options['merge']:
|
if options['single'] and not options['merge']:
|
||||||
results = None
|
results = None
|
||||||
|
|
||||||
# Loop over results, stop when first not None result is found.
|
# 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]
|
r = result[r_key]
|
||||||
if r[0] is True and r[1] is not None:
|
if r[0] is True and r[1] is not None:
|
||||||
results = r[1]
|
results = r[1]
|
||||||
@@ -121,7 +95,7 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
results = []
|
results = []
|
||||||
for r_key in result_keys:
|
for r_key in sorted(result.iterkeys(), cmp = natcmp):
|
||||||
r = result[r_key]
|
r = result[r_key]
|
||||||
if r[0] == True and r[1]:
|
if r[0] == True and r[1]:
|
||||||
results.append(r[1])
|
results.append(r[1])
|
||||||
@@ -130,22 +104,18 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
|
|
||||||
# Merge
|
# Merge
|
||||||
if options['merge'] and len(results) > 0:
|
if options['merge'] and len(results) > 0:
|
||||||
|
|
||||||
# Dict
|
# Dict
|
||||||
if isinstance(results[0], dict):
|
if type(results[0]) == dict:
|
||||||
results.reverse()
|
|
||||||
|
|
||||||
merged = {}
|
merged = {}
|
||||||
for result in results:
|
for result in results:
|
||||||
merged = mergeDicts(merged, result, prepend_list = True)
|
merged = mergeDicts(merged, result)
|
||||||
|
|
||||||
results = merged
|
results = merged
|
||||||
# Lists
|
# Lists
|
||||||
elif isinstance(results[0], list):
|
elif type(results[0]) == list:
|
||||||
merged = []
|
merged = []
|
||||||
for result in results:
|
for result in results:
|
||||||
if result not in merged:
|
merged += result
|
||||||
merged += result
|
|
||||||
|
|
||||||
results = merged
|
results = merged
|
||||||
|
|
||||||
@@ -161,24 +131,23 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
options['on_complete']()
|
options['on_complete']()
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
except KeyError, e:
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
log.error('%s: %s', (name, traceback.format_exc()))
|
log.error('%s: %s', (name, traceback.format_exc()))
|
||||||
|
|
||||||
|
|
||||||
def fireEventAsync(*args, **kwargs):
|
def fireEventAsync(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
||||||
t.setDaemon(True)
|
my_thread.setDaemon(True)
|
||||||
t.start()
|
my_thread.start()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
log.error('%s: %s', (args[0], e))
|
log.error('%s: %s', (args[0], e))
|
||||||
|
|
||||||
|
|
||||||
def errorHandler(error):
|
def errorHandler(error):
|
||||||
etype, value, tb = error
|
etype, value, tb = error
|
||||||
log.error(''.join(traceback.format_exception(etype, value, tb)))
|
log.error(''.join(traceback.format_exception(etype, value, tb)))
|
||||||
|
|
||||||
|
|
||||||
def getEvent(name):
|
def getEvent(name):
|
||||||
return events[name]
|
return events[name]
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
|
from couchpotato.core.logger import CPLog
|
||||||
from string import ascii_letters, digits
|
from string import ascii_letters, digits
|
||||||
from urllib import quote_plus
|
from urllib import quote_plus
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
def toSafeString(original):
|
def toSafeString(original):
|
||||||
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
|
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
|
||||||
cleaned_filename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
|
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
|
||||||
valid_string = ''.join(c for c in cleaned_filename if c in valid_chars)
|
return ''.join(c for c in cleanedFilename if c in valid_chars)
|
||||||
return ' '.join(valid_string.split())
|
|
||||||
|
|
||||||
|
|
||||||
def simplifyString(original):
|
def simplifyString(original):
|
||||||
string = stripAccents(original.lower())
|
string = stripAccents(original.lower())
|
||||||
@@ -25,14 +19,13 @@ def simplifyString(original):
|
|||||||
split = re.split('\W+|_', string.lower())
|
split = re.split('\W+|_', string.lower())
|
||||||
return toUnicode(' '.join(split))
|
return toUnicode(' '.join(split))
|
||||||
|
|
||||||
|
|
||||||
def toUnicode(original, *args):
|
def toUnicode(original, *args):
|
||||||
try:
|
try:
|
||||||
if isinstance(original, unicode):
|
if isinstance(original, unicode):
|
||||||
return original
|
return original
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return six.text_type(original, *args)
|
return unicode(original, *args)
|
||||||
except:
|
except:
|
||||||
try:
|
try:
|
||||||
return ek(original, *args)
|
return ek(original, *args)
|
||||||
@@ -43,43 +36,9 @@ def toUnicode(original, *args):
|
|||||||
ascii_text = str(original).encode('string_escape')
|
ascii_text = str(original).encode('string_escape')
|
||||||
return toUnicode(ascii_text)
|
return toUnicode(ascii_text)
|
||||||
|
|
||||||
|
|
||||||
def ss(original, *args):
|
def ss(original, *args):
|
||||||
|
from couchpotato.environment import Env
|
||||||
u_original = toUnicode(original, *args)
|
return toUnicode(original, *args).encode(Env.get('encoding'))
|
||||||
try:
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
return u_original.encode(Env.get('encoding'))
|
|
||||||
except Exception as e:
|
|
||||||
log.debug('Failed ss encoding char, force UTF8: %s', e)
|
|
||||||
return u_original.encode('UTF-8')
|
|
||||||
|
|
||||||
|
|
||||||
def sp(path, *args):
|
|
||||||
|
|
||||||
# Standardise encoding, normalise case, path and strip trailing '/' or '\'
|
|
||||||
if not path or len(path) == 0:
|
|
||||||
return path
|
|
||||||
|
|
||||||
# convert windows path (from remote box) to *nix path
|
|
||||||
if os.path.sep == '/' and '\\' in path:
|
|
||||||
path = '/' + path.replace(':', '').replace('\\', '/')
|
|
||||||
|
|
||||||
path = os.path.normpath(ss(path, *args))
|
|
||||||
|
|
||||||
# Remove any trailing path separators
|
|
||||||
if path != os.path.sep:
|
|
||||||
path = path.rstrip(os.path.sep)
|
|
||||||
|
|
||||||
# Add a trailing separator in case it is a root folder on windows (crashes guessit)
|
|
||||||
if len(path) == 2 and path[1] == ':':
|
|
||||||
path = path + os.path.sep
|
|
||||||
|
|
||||||
# Replace *NIX ambiguous '//' at the beginning of a path with '/' (crashes guessit)
|
|
||||||
path = re.sub('^//', '/', path)
|
|
||||||
|
|
||||||
return toUnicode(path)
|
|
||||||
|
|
||||||
|
|
||||||
def ek(original, *args):
|
def ek(original, *args):
|
||||||
if isinstance(original, (str, unicode)):
|
if isinstance(original, (str, unicode)):
|
||||||
@@ -91,7 +50,6 @@ def ek(original, *args):
|
|||||||
|
|
||||||
return original
|
return original
|
||||||
|
|
||||||
|
|
||||||
def isInt(value):
|
def isInt(value):
|
||||||
try:
|
try:
|
||||||
int(value)
|
int(value)
|
||||||
@@ -99,20 +57,18 @@ def isInt(value):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def stripAccents(s):
|
def stripAccents(s):
|
||||||
return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn'))
|
return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn'))
|
||||||
|
|
||||||
|
|
||||||
def tryUrlencode(s):
|
def tryUrlencode(s):
|
||||||
new = six.u('')
|
new = u''
|
||||||
if isinstance(s, dict):
|
if isinstance(s, (dict)):
|
||||||
for key, value in s.items():
|
for key, value in s.iteritems():
|
||||||
new += six.u('&%s=%s') % (key, tryUrlencode(value))
|
new += u'&%s=%s' % (key, tryUrlencode(value))
|
||||||
|
|
||||||
return new[1:]
|
return new[1:]
|
||||||
else:
|
else:
|
||||||
for letter in ss(s):
|
for letter in toUnicode(s):
|
||||||
try:
|
try:
|
||||||
new += quote_plus(letter)
|
new += quote_plus(letter)
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.helpers.variable import natcmp
|
||||||
|
from flask.globals import current_app
|
||||||
|
from flask.helpers import json, make_response
|
||||||
from urllib import unquote
|
from urllib import unquote
|
||||||
|
from werkzeug.urls import url_decode
|
||||||
|
import flask
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
def getParams():
|
||||||
from couchpotato.core.helpers.variable import natsortKey
|
|
||||||
|
|
||||||
|
|
||||||
def getParams(params):
|
|
||||||
|
|
||||||
|
params = url_decode(getattr(flask.request, 'environ').get('QUERY_STRING', ''))
|
||||||
reg = re.compile('^[a-z0-9_\.]+$')
|
reg = re.compile('^[a-z0-9_\.]+$')
|
||||||
|
|
||||||
# Sort keys
|
current = temp = {}
|
||||||
param_keys = params.keys()
|
for param, value in sorted(params.iteritems()):
|
||||||
param_keys.sort(key = natsortKey)
|
|
||||||
|
|
||||||
temp = {}
|
|
||||||
for param in param_keys:
|
|
||||||
value = params[param]
|
|
||||||
|
|
||||||
nest = re.split("([\[\]]+)", param)
|
nest = re.split("([\[\]]+)", param)
|
||||||
if len(nest) > 1:
|
if len(nest) > 1:
|
||||||
@@ -38,31 +36,16 @@ def getParams(params):
|
|||||||
current = current[item]
|
current = current[item]
|
||||||
else:
|
else:
|
||||||
temp[param] = toUnicode(unquote(value))
|
temp[param] = toUnicode(unquote(value))
|
||||||
if temp[param].lower() in ['true', 'false']:
|
|
||||||
temp[param] = temp[param].lower() != 'false'
|
|
||||||
|
|
||||||
return dictToList(temp)
|
return dictToList(temp)
|
||||||
|
|
||||||
non_decimal = re.compile(r'[^\d.]+')
|
|
||||||
|
|
||||||
def dictToList(params):
|
def dictToList(params):
|
||||||
|
|
||||||
if type(params) is dict:
|
if type(params) is dict:
|
||||||
new = {}
|
new = {}
|
||||||
for x, value in params.items():
|
for x, value in params.iteritems():
|
||||||
try:
|
try:
|
||||||
convert = lambda text: int(text) if text.isdigit() else text.lower()
|
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys(), cmp = natcmp)]
|
||||||
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
|
|
||||||
except:
|
except:
|
||||||
new_value = value
|
new_value = value
|
||||||
|
|
||||||
@@ -71,3 +54,29 @@ def dictToList(params):
|
|||||||
new = params
|
new = params
|
||||||
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def getParam(attr, default = None):
|
||||||
|
try:
|
||||||
|
return getParams().get(attr, default)
|
||||||
|
except:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def padded_jsonify(callback, *args, **kwargs):
|
||||||
|
content = str(callback) + '(' + json.dumps(dict(*args, **kwargs)) + ')'
|
||||||
|
return getattr(current_app, 'response_class')(content, mimetype = 'text/javascript')
|
||||||
|
|
||||||
|
def jsonify(mimetype, *args, **kwargs):
|
||||||
|
content = json.dumps(dict(*args, **kwargs))
|
||||||
|
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
|
||||||
|
|
||||||
|
def jsonified(*args, **kwargs):
|
||||||
|
callback = getParam('callback_func', None)
|
||||||
|
if callback:
|
||||||
|
content = padded_jsonify(callback, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
content = jsonify('application/json', *args, **kwargs)
|
||||||
|
|
||||||
|
response = make_response(content)
|
||||||
|
response.cache_control.no_cache = True
|
||||||
|
|
||||||
|
return response
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
|
from couchpotato.core.logger import CPLog
|
||||||
import xml.etree.ElementTree as XMLTree
|
import xml.etree.ElementTree as XMLTree
|
||||||
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RSS(object):
|
class RSS(object):
|
||||||
|
|
||||||
def getTextElements(self, xml, path):
|
def getTextElements(self, xml, path):
|
||||||
""" Find elements and return tree"""
|
''' Find elements and return tree'''
|
||||||
|
|
||||||
textelements = []
|
textelements = []
|
||||||
try:
|
try:
|
||||||
@@ -31,7 +28,7 @@ class RSS(object):
|
|||||||
return elements
|
return elements
|
||||||
|
|
||||||
def getElement(self, xml, path):
|
def getElement(self, xml, path):
|
||||||
""" Find element and return text"""
|
''' Find element and return text'''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return xml.find(path)
|
return xml.find(path)
|
||||||
@@ -39,7 +36,7 @@ class RSS(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def getTextElement(self, xml, path):
|
def getTextElement(self, xml, path):
|
||||||
""" Find element and return text"""
|
''' Find element and return text'''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return xml.find(path).text
|
return xml.find(path).text
|
||||||
@@ -49,6 +46,6 @@ class RSS(object):
|
|||||||
def getItems(self, data, path = 'channel/item'):
|
def getItems(self, data, path = 'channel/item'):
|
||||||
try:
|
try:
|
||||||
return XMLTree.parse(data).findall(path)
|
return XMLTree.parse(data).findall(path)
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
log.error('Error parsing RSS. %s', e)
|
log.error('Error parsing RSS. %s', e)
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -1,41 +1,14 @@
|
|||||||
import collections
|
from couchpotato.core.logger import CPLog
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os.path
|
||||||
import platform
|
import platform
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
import six
|
|
||||||
from six.moves import map, zip, filter
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
def fnEscape(pattern):
|
|
||||||
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()
|
|
||||||
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()
|
|
||||||
else:
|
|
||||||
os.symlink(src, dst)
|
|
||||||
|
|
||||||
|
|
||||||
def getUserDir():
|
def getUserDir():
|
||||||
try:
|
try:
|
||||||
import pwd
|
import pwd
|
||||||
@@ -45,7 +18,6 @@ def getUserDir():
|
|||||||
|
|
||||||
return os.path.expanduser('~')
|
return os.path.expanduser('~')
|
||||||
|
|
||||||
|
|
||||||
def getDownloadDir():
|
def getDownloadDir():
|
||||||
user_dir = getUserDir()
|
user_dir = getUserDir()
|
||||||
|
|
||||||
@@ -58,7 +30,6 @@ def getDownloadDir():
|
|||||||
|
|
||||||
return user_dir
|
return user_dir
|
||||||
|
|
||||||
|
|
||||||
def getDataDir():
|
def getDataDir():
|
||||||
|
|
||||||
# Windows
|
# Windows
|
||||||
@@ -78,12 +49,10 @@ def getDataDir():
|
|||||||
# Linux
|
# Linux
|
||||||
return os.path.join(user_dir, '.couchpotato')
|
return os.path.join(user_dir, '.couchpotato')
|
||||||
|
|
||||||
|
def isDict(object):
|
||||||
|
return isinstance(object, dict)
|
||||||
|
|
||||||
def isDict(obj):
|
def mergeDicts(a, b):
|
||||||
return isinstance(obj, dict)
|
|
||||||
|
|
||||||
|
|
||||||
def mergeDicts(a, b, prepend_list = False):
|
|
||||||
assert isDict(a), isDict(b)
|
assert isDict(a), isDict(b)
|
||||||
dst = a.copy()
|
dst = a.copy()
|
||||||
|
|
||||||
@@ -97,13 +66,12 @@ def mergeDicts(a, b, prepend_list = False):
|
|||||||
if isDict(current_src[key]) and isDict(current_dst[key]):
|
if isDict(current_src[key]) and isDict(current_dst[key]):
|
||||||
stack.append((current_dst[key], current_src[key]))
|
stack.append((current_dst[key], current_src[key]))
|
||||||
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
|
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
|
||||||
current_dst[key] = current_src[key] + current_dst[key] if prepend_list else current_dst[key] + current_src[key]
|
current_dst[key].extend(current_src[key])
|
||||||
current_dst[key] = removeListDuplicates(current_dst[key])
|
current_dst[key] = removeListDuplicates(current_dst[key])
|
||||||
else:
|
else:
|
||||||
current_dst[key] = current_src[key]
|
current_dst[key] = current_src[key]
|
||||||
return dst
|
return dst
|
||||||
|
|
||||||
|
|
||||||
def removeListDuplicates(seq):
|
def removeListDuplicates(seq):
|
||||||
checked = []
|
checked = []
|
||||||
for e in seq:
|
for e in seq:
|
||||||
@@ -111,79 +79,31 @@ def removeListDuplicates(seq):
|
|||||||
checked.append(e)
|
checked.append(e)
|
||||||
return checked
|
return checked
|
||||||
|
|
||||||
|
|
||||||
def flattenList(l):
|
def flattenList(l):
|
||||||
if isinstance(l, list):
|
if isinstance(l, list):
|
||||||
return sum(map(flattenList, l))
|
return sum(map(flattenList, l))
|
||||||
else:
|
else:
|
||||||
return l
|
return l
|
||||||
|
|
||||||
|
|
||||||
def md5(text):
|
def md5(text):
|
||||||
return hashlib.md5(ss(text)).hexdigest()
|
return hashlib.md5(text).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def sha1(text):
|
def sha1(text):
|
||||||
return hashlib.sha1(text).hexdigest()
|
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):
|
def getExt(filename):
|
||||||
return os.path.splitext(filename)[1][1:]
|
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):
|
if not host.endswith('/'):
|
||||||
"""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 += '/'
|
||||||
|
|
||||||
return host
|
return host
|
||||||
|
|
||||||
|
def getImdb(txt, check_inside = True, multiple = False):
|
||||||
def getImdb(txt, check_inside = False, multiple = False):
|
|
||||||
|
|
||||||
if not check_inside:
|
|
||||||
txt = simplifyString(txt)
|
|
||||||
else:
|
|
||||||
txt = ss(txt)
|
|
||||||
|
|
||||||
if check_inside and os.path.isfile(txt):
|
if check_inside and os.path.isfile(txt):
|
||||||
output = open(txt, 'r')
|
output = open(txt, 'r')
|
||||||
@@ -191,119 +111,50 @@ def getImdb(txt, check_inside = False, multiple = False):
|
|||||||
output.close()
|
output.close()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ids = re.findall('(tt\d{4,7})', txt)
|
ids = re.findall('(tt\d{7})', txt)
|
||||||
|
|
||||||
if multiple:
|
if multiple:
|
||||||
return removeDuplicate(['tt%07d' % tryInt(x[2:]) for x in ids]) if len(ids) > 0 else []
|
return ids if len(ids) > 0 else []
|
||||||
|
return ids[0]
|
||||||
return 'tt%07d' % tryInt(ids[0][2:])
|
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def tryInt(s):
|
||||||
def tryInt(s, default = 0):
|
|
||||||
try: return int(s)
|
try: return int(s)
|
||||||
except: return default
|
|
||||||
|
|
||||||
|
|
||||||
def tryFloat(s):
|
|
||||||
try:
|
|
||||||
if isinstance(s, str):
|
|
||||||
return float(s) if '.' in s else tryInt(s)
|
|
||||||
else:
|
|
||||||
return float(s)
|
|
||||||
except: return 0
|
except: return 0
|
||||||
|
|
||||||
|
def tryFloat(s):
|
||||||
|
try: return float(s) if '.' in s else tryInt(s)
|
||||||
|
except: return 0
|
||||||
|
|
||||||
def natsortKey(string_):
|
def natsortKey(s):
|
||||||
"""See http://www.codinghorror.com/blog/archives/001018.html"""
|
return map(tryInt, re.findall(r'(\d+|\D+)', s))
|
||||||
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):
|
def getTitle(library_dict):
|
||||||
if isinstance(value, collections.Iterable):
|
|
||||||
return value
|
|
||||||
return [value]
|
|
||||||
|
|
||||||
|
|
||||||
def getIdentifier(media):
|
|
||||||
return media.get('identifier') or media.get('identifiers', {}).get('imdb')
|
|
||||||
|
|
||||||
|
|
||||||
def getTitle(media_dict):
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
return media_dict['title']
|
return library_dict['titles'][0]['title']
|
||||||
except:
|
except:
|
||||||
try:
|
try:
|
||||||
return media_dict['titles'][0]
|
for title in library_dict.titles:
|
||||||
|
if title.default:
|
||||||
|
return title.title
|
||||||
except:
|
except:
|
||||||
try:
|
log.error('Could not get title for %s', library_dict.identifier)
|
||||||
return media_dict['info']['titles'][0]
|
return None
|
||||||
except:
|
|
||||||
try:
|
log.error('Could not get title for %s', library_dict['identifier'])
|
||||||
return media_dict['media']['info']['titles'][0]
|
return None
|
||||||
except:
|
except:
|
||||||
log.error('Could not get title for %s', getIdentifier(media_dict))
|
log.error('Could not get title for library item: %s', library_dict)
|
||||||
return None
|
|
||||||
except:
|
|
||||||
log.error('Could not get title for library item: %s', media_dict)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def possibleTitles(raw_title):
|
|
||||||
|
|
||||||
titles = [
|
|
||||||
toSafeString(raw_title).lower(),
|
|
||||||
raw_title.lower(),
|
|
||||||
simplifyString(raw_title)
|
|
||||||
]
|
|
||||||
|
|
||||||
# replace some chars
|
|
||||||
new_title = raw_title.replace('&', 'and')
|
|
||||||
titles.append(simplifyString(new_title))
|
|
||||||
|
|
||||||
return removeDuplicate(titles)
|
|
||||||
|
|
||||||
|
|
||||||
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||||
return ''.join(random.choice(chars) for x in range(size))
|
return ''.join(random.choice(chars) for x in range(size))
|
||||||
|
|
||||||
|
def splitString(str, split_on = ','):
|
||||||
def splitString(str, split_on = ',', clean = True):
|
return [x.strip() for x in str.split(split_on)]
|
||||||
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)]
|
|
||||||
|
|
||||||
|
|
||||||
def dictIsSubset(a, b):
|
|
||||||
return all([k in b and b[k] == v for k, v in a.items()])
|
|
||||||
|
|
||||||
|
|
||||||
def isSubFolder(sub_folder, base_folder):
|
|
||||||
# Returns True if sub_folder is the same as or inside base_folder
|
|
||||||
return base_folder and sub_folder and ss(os.path.normpath(base_folder).rstrip(os.path.sep) + os.path.sep) in ss(os.path.normpath(sub_folder).rstrip(os.path.sep) + os.path.sep)
|
|
||||||
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|||||||
+38
-84
@@ -1,71 +1,59 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from importhelper import import_module
|
import glob
|
||||||
import six
|
import os
|
||||||
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Loader(object):
|
class Loader(object):
|
||||||
|
|
||||||
def __init__(self):
|
plugins = {}
|
||||||
self.plugins = {}
|
providers = {}
|
||||||
self.providers = {}
|
|
||||||
self.modules = {}
|
modules = {}
|
||||||
self.paths = {}
|
|
||||||
|
|
||||||
def preload(self, root = ''):
|
def preload(self, root = ''):
|
||||||
|
|
||||||
core = os.path.join(root, 'couchpotato', 'core')
|
core = os.path.join(root, 'couchpotato', 'core')
|
||||||
|
|
||||||
self.paths.update({
|
self.paths = {
|
||||||
'core': (0, 'couchpotato.core._base', os.path.join(core, '_base')),
|
'core': (0, 'couchpotato.core._base', os.path.join(core, '_base')),
|
||||||
'plugin': (1, 'couchpotato.core.plugins', os.path.join(core, 'plugins')),
|
'plugin': (1, 'couchpotato.core.plugins', os.path.join(core, 'plugins')),
|
||||||
'notifications': (20, 'couchpotato.core.notifications', os.path.join(core, 'notifications')),
|
'notifications': (20, 'couchpotato.core.notifications', os.path.join(core, 'notifications')),
|
||||||
'downloaders': (20, 'couchpotato.core.downloaders', os.path.join(core, 'downloaders')),
|
'downloaders': (20, 'couchpotato.core.downloaders', os.path.join(core, 'downloaders')),
|
||||||
})
|
}
|
||||||
|
|
||||||
# Add media to loader
|
# Add providers to loader
|
||||||
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
|
provider_dir = os.path.join(root, 'couchpotato', 'core', 'providers')
|
||||||
|
for provider in os.listdir(provider_dir):
|
||||||
|
path = os.path.join(provider_dir, provider)
|
||||||
|
if os.path.isdir(path):
|
||||||
|
self.paths[provider + '_provider'] = (25, 'couchpotato.core.providers.' + provider, path)
|
||||||
|
|
||||||
# Add custom plugin folder
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
custom_plugin_dir = os.path.join(Env.get('data_dir'), 'custom_plugins')
|
|
||||||
if os.path.isdir(custom_plugin_dir):
|
|
||||||
sys.path.insert(0, custom_plugin_dir)
|
|
||||||
self.paths['custom_plugins'] = (30, '', custom_plugin_dir)
|
|
||||||
|
|
||||||
# Loop over all paths and add to module list
|
for plugin_type, plugin_tuple in self.paths.iteritems():
|
||||||
for plugin_type, plugin_tuple in self.paths.items():
|
|
||||||
priority, module, dir_name = plugin_tuple
|
priority, module, dir_name = plugin_tuple
|
||||||
self.addFromDir(plugin_type, priority, module, dir_name)
|
self.addFromDir(plugin_type, priority, module, dir_name)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
did_save = 0
|
did_save = 0
|
||||||
|
|
||||||
for priority in sorted(self.modules):
|
for priority in self.modules:
|
||||||
for module_name, plugin in sorted(self.modules[priority].items()):
|
for module_name, plugin in sorted(self.modules[priority].iteritems()):
|
||||||
|
|
||||||
# Load module
|
# Load module
|
||||||
try:
|
try:
|
||||||
if plugin.get('name')[:2] == '__':
|
m = getattr(self.loadModule(module_name), plugin.get('name'))
|
||||||
continue
|
|
||||||
|
|
||||||
m = self.loadModule(module_name)
|
log.info('Loading %s: %s', (plugin['type'], plugin['name']))
|
||||||
if m is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Save default settings for plugin/provider
|
# Save default settings for plugin/provider
|
||||||
did_save += self.loadSettings(m, module_name, save = False)
|
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:
|
except ImportError as e:
|
||||||
# todo:: subclass ImportError for missing requirements.
|
# todo:: subclass ImportError for missing requirements.
|
||||||
if e.message.lower().startswith("missing"):
|
if (e.message.lower().startswith("missing")):
|
||||||
log.error(e.message)
|
log.error(e.message)
|
||||||
pass
|
pass
|
||||||
# todo:: this needs to be more descriptive.
|
# todo:: this needs to be more descriptive.
|
||||||
@@ -77,40 +65,15 @@ class Loader(object):
|
|||||||
if did_save:
|
if did_save:
|
||||||
fireEvent('settings.save')
|
fireEvent('settings.save')
|
||||||
|
|
||||||
def addPath(self, root, base_path, priority, recursive = False):
|
|
||||||
root_path = os.path.join(root, *base_path)
|
|
||||||
for filename in os.listdir(root_path):
|
|
||||||
path = os.path.join(root_path, filename)
|
|
||||||
if os.path.isdir(path) and filename[:2] != '__':
|
|
||||||
if six.u('__init__.py') in os.listdir(path):
|
|
||||||
new_base_path = ''.join(s + '.' for s in base_path) + filename
|
|
||||||
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
|
|
||||||
|
|
||||||
if recursive:
|
|
||||||
self.addPath(root, base_path + [filename], priority, recursive = True)
|
|
||||||
|
|
||||||
def addFromDir(self, plugin_type, priority, module, dir_name):
|
def addFromDir(self, plugin_type, priority, module, dir_name):
|
||||||
|
|
||||||
# Load dir module
|
for cur_file in glob.glob(os.path.join(dir_name, '*')):
|
||||||
if module and len(module) > 0:
|
name = os.path.basename(cur_file)
|
||||||
self.addModule(priority, plugin_type, module, os.path.basename(dir_name))
|
if os.path.isdir(os.path.join(dir_name, 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
|
|
||||||
module_name = '%s.%s' % (module, name)
|
module_name = '%s.%s' % (module, name)
|
||||||
self.addModule(priority, plugin_type, module_name, name)
|
self.addModule(priority, plugin_type, module_name, name)
|
||||||
|
|
||||||
def loadSettings(self, module, name, save = True):
|
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__)
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for section in module.config:
|
for section in module.config:
|
||||||
fireEvent('settings.options', section['name'], section)
|
fireEvent('settings.options', section['name'], section)
|
||||||
@@ -124,22 +87,16 @@ class Loader(object):
|
|||||||
log.debug('Failed loading settings for "%s": %s', (name, traceback.format_exc()))
|
log.debug('Failed loading settings for "%s": %s', (name, traceback.format_exc()))
|
||||||
return False
|
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__)
|
|
||||||
return False
|
|
||||||
try:
|
try:
|
||||||
# Load single file plugin
|
klass = module.start()
|
||||||
if isinstance(module.autoload, (str, unicode)):
|
klass.registerPlugin()
|
||||||
getattr(module, module.autoload)()
|
|
||||||
# Load folder plugin
|
if klass and getattr(klass, 'auto_register_static'):
|
||||||
else:
|
klass.registerStatic(module.__file__)
|
||||||
module.autoload()
|
|
||||||
|
|
||||||
log.info('Loaded %s: %s', (type, name))
|
|
||||||
return True
|
return True
|
||||||
except:
|
except Exception, e:
|
||||||
log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc()))
|
log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc()))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -148,10 +105,6 @@ class Loader(object):
|
|||||||
if not self.modules.get(priority):
|
if not self.modules.get(priority):
|
||||||
self.modules[priority] = {}
|
self.modules[priority] = {}
|
||||||
|
|
||||||
module = module.lstrip('.')
|
|
||||||
if plugin_type.startswith('couchpotato_core'):
|
|
||||||
plugin_type = plugin_type[17:]
|
|
||||||
|
|
||||||
self.modules[priority][module] = {
|
self.modules[priority][module] = {
|
||||||
'priority': priority,
|
'priority': priority,
|
||||||
'module': module,
|
'module': module,
|
||||||
@@ -161,9 +114,10 @@ class Loader(object):
|
|||||||
|
|
||||||
def loadModule(self, name):
|
def loadModule(self, name):
|
||||||
try:
|
try:
|
||||||
return import_module(name)
|
m = __import__(name)
|
||||||
except ImportError:
|
splitted = name.split('.')
|
||||||
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
|
for sub in splitted[1:-1]:
|
||||||
return None
|
m = getattr(m, sub)
|
||||||
|
return m
|
||||||
except:
|
except:
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
class CPLog(object):
|
class CPLog(object):
|
||||||
|
|
||||||
context = ''
|
context = ''
|
||||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
|
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key']
|
||||||
|
|
||||||
Env = None
|
|
||||||
is_develop = False
|
|
||||||
|
|
||||||
def __init__(self, context = ''):
|
def __init__(self, context = ''):
|
||||||
if context.endswith('.main'):
|
if context.endswith('.main'):
|
||||||
@@ -17,14 +14,6 @@ class CPLog(object):
|
|||||||
self.context = context
|
self.context = context
|
||||||
self.logger = logging.getLogger()
|
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')
|
|
||||||
|
|
||||||
def info(self, msg, replace_tuple = ()):
|
def info(self, msg, replace_tuple = ()):
|
||||||
self.logger.info(self.addContext(msg, replace_tuple))
|
self.logger.info(self.addContext(msg, replace_tuple))
|
||||||
|
|
||||||
@@ -48,7 +37,8 @@ class CPLog(object):
|
|||||||
|
|
||||||
def safeMessage(self, msg, replace_tuple = ()):
|
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)
|
msg = ss(msg)
|
||||||
|
|
||||||
@@ -60,11 +50,10 @@ class CPLog(object):
|
|||||||
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
|
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
|
||||||
else:
|
else:
|
||||||
msg = msg % ss(replace_tuple)
|
msg = msg % ss(replace_tuple)
|
||||||
except Exception as e:
|
except:
|
||||||
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e))
|
self.logger.error(u'Failed encoding stuff to log: %s' % traceback.format_exc())
|
||||||
|
|
||||||
self.setup()
|
if not Env.get('dev'):
|
||||||
if not self.is_develop:
|
|
||||||
|
|
||||||
for replace in self.replace_private:
|
for replace in self.replace_private:
|
||||||
msg = re.sub('(\?%s=)[^\&]+' % replace, '?%s=xxx' % replace, msg)
|
msg = re.sub('(\?%s=)[^\&]+' % replace, '?%s=xxx' % replace, msg)
|
||||||
@@ -72,10 +61,10 @@ class CPLog(object):
|
|||||||
|
|
||||||
# Replace api key
|
# Replace api key
|
||||||
try:
|
try:
|
||||||
api_key = self.Env.setting('api_key')
|
api_key = Env.setting('api_key')
|
||||||
if api_key:
|
if api_key:
|
||||||
msg = msg.replace(api_key, 'API_KEY')
|
msg = msg.replace(api_key, 'API_KEY')
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return toUnicode(msg)
|
return msg
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import os
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from couchpotato import get_db, CPLog
|
|
||||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class MediaBase(Plugin):
|
|
||||||
|
|
||||||
_type = None
|
|
||||||
|
|
||||||
def initType(self):
|
|
||||||
addEvent('media.types', self.getType)
|
|
||||||
|
|
||||||
def getType(self):
|
|
||||||
return self._type
|
|
||||||
|
|
||||||
def createOnComplete(self, media_id):
|
|
||||||
|
|
||||||
def onComplete():
|
|
||||||
try:
|
|
||||||
db = get_db()
|
|
||||||
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))
|
|
||||||
except:
|
|
||||||
log.error('Failed creating onComplete: %s', traceback.format_exc())
|
|
||||||
|
|
||||||
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())
|
|
||||||
|
|
||||||
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 +0,0 @@
|
|||||||
from .main import Library
|
|
||||||
|
|
||||||
|
|
||||||
def autoload():
|
|
||||||
return Library()
|
|
||||||
|
|
||||||
config = []
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from couchpotato.core.event import addEvent
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
|
|
||||||
|
|
||||||
class LibraryBase(Plugin):
|
|
||||||
|
|
||||||
_type = None
|
|
||||||
|
|
||||||
def initType(self):
|
|
||||||
addEvent('library.types', self.getType)
|
|
||||||
|
|
||||||
def getType(self):
|
|
||||||
return self._type
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
from couchpotato.core.event import addEvent, fireEvent
|
|
||||||
from couchpotato.core.media._base.library.base import LibraryBase
|
|
||||||
|
|
||||||
|
|
||||||
class Library(LibraryBase):
|
|
||||||
def __init__(self):
|
|
||||||
addEvent('library.title', self.title)
|
|
||||||
|
|
||||||
def title(self, library):
|
|
||||||
return fireEvent(
|
|
||||||
'library.query',
|
|
||||||
library,
|
|
||||||
|
|
||||||
condense = False,
|
|
||||||
include_year = False,
|
|
||||||
include_identifier = False,
|
|
||||||
single = True
|
|
||||||
)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from .main import Matcher
|
|
||||||
|
|
||||||
|
|
||||||
def autoload():
|
|
||||||
return Matcher()
|
|
||||||
|
|
||||||
config = []
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
from couchpotato.core.event import addEvent
|
|
||||||
from couchpotato.core.helpers.encoding import simplifyString
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class MatcherBase(Plugin):
|
|
||||||
type = None
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if self.type:
|
|
||||||
addEvent('%s.matcher.correct' % self.type, self.correct)
|
|
||||||
|
|
||||||
def correct(self, chain, release, media, quality):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def flattenInfo(self, info):
|
|
||||||
# Flatten dictionary of matches (chain info)
|
|
||||||
if isinstance(info, dict):
|
|
||||||
return dict([(key, self.flattenInfo(value)) for key, value in info.items()])
|
|
||||||
|
|
||||||
# Flatten matches
|
|
||||||
result = None
|
|
||||||
|
|
||||||
for match in info:
|
|
||||||
if isinstance(match, dict):
|
|
||||||
if result is None:
|
|
||||||
result = {}
|
|
||||||
|
|
||||||
for key, value in match.items():
|
|
||||||
if key not in result:
|
|
||||||
result[key] = []
|
|
||||||
|
|
||||||
result[key].append(value)
|
|
||||||
else:
|
|
||||||
if result is None:
|
|
||||||
result = []
|
|
||||||
|
|
||||||
result.append(match)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def constructFromRaw(self, match):
|
|
||||||
if not match:
|
|
||||||
return None
|
|
||||||
|
|
||||||
parts = [
|
|
||||||
''.join([
|
|
||||||
y for y in x[1:] if y
|
|
||||||
]) for x in match
|
|
||||||
]
|
|
||||||
|
|
||||||
return ''.join(parts)[:-1].strip()
|
|
||||||
|
|
||||||
def simplifyValue(self, value):
|
|
||||||
if not value:
|
|
||||||
return value
|
|
||||||
|
|
||||||
if isinstance(value, basestring):
|
|
||||||
return simplifyString(value)
|
|
||||||
|
|
||||||
if isinstance(value, list):
|
|
||||||
return [self.simplifyValue(x) for x in value]
|
|
||||||
|
|
||||||
raise ValueError("Unsupported value type")
|
|
||||||
|
|
||||||
def chainMatch(self, chain, group, tags):
|
|
||||||
info = self.flattenInfo(chain.info[group])
|
|
||||||
|
|
||||||
found_tags = []
|
|
||||||
for tag, accepted in tags.items():
|
|
||||||
values = [self.simplifyValue(x) for x in info.get(tag, [None])]
|
|
||||||
|
|
||||||
if any([val in accepted for val in values]):
|
|
||||||
found_tags.append(tag)
|
|
||||||
|
|
||||||
log.debug('tags found: %s, required: %s' % (found_tags, tags.keys()))
|
|
||||||
|
|
||||||
if set(tags.keys()) == set(found_tags):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return all([key in found_tags for key, value in tags.items()])
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
from couchpotato.core.event import addEvent, fireEvent
|
|
||||||
from couchpotato.core.helpers.variable import possibleTitles
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.media._base.matcher.base import MatcherBase
|
|
||||||
from caper import Caper
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Matcher(MatcherBase):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(Matcher, self).__init__()
|
|
||||||
|
|
||||||
self.caper = Caper()
|
|
||||||
|
|
||||||
addEvent('matcher.parse', self.parse)
|
|
||||||
addEvent('matcher.match', self.match)
|
|
||||||
|
|
||||||
addEvent('matcher.flatten_info', self.flattenInfo)
|
|
||||||
addEvent('matcher.construct_from_raw', self.constructFromRaw)
|
|
||||||
|
|
||||||
addEvent('matcher.correct_title', self.correctTitle)
|
|
||||||
addEvent('matcher.correct_quality', self.correctQuality)
|
|
||||||
|
|
||||||
def parse(self, name, parser='scene'):
|
|
||||||
return self.caper.parse(name, parser)
|
|
||||||
|
|
||||||
def match(self, release, media, quality):
|
|
||||||
match = fireEvent('matcher.parse', release['name'], single = True)
|
|
||||||
|
|
||||||
if len(match.chains) < 1:
|
|
||||||
log.info2('Wrong: %s, unable to parse release name (no chains)', release['name'])
|
|
||||||
return False
|
|
||||||
|
|
||||||
for chain in match.chains:
|
|
||||||
if fireEvent('%s.matcher.correct' % media['type'], chain, release, media, quality, single = True):
|
|
||||||
return chain
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def correctTitle(self, chain, media):
|
|
||||||
root_library = media['library']['root_library']
|
|
||||||
|
|
||||||
if 'show_name' not in chain.info or not len(chain.info['show_name']):
|
|
||||||
log.info('Wrong: missing show name in parsed result')
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Get the lower-case parsed show name from the chain
|
|
||||||
chain_words = [x.lower() for x in chain.info['show_name']]
|
|
||||||
|
|
||||||
# Build a list of possible titles of the media we are searching for
|
|
||||||
titles = root_library['info']['titles']
|
|
||||||
|
|
||||||
# Add year suffix titles (will result in ['<name_one>', '<name_one> <suffix_one>', '<name_two>', ...])
|
|
||||||
suffixes = [None, root_library['info']['year']]
|
|
||||||
|
|
||||||
titles = [
|
|
||||||
title + ((' %s' % suffix) if suffix else '')
|
|
||||||
for title in titles
|
|
||||||
for suffix in suffixes
|
|
||||||
]
|
|
||||||
|
|
||||||
# Check show titles match
|
|
||||||
# TODO check xem names
|
|
||||||
for title in titles:
|
|
||||||
for valid_words in [x.split(' ') for x in possibleTitles(title)]:
|
|
||||||
|
|
||||||
if valid_words == chain_words:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def correctQuality(self, chain, quality, quality_map):
|
|
||||||
if quality['identifier'] not in quality_map:
|
|
||||||
log.info2('Wrong: unknown preferred quality %s', quality['identifier'])
|
|
||||||
return False
|
|
||||||
|
|
||||||
if 'video' not in chain.info:
|
|
||||||
log.info2('Wrong: no video tags found')
|
|
||||||
return False
|
|
||||||
|
|
||||||
video_tags = quality_map[quality['identifier']]
|
|
||||||
|
|
||||||
if not self.chainMatch(chain, 'video', video_tags):
|
|
||||||
log.info2('Wrong: %s tags not in chain', video_tags)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from .main import MediaPlugin
|
|
||||||
|
|
||||||
|
|
||||||
def autoload():
|
|
||||||
return MediaPlugin()
|
|
||||||
@@ -1,178 +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 = 2
|
|
||||||
|
|
||||||
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 ']:
|
|
||||||
if prefix == title[:len(prefix)]:
|
|
||||||
title = title[len(prefix):]
|
|
||||||
break
|
|
||||||
|
|
||||||
return str(nr_prefix + title).ljust(32, '_')[:32]
|
|
||||||
|
|
||||||
|
|
||||||
class StartsWithIndex(TreeBasedIndex):
|
|
||||||
_version = 2
|
|
||||||
|
|
||||||
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 ']:
|
|
||||||
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
|
|
||||||
|
|
||||||
@@ -1,451 +0,0 @@
|
|||||||
import traceback
|
|
||||||
from string import ascii_lowercase
|
|
||||||
|
|
||||||
from couchpotato import tryInt, get_db
|
|
||||||
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.logger import CPLog
|
|
||||||
from couchpotato.core.media import MediaBase
|
|
||||||
from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class MediaPlugin(MediaBase):
|
|
||||||
|
|
||||||
_database = {
|
|
||||||
'media': MediaIndex,
|
|
||||||
'media_search_title': TitleSearchIndex,
|
|
||||||
'media_status': MediaStatusIndex,
|
|
||||||
'media_by_type': MediaTypeIndex,
|
|
||||||
'media_title': TitleIndex,
|
|
||||||
'media_startswith': StartsWithIndex,
|
|
||||||
'media_children': MediaChildrenIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
addApiView('media.refresh', self.refresh, docs = {
|
|
||||||
'desc': 'Refresh a any media type by ID',
|
|
||||||
'params': {
|
|
||||||
'id': {'desc': 'Movie, Show, Season or Episode ID(s) you want to refresh.', 'type': 'int (comma separated)'},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
addApiView('media.list', self.listView, docs = {
|
|
||||||
'desc': 'List media',
|
|
||||||
'params': {
|
|
||||||
'type': {'type': 'string', 'desc': 'Media type to filter on.'},
|
|
||||||
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
|
|
||||||
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
|
|
||||||
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
|
|
||||||
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
|
|
||||||
'search': {'desc': 'Search movie title'},
|
|
||||||
},
|
|
||||||
'return': {'type': 'object', 'example': """{
|
|
||||||
'success': True,
|
|
||||||
'empty': bool, any movies returned or not,
|
|
||||||
'media': array, media found,
|
|
||||||
}"""}
|
|
||||||
})
|
|
||||||
|
|
||||||
addApiView('media.get', self.getView, docs = {
|
|
||||||
'desc': 'Get media by id',
|
|
||||||
'params': {
|
|
||||||
'id': {'desc': 'The id of the media'},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
addApiView('media.delete', self.deleteView, docs = {
|
|
||||||
'desc': 'Delete a media from the wanted list',
|
|
||||||
'params': {
|
|
||||||
'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'},
|
|
||||||
'delete_from': {'desc': 'Delete media from this page', 'type': 'string: all (default), wanted, manage'},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
addApiView('media.available_chars', self.charView)
|
|
||||||
|
|
||||||
addEvent('app.load', self.addSingleRefreshView, priority = 100)
|
|
||||||
addEvent('app.load', self.addSingleListView, priority = 100)
|
|
||||||
addEvent('app.load', self.addSingleCharView, priority = 100)
|
|
||||||
addEvent('app.load', self.addSingleDeleteView, priority = 100)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def refresh(self, id = '', **kwargs):
|
|
||||||
handlers = []
|
|
||||||
ids = splitString(id)
|
|
||||||
|
|
||||||
for x in ids:
|
|
||||||
|
|
||||||
refresh_handler = self.createRefreshHandler(x)
|
|
||||||
if refresh_handler:
|
|
||||||
handlers.append(refresh_handler)
|
|
||||||
|
|
||||||
fireEvent('notify.frontend', type = 'media.busy', data = {'_id': ids})
|
|
||||||
fireEventAsync('schedule.queue', handlers = handlers)
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
if handler:
|
|
||||||
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):
|
|
||||||
addApiView('%s.refresh' % media_type, self.refresh)
|
|
||||||
|
|
||||||
def get(self, media_id):
|
|
||||||
|
|
||||||
db = get_db()
|
|
||||||
|
|
||||||
imdb_id = getImdb(str(media_id))
|
|
||||||
|
|
||||||
media = None
|
|
||||||
if imdb_id:
|
|
||||||
media = db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc']
|
|
||||||
else:
|
|
||||||
media = db.get('id', media_id)
|
|
||||||
|
|
||||||
if media:
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
def getView(self, id = None, **kwargs):
|
|
||||||
|
|
||||||
media = self.get(id) if id else None
|
|
||||||
|
|
||||||
return {
|
|
||||||
'success': media is not None,
|
|
||||||
'media': media,
|
|
||||||
}
|
|
||||||
|
|
||||||
def withStatus(self, status, with_doc = True):
|
|
||||||
|
|
||||||
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, with_doc = with_doc):
|
|
||||||
yield ms['doc'] if with_doc else 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, starts_with = None, search = None):
|
|
||||||
|
|
||||||
db = get_db()
|
|
||||||
|
|
||||||
# Make a list from string
|
|
||||||
if status and not isinstance(status, (list, tuple)):
|
|
||||||
status = [status]
|
|
||||||
if release_status and not isinstance(release_status, (list, tuple)):
|
|
||||||
release_status = [release_status]
|
|
||||||
if types and not isinstance(types, (list, tuple)):
|
|
||||||
types = [types]
|
|
||||||
|
|
||||||
# query 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 = {}
|
|
||||||
|
|
||||||
# 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'))
|
|
||||||
|
|
||||||
# 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'))
|
|
||||||
|
|
||||||
# Add search filters
|
|
||||||
if starts_with:
|
|
||||||
filter_by['starts_with'] = set()
|
|
||||||
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)]
|
|
||||||
|
|
||||||
# Filter with search query
|
|
||||||
if search:
|
|
||||||
filter_by['search'] = [x['_id'] for x in db.get_many('media_search_title', 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']
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
if total_count == 0:
|
|
||||||
return 0, []
|
|
||||||
|
|
||||||
offset = 0
|
|
||||||
limit = -1
|
|
||||||
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])
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
media = fireEvent('media.get', media_id, single = True)
|
|
||||||
|
|
||||||
# Merge releases with movie dict
|
|
||||||
medias.append(media)
|
|
||||||
|
|
||||||
# remove from media ids
|
|
||||||
media_ids.remove(media_id)
|
|
||||||
if len(media_ids) == 0 or len(medias) == limit: break
|
|
||||||
|
|
||||||
return total_count, medias
|
|
||||||
|
|
||||||
def listView(self, **kwargs):
|
|
||||||
|
|
||||||
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'),
|
|
||||||
starts_with = kwargs.get('starts_with'),
|
|
||||||
search = kwargs.get('search')
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'empty': len(movies) == 0,
|
|
||||||
'total': total_movies,
|
|
||||||
'movies': movies,
|
|
||||||
}
|
|
||||||
|
|
||||||
def addSingleListView(self):
|
|
||||||
|
|
||||||
for media_type in fireEvent('media.types', merge = True):
|
|
||||||
def tempList(*args, **kwargs):
|
|
||||||
return self.listView(types = media_type, **kwargs)
|
|
||||||
addApiView('%s.list' % media_type, tempList)
|
|
||||||
|
|
||||||
def availableChars(self, types = None, status = None, release_status = None):
|
|
||||||
|
|
||||||
db = get_db()
|
|
||||||
|
|
||||||
# Make a list from string
|
|
||||||
if status and not isinstance(status, (list, tuple)):
|
|
||||||
status = [status]
|
|
||||||
if release_status and not isinstance(release_status, (list, tuple)):
|
|
||||||
release_status = [release_status]
|
|
||||||
if types and not isinstance(types, (list, tuple)):
|
|
||||||
types = [types]
|
|
||||||
|
|
||||||
# query 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 = {}
|
|
||||||
|
|
||||||
# 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'))
|
|
||||||
|
|
||||||
# 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]]
|
|
||||||
|
|
||||||
chars = set()
|
|
||||||
for x in db.all('media_startswith'):
|
|
||||||
if x['_id'] in media_ids:
|
|
||||||
chars.add(x['key'])
|
|
||||||
|
|
||||||
if len(chars) == 25:
|
|
||||||
break
|
|
||||||
|
|
||||||
return list(chars)
|
|
||||||
|
|
||||||
def charView(self, **kwargs):
|
|
||||||
|
|
||||||
type = splitString(kwargs.get('type', 'movie'))
|
|
||||||
status = splitString(kwargs.get('status', None))
|
|
||||||
release_status = splitString(kwargs.get('release_status', None))
|
|
||||||
chars = self.availableChars(type, status, release_status)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'empty': len(chars) == 0,
|
|
||||||
'chars': chars,
|
|
||||||
}
|
|
||||||
|
|
||||||
def addSingleCharView(self):
|
|
||||||
|
|
||||||
for media_type in fireEvent('media.types', merge = True):
|
|
||||||
def tempChar(*args, **kwargs):
|
|
||||||
return self.charView(types = media_type, **kwargs)
|
|
||||||
addApiView('%s.available_chars' % media_type, tempChar)
|
|
||||||
|
|
||||||
def delete(self, media_id, delete_from = None):
|
|
||||||
|
|
||||||
try:
|
|
||||||
db = get_db()
|
|
||||||
|
|
||||||
media = db.get('id', media_id)
|
|
||||||
if media:
|
|
||||||
deleted = False
|
|
||||||
if delete_from == 'all':
|
|
||||||
db.delete(media)
|
|
||||||
deleted = True
|
|
||||||
else:
|
|
||||||
|
|
||||||
media_releases = fireEvent('release.for_media', 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':
|
|
||||||
db.delete(release)
|
|
||||||
total_deleted += 1
|
|
||||||
|
|
||||||
if (total_releases == total_deleted and media['status'] != 'active') or (delete_from == 'wanted' and media['status'] == 'active'):
|
|
||||||
db.delete(media)
|
|
||||||
deleted = True
|
|
||||||
elif new_media_status:
|
|
||||||
media['status'] = new_media_status
|
|
||||||
db.update(media)
|
|
||||||
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())
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def deleteView(self, id = '', **kwargs):
|
|
||||||
|
|
||||||
ids = splitString(id)
|
|
||||||
for media_id in ids:
|
|
||||||
self.delete(media_id, delete_from = kwargs.get('delete_from', 'all'))
|
|
||||||
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def addSingleDeleteView(self):
|
|
||||||
|
|
||||||
for media_type in fireEvent('media.types', merge = True):
|
|
||||||
def tempDelete(*args, **kwargs):
|
|
||||||
return self.deleteView(types = media_type, *args, **kwargs)
|
|
||||||
addApiView('%s.delete' % media_type, tempDelete)
|
|
||||||
|
|
||||||
def restatus(self, media_id):
|
|
||||||
|
|
||||||
try:
|
|
||||||
db = get_db()
|
|
||||||
|
|
||||||
m = db.get('id', media_id)
|
|
||||||
previous_status = m['status']
|
|
||||||
|
|
||||||
log.debug('Changing status for %s', getTitle(m))
|
|
||||||
if not m['profile_id']:
|
|
||||||
m['status'] = 'done'
|
|
||||||
else:
|
|
||||||
move_to_wanted = True
|
|
||||||
|
|
||||||
profile = db.get('id', m['profile_id'])
|
|
||||||
media_releases = fireEvent('release.for_media', m['_id'], single = True)
|
|
||||||
|
|
||||||
for q_identifier in profile['qualities']:
|
|
||||||
index = profile['qualities'].index(q_identifier)
|
|
||||||
|
|
||||||
for release in media_releases:
|
|
||||||
if q_identifier == release['quality'] and (release.get('status') == 'done' and profile['finish'][index]):
|
|
||||||
move_to_wanted = False
|
|
||||||
|
|
||||||
m['status'] = 'active' if move_to_wanted else 'done'
|
|
||||||
|
|
||||||
# Only update when status has changed
|
|
||||||
if previous_status != m['status']:
|
|
||||||
db.update(m)
|
|
||||||
|
|
||||||
return True
|
|
||||||
except:
|
|
||||||
log.error('Failed restatus: %s', traceback.format_exc())
|
|
||||||
@@ -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,338 +0,0 @@
|
|||||||
from urlparse import urlparse
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
import xml.etree.ElementTree as XMLTree
|
|
||||||
|
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
|
||||||
from couchpotato.core.helpers.encoding import ss
|
|
||||||
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
|
|
||||||
possibleTitles
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class MultiProvider(Plugin):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._classes = []
|
|
||||||
|
|
||||||
for Type in self.getTypes():
|
|
||||||
klass = Type()
|
|
||||||
|
|
||||||
# Overwrite name so logger knows what we're talking about
|
|
||||||
klass.setName('%s:%s' % (self.getName(), klass.getName()))
|
|
||||||
|
|
||||||
self._classes.append(klass)
|
|
||||||
|
|
||||||
def getTypes(self):
|
|
||||||
return []
|
|
||||||
|
|
||||||
def getClasses(self):
|
|
||||||
return self._classes
|
|
||||||
|
|
||||||
|
|
||||||
class Provider(Plugin):
|
|
||||||
|
|
||||||
type = None # movie, show, subtitle, trailer, ...
|
|
||||||
http_time_between_calls = 10 # Default timeout for url requests
|
|
||||||
|
|
||||||
last_available_check = {}
|
|
||||||
is_available = {}
|
|
||||||
|
|
||||||
def isAvailable(self, test_url):
|
|
||||||
|
|
||||||
if Env.get('dev'): return True
|
|
||||||
|
|
||||||
now = time.time()
|
|
||||||
host = urlparse(test_url).hostname
|
|
||||||
|
|
||||||
if self.last_available_check.get(host) < now - 900:
|
|
||||||
self.last_available_check[host] = now
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.urlopen(test_url, 30)
|
|
||||||
self.is_available[host] = True
|
|
||||||
except:
|
|
||||||
log.error('"%s" unavailable, trying again in an 15 minutes.', host)
|
|
||||||
self.is_available[host] = False
|
|
||||||
|
|
||||||
return self.is_available.get(host, False)
|
|
||||||
|
|
||||||
def getJsonData(self, url, decode_from = None, **kwargs):
|
|
||||||
|
|
||||||
cache_key = md5(url)
|
|
||||||
data = self.getCache(cache_key, url, **kwargs)
|
|
||||||
|
|
||||||
if data:
|
|
||||||
try:
|
|
||||||
data = data.strip()
|
|
||||||
if decode_from:
|
|
||||||
data = data.decode(decode_from)
|
|
||||||
|
|
||||||
return json.loads(data)
|
|
||||||
except:
|
|
||||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
def getRSSData(self, url, item_path = 'channel/item', **kwargs):
|
|
||||||
|
|
||||||
cache_key = md5(url)
|
|
||||||
data = self.getCache(cache_key, url, **kwargs)
|
|
||||||
|
|
||||||
if data and len(data) > 0:
|
|
||||||
try:
|
|
||||||
data = XMLTree.fromstring(ss(data))
|
|
||||||
return self.getElements(data, item_path)
|
|
||||||
except:
|
|
||||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
def getHTMLData(self, url, **kwargs):
|
|
||||||
|
|
||||||
cache_key = md5(url)
|
|
||||||
return self.getCache(cache_key, url, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class YarrProvider(Provider):
|
|
||||||
|
|
||||||
protocol = None # nzb, torrent, torrent_magnet
|
|
||||||
|
|
||||||
cat_ids = {}
|
|
||||||
cat_backup_id = None
|
|
||||||
|
|
||||||
size_gb = ['gb', 'gib']
|
|
||||||
size_mb = ['mb', 'mib']
|
|
||||||
size_kb = ['kb', 'kib']
|
|
||||||
|
|
||||||
last_login_check = None
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
addEvent('provider.enabled_protocols', self.getEnabledProtocol)
|
|
||||||
addEvent('provider.belongs_to', self.belongsTo)
|
|
||||||
addEvent('provider.search.%s.%s' % (self.protocol, self.type), self.search)
|
|
||||||
|
|
||||||
def getEnabledProtocol(self):
|
|
||||||
if self.isEnabled():
|
|
||||||
return self.protocol
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def login(self):
|
|
||||||
|
|
||||||
# Check if we are still logged in every hour
|
|
||||||
now = time.time()
|
|
||||||
if self.last_login_check and self.last_login_check < (now - 3600):
|
|
||||||
try:
|
|
||||||
output = self.urlopen(self.urls['login_check'])
|
|
||||||
if self.loginCheckSuccess(output):
|
|
||||||
self.last_login_check = now
|
|
||||||
return True
|
|
||||||
except: pass
|
|
||||||
self.last_login_check = None
|
|
||||||
|
|
||||||
if self.last_login_check:
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
output = self.urlopen(self.urls['login'], data = self.getLoginParams())
|
|
||||||
|
|
||||||
if self.loginSuccess(output):
|
|
||||||
self.last_login_check = now
|
|
||||||
return True
|
|
||||||
|
|
||||||
error = 'unknown'
|
|
||||||
except:
|
|
||||||
error = traceback.format_exc()
|
|
||||||
|
|
||||||
self.last_login_check = None
|
|
||||||
log.error('Failed to login %s: %s', (self.getName(), error))
|
|
||||||
return False
|
|
||||||
|
|
||||||
def loginSuccess(self, output):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def loginCheckSuccess(self, output):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def loginDownload(self, url = '', nzb_id = ''):
|
|
||||||
try:
|
|
||||||
if not self.login():
|
|
||||||
log.error('Failed downloading from %s', self.getName())
|
|
||||||
return self.urlopen(url)
|
|
||||||
except:
|
|
||||||
log.error('Failed downloading from %s: %s', (self.getName(), traceback.format_exc()))
|
|
||||||
|
|
||||||
def getLoginParams(self):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def download(self, url = '', nzb_id = ''):
|
|
||||||
try:
|
|
||||||
return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()}, show_error = False)
|
|
||||||
except:
|
|
||||||
log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc()))
|
|
||||||
|
|
||||||
return 'try_next'
|
|
||||||
|
|
||||||
def search(self, media, quality):
|
|
||||||
|
|
||||||
if self.isDisabled():
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Login if needed
|
|
||||||
if self.urls.get('login') and not self.login():
|
|
||||||
log.error('Failed to login to: %s', self.getName())
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Create result container
|
|
||||||
imdb_results = hasattr(self, '_search')
|
|
||||||
results = ResultList(self, media, quality, imdb_results = imdb_results)
|
|
||||||
|
|
||||||
# Do search based on imdb id
|
|
||||||
if imdb_results:
|
|
||||||
self._search(media, quality, results)
|
|
||||||
# Search possible titles
|
|
||||||
else:
|
|
||||||
media_title = fireEvent('library.query', media, single = True)
|
|
||||||
|
|
||||||
for title in possibleTitles(media_title):
|
|
||||||
self._searchOnTitle(title, media, quality, results)
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def belongsTo(self, url, provider = None, host = None):
|
|
||||||
try:
|
|
||||||
if provider and provider == self.getName():
|
|
||||||
return self
|
|
||||||
|
|
||||||
hostname = urlparse(url).hostname
|
|
||||||
if host and hostname in host:
|
|
||||||
return self
|
|
||||||
else:
|
|
||||||
for url_type in self.urls:
|
|
||||||
download_url = self.urls[url_type]
|
|
||||||
if hostname in download_url:
|
|
||||||
return self
|
|
||||||
except:
|
|
||||||
log.debug('Url %s doesn\'t belong to %s', (url, self.getName()))
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def parseSize(self, size):
|
|
||||||
|
|
||||||
size_raw = size.lower()
|
|
||||||
size = tryFloat(re.sub(r'[^0-9.]', '', size).strip())
|
|
||||||
|
|
||||||
for s in self.size_gb:
|
|
||||||
if s in size_raw:
|
|
||||||
return size * 1024
|
|
||||||
|
|
||||||
for s in self.size_mb:
|
|
||||||
if s in size_raw:
|
|
||||||
return size
|
|
||||||
|
|
||||||
for s in self.size_kb:
|
|
||||||
if s in size_raw:
|
|
||||||
return size / 1024
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def getCatId(self, quality = None):
|
|
||||||
if not quality: quality = {}
|
|
||||||
identifier = quality.get('identifier')
|
|
||||||
|
|
||||||
want_3d = False
|
|
||||||
if quality.get('custom'):
|
|
||||||
want_3d = quality['custom'].get('3d')
|
|
||||||
|
|
||||||
for ids, qualities in self.cat_ids:
|
|
||||||
if identifier in qualities or (want_3d and '3d' in qualities):
|
|
||||||
return ids
|
|
||||||
|
|
||||||
if self.cat_backup_id:
|
|
||||||
return [self.cat_backup_id]
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class ResultList(list):
|
|
||||||
|
|
||||||
result_ids = None
|
|
||||||
provider = None
|
|
||||||
media = None
|
|
||||||
quality = None
|
|
||||||
|
|
||||||
def __init__(self, provider, media, quality, **kwargs):
|
|
||||||
|
|
||||||
self.result_ids = []
|
|
||||||
self.provider = provider
|
|
||||||
self.media = media
|
|
||||||
self.quality = quality
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
super(ResultList, self).__init__()
|
|
||||||
|
|
||||||
def extend(self, results):
|
|
||||||
for r in results:
|
|
||||||
self.append(r)
|
|
||||||
|
|
||||||
def append(self, result):
|
|
||||||
|
|
||||||
new_result = self.fillResult(result)
|
|
||||||
|
|
||||||
is_correct = fireEvent('searcher.correct_release', new_result, self.media, self.quality,
|
|
||||||
imdb_results = self.kwargs.get('imdb_results', False), single = True)
|
|
||||||
|
|
||||||
if is_correct and new_result['id'] not in self.result_ids:
|
|
||||||
is_correct_weight = float(is_correct)
|
|
||||||
|
|
||||||
new_result['score'] += fireEvent('score.calculate', new_result, self.media, single = True)
|
|
||||||
|
|
||||||
old_score = new_result['score']
|
|
||||||
new_result['score'] = int(old_score * is_correct_weight)
|
|
||||||
|
|
||||||
log.info('Found correct release with weight %.02f, old_score(%d) now scaled to score(%d)', (
|
|
||||||
is_correct_weight,
|
|
||||||
old_score,
|
|
||||||
new_result['score']
|
|
||||||
))
|
|
||||||
|
|
||||||
self.found(new_result)
|
|
||||||
self.result_ids.append(result['id'])
|
|
||||||
|
|
||||||
super(ResultList, self).append(new_result)
|
|
||||||
|
|
||||||
def fillResult(self, result):
|
|
||||||
|
|
||||||
defaults = {
|
|
||||||
'id': 0,
|
|
||||||
'protocol': self.provider.protocol,
|
|
||||||
'type': self.provider.type,
|
|
||||||
'provider': self.provider.getName(),
|
|
||||||
'download': self.provider.loginDownload if self.provider.urls.get('login') else self.provider.download,
|
|
||||||
'seed_ratio': Env.setting('seed_ratio', section = self.provider.getName().lower(), default = ''),
|
|
||||||
'seed_time': Env.setting('seed_time', section = self.provider.getName().lower(), default = ''),
|
|
||||||
'url': '',
|
|
||||||
'name': '',
|
|
||||||
'age': 0,
|
|
||||||
'size': 0,
|
|
||||||
'description': '',
|
|
||||||
'score': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeDicts(defaults, result)
|
|
||||||
|
|
||||||
def found(self, new_result):
|
|
||||||
if not new_result.get('provider_extra'):
|
|
||||||
new_result['provider_extra'] = ''
|
|
||||||
else:
|
|
||||||
new_result['provider_extra'] = ', %s' % new_result['provider_extra']
|
|
||||||
|
|
||||||
log.info('Found: score(%(score)s) on %(provider)s%(provider_extra)s: %(name)s', new_result)
|
|
||||||
@@ -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,14 +0,0 @@
|
|||||||
config = [{
|
|
||||||
'name': 'nzb_providers',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'label': 'Usenet Providers',
|
|
||||||
'description': 'Providers searching usenet for new releases',
|
|
||||||
'wizard': True,
|
|
||||||
'type': 'list',
|
|
||||||
'name': 'nzb_providers',
|
|
||||||
'tab': 'searcher',
|
|
||||||
'options': [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import re
|
|
||||||
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.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 = tryInt(parts.group('total'))
|
|
||||||
parts = 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': 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,
|
|
||||||
'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,248 +0,0 @@
|
|||||||
from urllib2 import HTTPError
|
|
||||||
from urlparse import urlparse
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
import urllib2
|
|
||||||
|
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
|
||||||
from couchpotato.core.helpers.rss import RSS
|
|
||||||
from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.media._base.providers.base import ResultList
|
|
||||||
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
from dateutil.parser import parse
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(NZBProvider, RSS):
|
|
||||||
|
|
||||||
urls = {
|
|
||||||
'detail': 'details&id=%s',
|
|
||||||
'download': 't=get&id=%s'
|
|
||||||
}
|
|
||||||
|
|
||||||
limits_reached = {}
|
|
||||||
|
|
||||||
http_time_between_calls = 1 # Seconds
|
|
||||||
|
|
||||||
def search(self, media, quality):
|
|
||||||
hosts = self.getHosts()
|
|
||||||
|
|
||||||
results = ResultList(self, media, quality, imdb_results = True)
|
|
||||||
|
|
||||||
for host in hosts:
|
|
||||||
if self.isDisabled(host):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._searchOnHost(host, media, quality, results)
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def _searchOnHost(self, host, media, quality, results):
|
|
||||||
|
|
||||||
query = self.buildUrl(media, host['api_key'])
|
|
||||||
|
|
||||||
url = '%s&%s' % (self.getUrl(host['host']), query)
|
|
||||||
|
|
||||||
nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
|
|
||||||
|
|
||||||
for nzb in nzbs:
|
|
||||||
|
|
||||||
date = None
|
|
||||||
spotter = None
|
|
||||||
for item in nzb:
|
|
||||||
if date and spotter:
|
|
||||||
break
|
|
||||||
if item.attrib.get('name') == 'usenetdate':
|
|
||||||
date = item.attrib.get('value')
|
|
||||||
break
|
|
||||||
|
|
||||||
# Get the name of the person who posts the spot
|
|
||||||
if item.attrib.get('name') == 'poster':
|
|
||||||
if "@spot.net" in item.attrib.get('value'):
|
|
||||||
spotter = item.attrib.get('value').split("@")[0]
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not date:
|
|
||||||
date = self.getTextElement(nzb, 'pubDate')
|
|
||||||
|
|
||||||
nzb_id = self.getTextElement(nzb, 'guid').split('/')[-1:].pop()
|
|
||||||
name = self.getTextElement(nzb, 'title')
|
|
||||||
|
|
||||||
if not name:
|
|
||||||
continue
|
|
||||||
|
|
||||||
name_extra = ''
|
|
||||||
if spotter:
|
|
||||||
name_extra = spotter
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'id': nzb_id,
|
|
||||||
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
|
||||||
'name': toUnicode(name),
|
|
||||||
'name_extra': name_extra,
|
|
||||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
|
||||||
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
|
||||||
'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
|
|
||||||
'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)),
|
|
||||||
'content': self.getTextElement(nzb, 'description'),
|
|
||||||
'score': host['extra_score'],
|
|
||||||
})
|
|
||||||
|
|
||||||
def getHosts(self):
|
|
||||||
|
|
||||||
uses = splitString(str(self.conf('use')), clean = False)
|
|
||||||
hosts = splitString(self.conf('host'), clean = False)
|
|
||||||
api_keys = splitString(self.conf('api_key'), clean = False)
|
|
||||||
extra_score = splitString(self.conf('extra_score'), clean = False)
|
|
||||||
custom_tags = splitString(self.conf('custom_tag'), clean = False)
|
|
||||||
|
|
||||||
list = []
|
|
||||||
for nr in range(len(hosts)):
|
|
||||||
|
|
||||||
try: key = api_keys[nr]
|
|
||||||
except: key = ''
|
|
||||||
|
|
||||||
try: host = hosts[nr]
|
|
||||||
except: host = ''
|
|
||||||
|
|
||||||
try: score = tryInt(extra_score[nr])
|
|
||||||
except: score = 0
|
|
||||||
|
|
||||||
try: custom_tag = custom_tags[nr]
|
|
||||||
except: custom_tag = ''
|
|
||||||
|
|
||||||
list.append({
|
|
||||||
'use': uses[nr],
|
|
||||||
'host': host,
|
|
||||||
'api_key': key,
|
|
||||||
'extra_score': score,
|
|
||||||
'custom_tag': custom_tag
|
|
||||||
})
|
|
||||||
|
|
||||||
return list
|
|
||||||
|
|
||||||
def belongsTo(self, url, provider = None, host = None):
|
|
||||||
|
|
||||||
hosts = self.getHosts()
|
|
||||||
|
|
||||||
for host in hosts:
|
|
||||||
result = super(Base, self).belongsTo(url, host = host['host'], provider = provider)
|
|
||||||
if result:
|
|
||||||
return result
|
|
||||||
|
|
||||||
def getUrl(self, host):
|
|
||||||
if '?page=newznabapi' in host:
|
|
||||||
return cleanHost(host)[:-1] + '&'
|
|
||||||
|
|
||||||
return cleanHost(host) + 'api?'
|
|
||||||
|
|
||||||
def isDisabled(self, host = None):
|
|
||||||
return not self.isEnabled(host)
|
|
||||||
|
|
||||||
def isEnabled(self, host = None):
|
|
||||||
|
|
||||||
# Return true if at least one is enabled and no host is given
|
|
||||||
if host is None:
|
|
||||||
for host in self.getHosts():
|
|
||||||
if self.isEnabled(host):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
return NZBProvider.isEnabled(self) and host['host'] and host['api_key'] and int(host['use'])
|
|
||||||
|
|
||||||
def getApiExt(self, host):
|
|
||||||
return '&apikey=%s' % host['api_key']
|
|
||||||
|
|
||||||
def download(self, url = '', nzb_id = ''):
|
|
||||||
host = urlparse(url).hostname
|
|
||||||
|
|
||||||
if self.limits_reached.get(host):
|
|
||||||
# Try again in 3 hours
|
|
||||||
if self.limits_reached[host] > time.time() - 10800:
|
|
||||||
return 'try_next'
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get final redirected url
|
|
||||||
log.debug('Checking %s for redirects.', url)
|
|
||||||
req = urllib2.Request(url)
|
|
||||||
req.add_header('User-Agent', self.user_agent)
|
|
||||||
res = urllib2.urlopen(req)
|
|
||||||
finalurl = res.geturl()
|
|
||||||
if finalurl != url:
|
|
||||||
log.debug('Redirect url used: %s', finalurl)
|
|
||||||
|
|
||||||
data = self.urlopen(finalurl, show_error = False)
|
|
||||||
self.limits_reached[host] = False
|
|
||||||
return data
|
|
||||||
except HTTPError as e:
|
|
||||||
if e.code == 503:
|
|
||||||
response = e.read().lower()
|
|
||||||
if 'maximum api' in response or 'download limit' in response:
|
|
||||||
if not self.limits_reached.get(host):
|
|
||||||
log.error('Limit reached for newznab provider: %s', host)
|
|
||||||
self.limits_reached[host] = time.time()
|
|
||||||
return 'try_next'
|
|
||||||
|
|
||||||
log.error('Failed download from %s: %s', (host, traceback.format_exc()))
|
|
||||||
|
|
||||||
return 'try_next'
|
|
||||||
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'newznab',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'searcher',
|
|
||||||
'list': 'nzb_providers',
|
|
||||||
'name': 'newznab',
|
|
||||||
'order': 10,
|
|
||||||
'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \
|
|
||||||
<a href="https://nzbs.org" target="_blank">NZBs.org</a>, <a href="http://dognzb.cr/" target="_blank">DOGnzb.cr</a>, \
|
|
||||||
<a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a>, <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>, \
|
|
||||||
<a href="https://smackdownonyou.com" target="_blank">SmackDown</a>, <a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'type': 'enabler',
|
|
||||||
'default': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'use',
|
|
||||||
'default': '0,0,0,0,0,0'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'default': 'api.nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws',
|
|
||||||
'description': 'The hostname of your newznab provider',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'extra_score',
|
|
||||||
'advanced': True,
|
|
||||||
'label': 'Extra Score',
|
|
||||||
'default': '0,0,0,0,0,0',
|
|
||||||
'description': 'Starting score for each release found via this provider.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'custom_tag',
|
|
||||||
'advanced': True,
|
|
||||||
'label': 'Custom tag',
|
|
||||||
'default': ',,,,,',
|
|
||||||
'description': 'Add custom tags, for example add rls=1 to get only scene releases from nzbs.org',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'api_key',
|
|
||||||
'default': ',,,,,',
|
|
||||||
'label': 'Api Key',
|
|
||||||
'description': 'Can be found on your profile page',
|
|
||||||
'type': 'combined',
|
|
||||||
'combine': ['use', 'host', 'api_key', 'extra_score', 'custom_tag'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
|
||||||
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://www.nzbclub.com/nzbfeeds.aspx?%s',
|
|
||||||
}
|
|
||||||
|
|
||||||
http_time_between_calls = 4 # seconds
|
|
||||||
|
|
||||||
def _search(self, media, quality, results):
|
|
||||||
|
|
||||||
nzbs = self.getRSSData(self.urls['search'] % self.buildUrl(media))
|
|
||||||
|
|
||||||
for nzb in nzbs:
|
|
||||||
|
|
||||||
nzbclub_id = tryInt(self.getTextElement(nzb, "link").split('/nzb_view/')[1].split('/')[0])
|
|
||||||
enclosure = self.getElement(nzb, "enclosure").attrib
|
|
||||||
size = enclosure['length']
|
|
||||||
date = self.getTextElement(nzb, "pubDate")
|
|
||||||
|
|
||||||
def extra_check(item):
|
|
||||||
full_description = self.getCache('nzbclub.%s' % nzbclub_id, item['detail_url'], cache_timeout = 25920000)
|
|
||||||
|
|
||||||
for ignored in ['ARCHIVE inside ARCHIVE', 'Incomplete', 'repair impossible']:
|
|
||||||
if ignored in full_description:
|
|
||||||
log.info('Wrong: Seems to be passworded or corrupted files: %s', item['name'])
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'id': nzbclub_id,
|
|
||||||
'name': toUnicode(self.getTextElement(nzb, "title")),
|
|
||||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
|
||||||
'size': tryInt(size) / 1024 / 1024,
|
|
||||||
'url': enclosure['url'].replace(' ', '_'),
|
|
||||||
'detail_url': self.getTextElement(nzb, "link"),
|
|
||||||
'get_more_info': self.getMoreInfo,
|
|
||||||
'extra_check': extra_check
|
|
||||||
})
|
|
||||||
|
|
||||||
def getMoreInfo(self, item):
|
|
||||||
full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
|
||||||
html = BeautifulSoup(full_description)
|
|
||||||
nfo_pre = html.find('pre', attrs = {'class': 'nfo'})
|
|
||||||
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
|
||||||
|
|
||||||
item['description'] = description
|
|
||||||
return item
|
|
||||||
|
|
||||||
def extraCheck(self, item):
|
|
||||||
full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
|
||||||
|
|
||||||
if 'ARCHIVE inside ARCHIVE' in full_description:
|
|
||||||
log.info('Wrong: Seems to be passworded files: %s', item['name'])
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'nzbclub',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'searcher',
|
|
||||||
'list': 'nzb_providers',
|
|
||||||
'name': 'NZBClub',
|
|
||||||
'description': 'Free provider, less accurate. See <a href="https://www.nzbclub.com/">NZBClub</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'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,125 +0,0 @@
|
|||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
|
||||||
from couchpotato.core.helpers.rss import RSS
|
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.event import fireEvent
|
|
||||||
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
|
|
||||||
from dateutil.parser import parse
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(NZBProvider, RSS):
|
|
||||||
|
|
||||||
urls = {
|
|
||||||
'download': 'https://www.nzbindex.com/download/',
|
|
||||||
'search': 'https://www.nzbindex.com/rss/?%s',
|
|
||||||
}
|
|
||||||
|
|
||||||
http_time_between_calls = 1 # Seconds
|
|
||||||
|
|
||||||
def _search(self, media, quality, results):
|
|
||||||
|
|
||||||
nzbs = self.getRSSData(self.urls['search'] % self.buildUrl(media, quality))
|
|
||||||
|
|
||||||
for nzb in nzbs:
|
|
||||||
|
|
||||||
enclosure = self.getElement(nzb, 'enclosure').attrib
|
|
||||||
nzbindex_id = int(self.getTextElement(nzb, "link").split('/')[4])
|
|
||||||
|
|
||||||
title = self.getTextElement(nzb, "title")
|
|
||||||
|
|
||||||
match = fireEvent('matcher.parse', title, parser='usenet', single = True)
|
|
||||||
if not match.chains:
|
|
||||||
log.info('Unable to parse release with title "%s"', title)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# TODO should we consider other lower-weight chains here?
|
|
||||||
info = fireEvent('matcher.flatten_info', match.chains[0].info, single = True)
|
|
||||||
|
|
||||||
release_name = fireEvent('matcher.construct_from_raw', info.get('release_name'), single = True)
|
|
||||||
|
|
||||||
file_name = info.get('detail', {}).get('file_name')
|
|
||||||
file_name = file_name[0] if file_name else None
|
|
||||||
|
|
||||||
title = release_name or file_name
|
|
||||||
|
|
||||||
# Strip extension from parsed title (if one exists)
|
|
||||||
ext_pos = title.rfind('.')
|
|
||||||
|
|
||||||
# Assume extension if smaller than 4 characters
|
|
||||||
# TODO this should probably be done a better way
|
|
||||||
if len(title[ext_pos + 1:]) <= 4:
|
|
||||||
title = title[:ext_pos]
|
|
||||||
|
|
||||||
if not title:
|
|
||||||
log.info('Unable to find release name from match')
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
description = self.getTextElement(nzb, "description")
|
|
||||||
except:
|
|
||||||
description = ''
|
|
||||||
|
|
||||||
def extra_check(item):
|
|
||||||
if '#c20000' in item['description'].lower():
|
|
||||||
log.info('Wrong: Seems to be passworded: %s', item['name'])
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'id': nzbindex_id,
|
|
||||||
'name': title,
|
|
||||||
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
|
|
||||||
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
|
||||||
'url': enclosure['url'],
|
|
||||||
'detail_url': enclosure['url'].replace('/download/', '/release/'),
|
|
||||||
'description': description,
|
|
||||||
'get_more_info': self.getMoreInfo,
|
|
||||||
'extra_check': extra_check,
|
|
||||||
})
|
|
||||||
|
|
||||||
def getMoreInfo(self, item):
|
|
||||||
try:
|
|
||||||
if '/nfo/' in item['description'].lower():
|
|
||||||
nfo_url = re.search('href=\"(?P<nfo>.+)\" ', item['description']).group('nfo')
|
|
||||||
full_description = self.getCache('nzbindex.%s' % item['id'], url = nfo_url, cache_timeout = 25920000)
|
|
||||||
html = BeautifulSoup(full_description)
|
|
||||||
item['description'] = toUnicode(html.find('pre', attrs = {'id': 'nfo0'}).text)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'nzbindex',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'searcher',
|
|
||||||
'list': 'nzb_providers',
|
|
||||||
'name': 'nzbindex',
|
|
||||||
'description': 'Free provider, less accurate. See <a href="https://www.nzbindex.com/">NZBIndex</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'type': 'enabler',
|
|
||||||
'default': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'extra_score',
|
|
||||||
'advanced': True,
|
|
||||||
'label': 'Extra Score',
|
|
||||||
'type': 'int',
|
|
||||||
'default': 0,
|
|
||||||
'description': 'Starting score for each release found via this provider.',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -1,102 +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,
|
|
||||||
'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,14 +0,0 @@
|
|||||||
config = [{
|
|
||||||
'name': 'torrent_providers',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'label': 'Torrent Providers',
|
|
||||||
'description': 'Providers searching torrent sites for new releases',
|
|
||||||
'wizard': True,
|
|
||||||
'type': 'list',
|
|
||||||
'name': 'torrent_providers',
|
|
||||||
'tab': 'searcher',
|
|
||||||
'options': [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -1,140 +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': 'See <a href="https://awesome-hd.net">AHD</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'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,77 +0,0 @@
|
|||||||
import time
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from couchpotato.core.helpers.variable import getImdb, md5, cleanHost
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.media._base.providers.base import YarrProvider
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class TorrentProvider(YarrProvider):
|
|
||||||
|
|
||||||
protocol = 'torrent'
|
|
||||||
|
|
||||||
proxy_domain = None
|
|
||||||
proxy_list = []
|
|
||||||
|
|
||||||
def imdbMatch(self, url, imdbId):
|
|
||||||
if getImdb(url) == imdbId:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if url[:4] == 'http':
|
|
||||||
try:
|
|
||||||
cache_key = md5(url)
|
|
||||||
data = self.getCache(cache_key, url)
|
|
||||||
except IOError:
|
|
||||||
log.error('Failed to open %s.', url)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return getImdb(data) == imdbId
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def getDomain(self, url = ''):
|
|
||||||
|
|
||||||
forced_domain = self.conf('domain')
|
|
||||||
if forced_domain:
|
|
||||||
return cleanHost(forced_domain).rstrip('/') + url
|
|
||||||
|
|
||||||
if not self.proxy_domain:
|
|
||||||
for proxy in self.proxy_list:
|
|
||||||
|
|
||||||
prop_name = 'proxy.%s' % proxy
|
|
||||||
last_check = float(Env.prop(prop_name, default = 0))
|
|
||||||
if last_check > time.time() - 1209600:
|
|
||||||
continue
|
|
||||||
|
|
||||||
data = ''
|
|
||||||
try:
|
|
||||||
data = self.urlopen(proxy, timeout = 3, show_error = False)
|
|
||||||
except:
|
|
||||||
log.debug('Failed %s proxy %s: %s', (self.getName(), proxy, traceback.format_exc()))
|
|
||||||
|
|
||||||
if self.correctProxy(data):
|
|
||||||
log.debug('Using proxy for %s: %s', (self.getName(), proxy))
|
|
||||||
self.proxy_domain = proxy
|
|
||||||
break
|
|
||||||
|
|
||||||
Env.prop(prop_name, time.time())
|
|
||||||
|
|
||||||
if not self.proxy_domain:
|
|
||||||
log.error('No %s proxies left, please add one in settings, or let us know which one to add on the forum.', self.getName())
|
|
||||||
return None
|
|
||||||
|
|
||||||
return cleanHost(self.proxy_domain).rstrip('/') + url
|
|
||||||
|
|
||||||
def correctProxy(self, data):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class TorrentMagnetProvider(TorrentProvider):
|
|
||||||
|
|
||||||
protocol = 'torrent_magnet'
|
|
||||||
|
|
||||||
download = None
|
|
||||||
@@ -1,138 +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)
|
|
||||||
|
|
||||||
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': 'See <a href="http://bit-hdtv.com">BiT-HDTV</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'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,139 +0,0 @@
|
|||||||
import traceback
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode
|
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.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?',
|
|
||||||
'baseurl': 'https://www.bitsoup.me/%s',
|
|
||||||
}
|
|
||||||
|
|
||||||
http_time_between_calls = 1 # Seconds
|
|
||||||
|
|
||||||
def _searchOnTitle(self, title, movie, quality, results):
|
|
||||||
|
|
||||||
q = '"%s" %s' % (simplifyString(title), movie['info']['year'])
|
|
||||||
arguments = tryUrlencode({
|
|
||||||
'search': q,
|
|
||||||
})
|
|
||||||
url = "%s&%s" % (self.urls['search'], arguments)
|
|
||||||
|
|
||||||
url = self.urls['search'] % self.buildUrl(movie, quality)
|
|
||||||
data = self.getHTMLData(url)
|
|
||||||
|
|
||||||
if data:
|
|
||||||
html = BeautifulSoup(data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
result_table = html.find('table', attrs = {'class': 'koptekst'})
|
|
||||||
if not result_table or 'nothing found!' in data.lower():
|
|
||||||
return
|
|
||||||
|
|
||||||
entries = result_table.find_all('tr')
|
|
||||||
for result in entries[1:]:
|
|
||||||
|
|
||||||
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': 'See <a href="https://bitsoup.me">Bitsoup</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'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 @@
|
|||||||
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',
|
|
||||||
'description': 'See <a href="http://hdbits.org">HDBits</a>',
|
|
||||||
'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,187 +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
|
|
||||||
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:
|
|
||||||
soup = BeautifulSoup(data)
|
|
||||||
|
|
||||||
results_table = soup.find('table', attrs = {'class': 'koptekst'})
|
|
||||||
if not results_table:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
pagelinks = soup.findAll(href = re.compile('page'))
|
|
||||||
pageNumbers = [int(re.search('page=(?P<pageNumber>.+'')', i['href']).group('pageNumber')) for i in pagelinks]
|
|
||||||
total_pages = max(pageNumbers)
|
|
||||||
|
|
||||||
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,
|
|
||||||
'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,171 +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
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(TorrentProvider):
|
|
||||||
|
|
||||||
urls = {
|
|
||||||
'test': 'https://www.iptorrents.com/',
|
|
||||||
'base_url': 'https://www.iptorrents.com',
|
|
||||||
'login': 'https://www.iptorrents.com/torrents/',
|
|
||||||
'login_check': 'https://www.iptorrents.com/inbox.php',
|
|
||||||
'search': 'https://www.iptorrents.com/torrents/?%s%%s&q=%s&qf=ti&p=%%d',
|
|
||||||
}
|
|
||||||
|
|
||||||
http_time_between_calls = 1 # Seconds
|
|
||||||
cat_backup_id = None
|
|
||||||
|
|
||||||
def buildUrl(self, title, media, quality):
|
|
||||||
return self._buildUrl(title.replace(':', ''), quality)
|
|
||||||
|
|
||||||
def _buildUrl(self, query, quality):
|
|
||||||
|
|
||||||
cat_ids = self.getCatId(quality)
|
|
||||||
|
|
||||||
if not cat_ids:
|
|
||||||
log.warning('Unable to find category ids for identifier "%s"', quality.get('identifier'))
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.urls['search'] % ("&".join(("l%d=" % x) for x in cat_ids), tryUrlencode(query).replace('%', '%%'))
|
|
||||||
|
|
||||||
def _searchOnTitle(self, title, media, quality, results):
|
|
||||||
|
|
||||||
freeleech = '' if not self.conf('freeleech') else '&free=on'
|
|
||||||
|
|
||||||
base_url = self.buildUrl(title, media, quality)
|
|
||||||
if not base_url: return
|
|
||||||
|
|
||||||
pages = 1
|
|
||||||
current_page = 1
|
|
||||||
while current_page <= pages and not self.shuttingDown():
|
|
||||||
data = self.getHTMLData(base_url % (freeleech, current_page))
|
|
||||||
|
|
||||||
if data:
|
|
||||||
html = BeautifulSoup(data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
page_nav = html.find('span', attrs = {'class': 'page_nav'})
|
|
||||||
if page_nav:
|
|
||||||
next_link = page_nav.find("a", text = "Next")
|
|
||||||
if next_link:
|
|
||||||
final_page_link = next_link.previous_sibling.previous_sibling
|
|
||||||
pages = int(final_page_link.string)
|
|
||||||
|
|
||||||
result_table = html.find('table', attrs = {'class': 'torrents'})
|
|
||||||
|
|
||||||
if not result_table or 'nothing found!' in data.lower():
|
|
||||||
return
|
|
||||||
|
|
||||||
entries = result_table.find_all('tr')
|
|
||||||
|
|
||||||
for result in entries[1:]:
|
|
||||||
|
|
||||||
torrent = result.find_all('td')
|
|
||||||
if len(torrent) <= 1:
|
|
||||||
break
|
|
||||||
|
|
||||||
torrent = torrent[1].find('a')
|
|
||||||
|
|
||||||
torrent_id = torrent['href'].replace('/details.php?id=', '')
|
|
||||||
torrent_name = six.text_type(torrent.string)
|
|
||||||
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
|
|
||||||
torrent_details_url = self.urls['base_url'] + torrent['href']
|
|
||||||
torrent_size = self.parseSize(result.find_all('td')[5].string)
|
|
||||||
torrent_seeders = tryInt(result.find('td', attrs = {'class': 'ac t_seeders'}).string)
|
|
||||||
torrent_leechers = tryInt(result.find('td', attrs = {'class': 'ac t_leechers'}).string)
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'id': torrent_id,
|
|
||||||
'name': torrent_name,
|
|
||||||
'url': torrent_download_url,
|
|
||||||
'detail_url': torrent_details_url,
|
|
||||||
'size': torrent_size,
|
|
||||||
'seeders': torrent_seeders,
|
|
||||||
'leechers': torrent_leechers,
|
|
||||||
})
|
|
||||||
|
|
||||||
except:
|
|
||||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
|
||||||
break
|
|
||||||
|
|
||||||
current_page += 1
|
|
||||||
|
|
||||||
def getLoginParams(self):
|
|
||||||
return {
|
|
||||||
'username': self.conf('username'),
|
|
||||||
'password': self.conf('password'),
|
|
||||||
'login': 'submit',
|
|
||||||
}
|
|
||||||
|
|
||||||
def loginSuccess(self, output):
|
|
||||||
return 'don\'t have an account' not in output.lower()
|
|
||||||
|
|
||||||
def loginCheckSuccess(self, output):
|
|
||||||
return '/logout.php' in output.lower()
|
|
||||||
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'iptorrents',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'searcher',
|
|
||||||
'list': 'torrent_providers',
|
|
||||||
'name': 'IPTorrents',
|
|
||||||
'description': 'See <a href="http://www.iptorrents.com">IPTorrents</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'type': 'enabler',
|
|
||||||
'default': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
'default': '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'password',
|
|
||||||
'default': '',
|
|
||||||
'type': 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'freeleech',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Only search for [FreeLeech] torrents.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'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,165 +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://www.kickassunblock.info',
|
|
||||||
'http://www.kickassproxy.info',
|
|
||||||
]
|
|
||||||
|
|
||||||
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')[1]
|
|
||||||
new['id'] = temp.get('id')[-8:]
|
|
||||||
new['name'] = link.text
|
|
||||||
new['url'] = td.find('a', 'imagnet')['href']
|
|
||||||
new['detail_url'] = self.urls['detail'] % (self.getDomain(), link['href'][1:])
|
|
||||||
new['score'] = 20 if td.find('a', 'iverif') 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
|
|
||||||
|
|
||||||
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': 'See <a href="https://kat.ph/">KickAssTorrents</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'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': 'extra_score',
|
|
||||||
'advanced': True,
|
|
||||||
'label': 'Extra Score',
|
|
||||||
'type': 'int',
|
|
||||||
'default': 0,
|
|
||||||
'description': 'Starting score for each release found via this provider.',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
import htmlentitydefs
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
|
||||||
from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts, getIdentifier
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
|
||||||
from dateutil.parser import parse
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(TorrentProvider):
|
|
||||||
|
|
||||||
urls = {
|
|
||||||
'domain': 'https://tls.passthepopcorn.me',
|
|
||||||
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s',
|
|
||||||
'torrent': 'https://tls.passthepopcorn.me/torrents.php',
|
|
||||||
'login': 'https://tls.passthepopcorn.me/ajax.php?action=login',
|
|
||||||
'login_check': 'https://tls.passthepopcorn.me/ajax.php?action=login',
|
|
||||||
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d'
|
|
||||||
}
|
|
||||||
|
|
||||||
http_time_between_calls = 2
|
|
||||||
|
|
||||||
def _search(self, media, quality, results):
|
|
||||||
|
|
||||||
movie_title = getTitle(media)
|
|
||||||
quality_id = quality['identifier']
|
|
||||||
|
|
||||||
params = mergeDicts(self.quality_search_params[quality_id].copy(), {
|
|
||||||
'order_by': 'relevance',
|
|
||||||
'order_way': 'descending',
|
|
||||||
'searchstr': getIdentifier(media)
|
|
||||||
})
|
|
||||||
|
|
||||||
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
|
|
||||||
res = self.getJsonData(url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not 'Movies' in res:
|
|
||||||
return
|
|
||||||
|
|
||||||
authkey = res['AuthKey']
|
|
||||||
passkey = res['PassKey']
|
|
||||||
|
|
||||||
for ptpmovie in res['Movies']:
|
|
||||||
if not 'Torrents' in ptpmovie:
|
|
||||||
log.debug('Movie %s (%s) has NO torrents', (ptpmovie['Title'], ptpmovie['Year']))
|
|
||||||
continue
|
|
||||||
|
|
||||||
log.debug('Movie %s (%s) has %d torrents', (ptpmovie['Title'], ptpmovie['Year'], len(ptpmovie['Torrents'])))
|
|
||||||
for torrent in ptpmovie['Torrents']:
|
|
||||||
torrent_id = tryInt(torrent['Id'])
|
|
||||||
torrentdesc = '%s %s %s' % (torrent['Resolution'], torrent['Source'], torrent['Codec'])
|
|
||||||
torrentscore = 0
|
|
||||||
|
|
||||||
if 'GoldenPopcorn' in torrent and torrent['GoldenPopcorn']:
|
|
||||||
torrentdesc += ' HQ'
|
|
||||||
if self.conf('prefer_golden'):
|
|
||||||
torrentscore += 5000
|
|
||||||
if 'Scene' in torrent and torrent['Scene']:
|
|
||||||
torrentdesc += ' Scene'
|
|
||||||
if self.conf('prefer_scene'):
|
|
||||||
torrentscore += 2000
|
|
||||||
if 'RemasterTitle' in torrent and torrent['RemasterTitle']:
|
|
||||||
torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle'])
|
|
||||||
|
|
||||||
torrentdesc += ' (%s)' % quality_id
|
|
||||||
torrent_name = re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) - %s' % (movie_title, ptpmovie['Year'], torrentdesc))
|
|
||||||
|
|
||||||
def extra_check(item):
|
|
||||||
return self.torrentMeetsQualitySpec(item, quality_id)
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'id': torrent_id,
|
|
||||||
'name': torrent_name,
|
|
||||||
'Source': torrent['Source'],
|
|
||||||
'Checked': 'true' if torrent['Checked'] else 'false',
|
|
||||||
'Resolution': torrent['Resolution'],
|
|
||||||
'url': '%s?action=download&id=%d&authkey=%s&torrent_pass=%s' % (self.urls['torrent'], torrent_id, authkey, passkey),
|
|
||||||
'detail_url': self.urls['detail'] % torrent_id,
|
|
||||||
'date': tryInt(time.mktime(parse(torrent['UploadTime']).timetuple())),
|
|
||||||
'size': tryInt(torrent['Size']) / (1024 * 1024),
|
|
||||||
'seeders': tryInt(torrent['Seeders']),
|
|
||||||
'leechers': tryInt(torrent['Leechers']),
|
|
||||||
'score': torrentscore,
|
|
||||||
'extra_check': extra_check,
|
|
||||||
})
|
|
||||||
|
|
||||||
except:
|
|
||||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
|
||||||
|
|
||||||
def torrentMeetsQualitySpec(self, torrent, quality):
|
|
||||||
|
|
||||||
if not quality in self.post_search_filters:
|
|
||||||
return True
|
|
||||||
|
|
||||||
reqs = self.post_search_filters[quality].copy()
|
|
||||||
|
|
||||||
if self.conf('require_approval'):
|
|
||||||
log.debug('Config: Require staff-approval activated')
|
|
||||||
reqs['Checked'] = ['true']
|
|
||||||
|
|
||||||
for field, specs in reqs.items():
|
|
||||||
matches_one = False
|
|
||||||
seen_one = False
|
|
||||||
|
|
||||||
if not field in torrent:
|
|
||||||
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"', (torrent['id'], field, quality))
|
|
||||||
continue
|
|
||||||
|
|
||||||
for spec in specs:
|
|
||||||
if len(spec) > 0 and spec[0] == '!':
|
|
||||||
# a negative rule; if the field matches, return False
|
|
||||||
if torrent[field] == spec[1:]:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
# a positive rule; if any of the possible positive values match the field, return True
|
|
||||||
log.debug('Checking if torrents field %s equals %s' % (field, spec))
|
|
||||||
seen_one = True
|
|
||||||
if torrent[field] == spec:
|
|
||||||
log.debug('Torrent satisfied %s == %s' % (field, spec))
|
|
||||||
matches_one = True
|
|
||||||
|
|
||||||
if seen_one and not matches_one:
|
|
||||||
log.debug('Torrent did not satisfy requirements, ignoring')
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def htmlToUnicode(self, text):
|
|
||||||
def fixup(m):
|
|
||||||
txt = m.group(0)
|
|
||||||
if txt[:2] == "&#":
|
|
||||||
# character reference
|
|
||||||
try:
|
|
||||||
if txt[:3] == "&#x":
|
|
||||||
return unichr(int(txt[3:-1], 16))
|
|
||||||
else:
|
|
||||||
return unichr(int(txt[2:-1]))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# named entity
|
|
||||||
try:
|
|
||||||
txt = unichr(htmlentitydefs.name2codepoint[txt[1:-1]])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
return txt # leave as is
|
|
||||||
return re.sub("&#?\w+;", fixup, six.u('%s') % text)
|
|
||||||
|
|
||||||
def unicodeToASCII(self, text):
|
|
||||||
import unicodedata
|
|
||||||
return ''.join(c for c in unicodedata.normalize('NFKD', text) if unicodedata.category(c) != 'Mn')
|
|
||||||
|
|
||||||
def htmlToASCII(self, text):
|
|
||||||
return self.unicodeToASCII(self.htmlToUnicode(text))
|
|
||||||
|
|
||||||
def getLoginParams(self):
|
|
||||||
return {
|
|
||||||
'username': self.conf('username'),
|
|
||||||
'password': self.conf('password'),
|
|
||||||
'passkey': self.conf('passkey'),
|
|
||||||
'keeplogged': '1',
|
|
||||||
'login': 'Login'
|
|
||||||
}
|
|
||||||
|
|
||||||
def loginSuccess(self, output):
|
|
||||||
try:
|
|
||||||
return json.loads(output).get('Result', '').lower() == 'ok'
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
loginCheckSuccess = loginSuccess
|
|
||||||
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'passthepopcorn',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'searcher',
|
|
||||||
'list': 'torrent_providers',
|
|
||||||
'name': 'PassThePopcorn',
|
|
||||||
'description': 'See <a href="https://passthepopcorn.me">PassThePopcorn.me</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'type': 'enabler',
|
|
||||||
'default': False
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'domain',
|
|
||||||
'advanced': True,
|
|
||||||
'label': 'Proxy server',
|
|
||||||
'description': 'Domain for requests (HTTPS only!), keep empty to use default (tls.passthepopcorn.me).',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
'default': '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'password',
|
|
||||||
'default': '',
|
|
||||||
'type': 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'passkey',
|
|
||||||
'default': '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'prefer_golden',
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'label': 'Prefer golden',
|
|
||||||
'default': 1,
|
|
||||||
'description': 'Favors Golden Popcorn-releases over all other releases.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'prefer_scene',
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'label': 'Prefer scene',
|
|
||||||
'default': 0,
|
|
||||||
'description': 'Favors scene-releases over non-scene releases.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'require_approval',
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'label': 'Require approval',
|
|
||||||
'default': 0,
|
|
||||||
'description': 'Require staff-approval for releases to be accepted.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'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 @@
|
|||||||
from urlparse import parse_qs
|
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(TorrentMagnetProvider):
|
|
||||||
|
|
||||||
urls = {
|
|
||||||
'test': 'https://publichd.se',
|
|
||||||
'detail': 'https://publichd.se/index.php?page=torrent-details&id=%s',
|
|
||||||
'search': 'https://publichd.se/index.php',
|
|
||||||
}
|
|
||||||
http_time_between_calls = 0
|
|
||||||
|
|
||||||
def search(self, movie, quality):
|
|
||||||
|
|
||||||
if not quality.get('hd', False):
|
|
||||||
return []
|
|
||||||
|
|
||||||
return super(Base, self).search(movie, quality)
|
|
||||||
|
|
||||||
def _search(self, media, quality, results):
|
|
||||||
|
|
||||||
query = self.buildUrl(media)
|
|
||||||
|
|
||||||
params = tryUrlencode({
|
|
||||||
'page': 'torrents',
|
|
||||||
'search': query,
|
|
||||||
'active': 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
data = self.getHTMLData('%s?%s' % (self.urls['search'], params))
|
|
||||||
|
|
||||||
if data:
|
|
||||||
|
|
||||||
try:
|
|
||||||
soup = BeautifulSoup(data)
|
|
||||||
|
|
||||||
results_table = soup.find('table', attrs = {'id': 'bgtorrlist2'})
|
|
||||||
entries = results_table.find_all('tr')
|
|
||||||
|
|
||||||
for result in entries[2:len(entries) - 1]:
|
|
||||||
info_url = result.find(href = re.compile('torrent-details'))
|
|
||||||
download = result.find(href = re.compile('magnet:'))
|
|
||||||
|
|
||||||
if info_url and download:
|
|
||||||
|
|
||||||
url = parse_qs(info_url['href'])
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'id': url['id'][0],
|
|
||||||
'name': six.text_type(info_url.string),
|
|
||||||
'url': download['href'],
|
|
||||||
'detail_url': self.urls['detail'] % url['id'][0],
|
|
||||||
'size': self.parseSize(result.find_all('td')[7].string),
|
|
||||||
'seeders': tryInt(result.find_all('td')[4].string),
|
|
||||||
'leechers': tryInt(result.find_all('td')[5].string),
|
|
||||||
'get_more_info': self.getMoreInfo
|
|
||||||
})
|
|
||||||
|
|
||||||
except:
|
|
||||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
|
||||||
|
|
||||||
def getMoreInfo(self, item):
|
|
||||||
|
|
||||||
cache_key = 'publichd.%s' % item['id']
|
|
||||||
description = self.getCache(cache_key)
|
|
||||||
|
|
||||||
if not description:
|
|
||||||
|
|
||||||
try:
|
|
||||||
full_description = self.urlopen(item['detail_url'])
|
|
||||||
html = BeautifulSoup(full_description)
|
|
||||||
nfo_pre = html.find('div', attrs = {'id': 'torrmain'})
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'publichd',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'searcher',
|
|
||||||
'list': 'torrent_providers',
|
|
||||||
'name': 'PublicHD',
|
|
||||||
'description': 'Public Torrent site with only HD content. See <a href="https://publichd.se/">PublicHD</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'type': 'enabler',
|
|
||||||
'default': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'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,134 +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 _search(self, media, quality, results):
|
|
||||||
|
|
||||||
url = self.buildUrl(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': 'See <a href="https://sceneaccess.eu/">SceneAccess</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'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,164 +0,0 @@
|
|||||||
import re
|
|
||||||
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 TorrentMagnetProvider
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(TorrentMagnetProvider):
|
|
||||||
|
|
||||||
urls = {
|
|
||||||
'detail': '%s/torrent/%s',
|
|
||||||
'search': '%s/search/%%s/%%s/7/%%s'
|
|
||||||
}
|
|
||||||
|
|
||||||
cat_backup_id = 200
|
|
||||||
disable_provider = False
|
|
||||||
http_time_between_calls = 0
|
|
||||||
|
|
||||||
proxy_list = [
|
|
||||||
'https://tpb.ipredator.se',
|
|
||||||
'https://thepiratebay.se',
|
|
||||||
'http://pirateproxy.ca',
|
|
||||||
'http://tpb.al',
|
|
||||||
'http://www.tpb.gr',
|
|
||||||
'http://bayproxy.me',
|
|
||||||
'http://proxybay.eu',
|
|
||||||
'http://www.getpirate.com',
|
|
||||||
'http://piratebay.io',
|
|
||||||
]
|
|
||||||
|
|
||||||
def _search(self, media, quality, results):
|
|
||||||
|
|
||||||
page = 0
|
|
||||||
total_pages = 1
|
|
||||||
cats = self.getCatId(quality)
|
|
||||||
|
|
||||||
base_search_url = self.urls['search'] % self.getDomain()
|
|
||||||
|
|
||||||
while page < total_pages:
|
|
||||||
|
|
||||||
search_url = base_search_url % self.buildUrl(media, page, cats)
|
|
||||||
|
|
||||||
page += 1
|
|
||||||
|
|
||||||
data = self.getHTMLData(search_url)
|
|
||||||
|
|
||||||
if data:
|
|
||||||
try:
|
|
||||||
soup = BeautifulSoup(data)
|
|
||||||
results_table = soup.find('table', attrs = {'id': 'searchResult'})
|
|
||||||
|
|
||||||
if not results_table:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
total_pages = len(soup.find('div', attrs = {'align': 'center'}).find_all('a'))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
entries = results_table.find_all('tr')
|
|
||||||
for result in entries[2:]:
|
|
||||||
link = result.find(href = re.compile('torrent\/\d+\/'))
|
|
||||||
download = result.find(href = re.compile('magnet:'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
size = re.search('Size (?P<size>.+),', six.text_type(result.select('font.detDesc')[0])).group('size')
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'id': re.search('/(?P<id>\d+)/', link['href']).group('id'),
|
|
||||||
'name': six.text_type(link.string),
|
|
||||||
'url': download['href'],
|
|
||||||
'detail_url': self.getDomain(link['href']),
|
|
||||||
'size': self.parseSize(size),
|
|
||||||
'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 isEnabled(self):
|
|
||||||
return super(Base, self).isEnabled() and self.getDomain()
|
|
||||||
|
|
||||||
def correctProxy(self, data):
|
|
||||||
return 'title="Pirate Search"' in data
|
|
||||||
|
|
||||||
def getMoreInfo(self, item):
|
|
||||||
full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
|
||||||
html = BeautifulSoup(full_description)
|
|
||||||
nfo_pre = html.find('div', attrs = {'class': 'nfo'})
|
|
||||||
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
|
||||||
|
|
||||||
item['description'] = description
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'thepiratebay',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'searcher',
|
|
||||||
'list': 'torrent_providers',
|
|
||||||
'name': 'ThePirateBay',
|
|
||||||
'description': 'The world\'s largest bittorrent tracker. See <a href="http://fucktimkuik.org/">ThePirateBay</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'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,135 +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': 'See <a href="http://torrentbytes.net">TorrentBytes</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'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,113 +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 _search(self, media, quality, results):
|
|
||||||
|
|
||||||
query = self.buildUrl(media)
|
|
||||||
|
|
||||||
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': 'See <a href="http://www.td.af/">TorrentDay</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'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,125 +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 _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': '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': 'See <a href="http://torrentleech.org">TorrentLeech</a>',
|
|
||||||
'wizard': True,
|
|
||||||
'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,187 +0,0 @@
|
|||||||
from urlparse import urlparse
|
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
|
||||||
from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.media._base.providers.base import ResultList
|
|
||||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(TorrentProvider):
|
|
||||||
|
|
||||||
urls = {}
|
|
||||||
limits_reached = {}
|
|
||||||
|
|
||||||
http_time_between_calls = 1 # Seconds
|
|
||||||
|
|
||||||
def search(self, media, quality):
|
|
||||||
hosts = self.getHosts()
|
|
||||||
|
|
||||||
results = ResultList(self, media, quality, imdb_results = True)
|
|
||||||
|
|
||||||
for host in hosts:
|
|
||||||
if self.isDisabled(host):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._searchOnHost(host, media, quality, results)
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def _searchOnHost(self, host, media, quality, results):
|
|
||||||
|
|
||||||
torrents = self.getJsonData(self.buildUrl(media, host), cache_timeout = 1800)
|
|
||||||
|
|
||||||
if torrents:
|
|
||||||
try:
|
|
||||||
if torrents.get('error'):
|
|
||||||
log.error('%s: %s', (torrents.get('error'), host['host']))
|
|
||||||
elif torrents.get('results'):
|
|
||||||
for torrent in torrents.get('results', []):
|
|
||||||
results.append({
|
|
||||||
'id': torrent.get('torrent_id'),
|
|
||||||
'protocol': 'torrent' if re.match('^(http|https|ftp)://.*$', torrent.get('download_url')) else 'torrent_magnet',
|
|
||||||
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
|
||||||
'name': toUnicode(torrent.get('release_name')),
|
|
||||||
'url': torrent.get('download_url'),
|
|
||||||
'detail_url': torrent.get('details_url'),
|
|
||||||
'size': torrent.get('size'),
|
|
||||||
'score': host['extra_score'],
|
|
||||||
'seeders': torrent.get('seeders'),
|
|
||||||
'leechers': torrent.get('leechers'),
|
|
||||||
'seed_ratio': host['seed_ratio'],
|
|
||||||
'seed_time': host['seed_time'],
|
|
||||||
})
|
|
||||||
|
|
||||||
except:
|
|
||||||
log.error('Failed getting results from %s: %s', (host['host'], traceback.format_exc()))
|
|
||||||
|
|
||||||
def getHosts(self):
|
|
||||||
|
|
||||||
uses = splitString(str(self.conf('use')), clean = False)
|
|
||||||
hosts = splitString(self.conf('host'), clean = False)
|
|
||||||
names = splitString(self.conf('name'), clean = False)
|
|
||||||
seed_times = splitString(self.conf('seed_time'), clean = False)
|
|
||||||
seed_ratios = splitString(self.conf('seed_ratio'), clean = False)
|
|
||||||
pass_keys = splitString(self.conf('pass_key'), clean = False)
|
|
||||||
extra_score = splitString(self.conf('extra_score'), clean = False)
|
|
||||||
|
|
||||||
host_list = []
|
|
||||||
for nr in range(len(hosts)):
|
|
||||||
|
|
||||||
try: key = pass_keys[nr]
|
|
||||||
except: key = ''
|
|
||||||
|
|
||||||
try: host = hosts[nr]
|
|
||||||
except: host = ''
|
|
||||||
|
|
||||||
try: name = names[nr]
|
|
||||||
except: name = ''
|
|
||||||
|
|
||||||
try: ratio = seed_ratios[nr]
|
|
||||||
except: ratio = ''
|
|
||||||
|
|
||||||
try: seed_time = seed_times[nr]
|
|
||||||
except: seed_time = ''
|
|
||||||
|
|
||||||
host_list.append({
|
|
||||||
'use': uses[nr],
|
|
||||||
'host': host,
|
|
||||||
'name': name,
|
|
||||||
'seed_ratio': tryFloat(ratio),
|
|
||||||
'seed_time': tryInt(seed_time),
|
|
||||||
'pass_key': key,
|
|
||||||
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
|
|
||||||
})
|
|
||||||
|
|
||||||
return host_list
|
|
||||||
|
|
||||||
def belongsTo(self, url, provider = None, host = None):
|
|
||||||
|
|
||||||
hosts = self.getHosts()
|
|
||||||
|
|
||||||
for host in hosts:
|
|
||||||
result = super(Base, self).belongsTo(url, host = host['host'], provider = provider)
|
|
||||||
if result:
|
|
||||||
return result
|
|
||||||
|
|
||||||
def isDisabled(self, host = None):
|
|
||||||
return not self.isEnabled(host)
|
|
||||||
|
|
||||||
def isEnabled(self, host = None):
|
|
||||||
|
|
||||||
# Return true if at least one is enabled and no host is given
|
|
||||||
if host is None:
|
|
||||||
for host in self.getHosts():
|
|
||||||
if self.isEnabled(host):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
return TorrentProvider.isEnabled(self) and host['host'] and host['pass_key'] and int(host['use'])
|
|
||||||
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'torrentpotato',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'searcher',
|
|
||||||
'list': 'torrent_providers',
|
|
||||||
'name': 'TorrentPotato',
|
|
||||||
'order': 10,
|
|
||||||
'description': 'CouchPotato torrent provider. Checkout <a href="https://github.com/RuudBurger/CouchPotatoServer/wiki/CouchPotato-Torrent-Provider">the wiki page about this provider</a> for more info.',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'type': 'enabler',
|
|
||||||
'default': False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'use',
|
|
||||||
'default': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'default': '',
|
|
||||||
'description': 'The url path of your TorrentPotato provider.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'extra_score',
|
|
||||||
'advanced': True,
|
|
||||||
'label': 'Extra Score',
|
|
||||||
'default': '0',
|
|
||||||
'description': 'Starting score for each release found via this provider.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'name',
|
|
||||||
'label': 'Username',
|
|
||||||
'default': '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'seed_ratio',
|
|
||||||
'label': 'Seed ratio',
|
|
||||||
'default': '1',
|
|
||||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'seed_time',
|
|
||||||
'label': 'Seed time',
|
|
||||||
'default': '40',
|
|
||||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'pass_key',
|
|
||||||
'default': ',',
|
|
||||||
'label': 'Pass Key',
|
|
||||||
'description': 'Can be found on your profile page',
|
|
||||||
'type': 'combined',
|
|
||||||
'combine': ['use', 'host', 'pass_key', 'name', 'seed_ratio', 'seed_time', 'extra_score'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -1,130 +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')[4].string),
|
|
||||||
'seeders': tryInt(result.find_all('td')[6].string),
|
|
||||||
'leechers': tryInt(result.find_all('td')[7].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': 'See <a href="https://www.torrentshack.net/">TorrentShack</a>',
|
|
||||||
'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,128 +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,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'type': 'enabler',
|
|
||||||
'default': False
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'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,117 +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 TorrentProvider
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(TorrentProvider):
|
|
||||||
|
|
||||||
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):
|
|
||||||
|
|
||||||
search_url = self.urls['search'] % (self.getDomain(), getIdentifier(movie), quality['identifier'])
|
|
||||||
|
|
||||||
data = self.getJsonData(search_url)
|
|
||||||
|
|
||||||
if data and data.get('MovieList'):
|
|
||||||
try:
|
|
||||||
for result in data.get('MovieList'):
|
|
||||||
|
|
||||||
try:
|
|
||||||
title = result['TorrentUrl'].split('/')[-1][:-8].replace('_', '.').strip('._')
|
|
||||||
title = title.replace('.-.', '-')
|
|
||||||
title = title.replace('..', '.')
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'id': result['MovieID'],
|
|
||||||
'name': title,
|
|
||||||
'url': result['TorrentMagnetUrl'],
|
|
||||||
'detail_url': self.urls['detail'] % (self.getDomain(), 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,
|
|
||||||
'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 +0,0 @@
|
|||||||
from .main import Search
|
|
||||||
|
|
||||||
|
|
||||||
def autoload():
|
|
||||||
return Search()
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
from couchpotato.api import addApiView
|
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
|
||||||
from couchpotato.core.helpers.variable import mergeDicts
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Search(Plugin):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
addApiView('search', self.search, docs = {
|
|
||||||
'desc': 'Search the info in providers for a movie',
|
|
||||||
'params': {
|
|
||||||
'q': {'desc': 'The (partial) movie name you want to search for'},
|
|
||||||
'type': {'desc': 'Search for a specific media type. Leave empty to search all.'},
|
|
||||||
},
|
|
||||||
'return': {'type': 'object', 'example': """{
|
|
||||||
'success': True,
|
|
||||||
'movies': array,
|
|
||||||
'show': array,
|
|
||||||
etc
|
|
||||||
}"""}
|
|
||||||
})
|
|
||||||
|
|
||||||
addEvent('app.load', self.addSingleSearches)
|
|
||||||
|
|
||||||
def search(self, q = '', types = None, **kwargs):
|
|
||||||
|
|
||||||
# Make sure types is the correct instance
|
|
||||||
if isinstance(types, (str, unicode)):
|
|
||||||
types = [types]
|
|
||||||
elif isinstance(types, (list, tuple, set)):
|
|
||||||
types = list(types)
|
|
||||||
|
|
||||||
if not types:
|
|
||||||
result = fireEvent('info.search', q = q, merge = True)
|
|
||||||
else:
|
|
||||||
result = {}
|
|
||||||
for media_type in types:
|
|
||||||
result[media_type] = fireEvent('%s.search' % media_type)
|
|
||||||
|
|
||||||
return mergeDicts({
|
|
||||||
'success': True,
|
|
||||||
}, result)
|
|
||||||
|
|
||||||
def createSingleSearch(self, media_type):
|
|
||||||
|
|
||||||
def singleSearch(q, **kwargs):
|
|
||||||
return self.search(q, type = media_type, **kwargs)
|
|
||||||
|
|
||||||
return singleSearch
|
|
||||||
|
|
||||||
def addSingleSearches(self):
|
|
||||||
|
|
||||||
for media_type in fireEvent('media.types', merge = True):
|
|
||||||
addApiView('%s.search' % media_type, self.createSingleSearch(media_type))
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
.search_form {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
position: absolute;
|
|
||||||
right: 105px;
|
|
||||||
top: 0;
|
|
||||||
text-align: right;
|
|
||||||
height: 100%;
|
|
||||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
|
||||||
position: absolute;
|
|
||||||
z-index: 20;
|
|
||||||
border: 0 solid transparent;
|
|
||||||
border-bottom-width: 4px;
|
|
||||||
}
|
|
||||||
.search_form:hover {
|
|
||||||
border-color: #047792;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 480px) {
|
|
||||||
.search_form {
|
|
||||||
right: 44px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search_form.focused,
|
|
||||||
.search_form.shown {
|
|
||||||
border-color: #04bce6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search_form .input {
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 45px;
|
|
||||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search_form.focused .input,
|
|
||||||
.search_form.shown .input {
|
|
||||||
width: 380px;
|
|
||||||
background: #4e5969;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search_form .input input {
|
|
||||||
border-radius: 0;
|
|
||||||
display: block;
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
color: #FFF;
|
|
||||||
font-size: 25px;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
padding: 0 40px 0 10px;
|
|
||||||
transition: all .4s ease-in-out .2s;
|
|
||||||
}
|
|
||||||
.search_form.focused .input input,
|
|
||||||
.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;
|
|
||||||
right: 0;
|
|
||||||
width: 44px;
|
|
||||||
height: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
vertical-align: middle;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 66px;
|
|
||||||
font-size: 15px;
|
|
||||||
color: #FFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search_form .input a:after {
|
|
||||||
content: "\e03e";
|
|
||||||
}
|
|
||||||
|
|
||||||
.search_form.shown.filled .input a:after {
|
|
||||||
content: "\e04e";
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 480px) {
|
|
||||||
.search_form .input a {
|
|
||||||
line-height: 44px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search_form .results_container {
|
|
||||||
text-align: left;
|
|
||||||
position: absolute;
|
|
||||||
background: #5c697b;
|
|
||||||
margin: 4px 0 0;
|
|
||||||
width: 470px;
|
|
||||||
min-height: 50px;
|
|
||||||
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media all and (max-width: 480px) {
|
|
||||||
.search_form .results_container {
|
|
||||||
width: 320px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.search_form.focused.filled .results_container,
|
|
||||||
.search_form.shown.filled .results_container {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search_form .results {
|
|
||||||
max-height: 570px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media_result {
|
|
||||||
overflow: hidden;
|
|
||||||
height: 50px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media_result .options {
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 30px;
|
|
||||||
right: 0;
|
|
||||||
padding: 13px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-width: 1px 0;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
|
|
||||||
}
|
|
||||||
.media_result .options > .in_library_wanted {
|
|
||||||
margin-top: -7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media_result .options > div {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media_result .options .thumbnail {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media_result .options select {
|
|
||||||
vertical-align: middle;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
.media_result .options select[name=title] { width: 170px; }
|
|
||||||
.media_result .options select[name=profile] { width: 90px; }
|
|
||||||
.media_result .options select[name=category] { width: 80px; }
|
|
||||||
|
|
||||||
@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 {
|
|
||||||
vertical-align: middle;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media_result .options .message {
|
|
||||||
height: 100%;
|
|
||||||
font-size: 20px;
|
|
||||||
color: #fff;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media_result .data {
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 30px;
|
|
||||||
right: 0;
|
|
||||||
background: #5c697b;
|
|
||||||
cursor: pointer;
|
|
||||||
border-top: 1px solid rgba(255,255,255, 0.08);
|
|
||||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
|
||||||
}
|
|
||||||
.media_result .data.open {
|
|
||||||
left: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media_result:last-child .data { border-bottom: 0; }
|
|
||||||
|
|
||||||
.media_result .in_wanted, .media_result .in_library {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 2px;
|
|
||||||
left: 14px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media_result .thumbnail {
|
|
||||||
width: 34px;
|
|
||||||
min-height: 100%;
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media_result .info {
|
|
||||||
position: absolute;
|
|
||||||
top: 20%;
|
|
||||||
left: 15px;
|
|
||||||
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%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media_result .info h2 .title {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
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;
|
|
||||||
position: absolute;
|
|
||||||
width: 12%;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 480px) {
|
|
||||||
|
|
||||||
.search_form .info h2 .year {
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.search_form .mask,
|
|
||||||
.media_result .mask {
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
Block.Search = new Class({
|
|
||||||
|
|
||||||
Extends: BlockBase,
|
|
||||||
|
|
||||||
cache: {},
|
|
||||||
|
|
||||||
create: function(){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var focus_timer = 0;
|
|
||||||
self.el = new Element('div.search_form').adopt(
|
|
||||||
new Element('div.input').adopt(
|
|
||||||
self.input = new Element('input', {
|
|
||||||
'placeholder': 'Search & add a new media',
|
|
||||||
'events': {
|
|
||||||
'keyup': self.keyup.bind(self),
|
|
||||||
'focus': function(){
|
|
||||||
if(focus_timer) clearTimeout(focus_timer);
|
|
||||||
self.el.addClass('focused');
|
|
||||||
if(this.get('value'))
|
|
||||||
self.hideResults(false)
|
|
||||||
},
|
|
||||||
'blur': function(){
|
|
||||||
focus_timer = (function(){
|
|
||||||
self.el.removeClass('focused')
|
|
||||||
}).delay(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new Element('a.icon2', {
|
|
||||||
'events': {
|
|
||||||
'click': self.clear.bind(self),
|
|
||||||
'touchend': self.clear.bind(self)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
),
|
|
||||||
self.result_container = new Element('div.results_container', {
|
|
||||||
'tween': {
|
|
||||||
'duration': 200
|
|
||||||
},
|
|
||||||
'events': {
|
|
||||||
'mousewheel': function(e){
|
|
||||||
(e).stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).adopt(
|
|
||||||
self.results = new Element('div.results')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
self.mask = new Element('div.mask').inject(self.result_container).fade('hide');
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
clear: function(e){
|
|
||||||
var self = this;
|
|
||||||
(e).preventDefault();
|
|
||||||
|
|
||||||
if(self.last_q === ''){
|
|
||||||
self.input.blur();
|
|
||||||
self.last_q = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
self.last_q = '';
|
|
||||||
self.input.set('value', '');
|
|
||||||
self.input.focus();
|
|
||||||
|
|
||||||
self.media = {};
|
|
||||||
self.results.empty();
|
|
||||||
self.el.removeClass('filled')
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hideResults: function(bool){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if(self.hidden == bool) return;
|
|
||||||
|
|
||||||
self.el[bool ? 'removeClass' : 'addClass']('shown');
|
|
||||||
|
|
||||||
if(bool){
|
|
||||||
History.removeEvent('change', self.hideResults.bind(self, !bool));
|
|
||||||
self.el.removeEvent('outerClick', self.hideResults.bind(self, !bool));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
History.addEvent('change', self.hideResults.bind(self, !bool));
|
|
||||||
self.el.addEvent('outerClick', self.hideResults.bind(self, !bool));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.hidden = bool;
|
|
||||||
},
|
|
||||||
|
|
||||||
keyup: function(){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled');
|
|
||||||
|
|
||||||
if(self.q() != self.last_q){
|
|
||||||
if(self.api_request && self.api_request.isRunning())
|
|
||||||
self.api_request.cancel();
|
|
||||||
|
|
||||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer);
|
|
||||||
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
autocomplete: function(){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if(!self.q()){
|
|
||||||
self.hideResults(true);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.list()
|
|
||||||
},
|
|
||||||
|
|
||||||
list: function(){
|
|
||||||
var self = this,
|
|
||||||
q = self.q(),
|
|
||||||
cache = self.cache[q];
|
|
||||||
|
|
||||||
self.hideResults(false);
|
|
||||||
|
|
||||||
if(!cache){
|
|
||||||
self.mask.fade('in');
|
|
||||||
|
|
||||||
if(!self.spinner)
|
|
||||||
self.spinner = createSpinner(self.mask);
|
|
||||||
|
|
||||||
self.api_request = Api.request('search', {
|
|
||||||
'data': {
|
|
||||||
'q': q
|
|
||||||
},
|
|
||||||
'onComplete': self.fill.bind(self, q)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else
|
|
||||||
self.fill(q, cache);
|
|
||||||
|
|
||||||
self.last_q = q;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
fill: function(q, json){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
self.cache[q] = json;
|
|
||||||
|
|
||||||
self.media = {};
|
|
||||||
self.results.empty();
|
|
||||||
|
|
||||||
Object.each(json, function(media){
|
|
||||||
if(typeOf(media) == 'array'){
|
|
||||||
Object.each(media, function(m){
|
|
||||||
|
|
||||||
var m = new Block.Search[m.type.capitalize() + 'Item'](m);
|
|
||||||
$(m).inject(self.results);
|
|
||||||
self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m;
|
|
||||||
|
|
||||||
if(q == m.imdb)
|
|
||||||
m.showOptions()
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate result heights
|
|
||||||
var w = window.getSize(),
|
|
||||||
rc = self.result_container.getCoordinates();
|
|
||||||
|
|
||||||
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px');
|
|
||||||
self.mask.fade('out')
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
loading: function(bool){
|
|
||||||
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
|
||||||
},
|
|
||||||
|
|
||||||
q: function(){
|
|
||||||
return this.input.get('value').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user