Compare commits
206 Commits
build/2.0.
...
build/2.0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96472a9a8f | ||
|
|
27252561e2 | ||
|
|
24b341005e | ||
|
|
749cf550ec | ||
|
|
650177803b | ||
|
|
003db92c9b | ||
|
|
b2b396bf17 | ||
|
|
f1a1db8d5b | ||
|
|
f515cd2477 | ||
|
|
65bb1bec27 | ||
|
|
cc84532824 | ||
|
|
5530fbf792 | ||
|
|
5658a85f61 | ||
|
|
0c5206f01b | ||
|
|
4bffce637e | ||
|
|
9f2941a45c | ||
|
|
f452106bfc | ||
|
|
da3055be30 | ||
|
|
f9b65e7216 | ||
|
|
07e2c56095 | ||
|
|
9a6cfe3a21 | ||
|
|
802338a934 | ||
|
|
f0a3358561 | ||
|
|
1c4c69211b | ||
|
|
c9e732651f | ||
|
|
7849e7170d | ||
|
|
087894eb4e | ||
|
|
77d57f5a09 | ||
|
|
618845a021 | ||
|
|
3aabcbf8f1 | ||
|
|
929c6fe3f9 | ||
|
|
c852949591 | ||
|
|
e36c8ec3ab | ||
|
|
afea12c7c0 | ||
|
|
c29a8b47d6 | ||
|
|
fdd0826b4f | ||
|
|
81b7ebaf51 | ||
|
|
eafc3db74d | ||
|
|
3464435a5c | ||
|
|
9f19902221 | ||
|
|
2ed72c9098 | ||
|
|
723f720280 | ||
|
|
daaa2154e5 | ||
|
|
95c5db2d17 | ||
|
|
e53a9ed30a | ||
|
|
2b49a4b5d6 | ||
|
|
4224a25e54 | ||
|
|
3635da1f59 | ||
|
|
71cca6b87f | ||
|
|
68c0496f8e | ||
|
|
6dc3c8d69d | ||
|
|
b03012e4aa | ||
|
|
5a1f05df8e | ||
|
|
62a5909856 | ||
|
|
813c078db0 | ||
|
|
904d1ea4f7 | ||
|
|
20b773bc3b | ||
|
|
be56b96bd0 | ||
|
|
655e847aeb | ||
|
|
f3fd0afb42 | ||
|
|
3782ad7f98 | ||
|
|
6f5031fa7c | ||
|
|
93604a45e5 | ||
|
|
28f4169e44 | ||
|
|
2361057e4c | ||
|
|
5caa40bd81 | ||
|
|
a22bd4abd4 | ||
|
|
a32ba7a763 | ||
|
|
5fe645cc11 | ||
|
|
f333d85907 | ||
|
|
3ec2df5780 | ||
|
|
25f1b8c7a7 | ||
|
|
e71da1f14d | ||
|
|
212d64143c | ||
|
|
51f9b5c673 | ||
|
|
2215c000b7 | ||
|
|
14797249ff | ||
|
|
49e2607f5d | ||
|
|
938b14ba18 | ||
|
|
c893d5bbb8 | ||
|
|
c4adab69cb | ||
|
|
2c9af74f7f | ||
|
|
7eb15c1a53 | ||
|
|
a02257a906 | ||
|
|
667075a006 | ||
|
|
b0f6f9b2ea | ||
|
|
c0900cfe94 | ||
|
|
24a4810919 | ||
|
|
70b15a5696 | ||
|
|
d6522d8f38 | ||
|
|
78eab890e7 | ||
|
|
35c0356734 | ||
|
|
1a56191f83 | ||
|
|
41c0f34d95 | ||
|
|
37bf205d7a | ||
|
|
b66af0b6c6 | ||
|
|
d1e798323c | ||
|
|
7420785eaf | ||
|
|
a86522a810 | ||
|
|
d636314971 | ||
|
|
e918e6b12f | ||
|
|
b71f003ad8 | ||
|
|
a432ad4f5a | ||
|
|
ac04121dd3 | ||
|
|
61a3a0386e | ||
|
|
7b1f17c062 | ||
|
|
6f7b565103 | ||
|
|
4fb7467e97 | ||
|
|
bcdc633a5e | ||
|
|
19f74e398f | ||
|
|
09f723bda5 | ||
|
|
fbeadb8d9e | ||
|
|
5bda44d419 | ||
|
|
84eccbf9cf | ||
|
|
9ebc4dbf38 | ||
|
|
907f821e50 | ||
|
|
9dc1843f25 | ||
|
|
ad0a1b1efe | ||
|
|
8bfad087e1 | ||
|
|
67c87444de | ||
|
|
4dfd8b4cd5 | ||
|
|
3ffc6e122e | ||
|
|
f204309ed7 | ||
|
|
d3ebe531d5 | ||
|
|
6106fd4e82 | ||
|
|
981ba61458 | ||
|
|
a5534c4bd2 | ||
|
|
2cd887b70a | ||
|
|
19ddd03204 | ||
|
|
d0d9ac07a6 | ||
|
|
0773d6e6ad | ||
|
|
1011e2e9b8 | ||
|
|
84f5dcc134 | ||
|
|
fef3eb1b84 | ||
|
|
ef6d0e04c0 | ||
|
|
269e98b049 | ||
|
|
378d1ccd1c | ||
|
|
f3e3632dd3 | ||
|
|
27635caa1d | ||
|
|
4836a9ffdc | ||
|
|
8874bd4e2b | ||
|
|
799b665f15 | ||
|
|
7f90135947 | ||
|
|
50a2bca459 | ||
|
|
a3b3b9c218 | ||
|
|
d38bd03422 | ||
|
|
9184a97fcd | ||
|
|
ce0bf7b51a | ||
|
|
151b100573 | ||
|
|
0e23413069 | ||
|
|
2ac2b0ff06 | ||
|
|
86bf08cbd4 | ||
|
|
ed0e54d64d | ||
|
|
3da0b1a804 | ||
|
|
39c2567d5a | ||
|
|
95c5d16991 | ||
|
|
08b450fc0a | ||
|
|
dc63796e48 | ||
|
|
c6cba2f6e5 | ||
|
|
ef945597d2 | ||
|
|
ba36c738c7 | ||
|
|
3a3a4fb1f3 | ||
|
|
3fa352e7c8 | ||
|
|
08ef153bbf | ||
|
|
7e3a6eeb83 | ||
|
|
24ad975917 | ||
|
|
952f29918e | ||
|
|
08ae51dbe6 | ||
|
|
d40ad1ddf2 | ||
|
|
0132012276 | ||
|
|
0f1e8eeff9 | ||
|
|
8eee2af49b | ||
|
|
3d26a53fbd | ||
|
|
d4600635e1 | ||
|
|
6fc9d383de | ||
|
|
5776b2caad | ||
|
|
f82e2a3e6e | ||
|
|
a5fa503970 | ||
|
|
6f7d2caa9b | ||
|
|
c0012c9243 | ||
|
|
5cc7250528 | ||
|
|
aa1fa3eb9a | ||
|
|
f474962225 | ||
|
|
0e2f8a612c | ||
|
|
ad7de32e70 | ||
|
|
d295b881af | ||
|
|
20f81d06c0 | ||
|
|
8fb24bb101 | ||
|
|
ce3efd3a3c | ||
|
|
ef7fc62c66 | ||
|
|
4be8d02cbb | ||
|
|
7ce8a4fc45 | ||
|
|
465e7b2abc | ||
|
|
102a0177de | ||
|
|
578fb45785 | ||
|
|
96995bbbe5 | ||
|
|
43e1adbff9 | ||
|
|
6e8b828cd4 | ||
|
|
5851f226b6 | ||
|
|
4cfdafebbc | ||
|
|
006163bd2b | ||
|
|
b97acb8ef5 | ||
|
|
de6d686fe5 | ||
|
|
45bb88460c | ||
|
|
d2901bc68a | ||
|
|
2c0af15325 |
@@ -100,7 +100,7 @@ class Loader(object):
|
||||
logging.shutdown()
|
||||
time.sleep(3)
|
||||
|
||||
args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:]
|
||||
args = [sys.executable] + [os.path.join(base_path, os.path.basename(__file__))] + sys.argv[1:]
|
||||
subprocess.Popen(args)
|
||||
except:
|
||||
self.log.critical(traceback.format_exc())
|
||||
|
||||
20
Desktop.py
20
Desktop.py
@@ -1,3 +1,4 @@
|
||||
from esky.util import appdir_from_executable #@UnresolvedImport
|
||||
from threading import Thread
|
||||
from wx.lib.softwareupdate import SoftwareUpdate
|
||||
import os
|
||||
@@ -5,8 +6,6 @@ import sys
|
||||
import time
|
||||
import webbrowser
|
||||
import wx
|
||||
import subprocess
|
||||
|
||||
|
||||
# Include proper dirs
|
||||
if hasattr(sys, 'frozen'):
|
||||
@@ -214,5 +213,18 @@ if __name__ == '__main__':
|
||||
time.sleep(1)
|
||||
|
||||
if app.restart:
|
||||
args = [sys.executable] + [os.path.join(base_path, 'Desktop.py')] + sys.argv[1:]
|
||||
subprocess.Popen(args)
|
||||
|
||||
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:])
|
||||
|
||||
@@ -17,6 +17,7 @@ Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for
|
||||
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`.
|
||||
* You can now start CP via `CouchPotatoServer\CouchPotato.py` to start
|
||||
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
|
||||
|
||||
OSx:
|
||||
|
||||
@@ -26,6 +27,7 @@ OSx:
|
||||
* Go to your App folder `cd /Applications`
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||
* Then do `python CouchPotatoServer/CouchPotato.py`
|
||||
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
|
||||
|
||||
Linux (ubuntu / debian):
|
||||
|
||||
@@ -33,7 +35,8 @@ Linux (ubuntu / debian):
|
||||
* 'cd' to the folder of your choosing.
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||
* Then do `python CouchPotatoServer/CouchPotato.py` to start
|
||||
* To run on boot copy the init script. `cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
|
||||
* Change the paths inside the init script. `nano /etc/init.d/couchpotato`
|
||||
* Make it executable. `chmod +x /etc/init.d/couchpotato`
|
||||
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
|
||||
* Change the paths inside the init script. `sudo nano /etc/init.d/couchpotato`
|
||||
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
||||
* Open your browser and go to: `http://localhost:5050/`
|
||||
|
||||
14
contributing.md
Normal file
14
contributing.md
Normal file
@@ -0,0 +1,14 @@
|
||||
#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem!
|
||||
|
||||
##Just make sure you think of the following things:
|
||||
|
||||
* Search through the existing (and closed) issues first. See if you can get your answer there.
|
||||
* Double check the result manually, because it could be an external issue.
|
||||
* Post logs! Without seeing what is going on, I can't reproduce the error.
|
||||
* What are you settings for the specific problem
|
||||
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby)
|
||||
* Give me a short step by step of how to reproduce
|
||||
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
|
||||
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
|
||||
|
||||
**If I don't get enough info, the change of the issue getting closed is a lot bigger ;)**
|
||||
@@ -27,6 +27,7 @@ config = [{
|
||||
'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.',
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ from tornado.ioloop import IOLoop
|
||||
from uuid import uuid4
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import time
|
||||
import traceback
|
||||
import webbrowser
|
||||
@@ -51,6 +52,8 @@ class Core(Plugin):
|
||||
addEvent('setting.save.core.password', self.md5Password)
|
||||
addEvent('setting.save.core.api_key', self.checkApikey)
|
||||
|
||||
# Make sure we can close-down with ctrl+c properly
|
||||
self.signalHandler()
|
||||
|
||||
def md5Password(self, value):
|
||||
return md5(value.encode(Env.get('encoding'))) if value else ''
|
||||
@@ -66,7 +69,7 @@ class Core(Plugin):
|
||||
|
||||
def available(self):
|
||||
return jsonified({
|
||||
'succes': True
|
||||
'success': True
|
||||
})
|
||||
|
||||
def shutdown(self):
|
||||
@@ -98,7 +101,7 @@ class Core(Plugin):
|
||||
|
||||
self.shutdown_started = True
|
||||
|
||||
fireEvent('app.shutdown')
|
||||
fireEvent('app.do_shutdown')
|
||||
log.debug('Every plugin got shutdown event')
|
||||
|
||||
loop = True
|
||||
@@ -170,3 +173,10 @@ class Core(Plugin):
|
||||
return jsonified({
|
||||
'version': self.version()
|
||||
})
|
||||
|
||||
def signalHandler(self):
|
||||
|
||||
def signal_handler(signal, frame):
|
||||
fireEvent('app.do_shutdown')
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from .main import Updater
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
|
||||
def start():
|
||||
return Updater()
|
||||
@@ -33,6 +35,7 @@ config = [{
|
||||
{
|
||||
'name': 'git_command',
|
||||
'default': 'git',
|
||||
'hidden': not os.path.isdir(os.path.join(Env.get('app_dir'), '.git')),
|
||||
'advanced': True
|
||||
},
|
||||
],
|
||||
|
||||
@@ -106,6 +106,10 @@ class Updater(Plugin):
|
||||
if success:
|
||||
fireEventAsync('app.restart')
|
||||
|
||||
# Assume the updater handles things
|
||||
if not success:
|
||||
success = True
|
||||
|
||||
return jsonified({
|
||||
'success': success
|
||||
})
|
||||
@@ -383,11 +387,6 @@ class SourceUpdater(BaseUpdater):
|
||||
|
||||
class DesktopUpdater(BaseUpdater):
|
||||
|
||||
version = None
|
||||
update_failed = False
|
||||
update_version = None
|
||||
last_check = 0
|
||||
|
||||
def __init__(self):
|
||||
self.desktop = Env.get('desktop')
|
||||
|
||||
@@ -396,11 +395,12 @@ class DesktopUpdater(BaseUpdater):
|
||||
def do_restart(e):
|
||||
if e['status'] == 'done':
|
||||
fireEventAsync('app.restart')
|
||||
else:
|
||||
elif e['status'] == 'error':
|
||||
log.error('Failed updating desktop: %s', e['exception'])
|
||||
self.update_failed = True
|
||||
|
||||
self.desktop._esky.auto_update(callback = do_restart)
|
||||
return
|
||||
except:
|
||||
self.update_failed = True
|
||||
|
||||
@@ -411,7 +411,7 @@ class DesktopUpdater(BaseUpdater):
|
||||
'last_check': self.last_check,
|
||||
'update_version': self.update_version,
|
||||
'version': self.getVersion(),
|
||||
'branch': 'desktop_build',
|
||||
'branch': self.branch,
|
||||
}
|
||||
|
||||
def check(self):
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
from base64 import b32decode, b16encode
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toSafeString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
|
||||
@@ -21,31 +18,33 @@ class Downloader(Plugin):
|
||||
'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.getDownloadStatus)
|
||||
addEvent('download.status', self.getAllDownloadStatus)
|
||||
addEvent('download.remove_failed', self.removeFailed)
|
||||
|
||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||
pass
|
||||
|
||||
def getDownloadStatus(self, data = {}, movie = {}):
|
||||
def getAllDownloadStatus(self):
|
||||
return False
|
||||
|
||||
def createNzbName(self, data, movie):
|
||||
tag = self.cpTag(movie)
|
||||
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
|
||||
|
||||
def createFileName(self, data, filedata, movie):
|
||||
name = os.path.join(self.createNzbName(data, movie))
|
||||
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
||||
return '%s.%s' % (name, 'rar')
|
||||
return '%s.%s' % (name, data.get('type'))
|
||||
|
||||
def cpTag(self, movie):
|
||||
if Env.setting('enabled', 'renamer'):
|
||||
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
|
||||
|
||||
return ''
|
||||
def removeFailed(self, name = {}, nzo_id = {}):
|
||||
return False
|
||||
|
||||
def isCorrectType(self, item_type):
|
||||
is_correct = item_type in self.type
|
||||
@@ -56,7 +55,7 @@ class Downloader(Plugin):
|
||||
return is_correct
|
||||
|
||||
def magnetToTorrent(self, magnet_link):
|
||||
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0]
|
||||
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0].upper()
|
||||
|
||||
# Convert base 32 to hex
|
||||
if len(torrent_hash) == 32:
|
||||
@@ -67,7 +66,10 @@ class Downloader(Plugin):
|
||||
|
||||
for source in sources:
|
||||
try:
|
||||
filedata = self.urlopen(source % torrent_hash, show_error = False)
|
||||
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))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .main import Blackhole
|
||||
from couchpotato.core.helpers.variable import getDownloadDir
|
||||
|
||||
def start():
|
||||
return Blackhole()
|
||||
@@ -16,7 +17,7 @@ config = [{
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'default': True,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb,torrent',
|
||||
},
|
||||
@@ -24,6 +25,7 @@ config = [{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
|
||||
'default': getDownloadDir()
|
||||
},
|
||||
{
|
||||
'name': 'use_for',
|
||||
|
||||
@@ -11,7 +11,6 @@ config = [{
|
||||
'name': 'nzbget',
|
||||
'label': 'NZBGet',
|
||||
'description': 'Send NZBs to your NZBGet installation.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -12,7 +12,6 @@ config = [{
|
||||
'name': 'pneumatic',
|
||||
'label': 'Pneumatic',
|
||||
'description': 'Download the .strm file to a specific folder.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
import traceback
|
||||
from urllib2 import URLError
|
||||
import json
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -27,7 +28,7 @@ class Sabnzbd(Downloader):
|
||||
|
||||
if filedata:
|
||||
if len(filedata) < 50:
|
||||
log.error('No proper nzb available!')
|
||||
log.error('No proper nzb available: %s', (filedata))
|
||||
return False
|
||||
|
||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||
@@ -36,126 +37,124 @@ class Sabnzbd(Downloader):
|
||||
else:
|
||||
params['name'] = data.get('url')
|
||||
|
||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
||||
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)
|
||||
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: %s', traceback.format_exc())
|
||||
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.")
|
||||
log.error('SABnzbd didn\'t return anything.')
|
||||
return False
|
||||
|
||||
log.debug("Result text from SAB: " + result[:40])
|
||||
if result == "ok":
|
||||
log.info("NZB sent to SAB successfully.")
|
||||
log.debug('Result text from SAB: %s', result[:40])
|
||||
if result[:2] == 'ok':
|
||||
log.info('NZB sent to SAB successfully.')
|
||||
return True
|
||||
elif result == "Missing authentication":
|
||||
log.error("Incorrect username/password.")
|
||||
return False
|
||||
else:
|
||||
log.error("Unknown error: " + result[:40])
|
||||
log.error(result[:40])
|
||||
return False
|
||||
|
||||
def getDownloadStatus(self, data = {}, movie = {}):
|
||||
if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')):
|
||||
return
|
||||
def getAllDownloadStatus(self):
|
||||
if self.isDisabled(manual = False):
|
||||
return False
|
||||
|
||||
nzbname = self.createNzbName(data, movie)
|
||||
log.info('Checking download status of "%s" at SABnzbd.', nzbname)
|
||||
log.debug('Checking SABnzbd download status.')
|
||||
|
||||
# Go through Queue
|
||||
params = {
|
||||
'apikey': self.conf('api_key'),
|
||||
'mode': 'queue',
|
||||
'output': 'json'
|
||||
}
|
||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
||||
|
||||
try:
|
||||
sab = self.urlopen(url, timeout = 60, show_error = False)
|
||||
queue = self.call({
|
||||
'mode': 'queue',
|
||||
})
|
||||
except:
|
||||
log.error('Failed checking status: %s', traceback.format_exc())
|
||||
log.error('Failed getting queue: %s', traceback.format_exc(1))
|
||||
return False
|
||||
|
||||
try:
|
||||
history = json.loads(sab)
|
||||
except:
|
||||
log.debug("Result text from SAB: " + sab[:40])
|
||||
log.error('Failed parsing json status: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
for slot in history['queue']['slots']:
|
||||
log.debug('Found %s in SabNZBd queue, which is %s, with %s left', (slot['filename'], slot['status'], slot['timeleft']))
|
||||
if slot['filename'] == nzbname:
|
||||
return slot['status'].lower()
|
||||
|
||||
# Go through history items
|
||||
params = {
|
||||
'apikey': self.conf('api_key'),
|
||||
'mode': 'history',
|
||||
'output': 'json'
|
||||
}
|
||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
||||
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:
|
||||
sab = self.urlopen(url, timeout = 60, show_error = False)
|
||||
self.call({
|
||||
'mode': 'history',
|
||||
'name': 'delete',
|
||||
'del_files': '1',
|
||||
'value': item['id']
|
||||
}, use_json = False)
|
||||
except:
|
||||
log.error('Failed getting history: %s', traceback.format_exc())
|
||||
return
|
||||
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||
return False
|
||||
|
||||
try:
|
||||
history = json.loads(sab)
|
||||
except:
|
||||
log.debug("Result text from SAB: " + sab[:40])
|
||||
log.error('Failed parsing history json: %s', traceback.format_exc())
|
||||
return
|
||||
return True
|
||||
|
||||
for slot in history['history']['slots']:
|
||||
log.debug('Found %s in SabNZBd history, which has %s', (slot['name'], slot['status']))
|
||||
if slot['name'] == nzbname:
|
||||
# Note: if post process even if failed is on in SabNZBd, it will complete with a fail message
|
||||
if slot['status'] == 'Failed' or (slot['status'] == 'Completed' and slot['fail_message'].strip()):
|
||||
def call(self, params, use_json = True):
|
||||
|
||||
# Delete failed download
|
||||
if self.conf('delete_failed', default = True):
|
||||
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
|
||||
'apikey': self.conf('api_key'),
|
||||
'output': 'json'
|
||||
}))
|
||||
|
||||
log.info('%s failed downloading, deleting...', slot['name'])
|
||||
params = {
|
||||
'apikey': self.conf('api_key'),
|
||||
'mode': 'history',
|
||||
'name': 'delete',
|
||||
'del_files': '1',
|
||||
'value': slot['nzo_id']
|
||||
}
|
||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
||||
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 {}
|
||||
|
||||
try:
|
||||
sab = self.urlopen(url, timeout = 60, show_error = False)
|
||||
except:
|
||||
log.error('Failed deleting: %s', traceback.format_exc())
|
||||
return False
|
||||
return d[params['mode']]
|
||||
else:
|
||||
return data
|
||||
|
||||
result = sab.strip()
|
||||
if not result:
|
||||
log.error("SABnzbd didn't return anything.")
|
||||
|
||||
log.debug("Result text from SAB: " + result[:40])
|
||||
if result == "ok":
|
||||
log.info('SabNZBd deleted failed release %s successfully.', slot['name'])
|
||||
elif result == "Missing authentication":
|
||||
log.error("Incorrect username/password or API?.")
|
||||
else:
|
||||
log.error("Unknown error: " + result[:40])
|
||||
|
||||
return 'failed'
|
||||
else:
|
||||
return slot['status'].lower()
|
||||
|
||||
return 'not_found'
|
||||
|
||||
44
couchpotato/core/downloaders/synology/__init__.py
Normal file
44
couchpotato/core/downloaders/synology/__init__.py
Normal file
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
108
couchpotato/core/downloaders/synology/main.py
Normal file
108
couchpotato/core/downloaders/synology/main.py
Normal file
@@ -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
|
||||
|
||||
|
||||
@@ -30,9 +30,15 @@ class Transmission(Downloader):
|
||||
return False
|
||||
|
||||
# Set parameters for Transmission
|
||||
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
|
||||
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
||||
|
||||
# Create the empty folder to download too
|
||||
self.makeDir(folder_path)
|
||||
|
||||
params = {
|
||||
'paused': self.conf('paused', default = 0),
|
||||
'download-dir': self.conf('directory', default = '').rstrip(os.path.sep)
|
||||
'download-dir': folder_path
|
||||
}
|
||||
|
||||
torrent_params = {
|
||||
@@ -49,6 +55,7 @@ class Transmission(Downloader):
|
||||
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)
|
||||
|
||||
|
||||
54
couchpotato/core/downloaders/utorrent/__init__.py
Normal file
54
couchpotato/core/downloaders/utorrent/__init__.py
Normal file
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
138
couchpotato/core/downloaders/utorrent/main.py
Normal file
138
couchpotato/core/downloaders/utorrent/main.py
Normal file
@@ -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)
|
||||
@@ -19,7 +19,7 @@ def addEvent(name, handler, priority = 100):
|
||||
if events.get(name):
|
||||
e = events[name]
|
||||
else:
|
||||
e = events[name] = Event(name = name, threads = 20, exc_info = True, traceback = True, lock = threading.RLock())
|
||||
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
|
||||
|
||||
def createHandle(*args, **kwargs):
|
||||
|
||||
@@ -46,49 +46,40 @@ def fireEvent(name, *args, **kwargs):
|
||||
#log.debug('Firing event %s', name)
|
||||
try:
|
||||
|
||||
# Fire after event
|
||||
is_after_event = False
|
||||
try:
|
||||
del kwargs['is_after_event']
|
||||
is_after_event = True
|
||||
except: pass
|
||||
options = {
|
||||
'is_after_event': False, # Fire after event
|
||||
'on_complete': False, # onComplete event
|
||||
'single': False, # Return single handler
|
||||
'merge': False, # Merge items
|
||||
'in_order': False, # Fire them in specific order, waits for the other to finish
|
||||
}
|
||||
|
||||
# onComplete event
|
||||
on_complete = False
|
||||
try:
|
||||
on_complete = kwargs['on_complete']
|
||||
del kwargs['on_complete']
|
||||
except: pass
|
||||
|
||||
# Return single handler
|
||||
single = False
|
||||
try:
|
||||
del kwargs['single']
|
||||
single = True
|
||||
except: pass
|
||||
|
||||
# Merge items
|
||||
merge = False
|
||||
try:
|
||||
del kwargs['merge']
|
||||
merge = True
|
||||
except: pass
|
||||
|
||||
# Merge items
|
||||
in_order = False
|
||||
try:
|
||||
del kwargs['in_order']
|
||||
in_order = True
|
||||
except: pass
|
||||
# Do options
|
||||
for x in options:
|
||||
try:
|
||||
val = kwargs[x]
|
||||
del kwargs[x]
|
||||
options[x] = val
|
||||
except: pass
|
||||
|
||||
e = events[name]
|
||||
if not in_order: e.lock.acquire()
|
||||
e.asynchronous = False
|
||||
e.in_order = in_order
|
||||
result = e(*args, **kwargs)
|
||||
if not in_order: e.lock.release()
|
||||
|
||||
if single and not merge:
|
||||
# Lock this event
|
||||
e.lock.acquire()
|
||||
|
||||
e.asynchronous = False
|
||||
|
||||
# Make sure only 1 event is fired at a time when order is wanted
|
||||
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
|
||||
kwargs['event_return_on_result'] = options['single']
|
||||
|
||||
# Fire
|
||||
result = e(*args, **kwargs)
|
||||
|
||||
# Release lock for this event
|
||||
e.lock.release()
|
||||
|
||||
if options['single'] and not options['merge']:
|
||||
results = None
|
||||
|
||||
# Loop over results, stop when first not None result is found.
|
||||
@@ -112,7 +103,7 @@ def fireEvent(name, *args, **kwargs):
|
||||
errorHandler(r[1])
|
||||
|
||||
# Merge
|
||||
if merge and len(results) > 0:
|
||||
if options['merge'] and len(results) > 0:
|
||||
# Dict
|
||||
if type(results[0]) == dict:
|
||||
merged = {}
|
||||
@@ -133,11 +124,11 @@ def fireEvent(name, *args, **kwargs):
|
||||
log.debug('Return modified results for %s', name)
|
||||
results = modified_results
|
||||
|
||||
if not is_after_event:
|
||||
if not options['is_after_event']:
|
||||
fireEvent('%s.after' % name, is_after_event = True)
|
||||
|
||||
if on_complete:
|
||||
on_complete()
|
||||
if options['on_complete']:
|
||||
options['on_complete']()
|
||||
|
||||
return results
|
||||
except KeyError, e:
|
||||
|
||||
@@ -9,15 +9,34 @@ import sys
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
def getUserDir():
|
||||
try:
|
||||
import pwd
|
||||
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
|
||||
except:
|
||||
pass
|
||||
|
||||
return os.path.expanduser('~')
|
||||
|
||||
def getDownloadDir():
|
||||
user_dir = getUserDir()
|
||||
|
||||
# OSX
|
||||
if 'darwin' in platform.platform().lower():
|
||||
return os.path.join(user_dir, 'Downloads')
|
||||
|
||||
if os.name == 'nt':
|
||||
return os.path.join(user_dir, 'Downloads')
|
||||
|
||||
return user_dir
|
||||
|
||||
def getDataDir():
|
||||
|
||||
# Windows
|
||||
if os.name == 'nt':
|
||||
return os.path.join(os.environ['APPDATA'], 'CouchPotato')
|
||||
|
||||
import pwd
|
||||
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
|
||||
user_dir = os.path.expanduser('~')
|
||||
user_dir = getUserDir()
|
||||
|
||||
# OSX
|
||||
if 'darwin' in platform.platform().lower():
|
||||
@@ -84,7 +103,7 @@ def cleanHost(host):
|
||||
|
||||
return host
|
||||
|
||||
def getImdb(txt, check_inside = True):
|
||||
def getImdb(txt, check_inside = True, multiple = False):
|
||||
|
||||
if check_inside and os.path.isfile(txt):
|
||||
output = open(txt, 'r')
|
||||
@@ -92,8 +111,10 @@ def getImdb(txt, check_inside = True):
|
||||
output.close()
|
||||
|
||||
try:
|
||||
id = re.findall('(tt\d{7})', txt)[0]
|
||||
return id
|
||||
ids = re.findall('(tt\d{7})', txt)
|
||||
if multiple:
|
||||
return ids if len(ids) > 0 else []
|
||||
return ids[0]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
@@ -118,8 +139,16 @@ def getTitle(library_dict):
|
||||
try:
|
||||
return library_dict['titles'][0]['title']
|
||||
except:
|
||||
log.error('Could not get title for %s', library_dict['identifier'])
|
||||
return None
|
||||
try:
|
||||
for title in library_dict.titles:
|
||||
if title.default:
|
||||
return title.title
|
||||
except:
|
||||
log.error('Could not get title for %s', library_dict.identifier)
|
||||
return None
|
||||
|
||||
log.error('Could not get title for %s', library_dict['identifier'])
|
||||
return None
|
||||
except:
|
||||
log.error('Could not get title for library item: %s', library_dict)
|
||||
return None
|
||||
@@ -127,3 +156,5 @@ def getTitle(library_dict):
|
||||
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
def splitString(str, split_on = ','):
|
||||
return [x.strip() for x in str.split(split_on)]
|
||||
|
||||
@@ -5,7 +5,7 @@ import traceback
|
||||
class CPLog(object):
|
||||
|
||||
context = ''
|
||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h']
|
||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key']
|
||||
|
||||
def __init__(self, context = ''):
|
||||
if context.endswith('.main'):
|
||||
@@ -17,6 +17,9 @@ class CPLog(object):
|
||||
def info(self, msg, replace_tuple = ()):
|
||||
self.logger.info(self.addContext(msg, replace_tuple))
|
||||
|
||||
def info2(self, msg, replace_tuple = ()):
|
||||
self.logger.log(19, self.addContext(msg, replace_tuple))
|
||||
|
||||
def debug(self, msg, replace_tuple = ()):
|
||||
self.logger.debug(self.addContext(msg, replace_tuple))
|
||||
|
||||
@@ -53,7 +56,8 @@ class CPLog(object):
|
||||
if not Env.get('dev'):
|
||||
|
||||
for replace in self.replace_private:
|
||||
msg = re.sub('(%s=)[^\&]+' % replace, '%s=xxx' % replace, msg)
|
||||
msg = re.sub('(\?%s=)[^\&]+' % replace, '?%s=xxx' % replace, msg)
|
||||
msg = re.sub('(&%s=)[^\&]+' % replace, '&%s=xxx' % replace, msg)
|
||||
|
||||
# Replace api key
|
||||
try:
|
||||
|
||||
@@ -14,7 +14,7 @@ class Notification(Plugin):
|
||||
test_message = 'ZOMG Lazors Pewpewpew!'
|
||||
|
||||
listen_to = [
|
||||
'movie.downloaded', 'movie.snatched',
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
]
|
||||
dont_listen_to = []
|
||||
@@ -30,10 +30,10 @@ class Notification(Plugin):
|
||||
addEvent(listener, self.createNotifyHandler(listener))
|
||||
|
||||
def createNotifyHandler(self, listener):
|
||||
def notify(message, data):
|
||||
def notify(message = None, group = {}, data = None):
|
||||
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
|
||||
return
|
||||
return self.notify(message = message, data = data, listener = listener)
|
||||
return self.notify(message = message, data = data if data else group, listener = listener)
|
||||
|
||||
return notify
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView, addNonBlockApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from couchpotato.core.settings.model import Notification as Notif
|
||||
@@ -22,7 +22,7 @@ class CoreNotifier(Notification):
|
||||
listeners = []
|
||||
|
||||
listen_to = [
|
||||
'movie.downloaded', 'movie.snatched',
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
]
|
||||
|
||||
@@ -67,7 +67,7 @@ class CoreNotifier(Notification):
|
||||
|
||||
ids = None
|
||||
if getParam('ids'):
|
||||
ids = [x.strip() for x in getParam('ids').split(',')]
|
||||
ids = splitString(getParam('ids'))
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -79,7 +79,6 @@ class CoreNotifier(Notification):
|
||||
q.update({Notif.read: True})
|
||||
|
||||
db.commit()
|
||||
#db.close()
|
||||
|
||||
return jsonified({
|
||||
'success': True
|
||||
@@ -93,7 +92,7 @@ class CoreNotifier(Notification):
|
||||
q = db.query(Notif)
|
||||
|
||||
if limit_offset:
|
||||
splt = [x.strip() for x in limit_offset.split(',')]
|
||||
splt = splitString(limit_offset)
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q = q.limit(limit).offset(offset)
|
||||
@@ -107,7 +106,6 @@ class CoreNotifier(Notification):
|
||||
ndict['type'] = 'notification'
|
||||
notifications.append(ndict)
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(notifications) == 0,
|
||||
@@ -133,7 +131,6 @@ class CoreNotifier(Notification):
|
||||
|
||||
self.frontend(type = listener, data = data)
|
||||
|
||||
#db.close()
|
||||
return True
|
||||
|
||||
def frontend(self, type = 'notification', data = {}, message = None):
|
||||
|
||||
@@ -69,7 +69,7 @@ class NMJ(Notification):
|
||||
'mount': mount,
|
||||
})
|
||||
|
||||
def addToLibrary(self, group = {}):
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
if self.isDisabled(): return
|
||||
|
||||
host = self.conf('host')
|
||||
@@ -114,8 +114,8 @@ class NMJ(Notification):
|
||||
|
||||
def failed(self):
|
||||
return jsonified({'success': False})
|
||||
|
||||
|
||||
def test(self):
|
||||
return jsonified({'success': self.addToLibrary()})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import pynma
|
||||
@@ -11,7 +12,7 @@ class NotifyMyAndroid(Notification):
|
||||
if self.isDisabled(): return
|
||||
|
||||
nma = pynma.PyNMA()
|
||||
keys = [x.strip() for x in self.conf('api_key').split(',')]
|
||||
keys = splitString(self.conf('api_key'))
|
||||
nma.addkey(keys)
|
||||
nma.developerkey(self.conf('dev_key'))
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from pynmwp import PyNMWP
|
||||
@@ -10,7 +11,7 @@ class NotifyMyWP(Notification):
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
keys = [x.strip() for x in self.conf('api_key').split(',')]
|
||||
keys = splitString(self.conf('api_key'))
|
||||
p = PyNMWP(keys, self.conf('dev_key'))
|
||||
|
||||
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
|
||||
|
||||
@@ -17,7 +17,7 @@ class Plex(Notification):
|
||||
super(Plex, self).__init__()
|
||||
addEvent('renamer.after', self.addToLibrary)
|
||||
|
||||
def addToLibrary(self, group = {}):
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
if self.isDisabled(): return
|
||||
|
||||
log.info('Sending notification to Plex')
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from httplib import HTTPSConnection
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Prowl(Notification):
|
||||
|
||||
urls = {
|
||||
'api': 'https://api.prowlapp.com/publicapi/add'
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
http_handler = HTTPSConnection('api.prowlapp.com')
|
||||
|
||||
data = {
|
||||
'apikey': self.conf('api_key'),
|
||||
'application': self.default_title,
|
||||
'description': toUnicode(message),
|
||||
'priority': self.conf('priority'),
|
||||
}
|
||||
headers = {
|
||||
'Content-type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
http_handler.request('POST',
|
||||
'/publicapi/add',
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'},
|
||||
body = tryUrlencode(data)
|
||||
)
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
|
||||
if request_status == 200:
|
||||
try:
|
||||
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
|
||||
log.info('Prowl notifications sent.')
|
||||
return True
|
||||
elif request_status == 401:
|
||||
log.error('Prowl auth failed: %s', response.reason)
|
||||
return False
|
||||
else:
|
||||
log.error('Prowl notification failed.')
|
||||
return False
|
||||
except:
|
||||
log.error('Prowl failed: %s', traceback.format_exc())
|
||||
|
||||
return False
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -8,14 +10,17 @@ log = CPLog(__name__)
|
||||
|
||||
class Synoindex(Notification):
|
||||
|
||||
index_path = '/usr/syno/bin/synoindex'
|
||||
|
||||
def __init__(self):
|
||||
super(Synoindex, self).__init__()
|
||||
addEvent('renamer.after', self.addToLibrary)
|
||||
|
||||
def addToLibrary(self, group = {}):
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
if self.isDisabled(): return
|
||||
|
||||
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
|
||||
log.info(u'Executing synoindex command: %s ', command)
|
||||
command = [self.index_path, '-A', group.get('destination_dir')]
|
||||
log.info('Executing synoindex command: %s ', command)
|
||||
try:
|
||||
p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
|
||||
out = p.communicate()
|
||||
@@ -26,3 +31,6 @@ class Synoindex(Notification):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def test(self):
|
||||
return jsonified({'success': os.path.isfile(self.index_path)})
|
||||
|
||||
@@ -38,22 +38,33 @@ class Twitter(Notification):
|
||||
|
||||
direct_message = self.conf('direct_message')
|
||||
direct_message_users = self.conf('screen_name')
|
||||
|
||||
|
||||
mention = self.conf('mention')
|
||||
mention_tag = None
|
||||
if mention:
|
||||
if direct_message:
|
||||
direct_message_users = '%s %s' % (direct_message_users, mention)
|
||||
direct_message_users = direct_message_users.replace('@',' ')
|
||||
direct_message_users = direct_message_users.replace(',',' ')
|
||||
direct_message_users = direct_message_users.replace('@', ' ')
|
||||
direct_message_users = direct_message_users.replace(',', ' ')
|
||||
else:
|
||||
message = '%s @%s' % (message, mention.lstrip('@'))
|
||||
mention_tag = '@%s' % mention.lstrip('@')
|
||||
message = '%s %s' % (message, mention_tag)
|
||||
|
||||
try:
|
||||
if direct_message:
|
||||
for user in direct_message_users.split():
|
||||
api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message))
|
||||
else:
|
||||
api.PostUpdate('[%s] %s' % (self.default_title, message))
|
||||
update_message = '[%s] %s' % (self.default_title, message)
|
||||
if len(update_message) > 140:
|
||||
if mention_tag:
|
||||
api.PostUpdate(update_message[:135 - len(mention_tag)] + ('%s 1/2 ' % mention_tag))
|
||||
api.PostUpdate(update_message[135 - len(mention_tag):] + ('%s 2/2 ' % mention_tag))
|
||||
else:
|
||||
api.PostUpdate(update_message[:135] + ' 1/2')
|
||||
api.PostUpdate(update_message[135:] + ' 2/2')
|
||||
else:
|
||||
api.PostUpdate(update_message)
|
||||
except Exception, e:
|
||||
log.error('Error sending tweet: %s', e)
|
||||
return False
|
||||
|
||||
64
couchpotato/core/notifications/xbmc/main.py
Normal file → Executable file
64
couchpotato/core/notifications/xbmc/main.py
Normal file → Executable file
@@ -1,43 +1,67 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import json
|
||||
import base64
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class XBMC(Notification):
|
||||
|
||||
listen_to = ['movie.downloaded']
|
||||
listen_to = ['renamer.after']
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
hosts = [x.strip() for x in self.conf('host').split(",")]
|
||||
hosts = splitString(self.conf('host'))
|
||||
successful = 0
|
||||
for host in hosts:
|
||||
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
|
||||
successful += 1
|
||||
if self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host):
|
||||
successful += 1
|
||||
response = self.request(host, [
|
||||
('GUI.ShowNotification', {"title":"CouchPotato", "message":message}),
|
||||
('VideoLibrary.Scan', {}),
|
||||
])
|
||||
|
||||
return successful == len(hosts)*2
|
||||
try:
|
||||
for result in response:
|
||||
if result['result'] == "OK":
|
||||
successful += 1
|
||||
except:
|
||||
log.error('Failed parsing results: %s', traceback.format_exc())
|
||||
|
||||
def send(self, command, host):
|
||||
return successful == len(hosts) * 2
|
||||
|
||||
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
|
||||
def request(self, host, requests):
|
||||
server = 'http://%s/jsonrpc' % host
|
||||
|
||||
data = []
|
||||
for req in requests:
|
||||
method, kwargs = req
|
||||
data.append({
|
||||
'method': method,
|
||||
'params': kwargs,
|
||||
'jsonrpc': '2.0',
|
||||
'id': method,
|
||||
})
|
||||
data = json.dumps(data)
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
headers = {}
|
||||
if self.conf('password'):
|
||||
headers = {
|
||||
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password')))[:-1]
|
||||
}
|
||||
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
|
||||
headers['Authorization'] = 'Basic %s' % base64string
|
||||
|
||||
try:
|
||||
self.urlopen(url, headers = headers, show_error = False)
|
||||
except:
|
||||
log.error("Couldn't sent command to XBMC")
|
||||
return False
|
||||
log.debug('Sending request to %s: %s', (host, data))
|
||||
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
|
||||
response = json.loads(rdata)
|
||||
log.debug('Returned from request %s: %s', (host, response))
|
||||
|
||||
return response
|
||||
except:
|
||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||
return []
|
||||
|
||||
log.info('XBMC notification to %s successful.', host)
|
||||
return True
|
||||
|
||||
@@ -5,13 +5,12 @@ def start():
|
||||
|
||||
config = [{
|
||||
'name': 'automation',
|
||||
'order': 30,
|
||||
'order': 101,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'automation',
|
||||
'name': 'automation',
|
||||
'label': 'Automation',
|
||||
'description': 'Minimal movie requirements',
|
||||
'label': 'Minimal movie requirements',
|
||||
'options': [
|
||||
{
|
||||
'name': 'year',
|
||||
|
||||
@@ -18,9 +18,17 @@ class Automation(Plugin):
|
||||
def addMovies(self):
|
||||
|
||||
movies = fireEvent('automation.get_movies', merge = True)
|
||||
movie_ids = []
|
||||
|
||||
for imdb_id in movies:
|
||||
prop_name = 'automation.added.%s' % imdb_id
|
||||
added = Env.prop(prop_name, default = False)
|
||||
if not added:
|
||||
fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False)
|
||||
added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_library = True, single = True)
|
||||
if added_movie:
|
||||
movie_ids.append(added_movie['id'])
|
||||
Env.prop(prop_name, True)
|
||||
|
||||
for movie_id in movie_ids:
|
||||
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||
fireEvent('searcher.single', movie_dict)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from StringIO import StringIO
|
||||
from couchpotato import addView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString, ss
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString, ss, \
|
||||
toSafeString
|
||||
from couchpotato.core.helpers.variable import getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
@@ -34,7 +35,7 @@ class Plugin(object):
|
||||
http_failed_disabled = {}
|
||||
|
||||
def registerPlugin(self):
|
||||
addEvent('app.shutdown', self.doShutdown)
|
||||
addEvent('app.do_shutdown', self.doShutdown)
|
||||
addEvent('plugin.running', self.isRunning)
|
||||
|
||||
def conf(self, attr, value = None, default = None):
|
||||
@@ -113,8 +114,11 @@ class Plugin(object):
|
||||
# Don't try for failed requests
|
||||
if self.http_failed_disabled.get(host, 0) > 0:
|
||||
if self.http_failed_disabled[host] > (time.time() - 900):
|
||||
log.info('Disabled calls to %s for 15 minutes because so many failed requests.', host)
|
||||
raise Exception
|
||||
log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host)
|
||||
if not show_error:
|
||||
raise
|
||||
else:
|
||||
return ''
|
||||
else:
|
||||
del self.http_failed_request[host]
|
||||
del self.http_failed_disabled[host]
|
||||
@@ -123,7 +127,7 @@ class Plugin(object):
|
||||
try:
|
||||
|
||||
if multipart:
|
||||
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()]))
|
||||
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
request = urllib2.Request(url, params, headers)
|
||||
|
||||
cookies = cookielib.CookieJar()
|
||||
@@ -238,13 +242,30 @@ class Plugin(object):
|
||||
self.setCache(cache_key, data, timeout = cache_timeout)
|
||||
return data
|
||||
except:
|
||||
pass
|
||||
if not kwargs.get('show_error'):
|
||||
raise
|
||||
|
||||
def setCache(self, cache_key, value, timeout = 300):
|
||||
log.debug('Setting cache %s', cache_key)
|
||||
Env.get('cache').set(cache_key, value, timeout)
|
||||
return value
|
||||
|
||||
def createNzbName(self, data, movie):
|
||||
tag = self.cpTag(movie)
|
||||
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
|
||||
|
||||
def createFileName(self, data, filedata, movie):
|
||||
name = os.path.join(self.createNzbName(data, movie))
|
||||
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
||||
return '%s.%s' % (name, 'rar')
|
||||
return '%s.%s' % (name, data.get('type'))
|
||||
|
||||
def cpTag(self, movie):
|
||||
if Env.setting('enabled', 'renamer'):
|
||||
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
|
||||
|
||||
return ''
|
||||
|
||||
def isDisabled(self):
|
||||
return not self.isEnabled()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.request import getParam, jsonified
|
||||
from couchpotato.core.helpers.variable import getUserDir
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import ctypes
|
||||
import os
|
||||
@@ -27,6 +28,8 @@ class FileBrowser(Plugin):
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'is_root': bool, //is top most folder
|
||||
'parent': string, //parent folder of requested path
|
||||
'home': string, //user home folder
|
||||
'empty': bool, //directory is empty
|
||||
'dirs': array, //directory names
|
||||
}"""}
|
||||
@@ -55,7 +58,7 @@ class FileBrowser(Plugin):
|
||||
|
||||
driveletters = []
|
||||
for drive in string.ascii_uppercase:
|
||||
if win32file.GetDriveType(drive + ":") in [win32file.DRIVE_FIXED, win32file.DRIVE_REMOTE]:
|
||||
if win32file.GetDriveType(drive + ":") in [win32file.DRIVE_FIXED, win32file.DRIVE_REMOTE, win32file.DRIVE_RAMDISK, win32file.DRIVE_REMOVABLE]:
|
||||
driveletters.append(drive + ":\\")
|
||||
|
||||
return driveletters
|
||||
@@ -63,15 +66,28 @@ class FileBrowser(Plugin):
|
||||
def view(self):
|
||||
|
||||
path = getParam('path', '/')
|
||||
home = getUserDir()
|
||||
|
||||
if not path:
|
||||
path = home
|
||||
|
||||
try:
|
||||
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
|
||||
except:
|
||||
dirs = []
|
||||
|
||||
parent = os.path.dirname(path.rstrip(os.path.sep))
|
||||
if parent == path.rstrip(os.path.sep):
|
||||
parent = '/'
|
||||
elif parent != '/' and parent[-2:] != ':\\':
|
||||
parent += os.path.sep
|
||||
|
||||
return jsonified({
|
||||
'is_root': path == '/' or not path,
|
||||
'is_root': path == '/',
|
||||
'empty': len(dirs) == 0,
|
||||
'parent': parent,
|
||||
'home': home + os.path.sep,
|
||||
'platform': os.name,
|
||||
'dirs': dirs,
|
||||
})
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import md5, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.scanner.main import Scanner
|
||||
from couchpotato.core.settings.model import FileType, File
|
||||
from couchpotato.environment import Env
|
||||
import os.path
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -28,6 +31,52 @@ class FileManager(Plugin):
|
||||
'return': {'type': 'file'}
|
||||
})
|
||||
|
||||
addApiView('file.types', self.getTypesView, docs = {
|
||||
'desc': 'Return a list of all the file types and their ids.',
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'types': [
|
||||
{
|
||||
"identifier": "poster_original",
|
||||
"type": "image",
|
||||
"id": 1,
|
||||
"name": "Poster_original"
|
||||
},
|
||||
{
|
||||
"identifier": "poster",
|
||||
"type": "image",
|
||||
"id": 2,
|
||||
"name": "Poster"
|
||||
},
|
||||
etc
|
||||
]
|
||||
}"""}
|
||||
})
|
||||
|
||||
addEvent('app.load', self.cleanup)
|
||||
addEvent('app.load', self.init)
|
||||
|
||||
def init(self):
|
||||
|
||||
for type_tuple in Scanner.file_types.values():
|
||||
self.getType(type_tuple)
|
||||
|
||||
def cleanup(self):
|
||||
|
||||
# Wait a bit after starting before cleanup
|
||||
time.sleep(3)
|
||||
log.debug('Cleaning up unused files')
|
||||
|
||||
try:
|
||||
db = get_session()
|
||||
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
|
||||
for filename in walk_files:
|
||||
file_path = os.path.join(root, filename)
|
||||
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
|
||||
if not f:
|
||||
os.remove(file_path)
|
||||
except:
|
||||
log.error('Failed removing unused file: %s', traceback.format_exc())
|
||||
|
||||
def showCacheFile(self, filename = ''):
|
||||
|
||||
cache_dir = Env.get('cache_dir')
|
||||
@@ -89,7 +138,6 @@ class FileManager(Plugin):
|
||||
db.commit()
|
||||
|
||||
type_dict = ft.to_dict()
|
||||
#db.close()
|
||||
return type_dict
|
||||
|
||||
def getTypes(self):
|
||||
@@ -102,5 +150,10 @@ class FileManager(Plugin):
|
||||
for type_object in results:
|
||||
types.append(type_object.to_dict())
|
||||
|
||||
#db.close()
|
||||
return types
|
||||
|
||||
def getTypesView(self):
|
||||
|
||||
return jsonified({
|
||||
'types': self.getTypes()
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
||||
@@ -52,7 +53,6 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
library_dict = l.to_dict(self.default_dict)
|
||||
|
||||
#db.close()
|
||||
return library_dict
|
||||
|
||||
def update(self, identifier, default_title = '', force = False):
|
||||
@@ -111,18 +111,20 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
# Files
|
||||
images = info.get('images', [])
|
||||
for type in images:
|
||||
for image in images[type]:
|
||||
if not isinstance(image, str):
|
||||
for image_type in ['poster']:
|
||||
for image in images.get(image_type, []):
|
||||
if not isinstance(image, (str, unicode)):
|
||||
continue
|
||||
|
||||
file_path = fireEvent('file.download', url = image, single = True)
|
||||
if file_path:
|
||||
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True)
|
||||
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
|
||||
try:
|
||||
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
|
||||
library.files.append(file_obj)
|
||||
db.commit()
|
||||
|
||||
break
|
||||
except:
|
||||
log.debug('Failed to attach to library: %s', traceback.format_exc())
|
||||
|
||||
@@ -136,26 +138,23 @@ class LibraryPlugin(Plugin):
|
||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||
|
||||
if not library.info:
|
||||
library_dict = self.update(identifier)
|
||||
dates = library_dict.get('info', {}).get('release_dates')
|
||||
library_dict = self.update(identifier, force = True)
|
||||
dates = library_dict.get('info', {}).get('release_date')
|
||||
else:
|
||||
dates = library.info.get('release_date')
|
||||
|
||||
if dates and dates.get('expires', 0) < time.time():
|
||||
if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||
library.info['release_date'] = dates
|
||||
library.info = library.info
|
||||
library.info = mergeDicts(library.info, {'release_date': dates })
|
||||
db.commit()
|
||||
|
||||
dates = library.info.get('release_date', {})
|
||||
#db.close()
|
||||
|
||||
return dates
|
||||
|
||||
|
||||
def simplifyTitle(self, title):
|
||||
|
||||
title = toUnicode(title)
|
||||
|
||||
nr_prefix = '' if title[0] in ascii_letters else '#'
|
||||
title = simplifyString(title)
|
||||
|
||||
|
||||
@@ -2,22 +2,32 @@ from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import getTitle, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class Manage(Plugin):
|
||||
|
||||
in_progress = False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
|
||||
|
||||
addEvent('manage.update', self.updateLibrary)
|
||||
|
||||
# Add files after renaming
|
||||
def after_rename(message = None, group = {}):
|
||||
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
|
||||
addEvent('renamer.after', after_rename, priority = 110)
|
||||
|
||||
addApiView('manage.update', self.updateLibraryView, docs = {
|
||||
'desc': 'Update the library by scanning for new movies',
|
||||
'params': {
|
||||
@@ -25,11 +35,23 @@ class Manage(Plugin):
|
||||
}
|
||||
})
|
||||
|
||||
addApiView('manage.progress', self.getProgress, docs = {
|
||||
'desc': 'Get the progress of current manage update',
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'progress': False || object, total & to_go,
|
||||
}"""},
|
||||
})
|
||||
|
||||
if not Env.get('dev'):
|
||||
def updateLibrary():
|
||||
self.updateLibrary(full = False)
|
||||
addEvent('app.load', updateLibrary)
|
||||
|
||||
def getProgress(self):
|
||||
return jsonified({
|
||||
'progress': self.in_progress
|
||||
})
|
||||
|
||||
def updateLibraryView(self):
|
||||
|
||||
full = getParam('full', default = 1)
|
||||
@@ -43,49 +65,127 @@ class Manage(Plugin):
|
||||
def updateLibrary(self, full = True):
|
||||
last_update = float(Env.prop('manage.last_update', default = 0))
|
||||
|
||||
if self.isDisabled() or (last_update > time.time() - 20):
|
||||
if self.in_progress:
|
||||
log.info('Already updating library: %s', self.in_progress)
|
||||
return
|
||||
elif self.isDisabled() or (last_update > time.time() - 20):
|
||||
return
|
||||
|
||||
directories = self.directories()
|
||||
added_identifiers = []
|
||||
self.in_progress = {}
|
||||
fireEvent('notify.frontend', type = 'manage.updating', data = True)
|
||||
|
||||
for directory in directories:
|
||||
try:
|
||||
|
||||
if not os.path.isdir(directory):
|
||||
if len(directory) > 0:
|
||||
log.error('Directory doesn\'t exist: %s', directory)
|
||||
continue
|
||||
directories = self.directories()
|
||||
added_identifiers = []
|
||||
|
||||
log.info('Updating manage library: %s', directory)
|
||||
identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update if not full else 0, single = True)
|
||||
if identifiers:
|
||||
added_identifiers.extend(identifiers)
|
||||
# Add some progress
|
||||
self.in_progress = {}
|
||||
for directory in directories:
|
||||
self.in_progress[os.path.normpath(directory)] = {
|
||||
'total': None,
|
||||
'to_go': None,
|
||||
}
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
for directory in directories:
|
||||
folder = os.path.normpath(directory)
|
||||
|
||||
if not os.path.isdir(folder):
|
||||
if len(directory) > 0:
|
||||
log.error('Directory doesn\'t exist: %s', folder)
|
||||
continue
|
||||
|
||||
log.info('Updating manage library: %s', folder)
|
||||
fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder)
|
||||
|
||||
onFound = self.createAddToLibrary(folder, added_identifiers)
|
||||
fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, on_found = onFound, single = True)
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
# If cleanup option is enabled, remove offline files from database
|
||||
if self.conf('cleanup') and full and not self.shuttingDown():
|
||||
|
||||
# Get movies with done status
|
||||
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
|
||||
|
||||
for done_movie in done_movies:
|
||||
if done_movie['library']['identifier'] not in added_identifiers:
|
||||
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
|
||||
else:
|
||||
for release in done_movie.get('releases', []):
|
||||
for release_file in release.get('files', []):
|
||||
# Remove release not available anymore
|
||||
if not os.path.isfile(ss(release_file['path'])):
|
||||
fireEvent('release.clean', release['id'])
|
||||
break
|
||||
|
||||
Env.prop('manage.last_update', time.time())
|
||||
except:
|
||||
log.error('Failed updating library: %s', (traceback.format_exc()))
|
||||
|
||||
while True and not self.shuttingDown():
|
||||
|
||||
delete_me = {}
|
||||
|
||||
for folder in self.in_progress:
|
||||
if self.in_progress[folder]['to_go'] <= 0:
|
||||
delete_me[folder] = True
|
||||
|
||||
for delete in delete_me:
|
||||
del self.in_progress[delete]
|
||||
|
||||
if len(self.in_progress) == 0:
|
||||
break
|
||||
|
||||
# If cleanup option is enabled, remove offline files from database
|
||||
if self.conf('cleanup') and full and not self.shuttingDown():
|
||||
time.sleep(1)
|
||||
|
||||
# Get movies with done status
|
||||
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
|
||||
fireEvent('notify.frontend', type = 'manage.updating', data = False)
|
||||
self.in_progress = False
|
||||
|
||||
for done_movie in done_movies:
|
||||
if done_movie['library']['identifier'] not in added_identifiers:
|
||||
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
|
||||
else:
|
||||
for release in done_movie.get('releases', []):
|
||||
for release_file in release.get('files', []):
|
||||
# Remove release not available anymore
|
||||
if not os.path.isfile(ss(release_file['path'])):
|
||||
fireEvent('release.clean', release['id'])
|
||||
break
|
||||
def createAddToLibrary(self, folder, added_identifiers = []):
|
||||
def addToLibrary(group, total_found, to_go):
|
||||
if self.in_progress[folder]['total'] is None:
|
||||
self.in_progress[folder] = {
|
||||
'total': total_found,
|
||||
'to_go': total_found,
|
||||
}
|
||||
|
||||
Env.prop('manage.last_update', time.time())
|
||||
if group['library']:
|
||||
identifier = group['library'].get('identifier')
|
||||
added_identifiers.append(identifier)
|
||||
|
||||
# Add it to release and update the info
|
||||
fireEvent('release.add', group = group)
|
||||
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
|
||||
|
||||
return addToLibrary
|
||||
|
||||
def createAfterUpdate(self, folder, identifier):
|
||||
|
||||
# Notify frontend
|
||||
def afterUpdate():
|
||||
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
|
||||
total = self.in_progress[folder]['total']
|
||||
movie_dict = fireEvent('movie.get', identifier, single = True)
|
||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library']))
|
||||
|
||||
return afterUpdate
|
||||
|
||||
def directories(self):
|
||||
try:
|
||||
return [x.strip() for x in self.conf('library', default = '').split('::')]
|
||||
return splitString(self.conf('library', default = ''), '::')
|
||||
except:
|
||||
return []
|
||||
|
||||
def scanFilesToLibrary(self, folder = None, files = None):
|
||||
|
||||
folder = os.path.normpath(folder)
|
||||
|
||||
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
|
||||
|
||||
for group in groups.itervalues():
|
||||
if group['library']:
|
||||
fireEvent('release.add', group = group)
|
||||
|
||||
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.helpers.request import getParams, jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import getImdb
|
||||
from couchpotato.core.helpers.variable import getImdb, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, Movie
|
||||
@@ -107,13 +107,18 @@ class MoviePlugin(Plugin):
|
||||
def get(self, movie_id):
|
||||
|
||||
db = get_session()
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
|
||||
imdb_id = getImdb(str(movie_id))
|
||||
|
||||
if(imdb_id):
|
||||
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
|
||||
else:
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
|
||||
results = None
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
#db.close()
|
||||
return results
|
||||
|
||||
def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None):
|
||||
@@ -161,7 +166,7 @@ class MoviePlugin(Plugin):
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
if limit_offset:
|
||||
splt = [x.strip() for x in limit_offset.split(',')]
|
||||
splt = splitString(limit_offset)
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q2 = q2.limit(limit).offset(offset)
|
||||
@@ -239,7 +244,7 @@ class MoviePlugin(Plugin):
|
||||
|
||||
db = get_session()
|
||||
|
||||
for id in getParam('id').split(','):
|
||||
for id in splitString(getParam('id')):
|
||||
movie = db.query(Movie).filter_by(id = id).first()
|
||||
|
||||
if movie:
|
||||
@@ -278,13 +283,27 @@ class MoviePlugin(Plugin):
|
||||
'movies': movies,
|
||||
})
|
||||
|
||||
def add(self, params = {}, force_readd = True, search_after = True):
|
||||
def add(self, params = {}, force_readd = True, search_after = True, update_library = False):
|
||||
|
||||
if not params.get('identifier'):
|
||||
log.error('Can\'t add movie without imdb identifier.')
|
||||
msg = 'Can\'t add movie without imdb identifier.'
|
||||
log.error(msg)
|
||||
fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
url = 'http://thetvdb.com/api/GetSeriesByRemoteID.php?imdbid=%s' % params.get('identifier')
|
||||
tvdb = self.getCache('thetvdb.%s' % params.get('identifier'), url = url, show_error = False)
|
||||
if tvdb and 'series' in tvdb.lower():
|
||||
msg = 'Can\'t add movie, seems to be a TV show.'
|
||||
log.error(msg)
|
||||
fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
library = fireEvent('library.add', single = True, attrs = params, update_after = False)
|
||||
|
||||
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
|
||||
|
||||
# Status
|
||||
status_active = fireEvent('status.add', 'active', single = True)
|
||||
@@ -367,7 +386,7 @@ class MoviePlugin(Plugin):
|
||||
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
|
||||
ids = [x.strip() for x in params.get('id').split(',')]
|
||||
ids = splitString(params.get('id'))
|
||||
for movie_id in ids:
|
||||
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
@@ -403,7 +422,7 @@ class MoviePlugin(Plugin):
|
||||
|
||||
params = getParams()
|
||||
|
||||
ids = [x.strip() for x in params.get('id').split(',')]
|
||||
ids = splitString(params.get('id'))
|
||||
for movie_id in ids:
|
||||
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
|
||||
|
||||
@@ -417,9 +436,11 @@ class MoviePlugin(Plugin):
|
||||
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
if movie:
|
||||
deleted = False
|
||||
if delete_from == 'all':
|
||||
db.delete(movie)
|
||||
db.commit()
|
||||
deleted = True
|
||||
else:
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
@@ -442,6 +463,7 @@ class MoviePlugin(Plugin):
|
||||
if total_releases == total_deleted:
|
||||
db.delete(movie)
|
||||
db.commit()
|
||||
deleted = True
|
||||
elif new_movie_status:
|
||||
new_status = fireEvent('status.get', new_movie_status, single = True)
|
||||
movie.profile_id = None
|
||||
@@ -450,6 +472,9 @@ class MoviePlugin(Plugin):
|
||||
else:
|
||||
fireEvent('movie.restatus', movie.id, single = True)
|
||||
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
||||
|
||||
#db.close()
|
||||
return True
|
||||
|
||||
|
||||
@@ -33,16 +33,34 @@ var MovieList = new Class({
|
||||
);
|
||||
self.getMovies();
|
||||
|
||||
if(options.add_new)
|
||||
App.addEvent('movie.added', self.movieAdded.bind(self))
|
||||
App.addEvent('movie.added', self.movieAdded.bind(self))
|
||||
App.addEvent('movie.deleted', self.movieDeleted.bind(self))
|
||||
},
|
||||
|
||||
movieDeleted: function(notification){
|
||||
var self = this;
|
||||
|
||||
if(self.movies_added[notification.data.id]){
|
||||
self.movies.each(function(movie){
|
||||
if(movie.get('id') == notification.data.id){
|
||||
movie.destroy();
|
||||
delete self.movies_added[notification.data.id]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.checkIfEmpty();
|
||||
},
|
||||
|
||||
movieAdded: function(notification){
|
||||
var self = this;
|
||||
window.scroll(0,0);
|
||||
|
||||
if(!self.movies_added[notification.data.id])
|
||||
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
|
||||
window.scroll(0,0);
|
||||
self.createMovie(notification.data, 'top');
|
||||
|
||||
self.checkIfEmpty();
|
||||
}
|
||||
},
|
||||
|
||||
create: function(){
|
||||
@@ -86,18 +104,19 @@ var MovieList = new Class({
|
||||
Object.each(movies, function(movie){
|
||||
self.createMovie(movie);
|
||||
});
|
||||
|
||||
|
||||
self.total_movies = total;
|
||||
self.setCounter(total);
|
||||
|
||||
},
|
||||
|
||||
|
||||
setCounter: function(count){
|
||||
var self = this;
|
||||
|
||||
|
||||
if(!self.navigation_counter) return;
|
||||
|
||||
|
||||
self.navigation_counter.set('text', (count || 0));
|
||||
|
||||
|
||||
},
|
||||
|
||||
createMovie: function(movie, inject_at){
|
||||
@@ -281,10 +300,11 @@ var MovieList = new Class({
|
||||
},
|
||||
|
||||
deleteSelected: function(){
|
||||
var self = this;
|
||||
var ids = self.getSelectedMovies()
|
||||
var self = this,
|
||||
ids = self.getSelectedMovies(),
|
||||
help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list';
|
||||
|
||||
var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!', [{
|
||||
var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{
|
||||
'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'),
|
||||
'class': 'delete',
|
||||
'events': {
|
||||
@@ -309,6 +329,8 @@ var MovieList = new Class({
|
||||
|
||||
erase_movies.each(function(movie){
|
||||
self.movies.erase(movie);
|
||||
|
||||
movie.destroy()
|
||||
});
|
||||
|
||||
self.calculateSelected();
|
||||
@@ -458,6 +480,8 @@ var MovieList = new Class({
|
||||
self.addMovies(json.movies, json.total);
|
||||
self.load_more.set('text', 'load more movies');
|
||||
if(self.scrollspy) self.scrollspy.start();
|
||||
|
||||
self.checkIfEmpty()
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -475,6 +499,28 @@ var MovieList = new Class({
|
||||
|
||||
},
|
||||
|
||||
checkIfEmpty: function(){
|
||||
var self = this;
|
||||
|
||||
var is_empty = self.movies.length == 0 && self.total_movies == 0;
|
||||
|
||||
if(is_empty && self.options.on_empty_element){
|
||||
self.el.grab(self.options.on_empty_element);
|
||||
|
||||
if(self.navigation)
|
||||
self.navigation.hide();
|
||||
|
||||
self.empty_element = self.options.on_empty_element;
|
||||
}
|
||||
else if(self.empty_element){
|
||||
self.empty_element.destroy();
|
||||
|
||||
if(self.navigation)
|
||||
self.navigation.show();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
|
||||
.movies .movie_container {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -313,7 +313,7 @@
|
||||
padding-bottom: 4px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .trailer_container {
|
||||
width: 100%;
|
||||
background: #000;
|
||||
@@ -324,7 +324,7 @@
|
||||
.movies .movie .trailer_container.hide {
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .hide_trailer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -340,12 +340,12 @@
|
||||
.movies .movie .hide_trailer.hide {
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .try_container {
|
||||
padding: 5px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .try_container a {
|
||||
margin: 0 5px;
|
||||
padding: 2px 5px;
|
||||
@@ -354,15 +354,15 @@
|
||||
.movies .movie .releases .next_release {
|
||||
border-left: 6px solid #2aa300;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .releases .next_release > :first-child {
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .releases .last_release {
|
||||
border-left: 6px solid #ffa200;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .releases .last_release > :first-child {
|
||||
margin-left: -6px;
|
||||
}
|
||||
@@ -394,8 +394,8 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.movies .alph_nav ul.numbers,
|
||||
.movies .alph_nav .counter,
|
||||
.movies .alph_nav ul.numbers,
|
||||
.movies .alph_nav .counter,
|
||||
.movies .alph_nav ul.actions {
|
||||
list-style: none;
|
||||
padding: 0 0 1px;
|
||||
@@ -518,7 +518,7 @@
|
||||
padding: 3px 7px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form .refresh,
|
||||
.movies .alph_nav .mass_edit_form .refresh,
|
||||
.movies .alph_nav .mass_edit_form .delete {
|
||||
float: left;
|
||||
padding: 8px 0 0 8px;
|
||||
@@ -534,5 +534,67 @@
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu > a {
|
||||
background-position: center -157px;
|
||||
background-position: center -158px;
|
||||
}
|
||||
|
||||
.movies .empty_wanted {
|
||||
background-image: url('../images/emptylist.png');
|
||||
height: 750px;
|
||||
width: 800px;
|
||||
padding-top: 260px;
|
||||
margin-top: -50px;
|
||||
}
|
||||
|
||||
.movies .empty_manage {
|
||||
text-align: center;
|
||||
font-size: 25px;
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
.movies .empty_manage .after_manage {
|
||||
margin-top: 30px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.movies .progress {
|
||||
border-radius: 2px;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.movies .progress > div {
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
width: 49%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
margin: 2px 0.5%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.movies .progress > div .folder {
|
||||
display: inline-block;
|
||||
padding: 5px 20px 5px 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 85%;
|
||||
direction: rtl;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.movies .progress > div .percentage {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
text-shadow: none;
|
||||
font-weight: normal;
|
||||
font-size: 20px;
|
||||
border-left: 1px solid rgba(255, 255, 255, .2);
|
||||
width: 15%;
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -16,14 +16,41 @@ var Movie = new Class({
|
||||
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||
self.parent(self, options);
|
||||
|
||||
App.addEvent('movie.update.'+data.id, self.update.bind(self));
|
||||
self.addEvents();
|
||||
},
|
||||
|
||||
addEvents: function(){
|
||||
var self = this;
|
||||
|
||||
App.addEvent('movie.update.'+self.data.id, self.update.bind(self));
|
||||
|
||||
['movie.busy', 'searcher.started'].each(function(listener){
|
||||
App.addEvent(listener+'.'+data.id, function(notification){
|
||||
App.addEvent(listener+'.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
self.busy(true)
|
||||
});
|
||||
})
|
||||
|
||||
App.addEvent('searcher.ended.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
self.busy(false)
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function(){
|
||||
var self = this;
|
||||
|
||||
self.el.destroy();
|
||||
delete self.list.movies_added[self.get('id')];
|
||||
self.list.movies.erase(self)
|
||||
|
||||
self.list.checkIfEmpty();
|
||||
|
||||
// Remove events
|
||||
App.removeEvents('movie.update.'+self.data.id);
|
||||
['movie.busy', 'searcher.started'].each(function(listener){
|
||||
App.removeEvents(listener+'.'+self.data.id);
|
||||
})
|
||||
},
|
||||
|
||||
busy: function(set_busy){
|
||||
@@ -359,7 +386,7 @@ var ReleaseAction = new Class({
|
||||
|
||||
var status = Status.get(release.status_id);
|
||||
|
||||
if((status.identifier == 'ignored' || status.identifier == 'failed') || (!self.next_release && status.identifier == 'available')){
|
||||
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
|
||||
self.hide_on_click = false;
|
||||
self.show();
|
||||
buttons_done = true;
|
||||
@@ -397,19 +424,11 @@ var ReleaseAction = new Class({
|
||||
var status = Status.get(release.status_id),
|
||||
quality = Quality.getProfile(release.quality_id) || {},
|
||||
info = release.info;
|
||||
|
||||
if( status.identifier == 'ignored' || status.identifier == 'failed'){
|
||||
self.last_release = release;
|
||||
}
|
||||
else if(!self.next_release && status.identifier == 'available'){
|
||||
self.next_release = release;
|
||||
}
|
||||
release.status = status;
|
||||
|
||||
// Create release
|
||||
new Element('div', {
|
||||
'class': 'item '+status.identifier +
|
||||
(self.next_release && self.next_release.id == release.id ? ' next_release' : '') +
|
||||
(self.last_release && self.last_release.id == release.id ? ' last_release' : ''),
|
||||
'class': 'item '+status.identifier,
|
||||
'id': 'release_'+release.id
|
||||
}).adopt(
|
||||
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
|
||||
@@ -442,31 +461,50 @@ var ReleaseAction = new Class({
|
||||
}
|
||||
})
|
||||
).inject(self.release_container)
|
||||
|
||||
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
|
||||
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
|
||||
self.last_release = release;
|
||||
}
|
||||
else if(!self.next_release && status.identifier == 'available'){
|
||||
self.next_release = release;
|
||||
}
|
||||
});
|
||||
|
||||
self.trynext_container.adopt(
|
||||
new Element('span.or', {
|
||||
'text': 'Download'
|
||||
}),
|
||||
self.last_release ? new Element('a.button.orange', {
|
||||
'text': 'the same release again',
|
||||
'events': {
|
||||
'click': self.trySameRelease.bind(self)
|
||||
}
|
||||
}) : null,
|
||||
self.next_release && self.last_release ? new Element('span.or', {
|
||||
'text': 'or'
|
||||
}) : null,
|
||||
self.next_release ? [new Element('a.button.green', {
|
||||
'text': self.last_release ? 'another release' : 'the best release',
|
||||
'events': {
|
||||
'click': self.tryNextRelease.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('span.or', {
|
||||
'text': 'or pick one below'
|
||||
})] : null
|
||||
)
|
||||
if(self.last_release){
|
||||
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
|
||||
}
|
||||
|
||||
if(self.next_release){
|
||||
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
|
||||
}
|
||||
|
||||
if(self.next_release || self.last_release){
|
||||
|
||||
self.trynext_container.adopt(
|
||||
new Element('span.or', {
|
||||
'text': 'This movie is snatched, if anything went wrong, download'
|
||||
}),
|
||||
self.last_release ? new Element('a.button.orange', {
|
||||
'text': 'the same release again',
|
||||
'events': {
|
||||
'click': self.trySameRelease.bind(self)
|
||||
}
|
||||
}) : null,
|
||||
self.next_release && self.last_release ? new Element('span.or', {
|
||||
'text': ','
|
||||
}) : null,
|
||||
self.next_release ? [new Element('a.button.green', {
|
||||
'text': self.last_release ? 'another release' : 'the best release',
|
||||
'events': {
|
||||
'click': self.tryNextRelease.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('span.or', {
|
||||
'text': 'or pick one below'
|
||||
})] : null
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -91,10 +91,15 @@
|
||||
.movie_result {
|
||||
overflow: hidden;
|
||||
height: 140px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.movie_result .options {
|
||||
height: 140px;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 1px solid transparent;
|
||||
border-width: 1px 0;
|
||||
border-radius: 0;
|
||||
@@ -133,17 +138,21 @@
|
||||
|
||||
.movie_result .data {
|
||||
padding: 0 15px;
|
||||
width: 470px;
|
||||
position: relative;
|
||||
height: 140px;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
margin: -140px 0 0 0;
|
||||
left: 0;
|
||||
background: #5c697b;
|
||||
cursor: pointer;
|
||||
|
||||
border-bottom: 1px solid #333;
|
||||
border-top: 1px solid rgba(255,255,255, 0.15);
|
||||
transition: all .6s cubic-bezier(0.9,0,0.1,1);
|
||||
}
|
||||
.movie_result .data.open {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.movie_result:last-child .data { border-bottom: 0; }
|
||||
|
||||
@@ -193,4 +202,9 @@
|
||||
|
||||
.search_form .mask {
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
@@ -19,7 +19,9 @@ Block.Search = new Class({
|
||||
self.hideResults(false)
|
||||
},
|
||||
'blur': function(){
|
||||
self.el.removeClass('focused')
|
||||
(function(){
|
||||
self.el.removeClass('focused')
|
||||
}).delay(2000);
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -117,7 +119,7 @@ Block.Search = new Class({
|
||||
self.hideResults(false);
|
||||
|
||||
if(!cache){
|
||||
self.positionMask().fade('in');
|
||||
self.mask.fade('in');
|
||||
|
||||
if(!self.spinner)
|
||||
self.spinner = createSpinner(self.mask);
|
||||
@@ -139,7 +141,6 @@ Block.Search = new Class({
|
||||
fill: function(q, json){
|
||||
var self = this;
|
||||
|
||||
self.positionMask()
|
||||
self.cache[q] = json
|
||||
|
||||
self.movies = {}
|
||||
@@ -168,19 +169,6 @@ Block.Search = new Class({
|
||||
|
||||
},
|
||||
|
||||
positionMask: function(){
|
||||
var self = this;
|
||||
|
||||
var s = self.result_container.getSize()
|
||||
|
||||
return self.mask.setStyles({
|
||||
'width': s.x,
|
||||
'height': s.y
|
||||
}).position({
|
||||
'relativeTo': self.result_container
|
||||
})
|
||||
},
|
||||
|
||||
loading: function(bool){
|
||||
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
||||
},
|
||||
@@ -193,7 +181,7 @@ Block.Search = new Class({
|
||||
|
||||
Block.Search.Item = new Class({
|
||||
|
||||
initialize: function(info){
|
||||
initialize: function(info, options){
|
||||
var self = this;
|
||||
|
||||
self.info = info;
|
||||
@@ -203,14 +191,13 @@ Block.Search.Item = new Class({
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var info = self.info;
|
||||
var self = this,
|
||||
info = self.info;
|
||||
|
||||
self.el = new Element('div.movie_result', {
|
||||
'id': info.imdb
|
||||
}).adopt(
|
||||
self.options = new Element('div.options.inlay'),
|
||||
self.options_el = new Element('div.options.inlay'),
|
||||
self.data_container = new Element('div.data', {
|
||||
'tween': {
|
||||
duration: 400,
|
||||
@@ -273,11 +260,7 @@ Block.Search.Item = new Class({
|
||||
|
||||
self.createOptions();
|
||||
|
||||
if(!self.width)
|
||||
self.width = self.data_container.getCoordinates().width
|
||||
|
||||
self.data_container.tween('left', 0, self.width);
|
||||
|
||||
self.data_container.addClass('open');
|
||||
self.el.addEvent('outerClick', self.closeOptions.bind(self))
|
||||
|
||||
},
|
||||
@@ -285,7 +268,7 @@ Block.Search.Item = new Class({
|
||||
closeOptions: function(){
|
||||
var self = this;
|
||||
|
||||
self.data_container.tween('left', self.width, 0);
|
||||
self.data_container.removeClass('open');
|
||||
self.el.removeEvents('outerClick')
|
||||
},
|
||||
|
||||
@@ -302,29 +285,32 @@ Block.Search.Item = new Class({
|
||||
'profile_id': self.profile_select.get('value')
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.options.empty();
|
||||
self.options.adopt(
|
||||
self.options_el.empty();
|
||||
self.options_el.adopt(
|
||||
new Element('div.message', {
|
||||
'text': json.added ? 'Movie succesfully added.' : 'Movie didn\'t add properly. Check logs'
|
||||
'text': json.added ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs'
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
},
|
||||
'onFailure': function(){
|
||||
self.options.empty();
|
||||
self.options.adopt(
|
||||
self.options_el.empty();
|
||||
self.options_el.adopt(
|
||||
new Element('div.message', {
|
||||
'text': 'Something went wrong, check the logs for more info.'
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createOptions: function(){
|
||||
var self = this;
|
||||
var self = this,
|
||||
info = self.info;
|
||||
|
||||
if(!self.options_el.hasClass('set')){
|
||||
|
||||
if(!self.options.hasClass('set')){
|
||||
|
||||
if(self.info.in_library){
|
||||
var in_library = [];
|
||||
self.info.in_library.releases.each(function(release){
|
||||
@@ -332,14 +318,14 @@ Block.Search.Item = new Class({
|
||||
});
|
||||
}
|
||||
|
||||
self.options.adopt(
|
||||
self.options_el.grab(
|
||||
new Element('div').adopt(
|
||||
self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||
'src': self.info.images.poster[0],
|
||||
self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', {
|
||||
'src': info.images.poster[0],
|
||||
'height': null,
|
||||
'width': null
|
||||
}) : null,
|
||||
self.info.in_wanted ? new Element('span.in_wanted', {
|
||||
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
|
||||
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
|
||||
}) : (in_library ? new Element('span.in_library', {
|
||||
'text': 'Already in library: ' + in_library.join(', ')
|
||||
@@ -372,7 +358,7 @@ Block.Search.Item = new Class({
|
||||
}).inject(self.profile_select)
|
||||
});
|
||||
|
||||
self.options.addClass('set');
|
||||
self.options_el.addClass('set');
|
||||
}
|
||||
|
||||
},
|
||||
@@ -380,17 +366,7 @@ Block.Search.Item = new Class({
|
||||
loadingMask: function(){
|
||||
var self = this;
|
||||
|
||||
var s = self.options.getSize();
|
||||
|
||||
self.mask = new Element('span.mask', {
|
||||
'styles': {
|
||||
'position': 'relative',
|
||||
'width': s.x,
|
||||
'height': s.y,
|
||||
'top': -s.y,
|
||||
'display': 'block'
|
||||
}
|
||||
}).inject(self.options).fade('hide')
|
||||
self.mask = new Element('span.mask').inject(self.el).fade('hide')
|
||||
|
||||
createSpinner(self.mask)
|
||||
self.mask.fade('in')
|
||||
|
||||
@@ -47,7 +47,6 @@ class ProfilePlugin(Plugin):
|
||||
for profile in profiles:
|
||||
temp.append(profile.to_dict(self.to_dict))
|
||||
|
||||
#db.close()
|
||||
return temp
|
||||
|
||||
def save(self):
|
||||
@@ -84,7 +83,6 @@ class ProfilePlugin(Plugin):
|
||||
|
||||
profile_dict = p.to_dict(self.to_dict)
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'profile': profile_dict
|
||||
@@ -95,7 +93,6 @@ class ProfilePlugin(Plugin):
|
||||
db = get_session()
|
||||
default = db.query(Profile).first()
|
||||
default_dict = default.to_dict(self.to_dict)
|
||||
#db.close()
|
||||
|
||||
return default_dict
|
||||
|
||||
@@ -113,7 +110,6 @@ class ProfilePlugin(Plugin):
|
||||
order += 1
|
||||
|
||||
db.commit()
|
||||
#db.close()
|
||||
|
||||
return jsonified({
|
||||
'success': True
|
||||
@@ -137,8 +133,6 @@ class ProfilePlugin(Plugin):
|
||||
except Exception, e:
|
||||
message = log.error('Failed deleting Profile: %s', e)
|
||||
|
||||
#db.close()
|
||||
|
||||
return jsonified({
|
||||
'success': success,
|
||||
'message': message
|
||||
@@ -181,10 +175,10 @@ class ProfilePlugin(Plugin):
|
||||
)
|
||||
p.types.append(profile_type)
|
||||
|
||||
db.commit()
|
||||
quality_order += 1
|
||||
|
||||
order += 1
|
||||
|
||||
#db.close()
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
|
||||
@@ -86,7 +86,10 @@ var Profile = new Class({
|
||||
},
|
||||
'onComplete': function(json){
|
||||
if(json.success){
|
||||
self.data = json.profile
|
||||
self.data = json.profile;
|
||||
self.type_container.getElement('li:first-child input[type=checkbox]')
|
||||
.set('checked', true)
|
||||
.getParent().addClass('checked');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -239,9 +242,17 @@ Profile.Type = new Class({
|
||||
),
|
||||
new Element('span.finish').adopt(
|
||||
self.finish = new Element('input.inlay.finish[type=checkbox]', {
|
||||
'checked': data.finish,
|
||||
'checked': data.finish !== undefined ? data.finish : 1,
|
||||
'events': {
|
||||
'change': self.fireEvent.bind(self, 'change')
|
||||
'change': function(e){
|
||||
if(self.el == self.el.getParent().getElement(':first-child')){
|
||||
self.finish_class.check();
|
||||
alert('Top quality always finishes the search')
|
||||
return;
|
||||
}
|
||||
|
||||
self.fireEvent('change');
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
@@ -255,7 +266,7 @@ Profile.Type = new Class({
|
||||
|
||||
self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
|
||||
|
||||
new Form.Check(self.finish);
|
||||
self.finish_class = new Form.Check(self.finish);
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Quality, Profile, ProfileType
|
||||
import os.path
|
||||
import re
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -21,11 +22,11 @@ class QualityPlugin(Plugin):
|
||||
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
|
||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
|
||||
]
|
||||
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
|
||||
@@ -68,7 +69,6 @@ class QualityPlugin(Plugin):
|
||||
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
|
||||
temp.append(q)
|
||||
|
||||
#db.close()
|
||||
return temp
|
||||
|
||||
def single(self, identifier = ''):
|
||||
@@ -80,7 +80,6 @@ class QualityPlugin(Plugin):
|
||||
if quality:
|
||||
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
|
||||
|
||||
#db.close()
|
||||
return quality_dict
|
||||
|
||||
def getQuality(self, identifier):
|
||||
@@ -100,7 +99,6 @@ class QualityPlugin(Plugin):
|
||||
setattr(quality, params.get('value_type'), params.get('value'))
|
||||
db.commit()
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True
|
||||
})
|
||||
@@ -113,46 +111,48 @@ class QualityPlugin(Plugin):
|
||||
for q in self.qualities:
|
||||
|
||||
# Create quality
|
||||
quality = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
|
||||
qual = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
|
||||
|
||||
if not quality:
|
||||
if not qual:
|
||||
log.info('Creating quality: %s', q.get('label'))
|
||||
quality = Quality()
|
||||
db.add(quality)
|
||||
qual = Quality()
|
||||
qual.order = order
|
||||
qual.identifier = q.get('identifier')
|
||||
qual.label = toUnicode(q.get('label'))
|
||||
qual.size_min, qual.size_max = q.get('size')
|
||||
|
||||
quality.order = order
|
||||
quality.identifier = q.get('identifier')
|
||||
quality.label = toUnicode(q.get('label'))
|
||||
quality.size_min, quality.size_max = q.get('size')
|
||||
db.add(qual)
|
||||
|
||||
# Create single quality profile
|
||||
profile = db.query(Profile).filter(
|
||||
prof = db.query(Profile).filter(
|
||||
Profile.core == True
|
||||
).filter(
|
||||
Profile.types.any(quality = quality)
|
||||
Profile.types.any(quality = qual)
|
||||
).all()
|
||||
|
||||
if not profile:
|
||||
if not prof:
|
||||
log.info('Creating profile: %s', q.get('label'))
|
||||
profile = Profile(
|
||||
prof = Profile(
|
||||
core = True,
|
||||
label = toUnicode(quality.label),
|
||||
label = toUnicode(qual.label),
|
||||
order = order
|
||||
)
|
||||
db.add(profile)
|
||||
db.add(prof)
|
||||
|
||||
profile_type = ProfileType(
|
||||
quality = quality,
|
||||
profile = profile,
|
||||
quality = qual,
|
||||
profile = prof,
|
||||
finish = True,
|
||||
order = 0
|
||||
)
|
||||
profile.types.append(profile_type)
|
||||
prof.types.append(profile_type)
|
||||
|
||||
order += 1
|
||||
db.commit()
|
||||
|
||||
#db.close()
|
||||
db.commit()
|
||||
|
||||
time.sleep(0.3) # Wait a moment
|
||||
|
||||
return True
|
||||
|
||||
def guess(self, files, extra = {}):
|
||||
|
||||
@@ -88,8 +88,6 @@ class Release(Plugin):
|
||||
|
||||
fireEvent('movie.restatus', movie.id)
|
||||
|
||||
#db.close()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -108,7 +106,6 @@ class Release(Plugin):
|
||||
|
||||
release_id = getParam('id')
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': self.delete(release_id)
|
||||
})
|
||||
@@ -152,7 +149,6 @@ class Release(Plugin):
|
||||
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True
|
||||
})
|
||||
@@ -161,6 +157,7 @@ class Release(Plugin):
|
||||
|
||||
db = get_session()
|
||||
id = getParam('id')
|
||||
status_snatched = fireEvent('status.add', 'snatched', single = True)
|
||||
|
||||
rel = db.query(Relea).filter_by(id = id).first()
|
||||
if rel:
|
||||
@@ -181,14 +178,16 @@ class Release(Plugin):
|
||||
'files': {}
|
||||
}), manual = True, single = True)
|
||||
|
||||
#db.close()
|
||||
if success:
|
||||
rel.status_id = status_snatched.get('id')
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
'success': success
|
||||
})
|
||||
else:
|
||||
log.error('Couldn\'t find release with id: %s', id)
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': False
|
||||
})
|
||||
|
||||
@@ -82,6 +82,15 @@ config = [{
|
||||
'unit': 'min(s)',
|
||||
'description': 'Detect movie status every X minutes. Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled',
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
'name': 'force_every',
|
||||
'label': 'Force every',
|
||||
'default': 2,
|
||||
'type': 'int',
|
||||
'unit': 'hour(s)',
|
||||
'description': 'Forces the renamer to scan every X hours',
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
'name': 'next_on_failed',
|
||||
|
||||
@@ -3,7 +3,8 @@ from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle
|
||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||
getImdb
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, File, Profile, Release
|
||||
@@ -20,6 +21,7 @@ log = CPLog(__name__)
|
||||
class Renamer(Plugin):
|
||||
|
||||
renaming_started = False
|
||||
checking_snatched = False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -32,7 +34,11 @@ class Renamer(Plugin):
|
||||
|
||||
addEvent('app.load', self.scan)
|
||||
|
||||
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
|
||||
if self.conf('run_every') > 0:
|
||||
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
|
||||
|
||||
if self.conf('force_every') > 0:
|
||||
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'))
|
||||
|
||||
def scanView(self):
|
||||
|
||||
@@ -48,7 +54,7 @@ class Renamer(Plugin):
|
||||
return
|
||||
|
||||
if self.renaming_started is True:
|
||||
log.info('Renamer is disabled to avoid infinite looping of the same error.')
|
||||
log.info('Renamer is already running, if you see this often, check the logs above for errors.')
|
||||
return
|
||||
|
||||
# Check to see if the "to" folder is inside the "from" folder.
|
||||
@@ -127,6 +133,8 @@ class Renamer(Plugin):
|
||||
'resolution_width': group['meta_data'].get('resolution_width'),
|
||||
'resolution_height': group['meta_data'].get('resolution_height'),
|
||||
'imdb_id': library['identifier'],
|
||||
'cd': '',
|
||||
'cd_nr': '',
|
||||
}
|
||||
|
||||
for file_type in group['files']:
|
||||
@@ -144,7 +152,7 @@ class Renamer(Plugin):
|
||||
continue
|
||||
|
||||
# Move other files
|
||||
multiple = len(group['files']['movie']) > 1 and not group['is_dvd']
|
||||
multiple = len(group['files'][file_type]) > 1 and not group['is_dvd']
|
||||
cd = 1 if multiple else 0
|
||||
|
||||
for current_file in sorted(list(group['files'][file_type])):
|
||||
@@ -157,23 +165,19 @@ class Renamer(Plugin):
|
||||
replacements['ext'] = getExt(current_file)
|
||||
|
||||
# cd #
|
||||
replacements['cd'] = ' cd%d' % cd if cd else ''
|
||||
replacements['cd_nr'] = cd
|
||||
replacements['cd'] = ' cd%d' % cd if multiple else ''
|
||||
replacements['cd_nr'] = cd if multiple else ''
|
||||
|
||||
# Naming
|
||||
final_folder_name = self.doReplace(folder_name, replacements)
|
||||
final_file_name = self.doReplace(file_name, replacements)
|
||||
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
|
||||
|
||||
# Group filename without cd extension
|
||||
replacements['cd'] = ''
|
||||
replacements['cd_nr'] = ''
|
||||
|
||||
# Meta naming
|
||||
if file_type is 'trailer':
|
||||
final_file_name = self.doReplace(trailer_name, replacements)
|
||||
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True)
|
||||
elif file_type is 'nfo':
|
||||
final_file_name = self.doReplace(nfo_name, replacements)
|
||||
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True)
|
||||
|
||||
# Seperator replace
|
||||
if separator:
|
||||
@@ -204,10 +208,16 @@ class Renamer(Plugin):
|
||||
# Check for extra subtitle files
|
||||
if file_type is 'subtitle':
|
||||
|
||||
# rename subtitles with or without language
|
||||
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
|
||||
remove_multiple = False
|
||||
if len(group['files']['movie']) == 1:
|
||||
remove_multiple = True
|
||||
|
||||
sub_langs = group['subtitle_language'].get(current_file, [])
|
||||
|
||||
# rename subtitles with or without language
|
||||
sub_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
|
||||
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
|
||||
|
||||
rename_extras = self.getRenameExtras(
|
||||
extra_type = 'subtitle_extra',
|
||||
replacements = replacements,
|
||||
@@ -215,20 +225,19 @@ class Renamer(Plugin):
|
||||
file_name = file_name,
|
||||
destination = destination,
|
||||
group = group,
|
||||
current_file = current_file
|
||||
current_file = current_file,
|
||||
remove_multiple = remove_multiple,
|
||||
)
|
||||
|
||||
# Don't add language if multiple languages in 1 file
|
||||
if len(sub_langs) > 1:
|
||||
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
|
||||
elif len(sub_langs) == 1:
|
||||
# Don't add language if multiple languages in 1 subtitle file
|
||||
if len(sub_langs) == 1:
|
||||
sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
|
||||
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
|
||||
|
||||
rename_files = mergeDicts(rename_files, rename_extras)
|
||||
|
||||
# Filename without cd etc
|
||||
if file_type is 'movie':
|
||||
elif file_type is 'movie':
|
||||
rename_extras = self.getRenameExtras(
|
||||
extra_type = 'movie_extra',
|
||||
replacements = replacements,
|
||||
@@ -240,7 +249,7 @@ class Renamer(Plugin):
|
||||
)
|
||||
rename_files = mergeDicts(rename_files, rename_extras)
|
||||
|
||||
group['filename'] = self.doReplace(file_name, replacements)[:-(len(getExt(final_file_name)) + 1)]
|
||||
group['filename'] = self.doReplace(file_name, replacements, remove_multiple = True)[:-(len(getExt(final_file_name)) + 1)]
|
||||
group['destination_dir'] = os.path.join(destination, final_folder_name)
|
||||
|
||||
if multiple:
|
||||
@@ -375,22 +384,22 @@ class Renamer(Plugin):
|
||||
except:
|
||||
log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
|
||||
|
||||
# Search for trailers etc
|
||||
fireEventAsync('renamer.after', group)
|
||||
|
||||
# Notify on download
|
||||
# Notify on download, search for trailers etc
|
||||
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
|
||||
fireEventAsync('movie.downloaded', message = download_message, data = group)
|
||||
try:
|
||||
fireEvent('renamer.after', message = download_message, group = group, in_order = True)
|
||||
except:
|
||||
log.error('Failed firing (some) of the renamer.after events: %s', traceback.format_exc())
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
#db.close()
|
||||
self.renaming_started = False
|
||||
|
||||
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = ''):
|
||||
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = '', remove_multiple = False):
|
||||
|
||||
replacements = replacements.copy()
|
||||
rename_files = {}
|
||||
|
||||
def test(s):
|
||||
@@ -399,8 +408,8 @@ class Renamer(Plugin):
|
||||
for extra in set(filter(test, group['files'][extra_type])):
|
||||
replacements['ext'] = getExt(extra)
|
||||
|
||||
final_folder_name = self.doReplace(folder_name, replacements)
|
||||
final_file_name = self.doReplace(file_name, replacements)
|
||||
final_folder_name = self.doReplace(folder_name, replacements, remove_multiple = remove_multiple)
|
||||
final_file_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
|
||||
rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name)
|
||||
|
||||
return rename_files
|
||||
@@ -455,11 +464,16 @@ class Renamer(Plugin):
|
||||
|
||||
return True
|
||||
|
||||
def doReplace(self, string, replacements):
|
||||
def doReplace(self, string, replacements, remove_multiple = False):
|
||||
'''
|
||||
replace confignames with the real thing
|
||||
'''
|
||||
|
||||
replacements = replacements.copy()
|
||||
if remove_multiple:
|
||||
replacements['cd'] = ''
|
||||
replacements['cd_nr'] = ''
|
||||
|
||||
replaced = toUnicode(string)
|
||||
for x, r in replacements.iteritems():
|
||||
if r is not None:
|
||||
@@ -495,6 +509,11 @@ class Renamer(Plugin):
|
||||
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
|
||||
|
||||
def checkSnatched(self):
|
||||
if self.checking_snatched:
|
||||
log.debug('Already checking snatched')
|
||||
|
||||
self.checking_snatched = True
|
||||
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
||||
failed_status = fireEvent('status.get', 'failed', single = True)
|
||||
@@ -504,57 +523,69 @@ class Renamer(Plugin):
|
||||
db = get_session()
|
||||
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
|
||||
|
||||
if rels:
|
||||
log.debug('Checking status snatched releases...')
|
||||
|
||||
scan_required = False
|
||||
|
||||
for rel in rels:
|
||||
if rels:
|
||||
self.checking_snatched = True
|
||||
log.debug('Checking status snatched releases...')
|
||||
|
||||
# Get current selected title
|
||||
default_title = ''
|
||||
for title in rel.movie.library.titles:
|
||||
if title.default: default_title = title.title
|
||||
|
||||
# Check if movie has already completed and is manage tab (legacy db correction)
|
||||
if rel.movie.status_id == done_status.get('id'):
|
||||
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
|
||||
rel.status_id = ignored_status.get('id')
|
||||
db.commit()
|
||||
continue
|
||||
|
||||
item = {}
|
||||
for info in rel.info:
|
||||
item[info.identifier] = info.value
|
||||
|
||||
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
|
||||
|
||||
# check status
|
||||
downloadstatus = fireEvent('download.status', data = item, movie = movie_dict, single = True)
|
||||
if not downloadstatus:
|
||||
statuses = fireEvent('download.status', merge = True)
|
||||
if not statuses:
|
||||
log.debug('Download status functionality is not implemented for active downloaders.')
|
||||
scan_required = True
|
||||
else:
|
||||
log.debug('Download status: %s' , downloadstatus)
|
||||
try:
|
||||
for rel in rels:
|
||||
rel_dict = rel.to_dict({'info': {}})
|
||||
|
||||
if downloadstatus == 'failed':
|
||||
if self.conf('next_on_failed'):
|
||||
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
|
||||
else:
|
||||
rel.status_id = failed_status.get('id')
|
||||
db.commit()
|
||||
# Get current selected title
|
||||
default_title = getTitle(rel.movie.library)
|
||||
|
||||
log.info('Download of %s failed.', item['name'])
|
||||
# Check if movie has already completed and is manage tab (legacy db correction)
|
||||
if rel.movie.status_id == done_status.get('id'):
|
||||
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
|
||||
rel.status_id = ignored_status.get('id')
|
||||
db.commit()
|
||||
continue
|
||||
|
||||
elif downloadstatus == 'completed':
|
||||
log.info('Download of %s completed!', item['name'])
|
||||
scan_required = True
|
||||
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
|
||||
|
||||
elif downloadstatus == 'not_found':
|
||||
log.info('%s not found in downloaders', item['name'])
|
||||
rel.status_id = ignored_status.get('id')
|
||||
db.commit()
|
||||
# check status
|
||||
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
|
||||
|
||||
found = False
|
||||
for item in statuses:
|
||||
if item['name'] == nzbname or getImdb(item['name']) == movie_dict['library']['identifier']:
|
||||
|
||||
timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
|
||||
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
|
||||
|
||||
if item['status'] == 'busy':
|
||||
pass
|
||||
elif item['status'] == 'failed':
|
||||
fireEvent('download.remove_failed', item, single = True)
|
||||
|
||||
if self.conf('next_on_failed'):
|
||||
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
|
||||
else:
|
||||
rel.status_id = failed_status.get('id')
|
||||
db.commit()
|
||||
elif item['status'] == 'completed':
|
||||
log.info('Download of %s completed!', item['name'])
|
||||
scan_required = True
|
||||
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
log.info('%s not found in downloaders', nzbname)
|
||||
|
||||
except:
|
||||
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
|
||||
|
||||
# Note that Queued, Downloading, Paused, Repair and Unpackimg are also available as status for SabNZBd
|
||||
if scan_required:
|
||||
fireEvent('renamer.scan')
|
||||
|
||||
self.checking_snatched = False
|
||||
|
||||
return True
|
||||
|
||||
@@ -23,7 +23,7 @@ class Scanner(Plugin):
|
||||
'media': 314572800, # 300MB
|
||||
'trailer': 1048576, # 1MB
|
||||
}
|
||||
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
|
||||
ignored_in_path = ['extracting', '_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
|
||||
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
|
||||
extensions = {
|
||||
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
|
||||
@@ -34,6 +34,7 @@ class Scanner(Plugin):
|
||||
'subtitle_extra': ['idx'],
|
||||
'trailer': ['mov', 'mp4', 'flv']
|
||||
}
|
||||
|
||||
file_types = {
|
||||
'subtitle': ('subtitle', 'subtitle'),
|
||||
'subtitle_extra': ('subtitle', 'subtitle_extra'),
|
||||
@@ -42,6 +43,8 @@ class Scanner(Plugin):
|
||||
'movie': ('video', 'movie'),
|
||||
'movie_extra': ('movie', 'movie_extra'),
|
||||
'backdrop': ('image', 'backdrop'),
|
||||
'poster': ('image', 'poster'),
|
||||
'thumbnail': ('image', 'thumbnail'),
|
||||
'leftover': ('leftover', 'leftover'),
|
||||
}
|
||||
|
||||
@@ -50,6 +53,20 @@ class Scanner(Plugin):
|
||||
'video': ['x264', 'h264', 'divx', 'xvid']
|
||||
}
|
||||
|
||||
audio_codec_map = {
|
||||
0x2000: 'ac3',
|
||||
0x2001: 'dts',
|
||||
0x0055: 'mp3',
|
||||
0x0050: 'mp2',
|
||||
0x0001: 'pcm',
|
||||
0x003: 'pcm',
|
||||
0x77a1: 'tta1',
|
||||
0x5756: 'wav',
|
||||
0x6750: 'vorbis',
|
||||
0xF1AC: 'flac',
|
||||
0x00ff: 'aac',
|
||||
}
|
||||
|
||||
source_media = {
|
||||
'bluray': ['bluray', 'blu-ray', 'brrip', 'br-rip'],
|
||||
'hddvd': ['hddvd', 'hd-dvd'],
|
||||
@@ -80,55 +97,10 @@ class Scanner(Plugin):
|
||||
addEvent('scanner.remove_cptag', self.removeCPTag)
|
||||
|
||||
addEvent('scanner.scan', self.scan)
|
||||
addEvent('scanner.files', self.scanFilesToLibrary)
|
||||
addEvent('scanner.folder', self.scanFolderToLibrary)
|
||||
addEvent('scanner.name_year', self.getReleaseNameYear)
|
||||
addEvent('scanner.partnumber', self.getPartNumber)
|
||||
|
||||
def after_rename(group):
|
||||
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
|
||||
|
||||
addEvent('renamer.after', after_rename)
|
||||
|
||||
def scanFilesToLibrary(self, folder = None, files = None):
|
||||
|
||||
folder = os.path.normpath(folder)
|
||||
|
||||
groups = self.scan(folder = folder, files = files)
|
||||
|
||||
for group in groups.itervalues():
|
||||
if group['library']:
|
||||
fireEvent('release.add', group = group)
|
||||
|
||||
def scanFolderToLibrary(self, folder = None, newer_than = 0, simple = True):
|
||||
|
||||
folder = os.path.normpath(folder)
|
||||
|
||||
if not os.path.isdir(folder):
|
||||
return
|
||||
|
||||
groups = self.scan(folder = folder, simple = simple, newer_than = newer_than)
|
||||
|
||||
added_identifier = []
|
||||
while True and not self.shuttingDown():
|
||||
try:
|
||||
identifier, group = groups.popitem()
|
||||
except:
|
||||
break
|
||||
|
||||
# Save to DB
|
||||
if group['library']:
|
||||
|
||||
# Add release
|
||||
fireEvent('release.add', group = group)
|
||||
library_item = fireEvent('library.update', identifier = group['library'].get('identifier'), single = True)
|
||||
if library_item:
|
||||
added_identifier.append(library_item['identifier'])
|
||||
|
||||
return added_identifier
|
||||
|
||||
|
||||
def scan(self, folder = None, files = [], simple = False, newer_than = 0):
|
||||
def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None):
|
||||
|
||||
folder = ss(os.path.normpath(folder))
|
||||
|
||||
@@ -141,7 +113,8 @@ class Scanner(Plugin):
|
||||
leftovers = []
|
||||
|
||||
# Scan all files of the folder if no files are set
|
||||
if len(files) == 0:
|
||||
if not files:
|
||||
check_file_date = True
|
||||
try:
|
||||
files = []
|
||||
for root, dirs, walk_files in os.walk(folder):
|
||||
@@ -150,6 +123,7 @@ class Scanner(Plugin):
|
||||
except:
|
||||
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
|
||||
else:
|
||||
check_file_date = False
|
||||
files = [ss(x) for x in files]
|
||||
|
||||
db = get_session()
|
||||
@@ -279,8 +253,8 @@ class Scanner(Plugin):
|
||||
del path_identifiers[identifier]
|
||||
del delete_identifiers
|
||||
|
||||
# Determine file types
|
||||
processed_movies = {}
|
||||
# Make sure we remove older / still extracting files
|
||||
valid_files = {}
|
||||
while True and not self.shuttingDown():
|
||||
try:
|
||||
identifier, group = movie_files.popitem()
|
||||
@@ -302,7 +276,7 @@ class Scanner(Plugin):
|
||||
if file_too_new:
|
||||
break
|
||||
|
||||
if file_too_new:
|
||||
if check_file_date and file_too_new:
|
||||
try:
|
||||
time_string = time.ctime(file_time[0])
|
||||
except:
|
||||
@@ -320,17 +294,33 @@ class Scanner(Plugin):
|
||||
|
||||
# Only process movies newer than x
|
||||
if newer_than and newer_than > 0:
|
||||
has_new_files = False
|
||||
for cur_file in group['unsorted_files']:
|
||||
file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
|
||||
if file_time[0] > time.time() or file_time[1] > time.time():
|
||||
if file_time[0] > newer_than or file_time[1] > newer_than:
|
||||
has_new_files = True
|
||||
break
|
||||
|
||||
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
|
||||
if not has_new_files:
|
||||
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
|
||||
|
||||
# Delete the unsorted list
|
||||
del group['unsorted_files']
|
||||
# Delete the unsorted list
|
||||
del group['unsorted_files']
|
||||
|
||||
continue
|
||||
continue
|
||||
|
||||
valid_files[identifier] = group
|
||||
|
||||
del movie_files
|
||||
|
||||
# Determine file types
|
||||
processed_movies = {}
|
||||
total_found = len(valid_files)
|
||||
while True and not self.shuttingDown():
|
||||
try:
|
||||
identifier, group = valid_files.popitem()
|
||||
except:
|
||||
break
|
||||
|
||||
# Group extra (and easy) files first
|
||||
# images = self.getImages(group['unsorted_files'])
|
||||
@@ -355,7 +345,7 @@ class Scanner(Plugin):
|
||||
continue
|
||||
|
||||
log.debug('Getting metadata for %s', identifier)
|
||||
group['meta_data'] = self.getMetaData(group)
|
||||
group['meta_data'] = self.getMetaData(group, folder = folder)
|
||||
|
||||
# Subtitle meta
|
||||
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
|
||||
@@ -392,9 +382,12 @@ class Scanner(Plugin):
|
||||
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
|
||||
group['movie_id'] = None if not movie else movie.id
|
||||
|
||||
|
||||
processed_movies[identifier] = group
|
||||
|
||||
# Notify parent & progress on something found
|
||||
if on_found:
|
||||
on_found(group, total_found, total_found - len(processed_movies))
|
||||
|
||||
if len(processed_movies) > 0:
|
||||
log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
|
||||
else:
|
||||
@@ -402,7 +395,7 @@ class Scanner(Plugin):
|
||||
|
||||
return processed_movies
|
||||
|
||||
def getMetaData(self, group):
|
||||
def getMetaData(self, group, folder = ''):
|
||||
|
||||
data = {}
|
||||
files = list(group['files']['movie'])
|
||||
@@ -431,7 +424,7 @@ class Scanner(Plugin):
|
||||
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 else 'SD'
|
||||
|
||||
filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
|
||||
data['group'] = self.getGroup(filename)
|
||||
data['group'] = self.getGroup(filename[len(folder):])
|
||||
data['source'] = self.getSourceMedia(filename)
|
||||
|
||||
return data
|
||||
@@ -440,9 +433,18 @@ class Scanner(Plugin):
|
||||
|
||||
try:
|
||||
p = enzyme.parse(filename)
|
||||
|
||||
# Video codec
|
||||
vc = ('h264' if p.video[0].codec == 'AVC1' else p.video[0].codec).lower()
|
||||
|
||||
# Audio codec
|
||||
ac = p.audio[0].codec
|
||||
try: ac = self.audio_codec_map.get(p.audio[0].codec)
|
||||
except: pass
|
||||
|
||||
return {
|
||||
'video': p.video[0].codec,
|
||||
'audio': p.audio[0].codec,
|
||||
'video': vc,
|
||||
'audio': ac,
|
||||
'resolution_width': tryInt(p.video[0].width),
|
||||
'resolution_height': tryInt(p.video[0].height),
|
||||
}
|
||||
@@ -539,7 +541,6 @@ class Scanner(Plugin):
|
||||
break
|
||||
except:
|
||||
pass
|
||||
#db.close()
|
||||
|
||||
# Search based on OpenSubtitleHash
|
||||
if not imdb_id and not group['is_dvd']:
|
||||
@@ -760,8 +761,8 @@ class Scanner(Plugin):
|
||||
|
||||
def getGroup(self, file):
|
||||
try:
|
||||
match = re.search('-(?P<group>[A-Z0-9]+).', file, re.I)
|
||||
return match.group('group') or ''
|
||||
match = re.findall('\-([A-Z0-9]+)[\.\/]', file, re.I)
|
||||
return match[-1] or ''
|
||||
except:
|
||||
return ''
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ name_scores = [
|
||||
# Video
|
||||
'x264:1', 'h264:1',
|
||||
# Audio
|
||||
'DTS:4', 'AC3:2',
|
||||
'dts:4', 'ac3:2',
|
||||
# Quality
|
||||
'720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1',
|
||||
# Language / Subs
|
||||
|
||||
@@ -24,18 +24,19 @@ config = [{
|
||||
'name': 'required_words',
|
||||
'label': 'Required words',
|
||||
'default': '',
|
||||
'description': 'Ignore releases that don\'t contain at least one of these words.'
|
||||
'placeholder': 'Example: DTS, AC3 & English',
|
||||
'description': 'Ignore releases that don\'t contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
|
||||
},
|
||||
{
|
||||
'name': 'ignored_words',
|
||||
'label': 'Ignored words',
|
||||
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
|
||||
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
|
||||
},
|
||||
{
|
||||
'name': 'preferred_method',
|
||||
'label': 'First search',
|
||||
'description': 'Which of the methods do you prefer',
|
||||
'default': 'nzb',
|
||||
'default': 'both',
|
||||
'type': 'dropdown',
|
||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import md5, getTitle
|
||||
from couchpotato.core.helpers.variable import md5, getTitle, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||
@@ -36,9 +36,38 @@ class Searcher(Plugin):
|
||||
},
|
||||
})
|
||||
|
||||
addApiView('searcher.full_search', self.allMoviesView, docs = {
|
||||
'desc': 'Starts a full search for all wanted movies',
|
||||
})
|
||||
|
||||
addApiView('searcher.progress', self.getProgress, docs = {
|
||||
'desc': 'Get the progress of current full search',
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'progress': False || object, total & to_go,
|
||||
}"""},
|
||||
})
|
||||
|
||||
# Schedule cronjob
|
||||
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
||||
|
||||
def allMoviesView(self):
|
||||
|
||||
in_progress = self.in_progress
|
||||
if not in_progress:
|
||||
fireEventAsync('searcher.all')
|
||||
fireEvent('notify.frontend', type = 'searcher.started', data = True, message = 'Full search started')
|
||||
else:
|
||||
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
|
||||
|
||||
return jsonified({
|
||||
'success': not in_progress
|
||||
})
|
||||
|
||||
def getProgress(self):
|
||||
|
||||
return jsonified({
|
||||
'progress': self.in_progress
|
||||
})
|
||||
|
||||
def allMovies(self):
|
||||
|
||||
@@ -54,6 +83,11 @@ class Searcher(Plugin):
|
||||
Movie.status.has(identifier = 'active')
|
||||
).all()
|
||||
|
||||
self.in_progress = {
|
||||
'total': len(movies),
|
||||
'to_go': len(movies),
|
||||
}
|
||||
|
||||
for movie in movies:
|
||||
movie_dict = movie.to_dict({
|
||||
'profile': {'types': {'quality': {}}},
|
||||
@@ -65,15 +99,17 @@ class Searcher(Plugin):
|
||||
try:
|
||||
self.single(movie_dict)
|
||||
except IndexError:
|
||||
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
|
||||
fireEvent('library.update', movie_dict['library']['identifier'], force = True)
|
||||
except:
|
||||
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
|
||||
|
||||
self.in_progress['to_go'] -= 1
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
#db.close()
|
||||
self.in_progress = False
|
||||
|
||||
def single(self, movie):
|
||||
@@ -93,6 +129,8 @@ class Searcher(Plugin):
|
||||
|
||||
default_title = getTitle(movie['library'])
|
||||
if not default_title:
|
||||
log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
|
||||
fireEvent('movie.delete', movie['id'], single = True)
|
||||
return
|
||||
|
||||
fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
|
||||
@@ -142,10 +180,10 @@ class Searcher(Plugin):
|
||||
status_id = available_status.get('id')
|
||||
)
|
||||
db.add(rls)
|
||||
db.commit()
|
||||
else:
|
||||
[db.delete(info) for info in rls.info]
|
||||
db.commit()
|
||||
[db.delete(old_info) for old_info in rls.info]
|
||||
|
||||
db.commit()
|
||||
|
||||
for info in nzb:
|
||||
try:
|
||||
@@ -157,14 +195,19 @@ class Searcher(Plugin):
|
||||
value = toUnicode(nzb[info])
|
||||
)
|
||||
rls.info.append(rls_info)
|
||||
db.commit()
|
||||
except InterfaceError:
|
||||
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
||||
|
||||
db.commit()
|
||||
|
||||
nzb['status_id'] = rls.status_id
|
||||
|
||||
|
||||
for nzb in sorted_results:
|
||||
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
|
||||
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
||||
continue
|
||||
|
||||
if nzb['status_id'] == ignored_status.get('id'):
|
||||
log.info('Ignored: %s', nzb['name'])
|
||||
continue
|
||||
@@ -190,7 +233,6 @@ class Searcher(Plugin):
|
||||
|
||||
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
|
||||
|
||||
#db.close()
|
||||
return ret
|
||||
|
||||
def download(self, data, movie, manual = False):
|
||||
@@ -201,47 +243,50 @@ class Searcher(Plugin):
|
||||
filedata = None
|
||||
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
|
||||
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||
if filedata is 'try_next':
|
||||
if filedata == 'try_next':
|
||||
return filedata
|
||||
|
||||
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
|
||||
|
||||
if successful:
|
||||
|
||||
# Mark release as snatched
|
||||
db = get_session()
|
||||
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
||||
rls.status_id = snatched_status.get('id')
|
||||
db.commit()
|
||||
try:
|
||||
# Mark release as snatched
|
||||
db = get_session()
|
||||
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
||||
if rls:
|
||||
rls.status_id = snatched_status.get('id')
|
||||
db.commit()
|
||||
|
||||
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||
log.info(snatch_message)
|
||||
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
|
||||
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||
log.info(snatch_message)
|
||||
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
|
||||
|
||||
# If renamer isn't used, mark movie done
|
||||
if not Env.setting('enabled', 'renamer'):
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
try:
|
||||
if movie['status_id'] == active_status.get('id'):
|
||||
for profile_type in movie['profile']['types']:
|
||||
if rls and profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
||||
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
||||
|
||||
# If renamer isn't used, mark movie done
|
||||
if not Env.setting('enabled', 'renamer'):
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
try:
|
||||
if movie['status_id'] == active_status.get('id'):
|
||||
for profile_type in movie['profile']['types']:
|
||||
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
||||
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
||||
# Mark release done
|
||||
rls.status_id = done_status.get('id')
|
||||
db.commit()
|
||||
|
||||
# Mark release done
|
||||
rls.status_id = done_status.get('id')
|
||||
db.commit()
|
||||
# Mark movie done
|
||||
mvie = db.query(Movie).filter_by(id = movie['id']).first()
|
||||
mvie.status_id = done_status.get('id')
|
||||
db.commit()
|
||||
except:
|
||||
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
|
||||
|
||||
# Mark movie done
|
||||
mvie = db.query(Movie).filter_by(id = movie['id']).first()
|
||||
mvie.status_id = done_status.get('id')
|
||||
db.commit()
|
||||
except Exception, e:
|
||||
log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
|
||||
except:
|
||||
log.error('Failed marking movie finished: %s', traceback.format_exc())
|
||||
|
||||
#db.close()
|
||||
return True
|
||||
|
||||
log.info('Tried to download, but none of the downloaders are enabled')
|
||||
@@ -252,24 +297,29 @@ class Searcher(Plugin):
|
||||
imdb_results = kwargs.get('imdb_results', False)
|
||||
retention = Env.setting('retention', section = 'nzb')
|
||||
|
||||
if nzb.get('seeds') is None and retention < nzb.get('age', 0):
|
||||
log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
||||
if nzb.get('seeds') is None and 0 < retention < nzb.get('age', 0):
|
||||
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
||||
return False
|
||||
|
||||
movie_name = getTitle(movie['library'])
|
||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||
nzb_name = simplifyString(nzb['name'])
|
||||
nzb_words = re.split('\W+', nzb_name)
|
||||
required_words = [x.strip().lower() for x in self.conf('required_words').lower().split(',')]
|
||||
required_words = splitString(self.conf('required_words').lower())
|
||||
|
||||
if self.conf('required_words') and not list(set(nzb_words) & set(required_words)):
|
||||
log.info("Wrong: Required word missing: %s" % nzb['name'])
|
||||
req_match = 0
|
||||
for req_set in required_words:
|
||||
req = splitString(req_set, '&')
|
||||
req_match += len(list(set(nzb_words) & set(req))) == len(req)
|
||||
|
||||
if self.conf('required_words') and req_match == 0:
|
||||
log.info2("Wrong: Required word missing: %s" % nzb['name'])
|
||||
return False
|
||||
|
||||
ignored_words = [x.strip().lower() for x in self.conf('ignored_words').split(',')]
|
||||
ignored_words = splitString(self.conf('ignored_words').lower())
|
||||
blacklisted = list(set(nzb_words) & set(ignored_words))
|
||||
if self.conf('ignored_words') and blacklisted:
|
||||
log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
|
||||
log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
|
||||
return False
|
||||
|
||||
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
|
||||
@@ -283,18 +333,18 @@ class Searcher(Plugin):
|
||||
|
||||
# Contains lower quality string
|
||||
if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality):
|
||||
log.info('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
|
||||
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
|
||||
return False
|
||||
|
||||
|
||||
# File to small
|
||||
if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
|
||||
log.info('"%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
|
||||
log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
|
||||
return False
|
||||
|
||||
# File to large
|
||||
if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
|
||||
log.info('"%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
|
||||
log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
|
||||
return False
|
||||
|
||||
|
||||
@@ -348,13 +398,20 @@ class Searcher(Plugin):
|
||||
if list(set(nzb_words) & set(quality['alternative'])):
|
||||
found[quality['identifier']] = True
|
||||
|
||||
# Try guessing via quality tags
|
||||
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
|
||||
if guess:
|
||||
found[guess['identifier']] = True
|
||||
|
||||
# Hack for older movies that don't contain quality tag
|
||||
year_name = fireEvent('scanner.name_year', name, single = True)
|
||||
if movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
|
||||
if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
|
||||
if size > 3000: # Assume dvdr
|
||||
return 'dvdr' == preferred_quality['identifier']
|
||||
log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size))
|
||||
found['dvdr'] = True
|
||||
else: # Assume dvdrip
|
||||
return 'dvdrip' == preferred_quality['identifier']
|
||||
log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size))
|
||||
found['dvdrip'] = True
|
||||
|
||||
# Allow other qualities
|
||||
for allowed in preferred_quality.get('allow'):
|
||||
@@ -371,12 +428,17 @@ class Searcher(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def correctYear(self, haystack, year, range):
|
||||
def correctYear(self, haystack, year, year_range):
|
||||
|
||||
for string in haystack:
|
||||
if str(year) in string or str(int(year) + range) in string or str(int(year) - range) in string: # 1 year of is fine too
|
||||
|
||||
year_name = fireEvent('scanner.name_year', string, single = True)
|
||||
|
||||
if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)):
|
||||
log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year))
|
||||
return True
|
||||
|
||||
log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year))
|
||||
return False
|
||||
|
||||
def correctName(self, check_name, movie_name):
|
||||
@@ -408,6 +470,11 @@ class Searcher(Plugin):
|
||||
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
|
||||
return True
|
||||
else:
|
||||
|
||||
# For movies before 1972
|
||||
if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
|
||||
return True
|
||||
|
||||
if wanted_quality in pre_releases:
|
||||
# Prerelease 1 week before theaters
|
||||
if dates.get('theater') - 604800 < now:
|
||||
|
||||
@@ -22,6 +22,7 @@ class StatusPlugin(Plugin):
|
||||
'failed': 'Failed',
|
||||
'deleted': 'Deleted',
|
||||
'ignored': 'Ignored',
|
||||
'available': 'Available',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -8,9 +8,9 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'renamer',
|
||||
'subtab': 'subtitles',
|
||||
'name': 'subtitle',
|
||||
'label': 'Download subtitles after rename',
|
||||
'label': 'Download subtitles',
|
||||
'description': 'after rename',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, FileType
|
||||
@@ -13,7 +14,7 @@ log = CPLog(__name__)
|
||||
|
||||
class Subtitle(Plugin):
|
||||
|
||||
services = ['opensubtitles', 'thesubdb', 'subswiki']
|
||||
services = ['opensubtitles', 'thesubdb', 'subswiki', 'podnapisi']
|
||||
|
||||
def __init__(self):
|
||||
addEvent('renamer.before', self.searchSingle)
|
||||
@@ -40,8 +41,6 @@ class Subtitle(Plugin):
|
||||
# get subtitles for those files
|
||||
subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
|
||||
|
||||
#db.close()
|
||||
|
||||
def searchSingle(self, group):
|
||||
|
||||
if self.isDisabled(): return
|
||||
@@ -69,4 +68,4 @@ class Subtitle(Plugin):
|
||||
return False
|
||||
|
||||
def getLanguages(self):
|
||||
return [x.strip() for x in self.conf('languages').split(',')]
|
||||
return splitString(self.conf('languages'))
|
||||
|
||||
@@ -8,9 +8,9 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'renamer',
|
||||
'subtab': 'trailer',
|
||||
'name': 'trailer',
|
||||
'label': 'Download trailer after rename',
|
||||
'label': 'Download trailer',
|
||||
'description': 'after rename',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
@@ -24,12 +24,6 @@ config = [{
|
||||
'type': 'dropdown',
|
||||
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
|
||||
},
|
||||
{
|
||||
'name': 'automatic',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'description': 'Automaticly search & download for movies in library',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -12,7 +12,7 @@ class Trailer(Plugin):
|
||||
def __init__(self):
|
||||
addEvent('renamer.after', self.searchSingle)
|
||||
|
||||
def searchSingle(self, group):
|
||||
def searchSingle(self, message = None, group = {}):
|
||||
|
||||
if self.isDisabled() or len(group['files']['trailer']) > 0: return
|
||||
|
||||
@@ -28,6 +28,8 @@ class Trailer(Plugin):
|
||||
else:
|
||||
log.debug('Trailer already exists: %s', destination)
|
||||
|
||||
group['renamed_files'].append(destination)
|
||||
|
||||
# Download first and break
|
||||
break
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class Userscript(Plugin):
|
||||
addApiView('userscript.get/<random>/<path:filename>', self.getUserScript, static = True)
|
||||
addApiView('userscript', self.iFrame)
|
||||
addApiView('userscript.add_via_url', self.getViaUrl)
|
||||
addApiView('userscript.includes', self.getIncludes)
|
||||
addApiView('userscript.bookmark', self.bookmark)
|
||||
|
||||
addEvent('userscript.get_version', self.getVersion)
|
||||
@@ -35,6 +36,13 @@ class Userscript(Plugin):
|
||||
|
||||
return self.renderTemplate(__file__, 'bookmark.js', **params)
|
||||
|
||||
def getIncludes(self):
|
||||
|
||||
return jsonified({
|
||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||
})
|
||||
|
||||
def getUserScript(self, random = '', filename = ''):
|
||||
|
||||
params = {
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
|
||||
.page.wizard .tab_wrapper {
|
||||
background: #5c697b;
|
||||
padding: 18px 0;
|
||||
font-size: 23px;
|
||||
padding: 10px 0;
|
||||
font-size: 18px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
min-width: 960px;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
||||
@@ -36,7 +37,7 @@
|
||||
display: inline-block;
|
||||
}
|
||||
.page.wizard .tabs li a {
|
||||
padding: 20px 30px;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.page.wizard .tab_wrapper .pointer {
|
||||
@@ -45,7 +46,7 @@
|
||||
border-top: 10px solid #5c697b;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
top: 44px;
|
||||
}
|
||||
|
||||
.page.wizard .tab_content {
|
||||
@@ -58,11 +59,25 @@
|
||||
.page.wizard .wgroup_finish {
|
||||
height: 300px;
|
||||
}
|
||||
.page.wizard .wgroup_finish h1 {
|
||||
text-align: center;
|
||||
}
|
||||
.page.wizard .wgroup_finish .wizard_support,
|
||||
.page.wizard .wgroup_finish .description {
|
||||
font-size: 25px;
|
||||
line-height: 120%;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page.wizard .button.green {
|
||||
padding: 20px;
|
||||
font-size: 25px;
|
||||
margin: 10px 30px;
|
||||
margin: 10px 30px 80px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.page.wizard .tab_nzb_providers {
|
||||
margin: 20px 0 0 0;
|
||||
}
|
||||
|
||||
@@ -1,214 +1,266 @@
|
||||
Page.Wizard = new Class({
|
||||
|
||||
Extends: Page.Settings,
|
||||
|
||||
name: 'wizard',
|
||||
has_tab: false,
|
||||
wizard_only: true,
|
||||
|
||||
headers: {
|
||||
'welcome': {
|
||||
'title': 'Welcome to the new CouchPotato',
|
||||
'description': 'To get started, fill in each of the following settings as much as your can. <br />Maybe first start with importing your movies from the previous CouchPotato',
|
||||
'content': new Element('div', {
|
||||
'styles': {
|
||||
'margin': '0 0 0 30px'
|
||||
}
|
||||
}).adopt(
|
||||
new Element('div', {
|
||||
'html': 'Select the <strong>data.db</strong>. It should be in your CouchPotato root directory.'
|
||||
}),
|
||||
self.import_iframe = new Element('iframe', {
|
||||
'styles': {
|
||||
'height': 40,
|
||||
'width': 300,
|
||||
'border': 0,
|
||||
'overflow': 'hidden'
|
||||
}
|
||||
})
|
||||
),
|
||||
'event': function(){
|
||||
self.import_iframe.set('src', Api.createUrl('v1.import'))
|
||||
}
|
||||
},
|
||||
'general': {
|
||||
'title': 'General',
|
||||
'description': 'If you want to access CP from outside your local network, you better secure it a bit with a username & password.'
|
||||
},
|
||||
'downloaders': {
|
||||
'title': 'What download apps are you using?',
|
||||
'description': 'If you don\'t have any of these listed, you have to use Blackhole. Or drop me a line, maybe I\'ll support your download app.'
|
||||
},
|
||||
'providers': {
|
||||
'title': 'Are you registered at any of these sites?',
|
||||
'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have a few more.'
|
||||
},
|
||||
'renamer': {
|
||||
'title': 'Move & rename the movies after downloading?',
|
||||
'description': ''
|
||||
},
|
||||
'finish': {
|
||||
'title': 'Finish Up',
|
||||
'description': 'Are you done? Did you fill in everything as much as possible? Yes, ok gogogo!',
|
||||
'content': new Element('div').adopt(
|
||||
new Element('a.button.green', {
|
||||
'text': 'I\'m ready to start the awesomeness, wow this button is big and green!',
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
Api.request('settings.save', {
|
||||
'data': {
|
||||
'section': 'core',
|
||||
'name': 'show_wizard',
|
||||
'value': 0
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerOptions': {
|
||||
'target': self.el
|
||||
},
|
||||
'onComplete': function(){
|
||||
window.location = App.createUrl();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
groups: ['welcome', 'general', 'downloaders', 'searcher', 'providers', 'renamer', 'finish'],
|
||||
|
||||
open: function(action, params){
|
||||
var self = this;
|
||||
|
||||
if(!self.initialized){
|
||||
App.fireEvent('unload');
|
||||
App.getBlock('header').hide();
|
||||
|
||||
self.parent(action, params);
|
||||
|
||||
self.addEvent('create', function(){
|
||||
self.order();
|
||||
});
|
||||
|
||||
self.initialized = true;
|
||||
|
||||
self.scroll = new Fx.Scroll(document.body, {
|
||||
'transition': 'quint:in:out'
|
||||
});
|
||||
}
|
||||
else
|
||||
(function(){
|
||||
var sc = self.el.getElement('.wgroup_'+action);
|
||||
self.scroll.start(0, sc.getCoordinates().top-80);
|
||||
}).delay(1)
|
||||
},
|
||||
|
||||
order: function(){
|
||||
var self = this;
|
||||
|
||||
var form = self.el.getElement('.uniForm');
|
||||
var tabs = self.el.getElement('.tabs');
|
||||
|
||||
self.groups.each(function(group){
|
||||
if(self.headers[group]){
|
||||
group_container = new Element('.wgroup_'+group, {
|
||||
'styles': {
|
||||
'opacity': 0.2
|
||||
},
|
||||
'tween': {
|
||||
'duration': 350
|
||||
}
|
||||
});
|
||||
group_container.adopt(
|
||||
new Element('h1', {
|
||||
'text': self.headers[group].title
|
||||
}),
|
||||
self.headers[group].description ? new Element('span.description', {
|
||||
'html': self.headers[group].description
|
||||
}) : null,
|
||||
self.headers[group].content ? self.headers[group].content : null
|
||||
).inject(form);
|
||||
}
|
||||
|
||||
var tab_navigation = tabs.getElement('.t_'+group);
|
||||
if(tab_navigation && group_container){
|
||||
tab_navigation.inject(tabs); // Tab navigation
|
||||
self.el.getElement('.tab_'+group).inject(group_container); // Tab content
|
||||
if(self.headers[group]){
|
||||
var a = tab_navigation.getElement('a');
|
||||
a.set('text', (self.headers[group].label || group).capitalize());
|
||||
var url_split = a.get('href').split('wizard')[1].split('/');
|
||||
if(url_split.length > 3)
|
||||
a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', ''));
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
new Element('li.t_'+group).adopt(
|
||||
new Element('a', {
|
||||
'href': App.createUrl('wizard/'+group),
|
||||
'text': (self.headers[group].label || group).capitalize()
|
||||
})
|
||||
).inject(tabs);
|
||||
}
|
||||
|
||||
if(self.headers[group] && self.headers[group].event)
|
||||
self.headers[group].event.call()
|
||||
});
|
||||
|
||||
// Remove toggle
|
||||
self.el.getElement('.advanced_toggle').destroy();
|
||||
|
||||
// Hide retention
|
||||
self.el.getElement('.tab_searcher').hide();
|
||||
self.el.getElement('.t_searcher').hide();
|
||||
|
||||
// Add pointer
|
||||
new Element('.tab_wrapper').wraps(tabs).adopt(
|
||||
self.pointer = new Element('.pointer', {
|
||||
'tween': {
|
||||
'transition': 'quint:in:out'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Add nav
|
||||
var minimum = self.el.getSize().y-window.getSize().y;
|
||||
self.groups.each(function(group, nr){
|
||||
|
||||
var g = self.el.getElement('.wgroup_'+group);
|
||||
if(!g || !g.isVisible()) return;
|
||||
var t = self.el.getElement('.t_'+group);
|
||||
if(!t) return;
|
||||
|
||||
var func = function(){
|
||||
var ct = t.getCoordinates();
|
||||
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
|
||||
g.tween('opacity', 1);
|
||||
}
|
||||
|
||||
if(nr == 0)
|
||||
func();
|
||||
|
||||
|
||||
var ss = new ScrollSpy( {
|
||||
min: function(){
|
||||
var c = g.getCoordinates();
|
||||
var top = c.top-(window.getSize().y/2);
|
||||
return top > minimum ? minimum : top
|
||||
},
|
||||
max: function(){
|
||||
var c = g.getCoordinates();
|
||||
return c.top+(c.height/2)
|
||||
},
|
||||
onEnter: func,
|
||||
onLeave: function(){
|
||||
g.tween('opacity', 0.2)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Page.Wizard = new Class({
|
||||
|
||||
Extends: Page.Settings,
|
||||
|
||||
name: 'wizard',
|
||||
has_tab: false,
|
||||
wizard_only: true,
|
||||
|
||||
headers: {
|
||||
'welcome': {
|
||||
'title': 'Welcome to the new CouchPotato',
|
||||
'description': 'To get started, fill in each of the following settings as much as you can. <br />Maybe first start with importing your movies from the previous CouchPotato',
|
||||
'content': new Element('div', {
|
||||
'styles': {
|
||||
'margin': '0 0 0 30px'
|
||||
}
|
||||
}).adopt(
|
||||
new Element('div', {
|
||||
'html': 'Select the <strong>data.db</strong>. It should be in your CouchPotato root directory.'
|
||||
}),
|
||||
self.import_iframe = new Element('iframe', {
|
||||
'styles': {
|
||||
'height': 40,
|
||||
'width': 300,
|
||||
'border': 0,
|
||||
'overflow': 'hidden'
|
||||
}
|
||||
})
|
||||
),
|
||||
'event': function(){
|
||||
self.import_iframe.set('src', Api.createUrl('v1.import'))
|
||||
}
|
||||
},
|
||||
'general': {
|
||||
'title': 'General',
|
||||
'description': 'If you want to access CP from outside your local network, you better secure it a bit with a username & password.'
|
||||
},
|
||||
'downloaders': {
|
||||
'title': 'What download apps are you using?',
|
||||
'description': 'CP needs an external download app to work with. Choose one below. For more downloaders check settings after you have filled in the wizard. If your download app isn\'t in the list, use the default Blackhole.'
|
||||
},
|
||||
'providers': {
|
||||
'title': 'Are you registered at any of these sites?',
|
||||
'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have a few more. Check settings for the full list of available providers.',
|
||||
'include': ['nzb_providers', 'torrent_providers']
|
||||
},
|
||||
'renamer': {
|
||||
'title': 'Move & rename the movies after downloading?',
|
||||
'description': 'The coolest part of CP is that it can move and organize your downloaded movies automagically. Check settings and you can even download trailers, subtitles and other data when it has finished downloading. It\'s awesome!'
|
||||
},
|
||||
'automation': {
|
||||
'title': 'Easily add movies to your wanted list!',
|
||||
'description': 'You can easily add movies from your favorite movie site, like IMDB, Rotten Tomatoes, Apple Trailers and more. Just install the userscript or drag the bookmarklet to your browsers bookmarks.' +
|
||||
'<br />Once installed, just click the bookmarklet on a movie page and watch the magic happen ;)',
|
||||
'content': function(){
|
||||
return App.createUserscriptButtons().setStyles({
|
||||
'background-image': "url('"+Api.createUrl('static/userscript/userscript.png')+"')"
|
||||
})
|
||||
}
|
||||
},
|
||||
'finish': {
|
||||
'title': 'Finishing Up',
|
||||
'description': 'Are you done? Did you fill in everything as much as possible?' +
|
||||
'<br />Be sure to check the settings to see what more CP can do!<br /><br />' +
|
||||
'<div class="wizard_support">After you\'ve used CP for a while, and you like it (which of course you will), consider supporting CP. Maybe even by writing some code. <br />Or by getting a subscription at <a href="https://usenetserver.com/partners/?a_aid=couchpotato&a_bid=3f357c6f">Usenet Server</a> or <a href="http://www.newshosting.com/partners/?a_aid=couchpotato&a_bid=a0b022df">Newshosting</a>.</div>',
|
||||
'content': new Element('div').adopt(
|
||||
new Element('a.button.green', {
|
||||
'styles': {
|
||||
'margin-top': 20
|
||||
},
|
||||
'text': 'I\'m ready to start the awesomeness, wow this button is big and green!',
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
Api.request('settings.save', {
|
||||
'data': {
|
||||
'section': 'core',
|
||||
'name': 'show_wizard',
|
||||
'value': 0
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerOptions': {
|
||||
'target': self.el
|
||||
},
|
||||
'onComplete': function(){
|
||||
window.location = App.createUrl();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
groups: ['welcome', 'general', 'downloaders', 'searcher', 'providers', 'renamer', 'automation', 'finish'],
|
||||
|
||||
open: function(action, params){
|
||||
var self = this;
|
||||
|
||||
if(!self.initialized){
|
||||
App.fireEvent('unload');
|
||||
App.getBlock('header').hide();
|
||||
|
||||
self.parent(action, params);
|
||||
|
||||
self.addEvent('create', function(){
|
||||
self.order();
|
||||
});
|
||||
|
||||
self.initialized = true;
|
||||
|
||||
self.scroll = new Fx.Scroll(document.body, {
|
||||
'transition': 'quint:in:out'
|
||||
});
|
||||
}
|
||||
else
|
||||
(function(){
|
||||
var sc = self.el.getElement('.wgroup_'+action);
|
||||
self.scroll.start(0, sc.getCoordinates().top-80);
|
||||
}).delay(1)
|
||||
},
|
||||
|
||||
order: function(){
|
||||
var self = this;
|
||||
|
||||
var form = self.el.getElement('.uniForm');
|
||||
var tabs = self.el.getElement('.tabs');
|
||||
|
||||
self.groups.each(function(group, nr){
|
||||
|
||||
if(self.headers[group]){
|
||||
group_container = new Element('.wgroup_'+group, {
|
||||
'styles': {
|
||||
'opacity': 0.2
|
||||
},
|
||||
'tween': {
|
||||
'duration': 350
|
||||
}
|
||||
});
|
||||
|
||||
if(self.headers[group].include){
|
||||
self.headers[group].include.each(function(inc){
|
||||
group_container.addClass('wgroup_'+inc);
|
||||
})
|
||||
}
|
||||
|
||||
var content = self.headers[group].content
|
||||
group_container.adopt(
|
||||
new Element('h1', {
|
||||
'text': self.headers[group].title
|
||||
}),
|
||||
self.headers[group].description ? new Element('span.description', {
|
||||
'html': self.headers[group].description
|
||||
}) : null,
|
||||
content ? (typeOf(content) == 'function' ? content() : content) : null
|
||||
).inject(form);
|
||||
}
|
||||
|
||||
var tab_navigation = tabs.getElement('.t_'+group);
|
||||
|
||||
if(!tab_navigation && self.headers[group] && self.headers[group].include){
|
||||
tab_navigation = []
|
||||
self.headers[group].include.each(function(inc){
|
||||
tab_navigation.include(tabs.getElement('.t_'+inc));
|
||||
})
|
||||
}
|
||||
|
||||
if(tab_navigation && group_container){
|
||||
tabs.adopt(tab_navigation); // Tab navigation
|
||||
|
||||
if(self.headers[group] && self.headers[group].include){
|
||||
|
||||
self.headers[group].include.each(function(inc){
|
||||
self.el.getElement('.tab_'+inc).inject(group_container);
|
||||
})
|
||||
|
||||
new Element('li.t_'+group).adopt(
|
||||
new Element('a', {
|
||||
'href': App.createUrl('wizard/'+group),
|
||||
'text': (self.headers[group].label || group).capitalize()
|
||||
})
|
||||
).inject(tabs);
|
||||
|
||||
}
|
||||
else
|
||||
self.el.getElement('.tab_'+group).inject(group_container); // Tab content
|
||||
|
||||
if(tab_navigation.getElement && self.headers[group]){
|
||||
var a = tab_navigation.getElement('a');
|
||||
a.set('text', (self.headers[group].label || group).capitalize());
|
||||
var url_split = a.get('href').split('wizard')[1].split('/');
|
||||
if(url_split.length > 3)
|
||||
a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', ''));
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
new Element('li.t_'+group).adopt(
|
||||
new Element('a', {
|
||||
'href': App.createUrl('wizard/'+group),
|
||||
'text': (self.headers[group].label || group).capitalize()
|
||||
})
|
||||
).inject(tabs);
|
||||
}
|
||||
|
||||
if(self.headers[group] && self.headers[group].event)
|
||||
self.headers[group].event.call()
|
||||
});
|
||||
|
||||
// Remove toggle
|
||||
self.el.getElement('.advanced_toggle').destroy();
|
||||
|
||||
// Hide retention
|
||||
self.el.getElement('.tab_searcher').hide();
|
||||
self.el.getElement('.t_searcher').hide();
|
||||
self.el.getElement('.t_nzb_providers').hide();
|
||||
self.el.getElement('.t_torrent_providers').hide();
|
||||
|
||||
// Add pointer
|
||||
new Element('.tab_wrapper').wraps(tabs).adopt(
|
||||
self.pointer = new Element('.pointer', {
|
||||
'tween': {
|
||||
'transition': 'quint:in:out'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Add nav
|
||||
var minimum = self.el.getSize().y-window.getSize().y;
|
||||
self.groups.each(function(group, nr){
|
||||
|
||||
var g = self.el.getElement('.wgroup_'+group);
|
||||
if(!g || !g.isVisible()) return;
|
||||
var t = self.el.getElement('.t_'+group);
|
||||
if(!t) return;
|
||||
|
||||
var func = function(){
|
||||
var ct = t.getCoordinates();
|
||||
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
|
||||
g.tween('opacity', 1);
|
||||
}
|
||||
|
||||
if(nr == 0)
|
||||
func();
|
||||
|
||||
|
||||
var ss = new ScrollSpy( {
|
||||
min: function(){
|
||||
var c = g.getCoordinates();
|
||||
var top = c.top-(window.getSize().y/2);
|
||||
return top > minimum ? minimum : top
|
||||
},
|
||||
max: function(){
|
||||
var c = g.getCoordinates();
|
||||
return c.top+(c.height/2)
|
||||
},
|
||||
onEnter: func,
|
||||
onLeave: function(){
|
||||
g.tween('opacity', 0.2)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@@ -28,9 +28,18 @@ class Automation(Plugin):
|
||||
return self.getIMDBids()
|
||||
|
||||
def search(self, name, year = None, imdb_only = False):
|
||||
|
||||
prop_name = 'automation.cached.%s.%s' % (name, year)
|
||||
cached_imdb = Env.prop(prop_name, default = False)
|
||||
if cached_imdb and imdb_only:
|
||||
return cached_imdb
|
||||
|
||||
result = fireEvent('movie.search', q = '%s %s' % (name, year if year else ''), limit = 1, merge = True)
|
||||
|
||||
if len(result) > 0:
|
||||
if imdb_only and result[0].get('imdb'):
|
||||
Env.prop(prop_name, result[0].get('imdb'))
|
||||
|
||||
return result[0].get('imdb') if imdb_only else result[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import md5, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.automation.base import Automation
|
||||
from couchpotato.environment import Env
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import md5, getImdb
|
||||
from couchpotato.core.helpers.variable import md5, getImdb, splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.automation.base import Automation
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
import time
|
||||
import traceback
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -22,44 +18,25 @@ class IMDB(Automation, RSS):
|
||||
|
||||
movies = []
|
||||
|
||||
enablers = self.conf('automation_urls_use').split(',')
|
||||
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
|
||||
urls = splitString(self.conf('automation_urls'))
|
||||
|
||||
index = -1
|
||||
for rss_url in self.conf('automation_urls').split(','):
|
||||
for url in urls:
|
||||
|
||||
index += 1
|
||||
if not enablers[index]:
|
||||
continue
|
||||
elif 'rss.imdb' not in rss_url:
|
||||
log.error('This isn\'t the correct url.: %s', rss_url)
|
||||
continue
|
||||
|
||||
prop_name = 'automation.imdb.last_update.%s' % md5(rss_url)
|
||||
last_update = float(Env.prop(prop_name, default = 0))
|
||||
|
||||
last_movie_added = 0
|
||||
try:
|
||||
cache_key = 'imdb.rss.%s' % md5(rss_url)
|
||||
|
||||
rss_data = self.getCache(cache_key, rss_url)
|
||||
data = XMLTree.fromstring(rss_data)
|
||||
rss_movies = self.getElements(data, 'channel/item')
|
||||
|
||||
for movie in rss_movies:
|
||||
created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
|
||||
imdb = getImdb(self.getTextElement(movie, "link"))
|
||||
|
||||
if created > last_movie_added:
|
||||
last_movie_added = created
|
||||
|
||||
if not imdb or created <= last_update:
|
||||
continue
|
||||
cache_key = 'imdb.rss.%s' % md5(url)
|
||||
rss_data = self.getCache(cache_key, url)
|
||||
imdbs = getImdb(rss_data, multiple = True)
|
||||
|
||||
for imdb in imdbs:
|
||||
movies.append(imdb)
|
||||
|
||||
except:
|
||||
log.error('Failed loading IMDB watchlist: %s %s', (rss_url, traceback.format_exc()))
|
||||
|
||||
Env.prop(prop_name, last_movie_added)
|
||||
log.error('Failed loading IMDB watchlist: %s %s', (url, traceback.format_exc()))
|
||||
|
||||
return movies
|
||||
|
||||
@@ -3,10 +3,7 @@ from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.automation.base import Automation
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
from xml.etree.ElementTree import ParseError
|
||||
import time
|
||||
import traceback
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
@@ -33,10 +30,6 @@ class MoviesIO(Automation, RSS):
|
||||
if not enablers[index]:
|
||||
continue
|
||||
|
||||
prop_name = 'automation.moviesio.last_update.%s' % md5(rss_url)
|
||||
last_update = float(Env.prop(prop_name, default = 0))
|
||||
|
||||
last_movie_added = 0
|
||||
try:
|
||||
cache_key = 'imdb.rss.%s' % md5(rss_url)
|
||||
|
||||
@@ -45,12 +38,6 @@ class MoviesIO(Automation, RSS):
|
||||
rss_movies = self.getElements(data, 'channel/item')
|
||||
|
||||
for movie in rss_movies:
|
||||
created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
|
||||
|
||||
if created > last_movie_added:
|
||||
last_movie_added = created
|
||||
if created <= last_update:
|
||||
continue
|
||||
|
||||
nameyear = fireEvent('scanner.name_year', self.getTextElement(movie, "title"), single = True)
|
||||
imdb = self.search(nameyear.get('name'), nameyear.get('year'), imdb_only = True)
|
||||
@@ -64,6 +51,4 @@ class MoviesIO(Automation, RSS):
|
||||
except:
|
||||
log.error('Failed loading Movies.io watchlist: %s %s', (rss_url, traceback.format_exc()))
|
||||
|
||||
Env.prop(prop_name, last_movie_added)
|
||||
|
||||
return movies
|
||||
|
||||
@@ -4,8 +4,11 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from urlparse import urlparse
|
||||
import cookielib
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -47,6 +50,8 @@ class YarrProvider(Provider):
|
||||
sizeMb = ['mb', 'mib']
|
||||
sizeKb = ['kb', 'kib']
|
||||
|
||||
login_opener = None
|
||||
|
||||
def __init__(self):
|
||||
addEvent('provider.belongs_to', self.belongsTo)
|
||||
|
||||
@@ -55,8 +60,41 @@ class YarrProvider(Provider):
|
||||
|
||||
addEvent('nzb.feed', self.feed)
|
||||
|
||||
def login(self):
|
||||
|
||||
try:
|
||||
cookiejar = cookielib.CookieJar()
|
||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
|
||||
urllib2.install_opener(opener)
|
||||
log.info2('Logging into %s', self.urls['login'])
|
||||
f = opener.open(self.urls['login'], self.getLoginParams())
|
||||
f.read()
|
||||
f.close()
|
||||
self.login_opener = opener
|
||||
return True
|
||||
except:
|
||||
log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return False
|
||||
|
||||
def loginDownload(self, url = '', nzb_id = ''):
|
||||
try:
|
||||
if not self.login_opener and not self.login():
|
||||
log.error('Failed downloading from %s', self.getName())
|
||||
return self.urlopen(url, opener = self.login_opener)
|
||||
except:
|
||||
log.error('Failed downloading from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return ''
|
||||
|
||||
def download(self, url = '', nzb_id = ''):
|
||||
return self.urlopen(url)
|
||||
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 feed(self):
|
||||
return []
|
||||
@@ -111,4 +149,9 @@ class YarrProvider(Provider):
|
||||
return [self.cat_backup_id]
|
||||
|
||||
def found(self, new):
|
||||
log.info('Found: score(%(score)s) on %(provider)s: %(name)s', new)
|
||||
if not new.get('provider_extra'):
|
||||
new['provider_extra'] = ''
|
||||
else:
|
||||
new['provider_extra'] = ', %s' % new['provider_extra']
|
||||
|
||||
log.info('Found: score(%(score)s) on %(provider)s%(provider_extra)s: %(name)s', new)
|
||||
|
||||
@@ -2,6 +2,7 @@ from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
import shutil
|
||||
import traceback
|
||||
@@ -16,23 +17,23 @@ class MetaDataBase(Plugin):
|
||||
def __init__(self):
|
||||
addEvent('renamer.after', self.create)
|
||||
|
||||
def create(self, release):
|
||||
def create(self, message = None, group = {}):
|
||||
if self.isDisabled(): return
|
||||
|
||||
log.info('Creating %s metadata.', self.getName())
|
||||
|
||||
# Update library to get latest info
|
||||
try:
|
||||
updated_library = fireEvent('library.update', release['library']['identifier'], force = True, single = True)
|
||||
release['library'] = mergeDicts(release['library'], updated_library)
|
||||
updated_library = fireEvent('library.update', group['library']['identifier'], force = True, single = True)
|
||||
group['library'] = mergeDicts(group['library'], updated_library)
|
||||
except:
|
||||
log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc())
|
||||
|
||||
root_name = self.getRootName(release)
|
||||
root_name = self.getRootName(group)
|
||||
meta_name = os.path.basename(root_name)
|
||||
root = os.path.dirname(root_name)
|
||||
|
||||
movie_info = release['library'].get('info')
|
||||
movie_info = group['library'].get('info')
|
||||
|
||||
for file_type in ['nfo', 'thumbnail', 'fanart']:
|
||||
try:
|
||||
@@ -42,13 +43,19 @@ class MetaDataBase(Plugin):
|
||||
if name and self.conf('meta_' + file_type):
|
||||
|
||||
# Get file content
|
||||
content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = release)
|
||||
content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = group)
|
||||
if content:
|
||||
log.debug('Creating %s file: %s', (file_type, name))
|
||||
if os.path.isfile(content):
|
||||
shutil.copy2(content, name)
|
||||
else:
|
||||
self.createFile(name, content)
|
||||
group['renamed_files'].append(name)
|
||||
|
||||
try:
|
||||
os.chmod(name, Env.getPermission('file'))
|
||||
except:
|
||||
log.debug('Failed setting permissions for %s: %s', (name, traceback.format_exc()))
|
||||
|
||||
except:
|
||||
log.error('Unable to create %s file: %s', (file_type, traceback.format_exc()))
|
||||
@@ -74,9 +81,18 @@ class MetaDataBase(Plugin):
|
||||
if file_type.get('identifier') == wanted_file_type:
|
||||
break
|
||||
|
||||
# See if it is in current files
|
||||
for cur_file in data['library'].get('files', []):
|
||||
if cur_file.get('type_id') is file_type.get('id') and os.path.isfile(cur_file.get('path')):
|
||||
return cur_file.get('path')
|
||||
|
||||
# Download using existing info
|
||||
try:
|
||||
images = data['library']['info']['images'][wanted_file_type]
|
||||
file_path = fireEvent('file.download', url = images[0], single = True)
|
||||
return file_path
|
||||
except:
|
||||
pass
|
||||
|
||||
def getFanart(self, movie_info = {}, data = {}):
|
||||
return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'backdrop_original')
|
||||
|
||||
@@ -31,6 +31,14 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': '<strong>%s</strong> is the rootname of the movie. For example "/path/to/movie cd1.mkv" will be "/path/to/movie"'
|
||||
},
|
||||
{
|
||||
'name': 'meta_url_only',
|
||||
'label': 'Only IMDB URL',
|
||||
'default': False,
|
||||
'advanced': True,
|
||||
'description': 'Create a nfo with only the IMDB url inside',
|
||||
'type': 'bool',
|
||||
},
|
||||
{
|
||||
'name': 'meta_fanart',
|
||||
'label': 'Fanart',
|
||||
|
||||
@@ -28,6 +28,11 @@ class XBMC(MetaDataBase):
|
||||
return os.path.join(root, basename.replace('%s', name))
|
||||
|
||||
def getNfo(self, movie_info = {}, data = {}):
|
||||
|
||||
# return imdb url only
|
||||
if self.conf('meta_url_only'):
|
||||
return 'http://www.imdb.com/title/%s/' % toUnicode(data['library']['identifier'])
|
||||
|
||||
nfoxml = Element('movie')
|
||||
|
||||
# Title
|
||||
|
||||
@@ -70,7 +70,6 @@ class MovieResultModifier(Plugin):
|
||||
except:
|
||||
log.error('Tried getting more info on searched movies: %s', traceback.format_exc())
|
||||
|
||||
#db.close()
|
||||
return temp
|
||||
|
||||
def checkLibrary(self, result):
|
||||
|
||||
@@ -1,36 +1,71 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.movie.base import MovieProvider
|
||||
from couchpotato.core.settings.model import Movie
|
||||
from flask.helpers import json
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class CouchPotatoApi(MovieProvider):
|
||||
|
||||
api_url = 'http://couchpota.to/api/%s/'
|
||||
urls = {
|
||||
'search': 'https://couchpota.to/api/search/%s/',
|
||||
'info': 'https://couchpota.to/api/info/%s/',
|
||||
'eta': 'https://couchpota.to/api/eta/%s/',
|
||||
'suggest': 'https://couchpota.to/api/suggest/%s/%s/',
|
||||
}
|
||||
http_time_between_calls = 0
|
||||
api_version = 1
|
||||
|
||||
def __init__(self):
|
||||
#addApiView('movie.suggest', self.suggestView)
|
||||
|
||||
addEvent('movie.info', self.getInfo)
|
||||
addEvent('movie.info', self.getInfo, priority = 1)
|
||||
addEvent('movie.search', self.search, priority = 1)
|
||||
addEvent('movie.release_date', self.getReleaseDate)
|
||||
|
||||
def search(self, q, limit = 12):
|
||||
|
||||
cache_key = 'cpapi.cache.%s' % q
|
||||
cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode(q), timeout = 3, headers = self.getRequestHeaders())
|
||||
|
||||
if cached:
|
||||
try:
|
||||
movies = json.loads(cached)
|
||||
return movies
|
||||
except:
|
||||
log.error('Failed parsing search results: %s', traceback.format_exc())
|
||||
|
||||
return []
|
||||
|
||||
def getInfo(self, identifier = None):
|
||||
return {
|
||||
'release_date': self.getReleaseDate(identifier)
|
||||
}
|
||||
|
||||
if not identifier:
|
||||
return
|
||||
|
||||
cache_key = 'cpapi.cache.info.%s' % identifier
|
||||
cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3, headers = self.getRequestHeaders())
|
||||
|
||||
if cached:
|
||||
try:
|
||||
movie = json.loads(cached)
|
||||
return movie
|
||||
except:
|
||||
log.error('Failed parsing info results: %s', traceback.format_exc())
|
||||
|
||||
return {}
|
||||
|
||||
def getReleaseDate(self, identifier = None):
|
||||
|
||||
if identifier is None: return {}
|
||||
try:
|
||||
headers = {'X-CP-Version': fireEvent('app.version', single = True)}
|
||||
data = self.urlopen((self.api_url % ('eta')) + (identifier + '/'), headers = headers)
|
||||
data = self.urlopen(self.urls['eta'] % identifier, headers = self.getRequestHeaders())
|
||||
dates = json.loads(data)
|
||||
log.debug('Found ETA for %s: %s', (identifier, dates))
|
||||
return dates
|
||||
@@ -41,7 +76,7 @@ class CouchPotatoApi(MovieProvider):
|
||||
|
||||
def suggest(self, movies = [], ignore = []):
|
||||
try:
|
||||
data = self.urlopen((self.api_url % ('suggest')) + ','.join(movies) + '/' + ','.join(ignore) + '/')
|
||||
data = self.urlopen(self.urls['suggest'] % (','.join(movies), ','.join(ignore)))
|
||||
suggestions = json.loads(data)
|
||||
log.info('Found Suggestions for %s', (suggestions))
|
||||
except Exception, e:
|
||||
@@ -59,7 +94,6 @@ class CouchPotatoApi(MovieProvider):
|
||||
db = get_session()
|
||||
active_movies = db.query(Movie).filter(Movie.status.has(identifier = 'active')).all()
|
||||
movies = [x.library.identifier for x in active_movies]
|
||||
#db.close()
|
||||
|
||||
suggestions = self.suggest(movies, ignore)
|
||||
|
||||
@@ -68,3 +102,10 @@ class CouchPotatoApi(MovieProvider):
|
||||
'count': len(suggestions),
|
||||
'suggestions': suggestions
|
||||
})
|
||||
|
||||
def getRequestHeaders(self):
|
||||
return {
|
||||
'X-CP-Version': fireEvent('app.version', single = True),
|
||||
'X-CP-API': self.api_version,
|
||||
'X-CP-Time': time.time(),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.movie.base import MovieProvider
|
||||
import json
|
||||
@@ -13,8 +13,8 @@ log = CPLog(__name__)
|
||||
class IMDBAPI(MovieProvider):
|
||||
|
||||
urls = {
|
||||
'search': 'http://www.imdbapi.com/?tomatoes=true&%s',
|
||||
'info': 'http://www.imdbapi.com/?tomatoes=true&i=%s',
|
||||
'search': 'http://www.imdbapi.com/?%s',
|
||||
'info': 'http://www.imdbapi.com/?i=%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 0
|
||||
@@ -27,8 +27,10 @@ class IMDBAPI(MovieProvider):
|
||||
|
||||
name_year = fireEvent('scanner.name_year', q, single = True)
|
||||
|
||||
if not q or not name_year or (name_year and not name_year.get('name')):
|
||||
return []
|
||||
if not name_year or (name_year and not name_year.get('name')):
|
||||
name_year = {
|
||||
'name': q
|
||||
}
|
||||
|
||||
cache_key = 'imdbapi.cache.%s' % q
|
||||
cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')}), timeout = 3)
|
||||
@@ -90,17 +92,17 @@ class IMDBAPI(MovieProvider):
|
||||
},
|
||||
'rating': {
|
||||
'imdb': (tryFloat(movie.get('imdbRating', 0)), tryInt(movie.get('imdbVotes', '').replace(',', ''))),
|
||||
'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', '').replace(',', ''))),
|
||||
#'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', '').replace(',', ''))),
|
||||
},
|
||||
'imdb': str(movie.get('imdbID', '')),
|
||||
'runtime': self.runtimeToMinutes(movie.get('Runtime', '')),
|
||||
'released': movie.get('Released', ''),
|
||||
'year': year if isinstance(year, (int)) else None,
|
||||
'plot': movie.get('Plot', ''),
|
||||
'genres': movie.get('Genre', '').split(','),
|
||||
'directors': movie.get('Director', '').split(','),
|
||||
'writers': movie.get('Writer', '').split(','),
|
||||
'actors': movie.get('Actors', '').split(','),
|
||||
'genres': splitString(movie.get('Genre', '')),
|
||||
'directors': splitString(movie.get('Director', '')),
|
||||
'writers': splitString(movie.get('Writer', '')),
|
||||
'actors': splitString(movie.get('Actors', '')),
|
||||
}
|
||||
except:
|
||||
log.error('Failed parsing IMDB API json: %s', traceback.format_exc())
|
||||
|
||||
@@ -11,8 +11,8 @@ class TheMovieDb(MovieProvider):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('movie.by_hash', self.byHash)
|
||||
addEvent('movie.search', self.search, priority = 1)
|
||||
addEvent('movie.info', self.getInfo, priority = 1)
|
||||
addEvent('movie.search', self.search, priority = 2)
|
||||
addEvent('movie.info', self.getInfo, priority = 2)
|
||||
addEvent('movie.info_by_tmdb', self.getInfoByTMDBId)
|
||||
|
||||
# Use base wrapper
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
from .main import Newzbin
|
||||
from .main import FTDWorld
|
||||
|
||||
def start():
|
||||
return Newzbin()
|
||||
return FTDWorld()
|
||||
|
||||
config = [{
|
||||
'name': 'newzbin',
|
||||
'name': 'ftdworld',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'newzbin',
|
||||
'description': 'See <a href="https://www.newzbin2.es/">Newzbin</a>',
|
||||
'wizard': True,
|
||||
'subtab': 'nzb_providers',
|
||||
'name': 'FTDWorld',
|
||||
'description': 'Free provider, less accurate. See <a href="http://ftdworld.net">FTDWorld</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
107
couchpotato/core/providers/nzb/ftdworld/main.py
Normal file
107
couchpotato/core/providers/nzb/ftdworld/main.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
|
||||
simplifyString
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
import re
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class FTDWorld(NZBProvider):
|
||||
|
||||
urls = {
|
||||
'search': 'http://ftdworld.net/category.php?%s',
|
||||
'detail': 'http://ftdworld.net/spotinfo.php?id=%s',
|
||||
'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s',
|
||||
'login': 'http://ftdworld.net/index.php',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
|
||||
cat_ids = [
|
||||
([4, 11], ['dvdr']),
|
||||
([1], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']),
|
||||
([10, 13, 14], ['bd50', '720p', '1080p']),
|
||||
]
|
||||
cat_backup_id = [1]
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
q = '%s %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'])
|
||||
|
||||
params = {
|
||||
'ctitle': q,
|
||||
'customQuery': 'usr',
|
||||
'cage': Env.setting('retention', 'nzb'),
|
||||
'csizemin': quality.get('size_min'),
|
||||
'csizemax': quality.get('size_max'),
|
||||
'ccategory': 14,
|
||||
'ctype': ','.join([str(x) for x in self.getCatId(quality['identifier'])]),
|
||||
}
|
||||
|
||||
cache_key = 'ftdworld.%s.%s' % (movie['library']['identifier'], q)
|
||||
data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params), opener = self.login_opener)
|
||||
|
||||
if data:
|
||||
try:
|
||||
|
||||
html = BeautifulSoup(data)
|
||||
main_table = html.find('table', attrs = {'id':'ftdresult'})
|
||||
|
||||
if not main_table:
|
||||
return results
|
||||
|
||||
items = main_table.find_all('tr', attrs = {'class': re.compile('tcontent')})
|
||||
|
||||
for item in items:
|
||||
tds = item.find_all('td')
|
||||
nzb_id = tryInt(item.attrs['data-spot'])
|
||||
|
||||
up = item.find('img', attrs = {'src': re.compile('up.png')})
|
||||
down = item.find('img', attrs = {'src': re.compile('down.png')})
|
||||
|
||||
new = {
|
||||
'id': nzb_id,
|
||||
'type': 'nzb',
|
||||
'provider': self.getName(),
|
||||
'name': toUnicode(item.find('a', attrs = {'href': re.compile('./spotinfo')}).text.strip()),
|
||||
'age': self.calculateAge(int(time.mktime(parse(tds[2].text).timetuple()))),
|
||||
'size': 0,
|
||||
'url': self.urls['download'] % nzb_id,
|
||||
'download': self.loginDownload,
|
||||
'detail_url': self.urls['detail'] % nzb_id,
|
||||
'description': '',
|
||||
'score': (tryInt(up.attrs['title'].split(' ')[0]) * 3) - (tryInt(down.attrs['title'].split(' ')[0]) * 3),
|
||||
}
|
||||
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = False, single = True)
|
||||
|
||||
if is_correct_movie:
|
||||
new['score'] += fireEvent('score.calculate', new, movie, single = True)
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
return results
|
||||
except SyntaxError:
|
||||
log.error('Failed to parse XML response from NZBClub')
|
||||
|
||||
return results
|
||||
|
||||
def getLoginParams(self):
|
||||
return tryUrlencode({
|
||||
'userlogin': self.conf('username'),
|
||||
'passlogin': self.conf('password'),
|
||||
'submit': 'Log In',
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
from .main import Mysterbin
|
||||
|
||||
def start():
|
||||
return Mysterbin()
|
||||
|
||||
config = [{
|
||||
'name': 'mysterbin',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'Mysterbin',
|
||||
'description': 'Free provider, less accurate. See <a href="http://www.mysterbin.com/">Mysterbin</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,102 +0,0 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
|
||||
simplifyString
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Mysterbin(NZBProvider):
|
||||
|
||||
urls = {
|
||||
'search': 'https://www.mysterbin.com/advsearch?%s',
|
||||
'download': 'https://www.mysterbin.com/nzb?c=%s',
|
||||
'nfo': 'https://www.mysterbin.com/nfo?c=%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
q = '"%s" %s %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
|
||||
for ignored in Env.setting('ignored_words', 'searcher').split(','):
|
||||
if len(q) + len(ignored.strip()) > 126:
|
||||
break
|
||||
q = '%s -%s' % (q, ignored.strip())
|
||||
|
||||
params = {
|
||||
'q': q,
|
||||
'match': 'normal',
|
||||
'minSize': quality.get('size_min'),
|
||||
'maxSize': quality.get('size_max'),
|
||||
'complete': 2,
|
||||
'maxAge': Env.setting('retention', 'nzb'),
|
||||
'nopasswd': 'on',
|
||||
}
|
||||
|
||||
cache_key = 'mysterbin.%s.%s.%s' % (movie['library']['identifier'], quality.get('identifier'), q)
|
||||
data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
|
||||
if data:
|
||||
|
||||
try:
|
||||
html = BeautifulSoup(data)
|
||||
resultable = html.find('table', attrs = {'class':'t'})
|
||||
for result in resultable.find_all('tr'):
|
||||
|
||||
try:
|
||||
myster_id = result.find('input', attrs = {'class': 'check4nzb'})['value']
|
||||
|
||||
# Age
|
||||
age = ''
|
||||
for temp in result.find('td', attrs = {'class': 'cdetail'}).find_all(text = True):
|
||||
if 'days' in temp:
|
||||
age = tryInt(temp.split(' ')[0])
|
||||
break
|
||||
|
||||
# size
|
||||
size = None
|
||||
for temp in result.find('div', attrs = {'class': 'cdetail'}).find_all(text = True):
|
||||
if 'gb' in temp.lower() or 'mb' in temp.lower() or 'kb' in temp.lower():
|
||||
size = self.parseSize(temp)
|
||||
break
|
||||
|
||||
description = ''
|
||||
if result.find('a', text = 'View NFO'):
|
||||
description = toUnicode(self.getCache('mysterbin.%s' % myster_id, self.urls['nfo'] % myster_id, cache_timeout = 25920000))
|
||||
|
||||
new = {
|
||||
'id': myster_id,
|
||||
'name': ''.join(result.find('span', attrs = {'class': 'cname'}).find_all(text = True)),
|
||||
'type': 'nzb',
|
||||
'provider': self.getName(),
|
||||
'age': age,
|
||||
'size': size,
|
||||
'url': self.urls['download'] % myster_id,
|
||||
'description': description,
|
||||
'download': self.download,
|
||||
'check_nzb': False,
|
||||
}
|
||||
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = False, single = True)
|
||||
if is_correct_movie:
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
except:
|
||||
pass
|
||||
|
||||
return results
|
||||
except AttributeError:
|
||||
log.debug('No search results found.')
|
||||
|
||||
return results
|
||||
@@ -1,156 +0,0 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from dateutil.parser import parse
|
||||
import base64
|
||||
import time
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Newzbin(NZBProvider, RSS):
|
||||
|
||||
urls = {
|
||||
'download': 'https://www.newzbin2.es/api/dnzb/',
|
||||
'search': 'https://www.newzbin2.es/search/',
|
||||
}
|
||||
|
||||
format_ids = {
|
||||
2: ['scr'],
|
||||
1: ['cam'],
|
||||
4: ['tc'],
|
||||
8: ['ts'],
|
||||
1024: ['r5'],
|
||||
}
|
||||
cat_ids = [
|
||||
([262144], ['bd50']),
|
||||
([2097152], ['1080p']),
|
||||
([524288], ['720p']),
|
||||
([262144], ['brrip']),
|
||||
([2], ['dvdr']),
|
||||
]
|
||||
cat_backup_id = -1
|
||||
|
||||
http_time_between_calls = 3 # Seconds
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
format_id = self.getFormatId(type)
|
||||
cat_id = self.getCatId(type)
|
||||
|
||||
arguments = tryUrlencode({
|
||||
'searchaction': 'Search',
|
||||
'u_url_posts_only': '0',
|
||||
'u_show_passworded': '0',
|
||||
'q_url': 'imdb.com/title/' + movie['library']['identifier'],
|
||||
'sort': 'ps_totalsize',
|
||||
'order': 'asc',
|
||||
'u_post_results_amt': '100',
|
||||
'feed': 'rss',
|
||||
'category': '6',
|
||||
'ps_rb_video_format': str(cat_id),
|
||||
'ps_rb_source': str(format_id),
|
||||
'u_post_larger_than': quality.get('size_min'),
|
||||
'u_post_smaller_than': quality.get('size_max'),
|
||||
})
|
||||
|
||||
url = "%s?%s" % (self.urls['search'], arguments)
|
||||
cache_key = str('newzbin.%s.%s.%s' % (movie['library']['identifier'], str(format_id), str(cat_id)))
|
||||
|
||||
data = self.getCache(cache_key)
|
||||
if not data:
|
||||
|
||||
headers = {
|
||||
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password')))[:-1]
|
||||
}
|
||||
try:
|
||||
data = self.urlopen(url, headers = headers)
|
||||
self.setCache(cache_key, data)
|
||||
except:
|
||||
return results
|
||||
|
||||
if data:
|
||||
try:
|
||||
try:
|
||||
data = XMLTree.fromstring(data)
|
||||
nzbs = self.getElements(data, 'channel/item')
|
||||
except Exception, e:
|
||||
log.debug('%s, %s', (self.getName(), e))
|
||||
return results
|
||||
|
||||
for nzb in nzbs:
|
||||
|
||||
title = self.getTextElement(nzb, "title")
|
||||
if 'error' in title.lower(): continue
|
||||
|
||||
REPORT_NS = 'http://www.newzbin2.es/DTD/2007/feeds/report/';
|
||||
|
||||
# Add attributes to name
|
||||
try:
|
||||
for attr in nzb.find('{%s}attributes' % REPORT_NS):
|
||||
title += ' ' + attr.text
|
||||
except:
|
||||
pass
|
||||
|
||||
id = int(self.getTextElement(nzb, '{%s}id' % REPORT_NS))
|
||||
size = str(int(self.getTextElement(nzb, '{%s}size' % REPORT_NS)) / 1024 / 1024) + ' mb'
|
||||
date = str(self.getTextElement(nzb, '{%s}postdate' % REPORT_NS))
|
||||
|
||||
new = {
|
||||
'id': id,
|
||||
'type': 'nzb',
|
||||
'provider': self.getName(),
|
||||
'name': title,
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
'size': self.parseSize(size),
|
||||
'url': str(self.getTextElement(nzb, '{%s}nzb' % REPORT_NS)),
|
||||
'download': self.download,
|
||||
'detail_url': str(self.getTextElement(nzb, 'link')),
|
||||
'description': self.getTextElement(nzb, "description"),
|
||||
'check_nzb': False,
|
||||
}
|
||||
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = True, single = True)
|
||||
if is_correct_movie:
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
return results
|
||||
except SyntaxError:
|
||||
log.error('Failed to parse XML response from newzbin')
|
||||
|
||||
return results
|
||||
|
||||
def download(self, url = '', nzb_id = ''):
|
||||
try:
|
||||
log.info('Download nzb from newzbin, report id: %s ', nzb_id)
|
||||
|
||||
return self.urlopen(self.urls['download'], params = {
|
||||
'username' : self.conf('username'),
|
||||
'password' : self.conf('password'),
|
||||
'reportid' : nzb_id
|
||||
}, show_error = False)
|
||||
except Exception, e:
|
||||
log.error('Failed downloading from newzbin, check credit: %s', e)
|
||||
return False
|
||||
|
||||
def getFormatId(self, format):
|
||||
for id, quality in self.format_ids.iteritems():
|
||||
for q in quality:
|
||||
if q == format:
|
||||
return id
|
||||
|
||||
return self.cat_backup_id
|
||||
|
||||
def isEnabled(self):
|
||||
return NZBProvider.isEnabled(self) and self.conf('enabled') and self.conf('username') and self.conf('password')
|
||||
@@ -8,9 +8,12 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'subtab': 'nzb_providers',
|
||||
'name': 'newznab',
|
||||
'description': 'Enable multiple NewzNab providers such as <a href="http://nzb.su" target="_blank">NZB.su</a> and <a href="http://nzbs.org" target="_blank">nzbs.org</a>',
|
||||
'order': 10,
|
||||
'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab providers</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \
|
||||
<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>',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.helpers.variable import cleanHost, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
@@ -132,6 +132,7 @@ class Newznab(NZBProvider, RSS):
|
||||
new = {
|
||||
'id': id,
|
||||
'provider': self.getName(),
|
||||
'provider_extra': host['host'],
|
||||
'type': 'nzb',
|
||||
'name': self.getTextElement(nzb, "title"),
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
@@ -161,9 +162,9 @@ class Newznab(NZBProvider, RSS):
|
||||
|
||||
def getHosts(self):
|
||||
|
||||
uses = [x.strip() for x in str(self.conf('use')).split(',')]
|
||||
hosts = [x.strip() for x in self.conf('host').split(',')]
|
||||
api_keys = [x.strip() for x in self.conf('api_key').split(',')]
|
||||
uses = splitString(str(self.conf('use')))
|
||||
hosts = splitString(self.conf('host'))
|
||||
api_keys = splitString(self.conf('api_key'))
|
||||
|
||||
list = []
|
||||
for nr in range(len(hosts)):
|
||||
@@ -218,4 +219,5 @@ class Newznab(NZBProvider, RSS):
|
||||
return 'try_next'
|
||||
|
||||
log.error('Failed download from %s', (host, traceback.format_exc()))
|
||||
raise
|
||||
|
||||
return 'try_next'
|
||||
|
||||
@@ -8,14 +8,13 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'subtab': 'nzb_providers',
|
||||
'name': 'NZBClub',
|
||||
'description': 'Free provider, less accurate. See <a href="https://www.nzbclub.com/">NZBClub</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ log = CPLog(__name__)
|
||||
class NZBClub(NZBProvider, RSS):
|
||||
|
||||
urls = {
|
||||
'search': 'https://www.nzbclub.com/nzbfeed.aspx?%s',
|
||||
'search': 'http://www.nzbclub.com/nzbfeed.aspx?%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 4 #seconds
|
||||
@@ -29,8 +29,6 @@ class NZBClub(NZBProvider, RSS):
|
||||
return results
|
||||
|
||||
q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
|
||||
for ignored in Env.setting('ignored_words', 'searcher').split(','):
|
||||
q = '%s -%s' % (q, ignored.strip())
|
||||
|
||||
params = {
|
||||
'q': q,
|
||||
|
||||
@@ -8,13 +8,14 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'subtab': 'nzb_providers',
|
||||
'name': 'nzbindex',
|
||||
'description': 'Free provider, less accurate. See <a href="http://www.nzbindex.nl/">NZBIndex</a>',
|
||||
'description': 'Free provider, less accurate. See <a href="https://www.nzbindex.com/">NZBIndex</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -19,8 +19,8 @@ log = CPLog(__name__)
|
||||
class NzbIndex(NZBProvider, RSS):
|
||||
|
||||
urls = {
|
||||
'download': 'http://www.nzbindex.nl/download/',
|
||||
'api': 'http://www.nzbindex.nl/rss/',
|
||||
'download': 'https://www.nzbindex.com/download/',
|
||||
'api': 'https://www.nzbindex.com/rss/',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
from .main import NZBMatrix
|
||||
|
||||
def start():
|
||||
return NZBMatrix()
|
||||
|
||||
config = [{
|
||||
'name': 'nzbmatrix',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'nzbmatrix',
|
||||
'label': 'NZBMatrix',
|
||||
'description': 'See <a href="https://nzbmatrix.com/">NZBMatrix</a>',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'default': '',
|
||||
'label': 'Api Key',
|
||||
},
|
||||
{
|
||||
'name': 'english_only',
|
||||
'default': 1,
|
||||
'type': 'bool',
|
||||
'label': 'English only',
|
||||
'description': 'Only search for English spoken movies on NZBMatrix',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,108 +0,0 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
import time
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class NZBMatrix(NZBProvider, RSS):
|
||||
|
||||
urls = {
|
||||
'download': 'https://api.nzbmatrix.com/v1.1/download.php?id=%s',
|
||||
'detail': 'https://nzbmatrix.com/nzb-details.php?id=%s&hit=1',
|
||||
'search': 'https://rss.nzbmatrix.com/rss.php',
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
([50], ['bd50']),
|
||||
([42, 53], ['720p', '1080p']),
|
||||
([2, 9], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
|
||||
([54], ['brrip']),
|
||||
([1], ['dvdr']),
|
||||
]
|
||||
cat_backup_id = 2
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
cat_ids = ','.join(['%s' % x for x in self.getCatId(quality.get('identifier'))])
|
||||
|
||||
arguments = tryUrlencode({
|
||||
'term': movie['library']['identifier'],
|
||||
'subcat': cat_ids,
|
||||
'username': self.conf('username'),
|
||||
'apikey': self.conf('api_key'),
|
||||
'searchin': 'weblink',
|
||||
'maxage': Env.setting('retention', section = 'nzb'),
|
||||
'english': self.conf('english_only'),
|
||||
})
|
||||
url = "%s?%s" % (self.urls['search'], arguments)
|
||||
|
||||
cache_key = 'nzbmatrix.%s.%s' % (movie['library'].get('identifier'), cat_ids)
|
||||
|
||||
data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
|
||||
if data:
|
||||
try:
|
||||
try:
|
||||
data = XMLTree.fromstring(data)
|
||||
nzbs = self.getElements(data, 'channel/item')
|
||||
except Exception, e:
|
||||
log.debug('%s, %s', (self.getName(), e))
|
||||
return results
|
||||
|
||||
for nzb in nzbs:
|
||||
|
||||
title = self.getTextElement(nzb, "title")
|
||||
if 'error' in title.lower(): continue
|
||||
|
||||
id = int(self.getTextElement(nzb, "link").split('&')[0].partition('id=')[2])
|
||||
size = self.getTextElement(nzb, "description").split('<br /><b>')[2].split('> ')[1]
|
||||
date = str(self.getTextElement(nzb, "description").split('<br /><b>')[3].partition('Added:</b> ')[2])
|
||||
|
||||
new = {
|
||||
'id': id,
|
||||
'type': 'nzb',
|
||||
'provider': self.getName(),
|
||||
'name': title,
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
'size': self.parseSize(size),
|
||||
'url': self.urls['download'] % id + self.getApiExt(),
|
||||
'download': self.download,
|
||||
'detail_url': self.urls['detail'] % id,
|
||||
'description': self.getTextElement(nzb, "description"),
|
||||
'check_nzb': True,
|
||||
}
|
||||
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = True, single = True)
|
||||
|
||||
if is_correct_movie:
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
return results
|
||||
except SyntaxError:
|
||||
log.error('Failed to parse XML response from NZBMatrix.com')
|
||||
|
||||
return results
|
||||
|
||||
def download(self, url = '', nzb_id = ''):
|
||||
return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()})
|
||||
|
||||
def getApiExt(self):
|
||||
return '&username=%s&apikey=%s' % (self.conf('username'), self.conf('api_key'))
|
||||
|
||||
def isEnabled(self):
|
||||
return NZBProvider.isEnabled(self) and self.conf('username') and self.conf('api_key')
|
||||
@@ -8,7 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'subtab': 'nzb_providers',
|
||||
'name': 'nzbsrus',
|
||||
'label': 'Nzbsrus',
|
||||
'description': 'See <a href="https://www.nzbsrus.com/">NZBsRus</a>',
|
||||
|
||||
@@ -97,8 +97,5 @@ class Nzbsrus(NZBProvider, RSS):
|
||||
|
||||
return results
|
||||
|
||||
def download(self, url = '', nzb_id = ''):
|
||||
return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()})
|
||||
|
||||
def getApiExt(self):
|
||||
return '/%s/' % (self.conf('userid'))
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
from couchpotato.core.helpers.variable import getImdb, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import YarrProvider
|
||||
import cookielib
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -11,7 +8,6 @@ log = CPLog(__name__)
|
||||
class TorrentProvider(YarrProvider):
|
||||
|
||||
type = 'torrent'
|
||||
login_opener = None
|
||||
|
||||
def imdbMatch(self, url, imdbId):
|
||||
if getImdb(url) == imdbId:
|
||||
@@ -28,30 +24,3 @@ class TorrentProvider(YarrProvider):
|
||||
return getImdb(data) == imdbId
|
||||
|
||||
return False
|
||||
|
||||
def login(self):
|
||||
|
||||
try:
|
||||
cookiejar = cookielib.CookieJar()
|
||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
|
||||
urllib2.install_opener(opener)
|
||||
f = opener.open(self.urls['login'], self.getLoginParams())
|
||||
f.read()
|
||||
f.close()
|
||||
self.login_opener = opener
|
||||
return True
|
||||
except:
|
||||
log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return False
|
||||
|
||||
def loginDownload(self, url = '', nzb_id = ''):
|
||||
try:
|
||||
if not self.login_opener and not self.login():
|
||||
log.error('Failed downloading from %s', self.getName())
|
||||
return self.urlopen(url, opener = self.login_opener)
|
||||
except:
|
||||
log.error('Failed downloading from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return ''
|
||||
|
||||
@@ -8,14 +8,15 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'subtab': 'torrent_providers',
|
||||
'name': 'KickAssTorrents',
|
||||
'description': 'See <a href="https://kat.ph/">KickAssTorrents</a>',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
'default': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
import re
|
||||
@@ -12,9 +13,9 @@ log = CPLog(__name__)
|
||||
class KickAssTorrents(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://kat.ph/',
|
||||
'detail': 'http://kat.ph/%s',
|
||||
'search': 'http://kat.ph/i%s/',
|
||||
'test': 'https://kat.ph/',
|
||||
'detail': 'https://kat.ph/%s',
|
||||
'search': 'https://kat.ph/%s-i%s/',
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
@@ -35,8 +36,10 @@ class KickAssTorrents(TorrentProvider):
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
title = simplifyString(getTitle(movie['library'])).replace(' ', '-')
|
||||
|
||||
cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
data = self.getCache(cache_key, self.urls['search'] % (movie['library']['identifier'].replace('tt', '')))
|
||||
data = self.getCache(cache_key, self.urls['search'] % (title, movie['library']['identifier'].replace('tt', '')))
|
||||
if data:
|
||||
|
||||
cat_ids = self.getCatId(quality['identifier'])
|
||||
@@ -91,6 +94,7 @@ class KickAssTorrents(TorrentProvider):
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = True, single = True)
|
||||
|
||||
if is_correct_movie:
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
@@ -5,32 +5,38 @@ def start():
|
||||
|
||||
config = [{
|
||||
'name': 'passthepopcorn',
|
||||
'groups': [{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'PassThePopcorn',
|
||||
'description': 'See <a href="http://passthepopcorn.me">PassThePopcorn.me</a>',
|
||||
'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',
|
||||
}
|
||||
],
|
||||
}]
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'torrent_providers',
|
||||
'name': 'PassThePopcorn',
|
||||
'description': 'See <a href="https://passthepopcorn.me">PassThePopcorn.me</a>',
|
||||
'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': '',
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}]
|
||||
|
||||
@@ -21,7 +21,7 @@ class PassThePopcorn(TorrentProvider):
|
||||
'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/login.php',
|
||||
'login': 'https://tls.passthepopcorn.me/ajax.php?action=login',
|
||||
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d'
|
||||
}
|
||||
|
||||
@@ -249,6 +249,7 @@ class PassThePopcorn(TorrentProvider):
|
||||
return tryUrlencode({
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'passkey': self.conf('passkey'),
|
||||
'keeplogged': '1',
|
||||
'login': 'Login'
|
||||
})
|
||||
|
||||
@@ -8,14 +8,14 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'subtab': 'torrent_providers',
|
||||
'name': 'PublicHD',
|
||||
'description': 'Public Torrent site with only HD content. See <a href="http://publichd.eu/">PublicHD</a>',
|
||||
'description': 'Public Torrent site with only HD content. See <a href="https://publichd.eu/">PublicHD</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
'default': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -14,9 +14,9 @@ log = CPLog(__name__)
|
||||
class PublicHD(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://publichd.eu',
|
||||
'detail': 'http://publichd.eu/index.php?page=torrent-details&id=%s',
|
||||
'search': 'http://publichd.eu/index.php',
|
||||
'test': 'https://publichd.eu',
|
||||
'detail': 'https://publichd.eu/index.php?page=torrent-details&id=%s',
|
||||
'search': 'https://publichd.eu/index.php',
|
||||
}
|
||||
http_time_between_calls = 0
|
||||
|
||||
@@ -24,7 +24,7 @@ class PublicHD(TorrentProvider):
|
||||
|
||||
results = []
|
||||
|
||||
if self.isDisabled() or quality['hd'] != True:
|
||||
if self.isDisabled() or not quality.get('hd', False):
|
||||
return results
|
||||
|
||||
params = tryUrlencode({
|
||||
|
||||
@@ -8,7 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'subtab': 'torrent_providers',
|
||||
'name': 'SceneAccess',
|
||||
'description': 'See <a href="https://sceneaccess.eu/">SceneAccess</a>',
|
||||
'options': [
|
||||
|
||||
@@ -8,9 +8,9 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'subtab': 'torrent_providers',
|
||||
'name': 'SceneHD',
|
||||
'description': 'See <a href="http://scenehd.org">SceneHD</a>',
|
||||
'description': 'See <a href="https://scenehd.org">SceneHD</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -12,11 +12,11 @@ log = CPLog(__name__)
|
||||
class SceneHD(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://scenehd.org/',
|
||||
'login' : 'http://scenehd.org/takelogin.php',
|
||||
'detail': 'http://scenehd.org/details.php?id=%s',
|
||||
'search': 'http://scenehd.org/browse.php?ajax',
|
||||
'download': 'http://scenehd.org/download.php?id=%s',
|
||||
'test': 'https://scenehd.org/',
|
||||
'login' : 'https://scenehd.org/takelogin.php',
|
||||
'detail': 'https://scenehd.org/details.php?id=%s',
|
||||
'search': 'https://scenehd.org/browse.php?ajax',
|
||||
'download': 'https://scenehd.org/download.php?id=%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
|
||||
@@ -5,23 +5,26 @@ def start():
|
||||
|
||||
config = [{
|
||||
'name': 'thepiratebay',
|
||||
'groups': [{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'ThePirateBay',
|
||||
'description': 'The world\'s largest bittorrent tracker. See <a href="http://fucktimkuik.org/">ThePirateBay</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False
|
||||
},
|
||||
{
|
||||
'name': 'domain',
|
||||
'advanced': True,
|
||||
'label': 'Proxy server',
|
||||
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
|
||||
}
|
||||
],
|
||||
}]
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': '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': True
|
||||
},
|
||||
{
|
||||
'name': 'domain',
|
||||
'advanced': True,
|
||||
'label': 'Proxy server',
|
||||
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user