Compare commits
159 Commits
build/2.0.
...
build/2.0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a314cfbc4 | ||
|
|
5941d0bf77 | ||
|
|
d326c1c25c | ||
|
|
7e6234298d | ||
|
|
5cf4b8b4d3 | ||
|
|
6e56072250 | ||
|
|
917c5552a4 | ||
|
|
73c5b90232 | ||
|
|
fd53ba0637 | ||
|
|
0ef3906b3d | ||
|
|
5ab0d7a97b | ||
|
|
dbbbbb2f84 | ||
|
|
1bfe948a45 | ||
|
|
0d2dcff7f0 | ||
|
|
d4da206f93 | ||
|
|
439cda8b63 | ||
|
|
bbe8362b08 | ||
|
|
985a168724 | ||
|
|
5e6aea97f7 | ||
|
|
6c7c4c7aba | ||
|
|
e2f59f5ff4 | ||
|
|
b225980ce7 | ||
|
|
b8e86b378f | ||
|
|
031a186d71 | ||
|
|
3c04eed218 | ||
|
|
17e01689d9 | ||
|
|
173c6194ed | ||
|
|
95c2e992b0 | ||
|
|
4bffb299af | ||
|
|
a2c4119508 | ||
|
|
4e9472f8ee | ||
|
|
f7911fe9f3 | ||
|
|
8ffa6a8392 | ||
|
|
382d49f895 | ||
|
|
570b79a67e | ||
|
|
e7aafc406f | ||
|
|
2dcc1e096e | ||
|
|
9f0746a668 | ||
|
|
d9c437bd7f | ||
|
|
7079647f87 | ||
|
|
65570ba479 | ||
|
|
a57ba9026d | ||
|
|
63246256ee | ||
|
|
1ac0dc3bbf | ||
|
|
bcd23ad10c | ||
|
|
342d31b48a | ||
|
|
ea7904ed9a | ||
|
|
ca37c2f018 | ||
|
|
5aa2146614 | ||
|
|
0fd49a2c67 | ||
|
|
b680d84cba | ||
|
|
898e6f487d | ||
|
|
96472a9a8f | ||
|
|
27252561e2 | ||
|
|
24b341005e | ||
|
|
749cf550ec | ||
|
|
650177803b | ||
|
|
bb7b4cbbed | ||
|
|
6618c3927c | ||
|
|
003db92c9b | ||
|
|
b2b396bf17 | ||
|
|
f1a1db8d5b | ||
|
|
f515cd2477 | ||
|
|
65bb1bec27 | ||
|
|
cc84532824 | ||
|
|
5530fbf792 | ||
|
|
5658a85f61 | ||
|
|
0c5206f01b | ||
|
|
4bffce637e | ||
|
|
9f2941a45c | ||
|
|
f452106bfc | ||
|
|
da3055be30 | ||
|
|
f9b65e7216 | ||
|
|
07e2c56095 | ||
|
|
9a6cfe3a21 | ||
|
|
802338a934 | ||
|
|
f0a3358561 | ||
|
|
1c4c69211b | ||
|
|
c9e732651f | ||
|
|
7849e7170d | ||
|
|
087894eb4e | ||
|
|
4b58b40226 | ||
|
|
77d57f5a09 | ||
|
|
618845a021 | ||
|
|
3aabcbf8f1 | ||
|
|
929c6fe3f9 | ||
|
|
c852949591 | ||
|
|
e36c8ec3ab | ||
|
|
afea12c7c0 | ||
|
|
c29a8b47d6 | ||
|
|
fdd0826b4f | ||
|
|
81b7ebaf51 | ||
|
|
eafc3db74d | ||
|
|
3464435a5c | ||
|
|
9f19902221 | ||
|
|
2ed72c9098 | ||
|
|
723f720280 | ||
|
|
daaa2154e5 | ||
|
|
95c5db2d17 | ||
|
|
e53a9ed30a | ||
|
|
2b49a4b5d6 | ||
|
|
4224a25e54 | ||
|
|
3635da1f59 | ||
|
|
71cca6b87f | ||
|
|
68c0496f8e | ||
|
|
6dc3c8d69d | ||
|
|
3ecc826629 | ||
|
|
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 | ||
|
|
32fe3796e4 | ||
|
|
359d1aaafa | ||
|
|
fb5d336351 | ||
|
|
eb30dff986 | ||
|
|
9312336962 | ||
|
|
ade4338ea6 | ||
|
|
55b20324c0 | ||
|
|
c0fb28301d | ||
|
|
f9c2503f81 | ||
|
|
5b4cdf05b1 | ||
|
|
6f25a6bdfd | ||
|
|
23427e95f7 | ||
|
|
90a09e573b | ||
|
|
e1d7440b9d |
@@ -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())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from esky.util import appdir_from_executable #@UnresolvedImport
|
||||
from threading import Thread
|
||||
from version import VERSION
|
||||
from wx.lib.softwareupdate import SoftwareUpdate
|
||||
import os
|
||||
import sys
|
||||
@@ -165,7 +166,7 @@ class CouchPotatoApp(wx.App, SoftwareUpdate):
|
||||
def OnInit(self):
|
||||
|
||||
# Updater
|
||||
base_url = 'http://couchpota.to/updates/'
|
||||
base_url = 'http://couchpota.to/updates/%s/' % VERSION
|
||||
self.InitUpdates(base_url, base_url + 'changelog.html',
|
||||
icon = wx.Icon('icon.png'))
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -37,3 +39,4 @@ Linux (ubuntu / debian):
|
||||
* 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/`
|
||||
|
||||
@@ -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,9 @@ 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
|
||||
if not Env.get('desktop'):
|
||||
self.signalHandler()
|
||||
|
||||
def md5Password(self, value):
|
||||
return md5(value.encode(Env.get('encoding'))) if value else ''
|
||||
@@ -66,7 +70,7 @@ class Core(Plugin):
|
||||
|
||||
def available(self):
|
||||
return jsonified({
|
||||
'succes': True
|
||||
'success': True
|
||||
})
|
||||
|
||||
def shutdown(self):
|
||||
@@ -98,7 +102,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 +174,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)
|
||||
|
||||
@@ -8,11 +8,9 @@ from couchpotato.environment import Env
|
||||
from datetime import datetime
|
||||
from dateutil.parser import parse
|
||||
from git.repository import LocalRepository
|
||||
import atexit
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tarfile
|
||||
import time
|
||||
import traceback
|
||||
|
||||
@@ -90,17 +90,18 @@ var UpdaterBase = new Class({
|
||||
doUpdate: function(){
|
||||
var self = this;
|
||||
|
||||
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
|
||||
Api.request('updater.update', {
|
||||
'onComplete': function(json){
|
||||
if(json.success){
|
||||
if(json.success)
|
||||
self.updating();
|
||||
}
|
||||
else
|
||||
App.unBlockPage()
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updating: function(){
|
||||
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
|
||||
App.checkAvailable.delay(500, App, [1000, function(){
|
||||
window.location.reload();
|
||||
}]);
|
||||
|
||||
@@ -14,10 +14,23 @@ class Downloader(Plugin):
|
||||
|
||||
torrent_sources = [
|
||||
'http://torrage.com/torrent/%s.torrent',
|
||||
'http://torrage.ws/torrent/%s.torrent',
|
||||
'http://torcache.net/torrent/%s.torrent',
|
||||
]
|
||||
|
||||
torrent_trackers = [
|
||||
'http://tracker.publicbt.com/announce',
|
||||
'udp://tracker.istole.it:80/announce',
|
||||
'udp://fr33domtracker.h33t.com:3310/announce',
|
||||
'http://tracker.istole.it/announce',
|
||||
'http://tracker.ccc.de/announce',
|
||||
'udp://tracker.publicbt.com:80/announce',
|
||||
'udp://tracker.ccc.de:80/announce',
|
||||
'http://exodus.desync.com/announce',
|
||||
'http://exodus.desync.com:6969/announce',
|
||||
'http://tracker.publichd.eu/announce',
|
||||
'http://tracker.openbittorrent.com/announce',
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
addEvent('download', self.download)
|
||||
addEvent('download.status', self.getAllDownloadStatus)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -28,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
|
||||
|
||||
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)
|
||||
@@ -12,7 +12,7 @@ def runHandler(name, handler, *args, **kwargs):
|
||||
return handler(*args, **kwargs)
|
||||
except:
|
||||
from couchpotato.environment import Env
|
||||
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all()))
|
||||
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all() if not Env.get('dev') else ''))
|
||||
|
||||
def addEvent(name, handler, priority = 100):
|
||||
|
||||
@@ -63,11 +63,21 @@ def fireEvent(name, *args, **kwargs):
|
||||
except: pass
|
||||
|
||||
e = events[name]
|
||||
if not options['in_order']: e.lock.acquire()
|
||||
|
||||
# Lock this event
|
||||
e.lock.acquire()
|
||||
|
||||
e.asynchronous = False
|
||||
e.in_order = options['in_order']
|
||||
|
||||
# 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)
|
||||
if not options['in_order']: e.lock.release()
|
||||
|
||||
# Release lock for this event
|
||||
e.lock.release()
|
||||
|
||||
if options['single'] and not options['merge']:
|
||||
results = None
|
||||
@@ -95,14 +105,14 @@ def fireEvent(name, *args, **kwargs):
|
||||
# Merge
|
||||
if options['merge'] and len(results) > 0:
|
||||
# Dict
|
||||
if type(results[0]) == dict:
|
||||
if isinstance(results[0], dict):
|
||||
merged = {}
|
||||
for result in results:
|
||||
merged = mergeDicts(merged, result)
|
||||
|
||||
results = merged
|
||||
# Lists
|
||||
elif type(results[0]) == list:
|
||||
elif isinstance(results[0], list):
|
||||
merged = []
|
||||
for result in results:
|
||||
merged += result
|
||||
|
||||
@@ -68,7 +68,7 @@ def tryUrlencode(s):
|
||||
|
||||
return new[1:]
|
||||
else:
|
||||
for letter in toUnicode(s):
|
||||
for letter in ss(s):
|
||||
try:
|
||||
new += quote_plus(letter)
|
||||
except:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString
|
||||
from couchpotato.core.logger import CPLog
|
||||
import hashlib
|
||||
import os.path
|
||||
@@ -9,15 +10,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 +104,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 +112,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
|
||||
|
||||
@@ -132,6 +154,16 @@ def getTitle(library_dict):
|
||||
log.error('Could not get title for library item: %s', library_dict)
|
||||
return None
|
||||
|
||||
def possibleTitles(raw_title):
|
||||
|
||||
titles = []
|
||||
|
||||
titles.append(toSafeString(raw_title).lower())
|
||||
titles.append(raw_title.lower())
|
||||
titles.append(simplifyString(raw_title))
|
||||
|
||||
return list(set(titles))
|
||||
|
||||
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -3,6 +3,7 @@ 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__)
|
||||
|
||||
@@ -22,9 +23,12 @@ class XBMC(Notification):
|
||||
('VideoLibrary.Scan', {}),
|
||||
])
|
||||
|
||||
for result in response:
|
||||
if result['result'] == "OK":
|
||||
successful += 1
|
||||
try:
|
||||
for result in response:
|
||||
if result['result'] == "OK":
|
||||
successful += 1
|
||||
except:
|
||||
log.error('Failed parsing results: %s', traceback.format_exc())
|
||||
|
||||
return successful == len(hosts) * 2
|
||||
|
||||
@@ -50,10 +54,14 @@ class XBMC(Notification):
|
||||
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
|
||||
headers['Authorization'] = 'Basic %s' % base64string
|
||||
|
||||
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))
|
||||
try:
|
||||
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
|
||||
return response
|
||||
except:
|
||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||
return []
|
||||
|
||||
|
||||
@@ -1,9 +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, \
|
||||
toSafeString
|
||||
from couchpotato.core.helpers.variable import getExt
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString
|
||||
from couchpotato.core.helpers.variable import getExt, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.templating import render_template_string
|
||||
@@ -35,7 +34,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):
|
||||
@@ -114,8 +113,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]
|
||||
@@ -219,7 +221,7 @@ class Plugin(object):
|
||||
|
||||
|
||||
def getCache(self, cache_key, url = None, **kwargs):
|
||||
cache_key = simplifyString(cache_key)
|
||||
cache_key = md5(ss(cache_key))
|
||||
cache = Env.get('cache').get(cache_key)
|
||||
if cache:
|
||||
if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
|
||||
@@ -239,9 +241,11 @@ class Plugin(object):
|
||||
self.setCache(cache_key, data, timeout = cache_timeout)
|
||||
return data
|
||||
except:
|
||||
if not kwargs.get('show_error'):
|
||||
if not kwargs.get('show_error', True):
|
||||
raise
|
||||
|
||||
return ''
|
||||
|
||||
def setCache(self, cache_key, value, timeout = 300):
|
||||
log.debug('Setting cache %s', cache_key)
|
||||
Env.get('cache').set(cache_key, value, timeout)
|
||||
|
||||
@@ -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
|
||||
@@ -65,15 +66,7 @@ class FileBrowser(Plugin):
|
||||
def view(self):
|
||||
|
||||
path = getParam('path', '/')
|
||||
|
||||
# Set proper home dir for some systems
|
||||
try:
|
||||
import pwd
|
||||
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
|
||||
except:
|
||||
pass
|
||||
|
||||
home = os.path.expanduser('~')
|
||||
home = getUserDir()
|
||||
|
||||
if not path:
|
||||
path = home
|
||||
|
||||
@@ -66,10 +66,12 @@ class FileManager(Plugin):
|
||||
time.sleep(3)
|
||||
log.debug('Cleaning up unused files')
|
||||
|
||||
python_cache = Env.get('cache')._path
|
||||
try:
|
||||
db = get_session()
|
||||
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
|
||||
for filename in walk_files:
|
||||
if root == python_cache: continue
|
||||
file_path = os.path.join(root, filename)
|
||||
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
|
||||
if not f:
|
||||
|
||||
@@ -115,12 +115,35 @@ class Manage(Plugin):
|
||||
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
|
||||
if len(release.get('files', [])) == 0:
|
||||
fireEvent('release.delete', release['id'])
|
||||
else:
|
||||
for release_file in release.get('files', []):
|
||||
# Remove release not available anymore
|
||||
if not os.path.isfile(ss(release_file['path'])):
|
||||
fireEvent('release.clean', release['id'])
|
||||
break
|
||||
|
||||
# Check if there are duplicate releases (different quality) use the last one, delete the rest
|
||||
if len(done_movie.get('releases', [])) > 1:
|
||||
used_files = {}
|
||||
for release in done_movie.get('releases', []):
|
||||
|
||||
for release_file in release.get('files', []):
|
||||
already_used = used_files.get(release_file['path'])
|
||||
|
||||
if already_used:
|
||||
print already_used, release['id']
|
||||
if already_used < release['id']:
|
||||
fireEvent('release.delete', release['id'], single = True) # delete this one
|
||||
else:
|
||||
fireEvent('release.delete', already_used, single = True) # delete previous one
|
||||
break
|
||||
else:
|
||||
used_files[release_file['path']] = release.get('id')
|
||||
del used_files
|
||||
|
||||
Env.prop('manage.last_update', time.time())
|
||||
except:
|
||||
@@ -153,7 +176,7 @@ class Manage(Plugin):
|
||||
'to_go': total_found,
|
||||
}
|
||||
|
||||
if group['library']:
|
||||
if group['library'] and group['library'].get('identifier'):
|
||||
identifier = group['library'].get('identifier')
|
||||
added_identifiers.append(identifier)
|
||||
|
||||
@@ -187,5 +210,5 @@ class Manage(Plugin):
|
||||
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
|
||||
|
||||
for group in groups.itervalues():
|
||||
if group['library']:
|
||||
if group['library'] and group['library'].get('identifier'):
|
||||
fireEvent('release.add', group = group)
|
||||
|
||||
@@ -300,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': {
|
||||
|
||||
@@ -479,29 +479,32 @@ var ReleaseAction = new Class({
|
||||
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_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
|
||||
)
|
||||
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,28 +285,31 @@ 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.hasClass('set')){
|
||||
if(!self.options_el.hasClass('set')){
|
||||
|
||||
if(self.info.in_library){
|
||||
var in_library = [];
|
||||
@@ -332,10 +318,10 @@ 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,
|
||||
@@ -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')
|
||||
|
||||
@@ -7,6 +7,7 @@ from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Quality, Profile, ProfileType
|
||||
from sqlalchemy.sql.expression import or_
|
||||
import os.path
|
||||
import re
|
||||
import time
|
||||
@@ -18,15 +19,15 @@ class QualityPlugin(Plugin):
|
||||
|
||||
qualities = [
|
||||
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
|
||||
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
||||
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
||||
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
||||
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], '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']
|
||||
@@ -76,7 +77,7 @@ class QualityPlugin(Plugin):
|
||||
db = get_session()
|
||||
quality_dict = {}
|
||||
|
||||
quality = db.query(Quality).filter_by(identifier = identifier).first()
|
||||
quality = db.query(Quality).filter(or_(Quality.identifier == identifier, Quality.id == identifier)).first()
|
||||
if quality:
|
||||
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
|
||||
|
||||
@@ -198,9 +199,14 @@ class QualityPlugin(Plugin):
|
||||
|
||||
for quality in self.all():
|
||||
|
||||
# Last check on resolution only
|
||||
if quality.get('width', 480) == extra.get('resolution_width', 0):
|
||||
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0)))
|
||||
# Check width resolution, range 20
|
||||
if (quality.get('width', 720) - 20) <= extra.get('resolution_width', 0) <= (quality.get('width', 720) + 20):
|
||||
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 720), extra.get('resolution_width', 0)))
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
# Check height resolution, range 20
|
||||
if (quality.get('height', 480) - 20) <= extra.get('resolution_height', 0) <= (quality.get('height', 480) + 20):
|
||||
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height', 480), extra.get('resolution_height', 0)))
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
if 480 <= extra.get('resolution_width', 0) <= 720:
|
||||
|
||||
@@ -133,6 +133,9 @@ class Release(Plugin):
|
||||
db.delete(release_file)
|
||||
db.commit()
|
||||
|
||||
if len(rel.files) == 0:
|
||||
self.delete(id)
|
||||
|
||||
return True
|
||||
|
||||
return 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',
|
||||
@@ -124,13 +133,6 @@ config = [{
|
||||
'type': 'choice',
|
||||
'options': rename_options
|
||||
},
|
||||
{
|
||||
'name': 'trailer_name',
|
||||
'label': 'Trailer naming',
|
||||
'default': '<filename>-trailer.<ext>',
|
||||
'type': 'choice',
|
||||
'options': rename_options
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -34,8 +34,11 @@ class Renamer(Plugin):
|
||||
|
||||
addEvent('app.load', self.scan)
|
||||
|
||||
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
|
||||
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = 2)
|
||||
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):
|
||||
|
||||
@@ -310,7 +313,10 @@ class Renamer(Plugin):
|
||||
elif release.status_id is snatched_status.get('id'):
|
||||
if release.quality.id is group['meta_data']['quality']['id']:
|
||||
log.debug('Marking release as downloaded')
|
||||
release.status_id = downloaded_status.get('id')
|
||||
try:
|
||||
release.status_id = downloaded_status.get('id')
|
||||
except Exception, e:
|
||||
log.error('Failed marking release as finished: %s %s', (e, traceback.format_exc()))
|
||||
db.commit()
|
||||
|
||||
# Remove leftover files
|
||||
@@ -334,6 +340,7 @@ class Renamer(Plugin):
|
||||
|
||||
log.info('Removing "%s"', src)
|
||||
try:
|
||||
src = ss(src)
|
||||
if os.path.isfile(src):
|
||||
os.remove(src)
|
||||
|
||||
@@ -347,7 +354,10 @@ class Renamer(Plugin):
|
||||
|
||||
# Delete leftover folder from older releases
|
||||
for delete_folder in delete_folders:
|
||||
self.deleteEmptyFolder(delete_folder, show_error = False)
|
||||
try:
|
||||
self.deleteEmptyFolder(delete_folder, show_error = False)
|
||||
except Exception, e:
|
||||
log.error('Failed to delete folder: %s %s', (e, traceback.format_exc()))
|
||||
|
||||
# Rename all files marked
|
||||
group['renamed_files'] = []
|
||||
@@ -383,7 +393,10 @@ class Renamer(Plugin):
|
||||
|
||||
# Notify on download, search for trailers etc
|
||||
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
|
||||
fireEvent('renamer.after', message = download_message, group = group, in_order = True)
|
||||
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():
|
||||
@@ -485,6 +498,7 @@ class Renamer(Plugin):
|
||||
return string.replace(' ', ' ').replace(' .', '.')
|
||||
|
||||
def deleteEmptyFolder(self, folder, show_error = True):
|
||||
folder = ss(folder)
|
||||
|
||||
loge = log.error if show_error else log.debug
|
||||
for root, dirs, files in os.walk(folder):
|
||||
|
||||
@@ -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'],
|
||||
@@ -53,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'],
|
||||
@@ -331,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 {}
|
||||
@@ -381,7 +395,7 @@ class Scanner(Plugin):
|
||||
|
||||
return processed_movies
|
||||
|
||||
def getMetaData(self, group):
|
||||
def getMetaData(self, group, folder = ''):
|
||||
|
||||
data = {}
|
||||
files = list(group['files']['movie'])
|
||||
@@ -407,10 +421,10 @@ class Scanner(Plugin):
|
||||
if not data['quality']:
|
||||
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
|
||||
|
||||
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 else 'SD'
|
||||
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') 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
|
||||
@@ -419,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),
|
||||
}
|
||||
@@ -738,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 ''
|
||||
|
||||
@@ -752,7 +775,7 @@ class Scanner(Plugin):
|
||||
return None
|
||||
|
||||
def findYear(self, text):
|
||||
matches = re.search('(?P<year>[12]{1}[0-9]{3})', text)
|
||||
matches = re.search('(?P<year>19[0-9]{2}|20[0-9]{2})', text)
|
||||
if matches:
|
||||
return matches.group('year')
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ class Score(Plugin):
|
||||
score += sizeScore(nzb['size'])
|
||||
|
||||
# Torrents only
|
||||
if nzb.get('seeds'):
|
||||
if nzb.get('seeders'):
|
||||
try:
|
||||
score += nzb.get('seeds') / 5
|
||||
score += nzb.get('seeders') / 5
|
||||
score += nzb.get('leechers') / 10
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -116,7 +116,7 @@ def sizeScore(size):
|
||||
|
||||
|
||||
def providerScore(provider):
|
||||
if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']:
|
||||
if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']:
|
||||
return 20
|
||||
|
||||
if provider in ['Newznab']:
|
||||
|
||||
@@ -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')],
|
||||
},
|
||||
|
||||
@@ -3,7 +3,8 @@ from couchpotato.api import addApiView
|
||||
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, \
|
||||
possibleTitles
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||
@@ -204,6 +205,10 @@ class Searcher(Plugin):
|
||||
|
||||
|
||||
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
|
||||
@@ -239,7 +244,7 @@ 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)
|
||||
@@ -293,49 +298,54 @@ class Searcher(Plugin):
|
||||
imdb_results = kwargs.get('imdb_results', False)
|
||||
retention = Env.setting('retention', section = 'nzb')
|
||||
|
||||
if nzb.get('seeds') is None and 0 < 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('seeders') 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(',')]
|
||||
blacklisted = list(set(nzb_words) & set(ignored_words))
|
||||
ignored_words = splitString(self.conf('ignored_words').lower())
|
||||
blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words))
|
||||
if self.conf('ignored_words') and blacklisted:
|
||||
log.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']
|
||||
for p_tag in pron_tags:
|
||||
if p_tag in nzb_words and p_tag not in movie_words:
|
||||
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
||||
return False
|
||||
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
|
||||
if pron_words:
|
||||
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
||||
return False
|
||||
|
||||
#qualities = fireEvent('quality.all', single = True)
|
||||
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
|
||||
|
||||
# Contains lower quality string
|
||||
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
|
||||
|
||||
|
||||
@@ -353,20 +363,21 @@ class Searcher(Plugin):
|
||||
return True
|
||||
|
||||
# Check if nzb contains imdb link
|
||||
if self.checkIMDB([nzb['description']], movie['library']['identifier']):
|
||||
if self.checkIMDB([nzb.get('description', '')], movie['library']['identifier']):
|
||||
return True
|
||||
|
||||
for movie_title in movie['library']['titles']:
|
||||
movie_words = re.split('\W+', simplifyString(movie_title['title']))
|
||||
for raw_title in movie['library']['titles']:
|
||||
for movie_title in possibleTitles(raw_title['title']):
|
||||
movie_words = re.split('\W+', simplifyString(movie_title))
|
||||
|
||||
if self.correctName(nzb['name'], movie_title['title']):
|
||||
# if no IMDB link, at least check year range 1
|
||||
if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1):
|
||||
return True
|
||||
if self.correctName(nzb['name'], movie_title):
|
||||
# if no IMDB link, at least check year range 1
|
||||
if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1):
|
||||
return True
|
||||
|
||||
# if no IMDB link, at least check year
|
||||
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
|
||||
return True
|
||||
# if no IMDB link, at least check year
|
||||
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
|
||||
return True
|
||||
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
|
||||
return False
|
||||
@@ -389,9 +400,14 @@ 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
|
||||
log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size))
|
||||
found['dvdr'] = True
|
||||
@@ -430,12 +446,16 @@ class Searcher(Plugin):
|
||||
def correctName(self, check_name, movie_name):
|
||||
|
||||
check_names = [check_name]
|
||||
try:
|
||||
check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
|
||||
except:
|
||||
pass
|
||||
|
||||
for check_name in check_names:
|
||||
# Match names between "
|
||||
try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
|
||||
except: pass
|
||||
|
||||
# Match longest name between []
|
||||
try: check_names.append(max(check_name.split('['), key = len))
|
||||
except: pass
|
||||
|
||||
for check_name in list(set(check_names)):
|
||||
check_movie = fireEvent('scanner.name_year', check_name, single = True)
|
||||
|
||||
try:
|
||||
|
||||
@@ -22,6 +22,7 @@ class StatusPlugin(Plugin):
|
||||
'failed': 'Failed',
|
||||
'deleted': 'Deleted',
|
||||
'ignored': 'Ignored',
|
||||
'available': 'Available',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -24,6 +24,13 @@ config = [{
|
||||
'type': 'dropdown',
|
||||
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
|
||||
},
|
||||
{
|
||||
'name': 'name',
|
||||
'label': 'Naming',
|
||||
'default': '<filename>-trailer',
|
||||
'advanced': True,
|
||||
'description': 'Use <filename> to use above settings.'
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -19,10 +19,11 @@ class Trailer(Plugin):
|
||||
trailers = fireEvent('trailer.search', group = group, merge = True)
|
||||
if not trailers or trailers == []:
|
||||
log.info('No trailers found for: %s', getTitle(group['library']))
|
||||
return
|
||||
return False
|
||||
|
||||
for trailer in trailers.get(self.conf('quality'), []):
|
||||
destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer))
|
||||
filename = self.conf('name').replace('<filename>', group['filename']) + ('.%s' % getExt(trailer))
|
||||
destination = os.path.join(group['destination_dir'], filename)
|
||||
if not os.path.isfile(destination):
|
||||
fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
|
||||
else:
|
||||
@@ -33,5 +34,5 @@ class Trailer(Plugin):
|
||||
# Download first and break
|
||||
break
|
||||
|
||||
def getRootName(self, data = {}):
|
||||
return os.path.join(data['destination_dir'], data['filename'])
|
||||
return True
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -9,7 +9,7 @@ Page.Wizard = new Class({
|
||||
headers: {
|
||||
'welcome': {
|
||||
'title': 'Welcome to the new CouchPotato',
|
||||
'description': 'To get started, fill in each of the following settings as much as your can. <br />Maybe first start with importing your movies from the previous CouchPotato',
|
||||
'description': 'To get started, fill in each of the following settings as much as you can. <br />Maybe first start with importing your movies from the previous CouchPotato',
|
||||
'content': new Element('div', {
|
||||
'styles': {
|
||||
'margin': '0 0 0 30px'
|
||||
@@ -37,7 +37,7 @@ Page.Wizard = new Class({
|
||||
},
|
||||
'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 Blackhole.'
|
||||
'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?',
|
||||
|
||||
@@ -1,9 +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
|
||||
import traceback
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -19,30 +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
|
||||
|
||||
try:
|
||||
cache_key = 'imdb.rss.%s' % md5(rss_url)
|
||||
cache_key = 'imdb.rss.%s' % md5(url)
|
||||
rss_data = self.getCache(cache_key, url)
|
||||
imdbs = getImdb(rss_data, multiple = True)
|
||||
|
||||
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:
|
||||
imdb = getImdb(self.getTextElement(movie, "link"))
|
||||
for imdb in imdbs:
|
||||
movies.append(imdb)
|
||||
|
||||
except:
|
||||
log.error('Failed loading IMDB watchlist: %s %s', (rss_url, traceback.format_exc()))
|
||||
log.error('Failed loading IMDB watchlist: %s %s', (url, traceback.format_exc()))
|
||||
|
||||
return movies
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.variable import tryFloat
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
|
||||
possibleTitles, getTitle
|
||||
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 json
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
import urllib2
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -47,22 +53,71 @@ class YarrProvider(Provider):
|
||||
sizeMb = ['mb', 'mib']
|
||||
sizeKb = ['kb', 'kib']
|
||||
|
||||
login_opener = None
|
||||
|
||||
def __init__(self):
|
||||
addEvent('provider.belongs_to', self.belongsTo)
|
||||
|
||||
addEvent('%s.search' % self.type, self.search)
|
||||
addEvent('yarr.search', self.search)
|
||||
|
||||
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()))
|
||||
|
||||
def feed(self):
|
||||
return []
|
||||
return 'try_next'
|
||||
|
||||
def search(self, movie, quality):
|
||||
return []
|
||||
|
||||
if self.isDisabled():
|
||||
return []
|
||||
|
||||
# Login if needed
|
||||
if self.urls.get('login') and (not self.login_opener and not self.login()):
|
||||
log.error('Failed to login to: %s', self.getName())
|
||||
return []
|
||||
|
||||
# Create result container
|
||||
imdb_result = hasattr(self, '_search')
|
||||
results = ResultList(self, movie, quality, imdb_result = imdb_result)
|
||||
|
||||
# Do search based on imdb id
|
||||
if imdb_result:
|
||||
self._search(movie, quality, results)
|
||||
# Search possible titles
|
||||
else:
|
||||
for title in possibleTitles(getTitle(movie['library'])):
|
||||
self._searchOnTitle(title, movie, quality, results)
|
||||
|
||||
return results
|
||||
|
||||
def belongsTo(self, url, provider = None, host = None):
|
||||
try:
|
||||
@@ -110,5 +165,93 @@ class YarrProvider(Provider):
|
||||
|
||||
return [self.cat_backup_id]
|
||||
|
||||
def found(self, new):
|
||||
log.info('Found: score(%(score)s) on %(provider)s: %(name)s', new)
|
||||
def getJsonData(self, url, **kwargs):
|
||||
|
||||
data = self.getCache(md5(url), url, **kwargs)
|
||||
|
||||
if data:
|
||||
try:
|
||||
return json.loads(data)
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return []
|
||||
|
||||
def getRSSData(self, url, **kwargs):
|
||||
|
||||
data = self.getCache(md5(url), url, **kwargs)
|
||||
|
||||
if data:
|
||||
try:
|
||||
data = XMLTree.fromstring(data)
|
||||
return self.getElements(data, 'channel/item')
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return []
|
||||
|
||||
def getHTMLData(self, url, **kwargs):
|
||||
return self.getCache(md5(url), url, **kwargs)
|
||||
|
||||
|
||||
class ResultList(list):
|
||||
|
||||
result_ids = None
|
||||
provider = None
|
||||
movie = None
|
||||
quality = None
|
||||
|
||||
def __init__(self, provider, movie, quality, **kwargs):
|
||||
|
||||
self.result_ids = []
|
||||
self.provider = provider
|
||||
self.movie = movie
|
||||
self.quality = quality
|
||||
self.kwargs = kwargs
|
||||
|
||||
super(ResultList, self).__init__()
|
||||
|
||||
def extend(self, results):
|
||||
for r in results:
|
||||
self.append(r)
|
||||
|
||||
def append(self, result):
|
||||
|
||||
new_result = self.fillResult(result)
|
||||
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new_result, movie = self.movie, quality = self.quality,
|
||||
imdb_results = self.kwargs.get('imdb_results', False), single = True)
|
||||
|
||||
if is_correct_movie and new_result['id'] not in self.result_ids:
|
||||
new_result['score'] += fireEvent('score.calculate', new_result, self.movie, single = True)
|
||||
|
||||
self.found(new_result)
|
||||
self.result_ids.append(result['id'])
|
||||
|
||||
super(ResultList, self).append(new_result)
|
||||
|
||||
def fillResult(self, result):
|
||||
|
||||
defaults = {
|
||||
'id': 0,
|
||||
'type': self.provider.type,
|
||||
'provider': self.provider.getName(),
|
||||
'download': self.provider.download,
|
||||
'url': '',
|
||||
'name': '',
|
||||
'age': 0,
|
||||
'size': 0,
|
||||
'description': '',
|
||||
'score': 0
|
||||
}
|
||||
|
||||
return mergeDicts(defaults, result)
|
||||
|
||||
def found(self, new_result):
|
||||
if not new_result.get('provider_extra'):
|
||||
new_result['provider_extra'] = ''
|
||||
else:
|
||||
new_result['provider_extra'] = ', %s' % new_result['provider_extra']
|
||||
|
||||
log.info('Found: score(%(score)s) on %(provider)s%(provider_extra)s: %(name)s', new_result)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.movie.base import MovieProvider
|
||||
from libs.themoviedb import tmdb
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -61,7 +62,12 @@ class TheMovieDb(MovieProvider):
|
||||
|
||||
if not results:
|
||||
log.debug('Searching for movie: %s', q)
|
||||
raw = tmdb.search(search_string)
|
||||
|
||||
raw = None
|
||||
try:
|
||||
raw = tmdb.search(search_string)
|
||||
except:
|
||||
log.error('Failed searching TMDB for "%s": %s', (search_string, traceback.format_exc()))
|
||||
|
||||
results = []
|
||||
if raw:
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
from .main import Mysterbin
|
||||
from .main import BinSearch
|
||||
|
||||
def start():
|
||||
return Mysterbin()
|
||||
return BinSearch()
|
||||
|
||||
config = [{
|
||||
'name': 'mysterbin',
|
||||
'name': 'binsearch',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'nzb_providers',
|
||||
'name': 'Mysterbin',
|
||||
'description': 'Free provider, less accurate. See <a href="https://www.mysterbin.com/">Mysterbin</a>',
|
||||
'name': 'binsearch',
|
||||
'description': 'Free provider, less accurate. See <a href="https://www.binsearch.info/">BinSearch</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
],
|
||||
},
|
||||
99
couchpotato/core/providers/nzb/binsearch/main.py
Normal file
99
couchpotato/core/providers/nzb/binsearch/main.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
import re
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class BinSearch(NZBProvider):
|
||||
|
||||
urls = {
|
||||
'download': 'https://www.binsearch.info/fcgi/nzb.fcgi?q=%s',
|
||||
'detail': 'https://www.binsearch.info%s',
|
||||
'search': 'https://www.binsearch.info/index.php?%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 4 # Seconds
|
||||
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
q = '%s %s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
arguments = tryUrlencode({
|
||||
'q': q,
|
||||
'm': 'n',
|
||||
'max': 250,
|
||||
'adv_age': Env.setting('retention', 'nzb'),
|
||||
'adv_sort': 'date',
|
||||
'adv_col': 'on',
|
||||
'adv_nfo': 'on',
|
||||
'minsize': quality.get('size_min'),
|
||||
'maxsize': quality.get('size_max'),
|
||||
})
|
||||
|
||||
data = self.getHTMLData(self.urls['search'] % arguments)
|
||||
|
||||
if data:
|
||||
try:
|
||||
|
||||
html = BeautifulSoup(data)
|
||||
main_table = html.find('table', attrs = {'id':'r2'})
|
||||
|
||||
if not main_table:
|
||||
return
|
||||
|
||||
items = main_table.find_all('tr')
|
||||
|
||||
for row in items:
|
||||
title = row.find('span', attrs = {'class':'s'})
|
||||
|
||||
if not title: continue
|
||||
|
||||
nzb_id = row.find('input', attrs = {'type':'checkbox'})['name']
|
||||
info = row.find('span', attrs = {'class':'d'})
|
||||
size_match = re.search('size:.(?P<size>[0-9\.]+.[GMB]+)', info.text)
|
||||
|
||||
def extra_check(item):
|
||||
parts = re.search('available:.(?P<parts>\d+)./.(?P<total>\d+)', info.text)
|
||||
total = tryInt(parts.group('total'))
|
||||
parts = tryInt(parts.group('parts'))
|
||||
|
||||
if (total / parts) < 0.95 or ((total / parts) >= 0.95 and not 'par2' in info.text.lower()):
|
||||
log.info2('Wrong: \'%s\', not complete: %s out of %s', (item['name'], parts, total))
|
||||
return False
|
||||
|
||||
if 'requires password' in info.text.lower():
|
||||
log.info2('Wrong: \'%s\', passworded', (item['name']))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'name': title.text,
|
||||
'age': tryInt(re.search('(?P<size>\d+d)', row.find_all('td')[-1:][0].text).group('size')[:-1]),
|
||||
'size': self.parseSize(size_match.group('size')),
|
||||
'url': self.urls['download'] % nzb_id,
|
||||
'detail_url': self.urls['detail'] % info.find('a')['href'],
|
||||
'extra_check': extra_check
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parse HTML response from BinSearch: %s', traceback.format_exc())
|
||||
|
||||
def download(self, url = '', nzb_id = ''):
|
||||
|
||||
params = {'action': 'nzb'}
|
||||
params[nzb_id] = 'on'
|
||||
|
||||
try:
|
||||
return self.urlopen(url, params = params, show_error = False)
|
||||
except:
|
||||
log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return 'try_next'
|
||||
|
||||
@@ -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': 'nzb_providers',
|
||||
'name': 'newzbin',
|
||||
'description': 'See <a href="https://www.newzbin2.es/">Newzbin</a>',
|
||||
'wizard': True,
|
||||
'name': 'FTDWorld',
|
||||
'description': 'Free provider, less accurate. See <a href="http://ftdworld.net">FTDWorld</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
84
couchpotato/core/providers/nzb/ftdworld/main.py
Normal file
84
couchpotato/core/providers/nzb/ftdworld/main.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.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 = 3 #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 _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
q = '"%s" %s' % (title, movie['library']['year'])
|
||||
|
||||
params = tryUrlencode({
|
||||
'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'])]),
|
||||
})
|
||||
|
||||
data = self.getHTMLData(self.urls['search'] % params, opener = self.login_opener)
|
||||
|
||||
if data:
|
||||
try:
|
||||
|
||||
html = BeautifulSoup(data)
|
||||
main_table = html.find('table', attrs = {'id':'ftdresult'})
|
||||
|
||||
if not main_table:
|
||||
return
|
||||
|
||||
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')})
|
||||
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'name': toUnicode(item.find('a', attrs = {'href': re.compile('./spotinfo')}).text.strip()),
|
||||
'age': self.calculateAge(int(time.mktime(parse(tds[2].text).timetuple()))),
|
||||
'url': self.urls['download'] % nzb_id,
|
||||
'download': self.loginDownload,
|
||||
'detail_url': self.urls['detail'] % nzb_id,
|
||||
'score': (tryInt(up.attrs['title'].split(' ')[0]) * 3) - (tryInt(down.attrs['title'].split(' ')[0]) * 3) if up else 0,
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parse HTML response from FTDWorld')
|
||||
|
||||
def getLoginParams(self):
|
||||
return tryUrlencode({
|
||||
'userlogin': self.conf('username'),
|
||||
'passlogin': self.conf('password'),
|
||||
'submit': 'Log In',
|
||||
})
|
||||
@@ -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')
|
||||
@@ -11,7 +11,9 @@ config = [{
|
||||
'subtab': 'nzb_providers',
|
||||
'name': 'newznab',
|
||||
'order': 10,
|
||||
'description': 'Enable multiple NewzNab providers such as <a href="https://nzb.su" target="_blank">NZB.su</a> and <a href="https://nzbs.org" target="_blank">nzbs.org</a>',
|
||||
'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,8 +1,8 @@
|
||||
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, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import ResultList
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
@@ -10,7 +10,6 @@ from urllib2 import HTTPError
|
||||
from urlparse import urlparse
|
||||
import time
|
||||
import traceback
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -25,139 +24,59 @@ class Newznab(NZBProvider, RSS):
|
||||
|
||||
limits_reached = {}
|
||||
|
||||
cat_ids = [
|
||||
([2010], ['dvdr']),
|
||||
([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
|
||||
([2040], ['720p', '1080p']),
|
||||
([2050], ['bd50']),
|
||||
]
|
||||
cat_backup_id = 2000
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
def feed(self):
|
||||
|
||||
hosts = self.getHosts()
|
||||
|
||||
results = []
|
||||
for host in hosts:
|
||||
result = self.singleFeed(host)
|
||||
|
||||
if result:
|
||||
results.extend(result)
|
||||
|
||||
return results
|
||||
|
||||
def singleFeed(self, host):
|
||||
|
||||
results = []
|
||||
if self.isDisabled(host):
|
||||
return results
|
||||
|
||||
arguments = tryUrlencode({
|
||||
't': self.cat_backup_id,
|
||||
'r': host['api_key'],
|
||||
'i': 58,
|
||||
})
|
||||
url = "%s?%s" % (cleanHost(host['host']) + 'rss', arguments)
|
||||
cache_key = 'newznab.%s.feed.%s' % (host['host'], arguments)
|
||||
|
||||
results = self.createItems(url, cache_key, host, for_feed = True)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
hosts = self.getHosts()
|
||||
|
||||
results = []
|
||||
for host in hosts:
|
||||
result = self.singleSearch(host, movie, quality)
|
||||
results = ResultList(self, movie, quality, imdb_result = True)
|
||||
|
||||
if result:
|
||||
results.extend(result)
|
||||
for host in hosts:
|
||||
if self.isDisabled(host):
|
||||
continue
|
||||
|
||||
self._searchOnHost(host, movie, quality, results)
|
||||
|
||||
return results
|
||||
|
||||
def singleSearch(self, host, movie, quality):
|
||||
def _searchOnHost(self, host, movie, quality, results):
|
||||
|
||||
results = []
|
||||
if self.isDisabled(host):
|
||||
return results
|
||||
|
||||
cat_id = self.getCatId(quality['identifier'])
|
||||
arguments = tryUrlencode({
|
||||
'imdbid': movie['library']['identifier'].replace('tt', ''),
|
||||
'cat': cat_id[0],
|
||||
'apikey': host['api_key'],
|
||||
'extended': 1
|
||||
})
|
||||
url = "%s&%s" % (self.getUrl(host['host'], self.urls['search']), arguments)
|
||||
url = '%s&%s' % (self.getUrl(host['host'], self.urls['search']), arguments)
|
||||
|
||||
cache_key = 'newznab.%s.%s.%s' % (host['host'], movie['library']['identifier'], cat_id[0])
|
||||
nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
|
||||
|
||||
results = self.createItems(url, cache_key, host, movie = movie, quality = quality)
|
||||
for nzb in nzbs:
|
||||
|
||||
return results
|
||||
date = None
|
||||
for item in nzb:
|
||||
if item.attrib.get('name') == 'usenetdate':
|
||||
date = item.attrib.get('value')
|
||||
break
|
||||
|
||||
def createItems(self, url, cache_key, host, movie = None, quality = None, for_feed = False):
|
||||
results = []
|
||||
if not date:
|
||||
date = self.getTextElement(nzb, 'pubDate')
|
||||
|
||||
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
|
||||
nzb_id = self.getTextElement(nzb, 'guid').split('/')[-1:].pop()
|
||||
name = self.getTextElement(nzb, 'title')
|
||||
|
||||
results = []
|
||||
for nzb in nzbs:
|
||||
if not name:
|
||||
continue
|
||||
|
||||
date = ''
|
||||
size = 0
|
||||
for item in nzb:
|
||||
if item.attrib.get('name') == 'size':
|
||||
size = item.attrib.get('value')
|
||||
elif item.attrib.get('name') == 'usenetdate':
|
||||
date = item.attrib.get('value')
|
||||
|
||||
if date is '': log.debug('Date not parsed properly or not available for %s: %s', (host['host'], self.getTextElement(nzb, "title")))
|
||||
if size is 0: log.debug('Size not parsed properly or not available for %s: %s', (host['host'], self.getTextElement(nzb, "title")))
|
||||
|
||||
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
|
||||
new = {
|
||||
'id': id,
|
||||
'provider': self.getName(),
|
||||
'type': 'nzb',
|
||||
'name': self.getTextElement(nzb, "title"),
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
'size': int(size) / 1024 / 1024,
|
||||
'url': (self.getUrl(host['host'], self.urls['download']) % id) + self.getApiExt(host),
|
||||
'download': self.download,
|
||||
'detail_url': '%sdetails/%s' % (cleanHost(host['host']), id),
|
||||
'content': self.getTextElement(nzb, "description"),
|
||||
}
|
||||
|
||||
if not for_feed:
|
||||
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)
|
||||
else:
|
||||
results.append(new)
|
||||
|
||||
return results
|
||||
except SyntaxError:
|
||||
log.error('Failed to parse XML response from Newznab: %s', host)
|
||||
return results
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'provider_extra': host['host'],
|
||||
'name': self.getTextElement(nzb, 'title'),
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
||||
'url': (self.getUrl(host['host'], self.urls['download']) % nzb_id) + self.getApiExt(host),
|
||||
'detail_url': '%sdetails/%s' % (cleanHost(host['host']), nzb_id),
|
||||
'content': self.getTextElement(nzb, 'description'),
|
||||
})
|
||||
|
||||
def getHosts(self):
|
||||
|
||||
@@ -218,4 +137,5 @@ class Newznab(NZBProvider, RSS):
|
||||
return 'try_next'
|
||||
|
||||
log.error('Failed download from %s', (host, traceback.format_exc()))
|
||||
raise
|
||||
|
||||
return 'try_next'
|
||||
|
||||
@@ -15,7 +15,6 @@ config = [{
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
|
||||
simplifyString
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
import time
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -17,87 +13,53 @@ 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
|
||||
|
||||
def search(self, movie, quality):
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
results = []
|
||||
if self.isDisabled():
|
||||
return results
|
||||
q = '"%s %s" %s' % (title, movie['library']['year'], quality.get('identifier'))
|
||||
|
||||
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 = {
|
||||
params = tryUrlencode({
|
||||
'q': q,
|
||||
'ig': '1',
|
||||
'rpp': 200,
|
||||
'st': 1,
|
||||
'sp': 1,
|
||||
'ns': 1,
|
||||
}
|
||||
})
|
||||
|
||||
cache_key = 'nzbclub.%s.%s.%s' % (movie['library']['identifier'], quality.get('identifier'), q)
|
||||
data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
|
||||
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
|
||||
nzbs = self.getRSSData(self.urls['search'] % params)
|
||||
|
||||
for nzb in nzbs:
|
||||
for nzb in nzbs:
|
||||
|
||||
nzbclub_id = tryInt(self.getTextElement(nzb, "link").split('/nzb_view/')[1].split('/')[0])
|
||||
enclosure = self.getElement(nzb, "enclosure").attrib
|
||||
size = enclosure['length']
|
||||
date = self.getTextElement(nzb, "pubDate")
|
||||
nzbclub_id = tryInt(self.getTextElement(nzb, "link").split('/nzb_view/')[1].split('/')[0])
|
||||
enclosure = self.getElement(nzb, "enclosure").attrib
|
||||
size = enclosure['length']
|
||||
date = self.getTextElement(nzb, "pubDate")
|
||||
|
||||
def extra_check(item):
|
||||
full_description = self.getCache('nzbclub.%s' % nzbclub_id, item['detail_url'], cache_timeout = 25920000)
|
||||
def extra_check(item):
|
||||
full_description = self.getCache('nzbclub.%s' % nzbclub_id, item['detail_url'], cache_timeout = 25920000)
|
||||
|
||||
for ignored in ['ARCHIVE inside ARCHIVE', 'Incomplete', 'repair impossible']:
|
||||
if ignored in full_description:
|
||||
log.info('Wrong: Seems to be passworded or corrupted files: %s', new['name'])
|
||||
return False
|
||||
for ignored in ['ARCHIVE inside ARCHIVE', 'Incomplete', 'repair impossible']:
|
||||
if ignored in full_description:
|
||||
log.info('Wrong: Seems to be passworded or corrupted files: %s', item['name'])
|
||||
return False
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
new = {
|
||||
'id': nzbclub_id,
|
||||
'type': 'nzb',
|
||||
'provider': self.getName(),
|
||||
'name': toUnicode(self.getTextElement(nzb, "title")),
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
'size': tryInt(size) / 1024 / 1024,
|
||||
'url': enclosure['url'].replace(' ', '_'),
|
||||
'download': self.download,
|
||||
'detail_url': self.getTextElement(nzb, "link"),
|
||||
'description': '',
|
||||
'get_more_info': self.getMoreInfo,
|
||||
'extra_check': extra_check
|
||||
}
|
||||
|
||||
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
|
||||
results.append({
|
||||
'id': nzbclub_id,
|
||||
'name': toUnicode(self.getTextElement(nzb, "title")),
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
'size': tryInt(size) / 1024 / 1024,
|
||||
'url': enclosure['url'].replace(' ', '_'),
|
||||
'detail_url': self.getTextElement(nzb, "link"),
|
||||
'get_more_info': self.getMoreInfo,
|
||||
'extra_check': extra_check
|
||||
})
|
||||
|
||||
def getMoreInfo(self, item):
|
||||
full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||
|
||||
@@ -15,6 +15,7 @@ config = [{
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
|
||||
simplifyString
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -20,18 +16,14 @@ class NzbIndex(NZBProvider, RSS):
|
||||
|
||||
urls = {
|
||||
'download': 'https://www.nzbindex.com/download/',
|
||||
'api': 'https://www.nzbindex.com/rss/',
|
||||
'search': 'https://www.nzbindex.com/rss/?%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
def search(self, movie, quality):
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
results = []
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
|
||||
q = '"%s" %s %s' % (title, movie['library']['year'], quality.get('identifier'))
|
||||
arguments = tryUrlencode({
|
||||
'q': q,
|
||||
'age': Env.setting('retention', 'nzb'),
|
||||
@@ -43,68 +35,37 @@ class NzbIndex(NZBProvider, RSS):
|
||||
'more': 1,
|
||||
'complete': 1,
|
||||
})
|
||||
url = "%s?%s" % (self.urls['api'], arguments)
|
||||
|
||||
cache_key = 'nzbindex.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
nzbs = self.getRSSData(self.urls['search'] % arguments)
|
||||
|
||||
for nzb in nzbs:
|
||||
|
||||
enclosure = self.getElement(nzb, 'enclosure').attrib
|
||||
nzbindex_id = int(self.getTextElement(nzb, "link").split('/')[4])
|
||||
|
||||
data = self.getCache(cache_key, url)
|
||||
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:
|
||||
|
||||
enclosure = self.getElement(nzb, 'enclosure').attrib
|
||||
|
||||
nzbindex_id = int(self.getTextElement(nzb, "link").split('/')[4])
|
||||
|
||||
try:
|
||||
description = self.getTextElement(nzb, "description")
|
||||
except:
|
||||
description = ''
|
||||
|
||||
def extra_check(new):
|
||||
if '#c20000' in new['description'].lower():
|
||||
log.info('Wrong: Seems to be passworded: %s', new['name'])
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
new = {
|
||||
'id': nzbindex_id,
|
||||
'type': 'nzb',
|
||||
'provider': self.getName(),
|
||||
'download': self.download,
|
||||
'name': self.getTextElement(nzb, "title"),
|
||||
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
|
||||
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
||||
'url': enclosure['url'],
|
||||
'detail_url': enclosure['url'].replace('/download/', '/release/'),
|
||||
'description': description,
|
||||
'get_more_info': self.getMoreInfo,
|
||||
'extra_check': extra_check,
|
||||
'check_nzb': True,
|
||||
}
|
||||
|
||||
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
|
||||
description = self.getTextElement(nzb, "description")
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
description = ''
|
||||
|
||||
return results
|
||||
def extra_check(item):
|
||||
if '#c20000' in item['description'].lower():
|
||||
log.info('Wrong: Seems to be passworded: %s', item['name'])
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
results.append({
|
||||
'id': nzbindex_id,
|
||||
'name': self.getTextElement(nzb, "title"),
|
||||
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
|
||||
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
||||
'url': enclosure['url'],
|
||||
'detail_url': enclosure['url'].replace('/download/', '/release/'),
|
||||
'description': description,
|
||||
'get_more_info': self.getMoreInfo,
|
||||
'extra_check': extra_check,
|
||||
})
|
||||
|
||||
def getMoreInfo(self, item):
|
||||
try:
|
||||
@@ -116,5 +77,3 @@ class NzbIndex(NZBProvider, RSS):
|
||||
except:
|
||||
pass
|
||||
|
||||
def isEnabled(self):
|
||||
return NZBProvider.isEnabled(self) and self.conf('enabled')
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
from .main import NZBMatrix
|
||||
|
||||
def start():
|
||||
return NZBMatrix()
|
||||
|
||||
config = [{
|
||||
'name': 'nzbmatrix',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'nzb_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')
|
||||
@@ -1,11 +1,9 @@
|
||||
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
|
||||
import time
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -23,15 +21,9 @@ class Nzbsrus(NZBProvider, RSS):
|
||||
]
|
||||
cat_backup_id = 240
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
|
||||
if self.isDisabled():
|
||||
return results
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
cat_id_string = '&'.join(['c%s=1' % x for x in self.getCatId(quality.get('identifier'))])
|
||||
|
||||
arguments = tryUrlencode({
|
||||
'searchtext': 'imdb:' + movie['library']['identifier'][2:],
|
||||
'uid': self.conf('userid'),
|
||||
@@ -42,63 +34,29 @@ class Nzbsrus(NZBProvider, RSS):
|
||||
|
||||
# check for english_only
|
||||
if self.conf('english_only'):
|
||||
arguments += "&lang0=1&lang3=1&lang1=1"
|
||||
arguments += '&lang0=1&lang3=1&lang1=1'
|
||||
|
||||
url = "%s&%s&%s" % (self.urls['search'], arguments , cat_id_string)
|
||||
url = '%s&%s&%s' % (self.urls['search'], arguments , cat_id_string)
|
||||
nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
|
||||
|
||||
cache_key = 'nzbsrus_1.%s.%s' % (movie['library'].get('identifier'), cat_id_string)
|
||||
single_cat = True
|
||||
for nzb in nzbs:
|
||||
|
||||
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, 'results/result')
|
||||
except Exception, e:
|
||||
log.debug('%s, %s', (self.getName(), e))
|
||||
return results
|
||||
title = self.getTextElement(nzb, 'name')
|
||||
if 'error' in title.lower(): continue
|
||||
|
||||
for nzb in nzbs:
|
||||
nzb_id = self.getTextElement(nzb, 'id')
|
||||
size = int(round(int(self.getTextElement(nzb, 'size')) / 1048576))
|
||||
age = int(round((time.time() - int(self.getTextElement(nzb, 'postdate'))) / 86400))
|
||||
|
||||
title = self.getTextElement(nzb, "name")
|
||||
if 'error' in title.lower(): continue
|
||||
|
||||
id = self.getTextElement(nzb, "id")
|
||||
size = int(round(int(self.getTextElement(nzb, "size")) / 1048576))
|
||||
age = int(round((time.time() - int(self.getTextElement(nzb, "postdate"))) / 86400))
|
||||
|
||||
new = {
|
||||
'id': id,
|
||||
'type': 'nzb',
|
||||
'provider': self.getName(),
|
||||
'name': title,
|
||||
'age': age,
|
||||
'size': size,
|
||||
'url': self.urls['download'] % id + self.getApiExt() + self.getTextElement(nzb, "key"),
|
||||
'download': self.download,
|
||||
'detail_url': self.urls['detail'] % id,
|
||||
'description': self.getTextElement(nzb, "addtext"),
|
||||
'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 Nzbsrus.com')
|
||||
|
||||
return results
|
||||
|
||||
def download(self, url = '', nzb_id = ''):
|
||||
return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()})
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'name': title,
|
||||
'age': age,
|
||||
'size': size,
|
||||
'url': self.urls['download'] % id + self.getApiExt() + self.getTextElement(nzb, 'key'),
|
||||
'detail_url': self.urls['detail'] % nzb_id,
|
||||
'description': self.getTextElement(nzb, 'addtext'),
|
||||
})
|
||||
|
||||
def getApiExt(self):
|
||||
return '/%s/' % (self.conf('userid'))
|
||||
|
||||
23
couchpotato/core/providers/nzb/nzbx/__init__.py
Normal file
23
couchpotato/core/providers/nzb/nzbx/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from .main import Nzbx
|
||||
|
||||
def start():
|
||||
return Nzbx()
|
||||
|
||||
config = [{
|
||||
'name': 'nzbx',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'nzb_providers',
|
||||
'name': 'nzbX',
|
||||
'description': 'Free provider, less accurate. See <a href="https://www.nzbx.co/">nzbX</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
37
couchpotato/core/providers/nzb/nzbx/main.py
Normal file
37
couchpotato/core/providers/nzb/nzbx/main.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Nzbx(NZBProvider):
|
||||
|
||||
urls = {
|
||||
'search': 'https://nzbx.co/api/search?%s',
|
||||
'details': 'https://nzbx.co/api/details?guid=%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
# Get nbzs
|
||||
arguments = tryUrlencode({
|
||||
'q': movie['library']['identifier'].replace('tt', ''),
|
||||
'sf': quality.get('size_min'),
|
||||
})
|
||||
nzbs = self.getJsonData(self.urls['search'] % arguments)
|
||||
|
||||
for nzb in nzbs:
|
||||
|
||||
results.append({
|
||||
'id': nzb['guid'],
|
||||
'url': nzb['nzb'],
|
||||
'detail_url': self.urls['details'] % nzb['guid'],
|
||||
'name': nzb['name'],
|
||||
'age': self.calculateAge(int(nzb['postdate'])),
|
||||
'size': tryInt(nzb['size']) / 1024 / 1024,
|
||||
'score': 5 if nzb['votes']['upvotes'] > nzb['votes']['downvotes'] else 0
|
||||
})
|
||||
31
couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py
Normal file
31
couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from .main import OMGWTFNZBs
|
||||
|
||||
def start():
|
||||
return OMGWTFNZBs()
|
||||
|
||||
config = [{
|
||||
'name': 'omgwtfnzbs',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'nzb_providers',
|
||||
'name': 'OMGWTFNZBs',
|
||||
'description': 'See <a href="http://www.omgwtfnzbs.com/">OMGWTFNZBs</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'Api Key',
|
||||
'default': '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
61
couchpotato/core/providers/nzb/omgwtfnzbs/main.py
Normal file
61
couchpotato/core/providers/nzb/omgwtfnzbs/main.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from dateutil.parser import parse
|
||||
from urlparse import urlparse, parse_qs
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class OMGWTFNZBs(NZBProvider, RSS):
|
||||
|
||||
urls = {
|
||||
'search': 'http://rss.omgwtfnzbs.com/rss-search.php?%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
|
||||
cat_ids = [
|
||||
([15], ['dvdrip']),
|
||||
([15, 16], ['brrip']),
|
||||
([16], ['720p', '1080p', 'bd50']),
|
||||
([17], ['dvdr']),
|
||||
]
|
||||
cat_backup_id = 'movie'
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
if quality['identifier'] in fireEvent('quality.pre_releases', single = True):
|
||||
return []
|
||||
|
||||
return super(OMGWTFNZBs, self).search(movie, quality)
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
q = '%s %s' % (title, movie['library']['year'])
|
||||
params = tryUrlencode({
|
||||
'search': q,
|
||||
'catid': ','.join([str(x) for x in self.getCatId(quality['identifier'])]),
|
||||
'user': self.conf('username', default = ''),
|
||||
'api': self.conf('api_key', default = ''),
|
||||
})
|
||||
|
||||
nzbs = self.getRSSData(self.urls['search'] % params)
|
||||
|
||||
for nzb in nzbs:
|
||||
|
||||
enclosure = self.getElement(nzb, 'enclosure').attrib
|
||||
|
||||
results.append({
|
||||
'id': parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0],
|
||||
'name': toUnicode(self.getTextElement(nzb, 'title')),
|
||||
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, 'pubDate')).timetuple()))),
|
||||
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
||||
'url': enclosure['url'],
|
||||
'detail_url': self.getTextElement(nzb, 'link'),
|
||||
'description': self.getTextElement(nzb, 'description')
|
||||
})
|
||||
@@ -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:
|
||||
@@ -29,29 +25,8 @@ class TorrentProvider(YarrProvider):
|
||||
|
||||
return False
|
||||
|
||||
def login(self):
|
||||
class TorrentMagnetProvider(TorrentProvider):
|
||||
|
||||
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()))
|
||||
type = 'torrent_magnet'
|
||||
|
||||
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 ''
|
||||
download = None
|
||||
|
||||
@@ -16,7 +16,7 @@ config = [{
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
'default': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
from couchpotato.core.providers.torrent.base import TorrentMagnetProvider
|
||||
import re
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class KickAssTorrents(TorrentProvider):
|
||||
class KickAssTorrents(TorrentMagnetProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://kat.ph/',
|
||||
@@ -30,16 +28,10 @@ class KickAssTorrents(TorrentProvider):
|
||||
http_time_between_calls = 1 #seconds
|
||||
cat_backup_id = None
|
||||
|
||||
def search(self, movie, quality):
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
results = []
|
||||
if self.isDisabled():
|
||||
return results
|
||||
data = self.getHTMLData(self.urls['search'] % ('m', movie['library']['identifier'].replace('tt', '')))
|
||||
|
||||
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'] % (title, movie['library']['identifier'].replace('tt', '')))
|
||||
if data:
|
||||
|
||||
cat_ids = self.getCatId(quality['identifier'])
|
||||
@@ -53,61 +45,42 @@ class KickAssTorrents(TorrentProvider):
|
||||
continue
|
||||
|
||||
try:
|
||||
for temp in result.find_all('tr'):
|
||||
if temp['class'] is 'firstr' or not temp.get('id'):
|
||||
continue
|
||||
|
||||
try:
|
||||
for temp in result.find_all('tr'):
|
||||
if temp['class'] is 'firstr' or not temp.get('id'):
|
||||
continue
|
||||
new = {}
|
||||
|
||||
new = {
|
||||
'type': 'torrent_magnet',
|
||||
'check_nzb': False,
|
||||
'description': '',
|
||||
'provider': self.getName(),
|
||||
'score': 0,
|
||||
}
|
||||
nr = 0
|
||||
for td in temp.find_all('td'):
|
||||
column_name = table_order[nr]
|
||||
if column_name:
|
||||
|
||||
nr = 0
|
||||
for td in temp.find_all('td'):
|
||||
column_name = table_order[nr]
|
||||
if column_name:
|
||||
if column_name is 'name':
|
||||
link = td.find('div', {'class': 'torrentname'}).find_all('a')[1]
|
||||
new['id'] = temp.get('id')[-8:]
|
||||
new['name'] = link.text
|
||||
new['url'] = td.find('a', 'imagnet')['href']
|
||||
new['detail_url'] = self.urls['detail'] % link['href'][1:]
|
||||
new['score'] = 20 if td.find('a', 'iverif') else 0
|
||||
elif column_name is 'size':
|
||||
new['size'] = self.parseSize(td.text)
|
||||
elif column_name is 'age':
|
||||
new['age'] = self.ageToDays(td.text)
|
||||
elif column_name is 'seeds':
|
||||
new['seeders'] = tryInt(td.text)
|
||||
elif column_name is 'leechers':
|
||||
new['leechers'] = tryInt(td.text)
|
||||
|
||||
if column_name is 'name':
|
||||
link = td.find('div', {'class': 'torrentname'}).find_all('a')[1]
|
||||
new['id'] = temp.get('id')[-8:]
|
||||
new['name'] = link.text
|
||||
new['url'] = td.find('a', 'imagnet')['href']
|
||||
new['detail_url'] = self.urls['detail'] % link['href'][1:]
|
||||
new['score'] = 20 if td.find('a', 'iverif') else 0
|
||||
elif column_name is 'size':
|
||||
new['size'] = self.parseSize(td.text)
|
||||
elif column_name is 'age':
|
||||
new['age'] = self.ageToDays(td.text)
|
||||
elif column_name is 'seeds':
|
||||
new['seeds'] = tryInt(td.text)
|
||||
elif column_name is 'leechers':
|
||||
new['leechers'] = tryInt(td.text)
|
||||
nr += 1
|
||||
|
||||
nr += 1
|
||||
|
||||
new['score'] += fireEvent('score.calculate', new, movie, single = True)
|
||||
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)
|
||||
except:
|
||||
log.error('Failed parsing KickAssTorrents: %s', traceback.format_exc())
|
||||
results.append(new)
|
||||
except:
|
||||
pass
|
||||
log.error('Failed parsing KickAssTorrents: %s', traceback.format_exc())
|
||||
|
||||
return results
|
||||
except AttributeError:
|
||||
log.debug('No search results found.')
|
||||
|
||||
return results
|
||||
|
||||
def ageToDays(self, age_str):
|
||||
age = 0
|
||||
age_str = age_str.replace(' ', ' ')
|
||||
|
||||
@@ -31,6 +31,10 @@ config = [{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'passkey',
|
||||
'default': '',
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
@@ -21,7 +20,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'
|
||||
}
|
||||
|
||||
@@ -65,18 +64,11 @@ class PassThePopcorn(TorrentProvider):
|
||||
else:
|
||||
raise PassThePopcorn.NotLoggedInHTTPError(req.get_full_url(), code, msg, headers, fp)
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
|
||||
if self.isDisabled():
|
||||
return results
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
movie_title = getTitle(movie['library'])
|
||||
quality_id = quality['identifier']
|
||||
|
||||
log.info('Searching for %s at quality %s' % (movie_title, quality_id))
|
||||
|
||||
params = mergeDicts(self.quality_search_params[quality_id].copy(), {
|
||||
'order_by': 'relevance',
|
||||
'order_way': 'descending',
|
||||
@@ -85,7 +77,7 @@ class PassThePopcorn(TorrentProvider):
|
||||
|
||||
# Do login for the cookies
|
||||
if not self.login_opener and not self.login():
|
||||
return results
|
||||
return
|
||||
|
||||
try:
|
||||
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
|
||||
@@ -93,12 +85,11 @@ class PassThePopcorn(TorrentProvider):
|
||||
res = json.loads(txt)
|
||||
except:
|
||||
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)' % params)
|
||||
return []
|
||||
return
|
||||
|
||||
try:
|
||||
if not 'Movies' in res:
|
||||
log.info("PTP search returned nothing for '%s' at quality '%s' with search parameters %s" % (movie_title, quality_id, params))
|
||||
return []
|
||||
return
|
||||
|
||||
authkey = res['AuthKey']
|
||||
passkey = res['PassKey']
|
||||
@@ -118,7 +109,6 @@ class PassThePopcorn(TorrentProvider):
|
||||
if 'Scene' in torrent and torrent['Scene']:
|
||||
torrentdesc += ' Scene'
|
||||
if 'RemasterTitle' in torrent and torrent['RemasterTitle']:
|
||||
# eliminate odd characters...
|
||||
torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle'])
|
||||
|
||||
torrentdesc += ' (%s)' % quality_id
|
||||
@@ -127,39 +117,23 @@ class PassThePopcorn(TorrentProvider):
|
||||
def extra_check(item):
|
||||
return self.torrentMeetsQualitySpec(item, type)
|
||||
|
||||
def extra_score(item):
|
||||
return 50 if torrent['GoldenPopcorn'] else 0
|
||||
|
||||
new = {
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'type': 'torrent',
|
||||
'provider': self.getName(),
|
||||
'name': torrent_name,
|
||||
'description': '',
|
||||
'url': '%s?action=download&id=%d&authkey=%s&torrent_pass=%s' % (self.urls['torrent'], torrent_id, authkey, passkey),
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'date': tryInt(time.mktime(parse(torrent['UploadTime']).timetuple())),
|
||||
'size': tryInt(torrent['Size']) / (1024 * 1024),
|
||||
'provider': self.getName(),
|
||||
'seeders': tryInt(torrent['Seeders']),
|
||||
'leechers': tryInt(torrent['Leechers']),
|
||||
'extra_score': extra_score,
|
||||
'score': 50 if torrent['GoldenPopcorn'] else 0,
|
||||
'extra_check': extra_check,
|
||||
'download': self.loginDownload,
|
||||
}
|
||||
})
|
||||
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
|
||||
if fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality):
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
return results
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return []
|
||||
|
||||
def login(self):
|
||||
|
||||
cookieprocessor = urllib2.HTTPCookieProcessor(cookielib.CookieJar())
|
||||
@@ -249,6 +223,7 @@ class PassThePopcorn(TorrentProvider):
|
||||
return tryUrlencode({
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'passkey': self.conf('passkey'),
|
||||
'keeplogged': '1',
|
||||
'login': 'Login'
|
||||
})
|
||||
|
||||
@@ -10,12 +10,12 @@ config = [{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'torrent_providers',
|
||||
'name': 'PublicHD',
|
||||
'description': 'Public Torrent site with only HD content. See <a href="https://publichd.eu/">PublicHD</a>',
|
||||
'description': 'Public Torrent site with only HD content. See <a href="https://publichd.se/">PublicHD</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
'default': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
||||
from couchpotato.core.helpers.variable import getTitle, tryInt
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
from couchpotato.core.providers.torrent.base import TorrentMagnetProvider
|
||||
from urlparse import parse_qs
|
||||
import re
|
||||
import traceback
|
||||
@@ -11,31 +10,31 @@ import traceback
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class PublicHD(TorrentProvider):
|
||||
class PublicHD(TorrentMagnetProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://publichd.eu',
|
||||
'detail': 'https://publichd.eu/index.php?page=torrent-details&id=%s',
|
||||
'search': 'https://publichd.eu/index.php',
|
||||
'test': 'https://publichd.se',
|
||||
'detail': 'https://publichd.se/index.php?page=torrent-details&id=%s',
|
||||
'search': 'https://publichd.se/index.php',
|
||||
}
|
||||
http_time_between_calls = 0
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if not quality.get('hd', False):
|
||||
return []
|
||||
|
||||
if self.isDisabled() or not quality.get('hd', False):
|
||||
return results
|
||||
return super(PublicHD, self).search(movie, quality)
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
params = tryUrlencode({
|
||||
'page':'torrents',
|
||||
'search': '%s %s' % (getTitle(movie['library']), movie['library']['year']),
|
||||
'search': '%s %s' % (title, movie['library']['year']),
|
||||
'active': 1,
|
||||
})
|
||||
url = '%s?%s' % (self.urls['search'], params)
|
||||
|
||||
cache_key = 'publichd.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
data = self.getCache(cache_key, url)
|
||||
data = self.getHTMLData('%s?%s' % (self.urls['search'], params))
|
||||
|
||||
if data:
|
||||
|
||||
@@ -53,36 +52,20 @@ class PublicHD(TorrentProvider):
|
||||
|
||||
url = parse_qs(info_url['href'])
|
||||
|
||||
new = {
|
||||
results.append({
|
||||
'id': url['id'][0],
|
||||
'name': info_url.string,
|
||||
'type': 'torrent_magnet',
|
||||
'check_nzb': False,
|
||||
'description': '',
|
||||
'provider': self.getName(),
|
||||
'url': download['href'],
|
||||
'detail_url': self.urls['detail'] % url['id'][0],
|
||||
'size': self.parseSize(result.find_all('td')[7].string),
|
||||
'seeders': tryInt(result.find_all('td')[4].string),
|
||||
'leechers': tryInt(result.find_all('td')[5].string),
|
||||
'get_more_info': self.getMoreInfo
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return results
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return []
|
||||
|
||||
def getMoreInfo(self, item):
|
||||
full_description = self.getCache('publichd.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||
html = BeautifulSoup(full_description)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
@@ -27,30 +26,24 @@ class SceneAccess(TorrentProvider):
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled():
|
||||
return results
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
url = self.urls['search'] % (
|
||||
self.getCatId(quality['identifier'])[0],
|
||||
self.getCatId(quality['identifier'])[0]
|
||||
)
|
||||
|
||||
q = '%s %s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
arguments = tryUrlencode({
|
||||
'search': q,
|
||||
'search': movie['library']['identifier'],
|
||||
'method': 1,
|
||||
})
|
||||
url = "%s&%s" % (url, arguments)
|
||||
|
||||
# Do login for the cookies
|
||||
if not self.login_opener and not self.login():
|
||||
return results
|
||||
return
|
||||
|
||||
cache_key = 'sceneaccess.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
data = self.getCache(cache_key, url, opener = self.login_opener)
|
||||
data = self.getHTMLData(url, opener = self.login_opener)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
@@ -58,7 +51,7 @@ class SceneAccess(TorrentProvider):
|
||||
try:
|
||||
resultsTable = html.find('table', attrs = {'id' : 'torrents-table'})
|
||||
if resultsTable is None:
|
||||
return results
|
||||
return
|
||||
|
||||
entries = resultsTable.find_all('tr', attrs = {'class' : 'tt_row'})
|
||||
for result in entries:
|
||||
@@ -66,38 +59,23 @@ class SceneAccess(TorrentProvider):
|
||||
link = result.find('td', attrs = {'class' : 'ttr_name'}).find('a')
|
||||
url = result.find('td', attrs = {'class' : 'td_dl'}).find('a')
|
||||
leechers = result.find('td', attrs = {'class' : 'ttr_leechers'}).find('a')
|
||||
id = link['href'].replace('details?id=', '')
|
||||
torrent_id = link['href'].replace('details?id=', '')
|
||||
|
||||
new = {
|
||||
'id': id,
|
||||
'type': 'torrent',
|
||||
'check_nzb': False,
|
||||
'description': '',
|
||||
'provider': self.getName(),
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': link['title'],
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['detail'] % id,
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'size': self.parseSize(result.find('td', attrs = {'class' : 'ttr_size'}).contents[0]),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class' : 'ttr_seeders'}).find('a').string),
|
||||
'leechers': tryInt(leechers.string) if leechers else 0,
|
||||
'download': self.loginDownload,
|
||||
'get_more_info': self.getMoreInfo,
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
return results
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return []
|
||||
|
||||
def getLoginParams(self):
|
||||
return tryUrlencode({
|
||||
'username': self.conf('username'),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode
|
||||
from couchpotato.core.helpers.variable import getTitle, tryInt
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
import traceback
|
||||
@@ -21,13 +20,9 @@ class SceneHD(TorrentProvider):
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
|
||||
def search(self, movie, quality):
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
results = []
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
|
||||
q = '"%s %s" %s' % (simplifyString(title), movie['library']['year'], quality.get('identifier'))
|
||||
arguments = tryUrlencode({
|
||||
'search': q,
|
||||
})
|
||||
@@ -35,10 +30,9 @@ class SceneHD(TorrentProvider):
|
||||
|
||||
# Cookie login
|
||||
if not self.login_opener and not self.login():
|
||||
return results
|
||||
return
|
||||
|
||||
cache_key = 'scenehd.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
data = self.getCache(cache_key, url, opener = self.login_opener)
|
||||
data = self.getHTMLData(url, opener = self.login_opener)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
@@ -52,7 +46,7 @@ class SceneHD(TorrentProvider):
|
||||
|
||||
detail_link = all_cells[2].find('a')
|
||||
details = detail_link['href']
|
||||
id = details.replace('details.php?id=', '')
|
||||
torrent_id = details.replace('details.php?id=', '')
|
||||
|
||||
leechers = all_cells[11].find('a')
|
||||
if leechers:
|
||||
@@ -60,38 +54,20 @@ class SceneHD(TorrentProvider):
|
||||
else:
|
||||
leechers = all_cells[11].string
|
||||
|
||||
new = {
|
||||
'id': id,
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': detail_link['title'],
|
||||
'type': 'torrent',
|
||||
'check_nzb': False,
|
||||
'description': '',
|
||||
'provider': self.getName(),
|
||||
'size': self.parseSize(all_cells[7].string),
|
||||
'seeders': tryInt(all_cells[10].find('a').string),
|
||||
'leechers': tryInt(leechers),
|
||||
'url': self.urls['download'] % id,
|
||||
'url': self.urls['download'] % torrent_id,
|
||||
'download': self.loginDownload,
|
||||
}
|
||||
|
||||
imdb_link = all_cells[1].find('a')
|
||||
imdb_results = self.imdbMatch(imdb_link['href'], movie['library']['identifier']) if imdb_link else 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 = imdb_results, single = True)
|
||||
|
||||
if is_correct_movie:
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
return results
|
||||
'description': all_cells[1].find('a')['href'],
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def getLoginParams(self, params):
|
||||
return tryUrlencode({
|
||||
|
||||
@@ -16,7 +16,7 @@ config = [{
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False
|
||||
'default': True
|
||||
},
|
||||
{
|
||||
'name': 'domain',
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import getTitle, tryInt, cleanHost
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt, cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
from couchpotato.core.providers.torrent.base import TorrentMagnetProvider
|
||||
from couchpotato.environment import Env
|
||||
from urllib import quote_plus
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
@@ -13,7 +11,7 @@ import traceback
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class ThePirateBay(TorrentProvider):
|
||||
class ThePirateBay(TorrentMagnetProvider):
|
||||
|
||||
urls = {
|
||||
'detail': '%s/torrent/%s',
|
||||
@@ -45,6 +43,58 @@ class ThePirateBay(TorrentProvider):
|
||||
self.domain = self.conf('domain')
|
||||
super(ThePirateBay, self).__init__()
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
search_url = self.urls['search'] % (self.getDomain(), tryUrlencode(title + ' ' + quality['identifier']), self.getCatId(quality['identifier'])[0])
|
||||
|
||||
data = self.getHTMLData(search_url)
|
||||
|
||||
if data:
|
||||
try:
|
||||
soup = BeautifulSoup(data)
|
||||
results_table = soup.find('table', attrs = {'id': 'searchResult'})
|
||||
|
||||
if not results_table:
|
||||
return
|
||||
|
||||
entries = results_table.find_all('tr')
|
||||
for result in entries[2:]:
|
||||
link = result.find(href = re.compile('torrent\/\d+\/'))
|
||||
download = result.find(href = re.compile('magnet:'))
|
||||
|
||||
try:
|
||||
size = re.search('Size (?P<size>.+),', unicode(result.select('font.detDesc')[0])).group('size')
|
||||
except:
|
||||
continue
|
||||
|
||||
if link and download:
|
||||
|
||||
def extra_score(item):
|
||||
trusted = (0, 10)[result.find('img', alt = re.compile('Trusted')) != None]
|
||||
vip = (0, 20)[result.find('img', alt = re.compile('VIP')) != None]
|
||||
confirmed = (0, 30)[result.find('img', alt = re.compile('Helpers')) != None]
|
||||
moderated = (0, 50)[result.find('img', alt = re.compile('Moderator')) != None]
|
||||
|
||||
return confirmed + trusted + vip + moderated
|
||||
|
||||
results.append({
|
||||
'id': re.search('/(?P<id>\d+)/', link['href']).group('id'),
|
||||
'name': link.string,
|
||||
'url': download['href'],
|
||||
'detail_url': self.getDomain(link['href']),
|
||||
'size': self.parseSize(size),
|
||||
'seeders': tryInt(result.find_all('td')[2].string),
|
||||
'leechers': tryInt(result.find_all('td')[3].string),
|
||||
'extra_score': extra_score,
|
||||
'get_more_info': self.getMoreInfo
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def isEnabled(self):
|
||||
return super(ThePirateBay, self).isEnabled() and self.getDomain()
|
||||
|
||||
def getDomain(self, url = ''):
|
||||
|
||||
if not self.domain:
|
||||
@@ -57,7 +107,7 @@ class ThePirateBay(TorrentProvider):
|
||||
|
||||
data = ''
|
||||
try:
|
||||
data = self.urlopen(proxy, timeout = 3)
|
||||
data = self.urlopen(proxy, timeout = 3, show_error = False)
|
||||
except:
|
||||
log.debug('Failed tpb proxy %s', proxy)
|
||||
|
||||
@@ -74,66 +124,6 @@ class ThePirateBay(TorrentProvider):
|
||||
|
||||
return cleanHost(self.domain).rstrip('/') + url
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled() or not self.getDomain():
|
||||
return results
|
||||
|
||||
cache_key = 'thepiratebay.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
search_url = self.urls['search'] % (self.getDomain(), quote_plus(getTitle(movie['library']) + ' ' + quality['identifier']), self.getCatId(quality['identifier'])[0])
|
||||
data = self.getCache(cache_key, search_url)
|
||||
|
||||
if data:
|
||||
try:
|
||||
soup = BeautifulSoup(data)
|
||||
results_table = soup.find('table', attrs = {'id': 'searchResult'})
|
||||
entries = results_table.find_all('tr')
|
||||
for result in entries[1:]:
|
||||
link = result.find(href = re.compile('torrent\/\d+\/'))
|
||||
download = result.find(href = re.compile('magnet:'))
|
||||
|
||||
size = re.search('Size (?P<size>.+),', unicode(result.select('font.detDesc')[0])).group('size')
|
||||
if link and download:
|
||||
|
||||
def extra_score(item):
|
||||
trusted = (0, 10)[result.find('img', alt = re.compile('Trusted')) != None]
|
||||
vip = (0, 20)[result.find('img', alt = re.compile('VIP')) != None]
|
||||
confirmed = (0, 30)[result.find('img', alt = re.compile('Helpers')) != None]
|
||||
moderated = (0, 50)[result.find('img', alt = re.compile('Moderator')) != None]
|
||||
|
||||
return confirmed + trusted + vip + moderated
|
||||
|
||||
new = {
|
||||
'id': re.search('/(?P<id>\d+)/', link['href']).group('id'),
|
||||
'type': 'torrent_magnet',
|
||||
'name': link.string,
|
||||
'check_nzb': False,
|
||||
'description': '',
|
||||
'provider': self.getName(),
|
||||
'url': download['href'],
|
||||
'detail_url': self.getDomain(link['href']),
|
||||
'size': self.parseSize(size),
|
||||
'seeders': tryInt(result.find_all('td')[2].string),
|
||||
'leechers': tryInt(result.find_all('td')[3].string),
|
||||
'extra_score': extra_score,
|
||||
'get_more_info': self.getMoreInfo
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return results
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return []
|
||||
|
||||
def getMoreInfo(self, item):
|
||||
full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||
html = BeautifulSoup(full_description)
|
||||
|
||||
32
couchpotato/core/providers/torrent/torrentday/__init__.py
Normal file
32
couchpotato/core/providers/torrent/torrentday/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from .main import TorrentDay
|
||||
|
||||
def start():
|
||||
return TorrentDay()
|
||||
|
||||
config = [{
|
||||
'name': 'torrentday',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'torrent_providers',
|
||||
'name': 'TorrentDay',
|
||||
'description': 'See <a href="http://www.td.af/">TorrentDay</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
61
couchpotato/core/providers/torrent/torrentday/main.py
Normal file
61
couchpotato/core/providers/torrent/torrentday/main.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class TorrentDay(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://www.td.af/',
|
||||
'login' : 'http://www.td.af/torrents/',
|
||||
'detail': 'http://www.td.af/details.php?id=%s',
|
||||
'search': 'http://www.td.af/V3/API/API.php',
|
||||
'download': 'http://www.td.af/download.php/%s/%s',
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
([11], ['720p', '1080p']),
|
||||
([1, 21, 25], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']),
|
||||
([3], ['dvdr']),
|
||||
([5], ['bd50']),
|
||||
]
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
q = '"%s %s"' % (title, movie['library']['year'])
|
||||
|
||||
params = {
|
||||
'/browse.php?': None,
|
||||
'cata': 'yes',
|
||||
'jxt': 8,
|
||||
'jxw': 'b',
|
||||
'search': q,
|
||||
}
|
||||
|
||||
data = self.getJsonData(self.urls['search'], params = params, opener = self.login_opener)
|
||||
try: torrents = data.get('Fs', [])[0].get('Cn', {}).get('torrents', [])
|
||||
except: return
|
||||
|
||||
for torrent in torrents:
|
||||
results.append({
|
||||
'id': torrent['id'],
|
||||
'name': torrent['name'],
|
||||
'url': self.urls['download'] % (torrent['id'], torrent['fname']),
|
||||
'detail_url': self.urls['detail'] % torrent['id'],
|
||||
'size': self.parseSize(torrent.get('size')),
|
||||
'seeders': tryInt(torrent.get('seed')),
|
||||
'leechers': tryInt(torrent.get('leech')),
|
||||
'download': self.loginDownload,
|
||||
})
|
||||
|
||||
def getLoginParams(self):
|
||||
return tryUrlencode({
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'submit': 'submit',
|
||||
})
|
||||
@@ -1,10 +1,8 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import getTitle, tryInt
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
from urllib import quote_plus
|
||||
import traceback
|
||||
|
||||
|
||||
@@ -14,11 +12,11 @@ log = CPLog(__name__)
|
||||
class TorrentLeech(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test' : 'http://torrentleech.org/',
|
||||
'login' : 'http://torrentleech.org/user/account/login/',
|
||||
'detail' : 'http://torrentleech.org/torrent/%s',
|
||||
'search' : 'http://torrentleech.org/torrents/browse/index/query/%s/categories/%d',
|
||||
'download' : 'http://torrentleech.org%s',
|
||||
'test' : 'http://www.torrentleech.org/',
|
||||
'login' : 'http://www.torrentleech.org/user/account/login/',
|
||||
'detail' : 'http://www.torrentleech.org/torrent/%s',
|
||||
'search' : 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
|
||||
'download' : 'http://www.torrentleech.org%s',
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
@@ -32,20 +30,12 @@ class TorrentLeech(TorrentProvider):
|
||||
]
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
cat_backup_id = None
|
||||
|
||||
def search(self, movie, quality):
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
results = []
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
# Cookie login
|
||||
if not self.login_opener and not self.login():
|
||||
return results
|
||||
|
||||
cache_key = 'torrentleech.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
url = self.urls['search'] % (quote_plus(getTitle(movie['library']).replace(':', '') + ' ' + quality['identifier']), self.getCatId(quality['identifier'])[0])
|
||||
data = self.getCache(cache_key, url, opener = self.login_opener)
|
||||
url = self.urls['search'] % (tryUrlencode(title.replace(':', '') + ' ' + quality['identifier']), self.getCatId(quality['identifier'])[0])
|
||||
data = self.getHTMLData(url, opener = self.login_opener)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
@@ -53,7 +43,7 @@ class TorrentLeech(TorrentProvider):
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'id' : 'torrenttable'})
|
||||
if not result_table:
|
||||
return results
|
||||
return
|
||||
|
||||
entries = result_table.find_all('tr')
|
||||
|
||||
@@ -61,37 +51,22 @@ class TorrentLeech(TorrentProvider):
|
||||
|
||||
link = result.find('td', attrs = {'class' : 'name'}).find('a')
|
||||
url = result.find('td', attrs = {'class' : 'quickdownload'}).find('a')
|
||||
details = result.find('td', attrs = {'class' : 'name'}).find('a')
|
||||
|
||||
new = {
|
||||
results.append({
|
||||
'id': link['href'].replace('/torrent/', ''),
|
||||
'name': link.string,
|
||||
'type': 'torrent',
|
||||
'check_nzb': False,
|
||||
'description': '',
|
||||
'provider': self.getName(),
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % details['href'],
|
||||
'download': self.loginDownload,
|
||||
'size': self.parseSize(result.find_all('td')[4].string),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class' : 'seeders'}).string),
|
||||
'leechers': tryInt(result.find('td', attrs = {'class' : 'leechers'}).string),
|
||||
}
|
||||
})
|
||||
|
||||
imdb_results = self.imdbMatch(self.urls['detail'] % new['id'], movie['library']['identifier'])
|
||||
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = imdb_results, single = True)
|
||||
|
||||
if is_correct_movie:
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
return results
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return []
|
||||
|
||||
def getLoginParams(self):
|
||||
return tryUrlencode({
|
||||
'username': self.conf('username'),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from .main import Filmweb
|
||||
|
||||
def start():
|
||||
return Filmweb()
|
||||
|
||||
config = []
|
||||
28
couchpotato/core/providers/userscript/filmweb/main.py
Normal file
28
couchpotato/core/providers/userscript/filmweb/main.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from couchpotato.core.providers.userscript.base import UserscriptBase
|
||||
import re
|
||||
|
||||
|
||||
class Filmweb(UserscriptBase):
|
||||
|
||||
includes = ['http://www.filmweb.pl/*']
|
||||
|
||||
def getMovie(self, url):
|
||||
|
||||
cookie = {'Cookie': 'welcomeScreen=welcome_screen'}
|
||||
|
||||
try:
|
||||
data = self.urlopen(url, headers = cookie)
|
||||
except:
|
||||
return
|
||||
|
||||
name = re.search("<h2.*?class=\"text-large caption\">(?P<name>[^<]+)</h2>", data)
|
||||
|
||||
if name is None:
|
||||
name = re.search("<a.*?property=\"v:name\".*?>(?P<name>[^<]+)</a>", data)
|
||||
|
||||
name = name.group('name').decode('string_escape')
|
||||
|
||||
year = re.search("<span.*?id=filmYear.*?>\((?P<year>[^\)]+)\).*?</span>", data)
|
||||
year = year.group('year')
|
||||
|
||||
return self.search(name, year)
|
||||
@@ -90,7 +90,7 @@ class Settings(object):
|
||||
def set(self, section, option, value):
|
||||
return self.p.set(section, option, value)
|
||||
|
||||
def get(self, option = '', section = 'core', default = '', type = None):
|
||||
def get(self, option = '', section = 'core', default = None, type = None):
|
||||
try:
|
||||
|
||||
try: type = self.types[section][option]
|
||||
@@ -111,7 +111,7 @@ class Settings(object):
|
||||
try:
|
||||
return self.p.getboolean(section, option)
|
||||
except:
|
||||
return self.p.get(section, option)
|
||||
return self.p.get(section, option) == 1
|
||||
|
||||
def getInt(self, section, option):
|
||||
try:
|
||||
|
||||
@@ -100,7 +100,7 @@ class Release(Entity):
|
||||
movie = ManyToOne('Movie')
|
||||
status = ManyToOne('Status')
|
||||
quality = ManyToOne('Quality')
|
||||
files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
|
||||
files = ManyToMany('File')
|
||||
info = OneToMany('ReleaseInfo', cascade = 'all, delete-orphan')
|
||||
|
||||
def to_dict(self, deep = {}, exclude = []):
|
||||
|
||||
@@ -4,10 +4,8 @@ from couchpotato.api import api, NonBlockHandler
|
||||
from couchpotato.core.event import fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.variable import getDataDir, tryInt
|
||||
from logging import handlers
|
||||
from tornado import autoreload
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.web import RequestHandler, Application, FallbackHandler
|
||||
from tornado.web import Application, FallbackHandler
|
||||
from tornado.wsgi import WSGIContainer
|
||||
from werkzeug.contrib.cache import FileSystemCache
|
||||
import locale
|
||||
@@ -57,10 +55,8 @@ def _log(status_code, request):
|
||||
|
||||
if status_code < 400:
|
||||
return
|
||||
elif status_code < 500:
|
||||
log_method = logging.warning
|
||||
else:
|
||||
log_method = logging.error
|
||||
log_method = logging.debug
|
||||
request_time = 1000.0 * request.request_time()
|
||||
summary = request.method + " " + request.uri + " (" + \
|
||||
request.remote_ip + ")"
|
||||
@@ -148,6 +144,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%m-%d %H:%M:%S')
|
||||
level = logging.DEBUG if debug else logging.INFO
|
||||
logger.setLevel(level)
|
||||
logging.addLevelName(19, 'INFO')
|
||||
|
||||
# To screen
|
||||
if (debug or options.console_log) and not options.quiet and not options.daemon:
|
||||
@@ -192,7 +189,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
version_control(db, repo, version = latest_db_version)
|
||||
current_db_version = db_version(db, repo)
|
||||
|
||||
if current_db_version < latest_db_version and not debug:
|
||||
if current_db_version < latest_db_version and not development:
|
||||
log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version))
|
||||
upgrade(db, repo)
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ Page.Manage = new Class({
|
||||
'text': 'When you\'ve done that, hit this button → '
|
||||
}).adopt(
|
||||
new Element('a.button.green', {
|
||||
'text': 'Hit me, but not to hard',
|
||||
'text': 'Hit me, but not too hard',
|
||||
'events':{
|
||||
'click': self.refresh.bind(self, true)
|
||||
}
|
||||
|
||||
@@ -436,9 +436,14 @@ Option.String = new Class({
|
||||
self.input = new Element('input.inlay', {
|
||||
'type': 'text',
|
||||
'name': self.postName(),
|
||||
'value': self.getSettingValue()
|
||||
'value': self.getSettingValue(),
|
||||
'placeholder': self.getPlaceholder()
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
getPlaceholder: function(){
|
||||
return this.options.placeholder
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
'is_remote': false
|
||||
});
|
||||
|
||||
$(document.body).set('data-api', window.location.protocol + '//' + window.location.host + Api.createUrl().replace('/default/', '/'));
|
||||
|
||||
// Catch errors
|
||||
window.onerror = function(message, file, line){
|
||||
|
||||
|
||||
12
init/fedora
12
init/fedora
@@ -11,11 +11,6 @@
|
||||
# Source function library.
|
||||
. /etc/init.d/functions
|
||||
|
||||
# Source couchpotato configuration
|
||||
if [ -f /etc/sysconfig/couchpotato ]; then
|
||||
. /etc/sysconfig/couchpotato
|
||||
fi
|
||||
|
||||
prog=couchpotato
|
||||
lockfile=/var/lock/subsys/$prog
|
||||
|
||||
@@ -27,8 +22,13 @@ datadir=${CP_DATA-~/.couchpotato}
|
||||
pidfile=${CP_PIDFILE-/var/run/couchpotato/couchpotato.pid}
|
||||
##
|
||||
|
||||
# Source couchpotato configuration
|
||||
if [ -f /etc/sysconfig/couchpotato ]; then
|
||||
. /etc/sysconfig/couchpotato
|
||||
fi
|
||||
|
||||
pidpath=`dirname ${pidfile}`
|
||||
options=" --daemon --pid_file=${pidfile} --datadir=${datadir}"
|
||||
options=" --daemon --pid_file=${pidfile} --data_dir=${datadir}"
|
||||
|
||||
# create PID directory if not exist and ensure the couchpotato user can write to it
|
||||
if [ ! -d $pidpath ]; then
|
||||
|
||||
85
init/ffpstick
Normal file
85
init/ffpstick
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/ffp/bin/sh
|
||||
|
||||
# PROVIDE: Couchpotato
|
||||
# REQUIRE: LOGIN
|
||||
|
||||
. /ffp/etc/ffp.subr
|
||||
|
||||
# script name
|
||||
NAME=couchpotato
|
||||
APP_PATH=/ffp/usr/local/CouchPotatoServer
|
||||
APP_CONFIG_DIF=/ffp/root/.couchpotato
|
||||
|
||||
# path to python bin
|
||||
DAEMON=/ffp/bin/python
|
||||
|
||||
# Path to store PID file
|
||||
PID_FILE=/ffp/var/run/couchpotato.pid
|
||||
PID_PATH=$(dirname $PID_FILE)
|
||||
|
||||
# startup args
|
||||
DAEMON_OPTS=" ${APP_PATH}/CouchPotato.py --daemon --pid_file=${PID_FILE}"
|
||||
|
||||
command=$NAME
|
||||
start_cmd="couchpotato_start"
|
||||
stop_cmd="couchpotato_stop"
|
||||
status_cmd="couchpotato_status"
|
||||
|
||||
wait_for_status()
|
||||
{
|
||||
counter=$2
|
||||
while [ ${counter} -gt 0 ]; do
|
||||
daemon_status
|
||||
[ $? -eq $1 ] && break
|
||||
let counter=counter-1
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
daemon_status()
|
||||
{
|
||||
if [ -f ${PID_FILE} ] && [ -d /proc/`cat ${PID_FILE}` ]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
couchpotato_start()
|
||||
{
|
||||
if [ -f ${PID_FILE} ] && [ -d /proc/`cat ${PID_FILE}` ]; then
|
||||
echo "$NAME already running"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Starting $NAME"
|
||||
$DAEMON $DAEMON_OPTS
|
||||
}
|
||||
|
||||
couchpotato_stop()
|
||||
{
|
||||
if [ -f $PID_FILE ] ; then
|
||||
echo "Stopping $NAME"
|
||||
kill `cat ${PID_FILE}`
|
||||
wait_for_status 1 20
|
||||
rm -f ${PID_FILE}
|
||||
else
|
||||
echo "Cannot find $PID_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
couchpotato_status()
|
||||
{
|
||||
local pid=` cat ${PID_FILE} 2>/dev/null `
|
||||
if test -n "$pid"; then
|
||||
if cat /proc/$pid/cmdline | grep ${APP_PATH} >/dev/null ; then
|
||||
[ "$1" == "silent" ] || echo "$NAME is running"
|
||||
return 0
|
||||
fi
|
||||
rm $pidfile
|
||||
fi
|
||||
|
||||
[ "$1" == "silent" ] || echo "$NAME not running"
|
||||
return 1
|
||||
}
|
||||
|
||||
run_rc_command "$1"
|
||||
@@ -1,5 +1,5 @@
|
||||
#define MyAppName "CouchPotato"
|
||||
#define MyAppVer "2.0.1"
|
||||
#define MyAppVer "2.0.4"
|
||||
|
||||
[Setup]
|
||||
AppName={#MyAppName}
|
||||
@@ -25,4 +25,10 @@ Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppName}.exe"; Tasks: s
|
||||
Name: "startup"; Description: "Run {#MyAppName} at startup"; Flags: unchecked
|
||||
|
||||
[UninstallDelete]
|
||||
Type: filesandordirs; Name: "{app}\*.*"
|
||||
Type: filesandordirs; Name: "{app}\appdata"
|
||||
Type: filesandordirs; Name: "{app}\Microsoft.VC90.CRT"
|
||||
Type: filesandordirs; Name: "{app}\updates"
|
||||
Type: filesandordirs; Name: "{app}\CouchPotato*"
|
||||
Type: filesandordirs; Name: "{app}\python27.dll"
|
||||
Type: filesandordirs; Name: "{app}\unins000.dat"
|
||||
Type: filesandordirs; Name: "{app}\unins000.exe"
|
||||
|
||||
@@ -95,7 +95,6 @@ class Event(object):
|
||||
(None, None, handler), ... # asynchronous execution
|
||||
)
|
||||
"""
|
||||
self.in_order = False
|
||||
self.name = name
|
||||
self.asynchronous = asynch
|
||||
self.exc_info = exc_info
|
||||
@@ -142,11 +141,16 @@ class Event(object):
|
||||
def fire(self, *args, **kwargs):
|
||||
""" Stores all registered handlers in a queue for processing """
|
||||
self.queue = Queue.Queue()
|
||||
self.result = {}
|
||||
result = {}
|
||||
|
||||
if self.handlers:
|
||||
|
||||
max_threads = self._threads()
|
||||
max_threads = 1 if kwargs.get('event_order_lock') else self._threads()
|
||||
|
||||
# Set global result
|
||||
def add_to(key, value):
|
||||
result[key] = value
|
||||
kwargs['event_add_to_result'] = add_to
|
||||
|
||||
for i in range(max_threads):
|
||||
t = threading.Thread(target = self._execute,
|
||||
@@ -159,17 +163,12 @@ class Event(object):
|
||||
|
||||
if self.asynchronous:
|
||||
handler_, memoize, timeout = self.handlers[handler]
|
||||
self.result[handler] = (None, None, handler_)
|
||||
result[handler] = (None, None, handler_)
|
||||
|
||||
if not self.asynchronous:
|
||||
self.queue.join()
|
||||
|
||||
res = self.result or None
|
||||
|
||||
# Cleanup
|
||||
self.result = {}
|
||||
|
||||
return res
|
||||
return result
|
||||
|
||||
def count(self):
|
||||
""" Returns the count of registered handlers """
|
||||
@@ -181,24 +180,51 @@ class Event(object):
|
||||
self.memoize.clear()
|
||||
|
||||
def _execute(self, *args, **kwargs):
|
||||
|
||||
# Remove get and set from kwargs
|
||||
add_to_result = kwargs.get('event_add_to_result')
|
||||
del kwargs['event_add_to_result']
|
||||
|
||||
# Get and remove order lock
|
||||
order_lock = kwargs.get('event_order_lock')
|
||||
try: del kwargs['event_order_lock']
|
||||
except: pass
|
||||
|
||||
# Get and remove return on first
|
||||
return_on_result = kwargs.get('event_return_on_result')
|
||||
try: del kwargs['event_return_on_result']
|
||||
except: pass
|
||||
|
||||
got_results = False
|
||||
|
||||
""" Executes all handlers stored in the queue """
|
||||
while True:
|
||||
|
||||
try:
|
||||
h_ = self.queue.get(timeout = 2)
|
||||
handler, memoize, timeout = self.handlers[h_]
|
||||
|
||||
if self.lock and self.in_order:
|
||||
self.lock.acquire()
|
||||
if return_on_result and got_results:
|
||||
|
||||
if not self.asynchronous:
|
||||
self.queue.task_done()
|
||||
|
||||
continue
|
||||
|
||||
if order_lock:
|
||||
order_lock.acquire()
|
||||
|
||||
try:
|
||||
r = self._memoize(memoize, timeout, handler, *args, **kwargs)
|
||||
if not self.asynchronous:
|
||||
self.result[h_] = tuple(r)
|
||||
if not return_on_result or (return_on_result and r[1] is not None):
|
||||
add_to_result(h_, tuple(r))
|
||||
got_results = True
|
||||
|
||||
except Exception:
|
||||
if not self.asynchronous:
|
||||
self.result[h_] = (False, self._error(sys.exc_info()),
|
||||
handler)
|
||||
add_to_result(h_, (False, self._error(sys.exc_info()),
|
||||
handler))
|
||||
else:
|
||||
self.error_handler(sys.exc_info())
|
||||
finally:
|
||||
@@ -206,8 +232,8 @@ class Event(object):
|
||||
if not self.asynchronous:
|
||||
self.queue.task_done()
|
||||
|
||||
if self.lock and self.in_order:
|
||||
self.lock.release()
|
||||
if order_lock:
|
||||
order_lock.release()
|
||||
|
||||
if self.queue.empty():
|
||||
raise Queue.Empty
|
||||
@@ -257,7 +283,7 @@ class Event(object):
|
||||
args.insert(0, self.sender)
|
||||
|
||||
if not memoize:
|
||||
if timeout <= 0: #no time restriction
|
||||
if timeout <= 0: #no time restriction
|
||||
result = [True, handler(*args, **kwargs), handler]
|
||||
return result
|
||||
|
||||
@@ -273,7 +299,7 @@ class Event(object):
|
||||
if args_ == args and kwargs_ == kwargs:
|
||||
return [True, result, handler]
|
||||
|
||||
if timeout <= 0: #no time restriction
|
||||
if timeout <= 0: #no time restriction
|
||||
result = handler(*args, **kwargs)
|
||||
else:
|
||||
result = self._timeout(timeout, handler, *args, **kwargs)
|
||||
|
||||
2
libs/bencode/BTL.py
Normal file
2
libs/bencode/BTL.py
Normal file
@@ -0,0 +1,2 @@
|
||||
class BTFailure(Exception):
|
||||
pass
|
||||
143
libs/bencode/LICENSE.txt
Normal file
143
libs/bencode/LICENSE.txt
Normal file
@@ -0,0 +1,143 @@
|
||||
BitTorrent Open Source License
|
||||
|
||||
Version 1.1
|
||||
|
||||
This BitTorrent Open Source License (the "License") applies to the BitTorrent client and related software products as well as any updates or maintenance releases of that software ("BitTorrent Products") that are distributed by BitTorrent, Inc. ("Licensor"). Any BitTorrent Product licensed pursuant to this License is a Licensed Product. Licensed Product, in its entirety, is protected by U.S. copyright law. This License identifies the terms under which you may use, copy, distribute or modify Licensed Product.
|
||||
|
||||
Preamble
|
||||
|
||||
This Preamble is intended to describe, in plain English, the nature and scope of this License. However, this Preamble is not a part of this license. The legal effect of this License is dependent only upon the terms of the License and not this Preamble.
|
||||
|
||||
This License complies with the Open Source Definition and is derived from the Jabber Open Source License 1.0 (the "JOSL"), which has been approved by Open Source Initiative. Sections 4(c) and 4(f)(iii) from the JOSL have been deleted.
|
||||
|
||||
This License provides that:
|
||||
|
||||
1. You may use or give away the Licensed Product, alone or as a component of an aggregate software distribution containing programs from several different sources. No royalty or other fee is required.
|
||||
|
||||
2. Both Source Code and executable versions of the Licensed Product, including Modifications made by previous Contributors, are available for your use. (The terms "Licensed Product," "Modifications," "Contributors" and "Source Code" are defined in the License.)
|
||||
|
||||
3. You are allowed to make Modifications to the Licensed Product, and you can create Derivative Works from it. (The term "Derivative Works" is defined in the License.)
|
||||
|
||||
4. By accepting the Licensed Product under the provisions of this License, you agree that any Modifications you make to the Licensed Product and then distribute are governed by the provisions of this License. In particular, you must make the Source Code of your Modifications available to others free of charge and without a royalty.
|
||||
|
||||
5. You may sell, accept donations or otherwise receive compensation for executable versions of a Licensed Product, without paying a royalty or other fee to the Licensor or any Contributor, provided that such executable versions contain your or another Contributor?s material Modifications. For the avoidance of doubt, to the extent your executable version of a Licensed Product does not contain your or another Contributor?s material Modifications, you may not sell, accept donations or otherwise receive compensation for such executable.
|
||||
|
||||
You may use the Licensed Product for any purpose, but the Licensor is not providing you any warranty whatsoever, nor is the Licensor accepting any liability in the event that the Licensed Product doesn't work properly or causes you any injury or damages.
|
||||
|
||||
6. If you sublicense the Licensed Product or Derivative Works, you may charge fees for warranty or support, or for accepting indemnity or liability obligations to your customers. You cannot charge for, sell, accept donations or otherwise receive compensation for the Source Code.
|
||||
|
||||
7. If you assert any patent claims against the Licensor relating to the Licensed Product, or if you breach any terms of the License, your rights to the Licensed Product under this License automatically terminate.
|
||||
|
||||
You may use this License to distribute your own Derivative Works, in which case the provisions of this License will apply to your Derivative Works just as they do to the original Licensed Product.
|
||||
|
||||
Alternatively, you may distribute your Derivative Works under any other OSI-approved Open Source license, or under a proprietary license of your choice. If you use any license other than this License, however, you must continue to fulfill the requirements of this License (including the provisions relating to publishing the Source Code) for those portions of your Derivative Works that consist of the Licensed Product, including the files containing Modifications.
|
||||
|
||||
New versions of this License may be published from time to time in connection with new versions of a Licensed Product or otherwise. You may choose to continue to use the license terms in this version of the License for the Licensed Product that was originally licensed hereunder, however, the new versions of this License will at all times apply to new versions of the Licensed Product released by Licensor after the release of the new version of this License. Only the Licensor has the right to change the License terms as they apply to the Licensed Product.
|
||||
|
||||
This License relies on precise definitions for certain terms. Those terms are defined when they are first used, and the definitions are repeated for your convenience in a Glossary at the end of the License.
|
||||
|
||||
License Terms
|
||||
|
||||
1. Grant of License From Licensor. Subject to the terms and conditions of this License, Licensor hereby grants you a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims, to do the following:
|
||||
|
||||
a. Use, reproduce, modify, display, perform, sublicense and distribute any Modifications created by a Contributor or portions thereof, in both Source Code or as an executable program, either on an unmodified basis or as part of Derivative Works.
|
||||
|
||||
b. Under claims of patents now or hereafter owned or controlled by Contributor, to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof, but solely to the extent that any such claim is necessary to enable you to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof or Derivative Works thereof.
|
||||
|
||||
2. Grant of License to Modifications From Contributor. "Modifications" means any additions to or deletions from the substance or structure of (i) a file containing a Licensed Product, or (ii) any new file that contains any part of a Licensed Product. Hereinafter in this License, the term "Licensed Product" shall include all previous Modifications that you receive from any Contributor. Subject to the terms and conditions of this License, By application of the provisions in Section 4(a) below, each person or entity who created or contributed to the creation of, and distributed, a Modification (a "Contributor") hereby grants you a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims, to do the following:
|
||||
|
||||
a. Use, reproduce, modify, display, perform, sublicense and distribute any Modifications created by such Contributor or portions thereof, in both Source Code or as an executable program, either on an unmodified basis or as part of Derivative Works.
|
||||
|
||||
b. Under claims of patents now or hereafter owned or controlled by Contributor, to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof, but solely to the extent that any such claim is necessary to enable you to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof or Derivative Works thereof.
|
||||
|
||||
3. Exclusions From License Grant. Nothing in this License shall be deemed to grant any rights to trademarks, copyrights, patents, trade secrets or any other intellectual property of Licensor or any Contributor except as expressly stated herein. No patent license is granted separate from the Licensed Product, for code that you delete from the Licensed Product, or for combinations of the Licensed Product with other software or hardware. No right is granted to the trademarks of Licensor or any Contributor even if such marks are included in the Licensed Product. Nothing in this License shall be interpreted to prohibit Licensor from licensing under different terms from this License any code that Licensor otherwise would have a right to license. As an express condition for your use of the Licensed Product, you hereby agree that you will not, without the prior written consent of Licensor, use any trademarks, copyrights, patents, trade secrets or any other intellectual property of Licensor or any Contributor except as expressly stated herein. For the avoidance of doubt and without limiting the foregoing, you hereby agree that you will not use or display any trademark of Licensor or any Contributor in any domain name, directory filepath, advertisement, link or other reference to you in any manner or in any media.
|
||||
|
||||
4. Your Obligations Regarding Distribution.
|
||||
|
||||
a. Application of This License to Your Modifications. As an express condition for your use of the Licensed Product, you hereby agree that any Modifications that you create or to which you contribute, and which you distribute, are governed by the terms of this License including, without limitation, Section 2. Any Modifications that you create or to which you contribute may be distributed only under the terms of this License or a future version of this License released under Section 7. You must include a copy of this License with every copy of the Modifications you distribute. You agree not to offer or impose any terms on any Source Code or executable version of the Licensed Product or Modifications that alter or restrict the applicable version of this License or the recipients' rights hereunder. However, you may include an additional document offering the additional rights described in Section 4(d).
|
||||
|
||||
b. Availability of Source Code. You must make available, without charge, under the terms of this License, the Source Code of the Licensed Product and any Modifications that you distribute, either on the same media as you distribute any executable or other form of the Licensed Product, or via a mechanism generally accepted in the software development community for the electronic transfer of data (an "Electronic Distribution Mechanism"). The Source Code for any version of Licensed Product or Modifications that you distribute must remain available for as long as any executable or other form of the Licensed Product is distributed by you. You are responsible for ensuring that the Source Code version remains available even if the Electronic Distribution Mechanism is maintained by a third party.
|
||||
|
||||
c. Intellectual Property Matters.
|
||||
|
||||
i. Third Party Claims. If you have knowledge that a license to a third party's intellectual property right is required to exercise the rights granted by this License, you must include a text file with the Source Code distribution titled "LEGAL" that describes the claim and the party making the claim in sufficient detail that a recipient will know whom to contact. If you obtain such knowledge after you make any Modifications available as described in Section 4(b), you shall promptly modify the LEGAL file in all copies you make available thereafter and shall take other steps (such as notifying appropriate mailing lists or newsgroups) reasonably calculated to inform those who received the Licensed Product from you that new knowledge has been obtained.
|
||||
|
||||
ii. Contributor APIs. If your Modifications include an application programming interface ("API") and you have knowledge of patent licenses that are reasonably necessary to implement that API, you must also include this information in the LEGAL file.
|
||||
|
||||
iii. Representations. You represent that, except as disclosed pursuant to 4(c)(i) above, you believe that any Modifications you distribute are your original creations and that you have sufficient rights to grant the rights conveyed by this License.
|
||||
|
||||
d. Required Notices. You must duplicate this License in any documentation you provide along with the Source Code of any Modifications you create or to which you contribute, and which you distribute, wherever you describe recipients' rights relating to Licensed Product. You must duplicate the notice contained in Exhibit A (the "Notice") in each file of the Source Code of any copy you distribute of the Licensed Product. If you created a Modification, you may add your name as a Contributor to the Notice. If it is not possible to put the Notice in a particular Source Code file due to its structure, then you must include such Notice in a location (such as a relevant directory file) where a user would be likely to look for such a notice. You may choose to offer, and charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Licensed Product. However, you may do so only on your own behalf, and not on behalf of the Licensor or any Contributor. You must make it clear that any such warranty, support, indemnity or liability obligation is offered by you alone, and you hereby agree to indemnify the Licensor and every Contributor for any liability incurred by the Licensor or such Contributor as a result of warranty, support, indemnity or liability terms you offer.
|
||||
|
||||
e. Distribution of Executable Versions. You may distribute Licensed Product as an executable program under a license of your choice that may contain terms different from this License provided (i) you have satisfied the requirements of Sections 4(a) through 4(d) for that distribution, (ii) you include a conspicuous notice in the executable version, related documentation and collateral materials stating that the Source Code version of the
|
||||
Licensed Product is available under the terms of this License, including a description of how and where you have fulfilled the obligations of Section 4(b), and (iii) you make it clear that any terms that differ from this License are offered by you alone, not by Licensor or any Contributor. You hereby agree to indemnify the Licensor and every Contributor for any liability incurred by Licensor or such Contributor as a result of any terms you offer.
|
||||
|
||||
f. Distribution of Derivative Works. You may create Derivative Works (e.g., combinations of some or all of the Licensed Product with other code) and distribute the Derivative Works as products under any other license you select, with the proviso that the requirements of this License are fulfilled for those portions of the Derivative Works that consist of the Licensed Product or any Modifications thereto.
|
||||
|
||||
g. Compensation for Distribution of Executable Versions of Licensed Products, Modifications or Derivative Works. Notwithstanding any provision of this License to the contrary, by distributing, selling, licensing, sublicensing or otherwise making available any Licensed Product, or Modification or Derivative Work thereof, you and Licensor hereby acknowledge and agree that you may sell, license or sublicense for a fee, accept donations or otherwise receive compensation for executable versions of a Licensed Product, without paying a royalty or other fee to the Licensor or any other Contributor, provided that such executable versions (i) contain your or another Contributor?s material Modifications, or (ii) are otherwise material Derivative Works. For purposes of this License, an executable version of the Licensed Product will be deemed to contain a material Modification, or will otherwise be deemed a material Derivative Work, if (a) the Licensed Product is modified with your own or a third party?s software programs or other code, and/or the Licensed Product is combined with a number of your own or a third party?s software programs or code, respectively, and (b) such software programs or code add or contribute material value, functionality or features to the License Product. For the avoidance of doubt, to the extent your executable version of a Licensed Product does not contain your or another Contributor?s material Modifications or is otherwise not a material Derivative Work, in each case as contemplated herein, you may not sell, license or sublicense for a fee, accept donations or otherwise receive compensation for such executable. Additionally, without limitation of the foregoing and notwithstanding any provision of this License to the contrary, you cannot charge for, sell, license or sublicense for a fee, accept donations or otherwise receive compensation for the Source Code.
|
||||
|
||||
5. Inability to Comply Due to Statute or Regulation. If it is impossible for you to comply with any of the terms of this License with respect to some or all of the Licensed Product due to statute, judicial order, or regulation, then you must (i) comply with the terms of this License to the maximum extent possible, (ii) cite the statute or regulation that prohibits you from adhering to the License, and (iii) describe the limitations and the code they affect. Such description must be included in the LEGAL file described in Section 4(d), and must be included with all distributions of the Source Code. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill at computer programming to be able to understand it.
|
||||
|
||||
6. Application of This License. This License applies to code to which Licensor or Contributor has attached the Notice in Exhibit A, which is incorporated herein by this reference.
|
||||
|
||||
7. Versions of This License.
|
||||
|
||||
a. New Versions. Licensor may publish from time to time revised and/or new versions of the License.
|
||||
|
||||
b. Effect of New Versions. Once Licensed Product has been published under a particular version of the License, you may always continue to use it under the terms of that version, provided that any such license be in full force and effect at the time, and has not been revoked or otherwise terminated. You may also choose to use such Licensed Product under the terms of any subsequent version (but not any prior version) of the License published by Licensor. No one other than Licensor has the right to modify the terms applicable to Licensed Product created under this License.
|
||||
|
||||
c. Derivative Works of this License. If you create or use a modified version of this License, which you may do only in order to apply it to software that is not already a Licensed Product under this License, you must rename your license so that it is not confusingly similar to this License, and must make it clear that your license contains terms that differ from this License. In so naming your license, you may not use any trademark of Licensor or any Contributor.
|
||||
|
||||
8. Disclaimer of Warranty. LICENSED PRODUCT IS PROVIDED UNDER THIS LICENSE ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE LICENSED PRODUCT IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LICENSED PRODUCT IS WITH YOU. SHOULD LICENSED PRODUCT PROVE DEFECTIVE IN ANY RESPECT, YOU (AND NOT THE LICENSOR OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS
|
||||
DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF LICENSED PRODUCT IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
|
||||
|
||||
9. Termination.
|
||||
|
||||
a. Automatic Termination Upon Breach. This license and the rights granted hereunder will terminate automatically if you fail to comply with the terms herein and fail to cure such breach within ten (10) days of being notified of the breach by the Licensor. For purposes of this provision, proof of delivery via email to the address listed in the ?WHOIS? database of the registrar for any website through which you distribute or market any Licensed Product, or to any alternate email address which you designate in writing to the Licensor, shall constitute sufficient notification. All sublicenses to the Licensed Product that are properly granted shall survive any termination of this license so long as they continue to complye with the terms of this License. Provisions that, by their nature, must remain in effect beyond the termination of this License, shall survive.
|
||||
|
||||
b. Termination Upon Assertion of Patent Infringement. If you initiate litigation by asserting a patent infringement claim (excluding declaratory judgment actions) against Licensor or a Contributor (Licensor or Contributor against whom you file such an action is referred to herein as Respondent) alleging that Licensed Product directly or indirectly infringes any patent, then any and all rights granted by such Respondent to you under Sections 1 or 2 of this License shall terminate prospectively upon sixty (60) days notice from Respondent (the "Notice Period") unless within that Notice Period you either agree in writing (i) to pay Respondent a mutually agreeable reasonably royalty for your past or future use of Licensed Product made by such Respondent, or (ii) withdraw your litigation claim with respect to Licensed Product against such Respondent. If within said Notice Period a reasonable royalty and payment arrangement are not mutually agreed upon in writing by the parties or the litigation claim is not withdrawn, the rights granted by Licensor to you under Sections 1 and 2 automatically terminate at the expiration of said Notice Period.
|
||||
|
||||
c. Reasonable Value of This License. If you assert a patent infringement claim against Respondent alleging that Licensed Product directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by said Respondent under Sections 1 and 2 shall be taken into account in determining the amount or value of any payment or license.
|
||||
|
||||
d. No Retroactive Effect of Termination. In the event of termination under Sections 9(a) or 9(b) above, all end user license agreements (excluding licenses to distributors and resellers) that have been validly granted by you or any distributor hereunder prior to termination shall survive termination.
|
||||
|
||||
10. Limitation of Liability. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL THE LICENSOR, ANY CONTRIBUTOR, OR ANY DISTRIBUTOR OF LICENSED PRODUCT, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTYS NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
11. Responsibility for Claims. As between Licensor and Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License. You agree to work with Licensor and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.
|
||||
|
||||
12. U.S. Government End Users. The Licensed Product is a commercial item, as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of commercial computer software and commercial computer software documentation, as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Licensed Product with only those rights set forth herein.
|
||||
|
||||
13. Miscellaneous. This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by California law provisions (except to the extent applicable law, if any, provides otherwise), excluding its conflict-of-law provisions. You expressly agree that in any litigation relating to this license the losing party shall be responsible for costs including, without limitation, court costs and reasonable attorneys fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation that provides that the language of a contract shall be construed against the drafter shall not apply to this License.
|
||||
|
||||
14. Definition of You in This License. You throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License or a future version of this License issued under Section 7. For legal entities, you includes any entity that controls, is controlled by, is under common control with, or affiliated with, you. For purposes of this definition, control means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. You are responsible for advising any affiliated entity of the terms of this License, and that any rights or privileges derived from or obtained by way of this License are subject to the restrictions outlined herein.
|
||||
|
||||
15. Glossary. All defined terms in this License that are used in more than one Section of this License are repeated here, in alphabetical order, for the convenience of the reader. The Section of this License in which each defined term is first used is shown in parentheses.
|
||||
|
||||
Contributor: Each person or entity who created or contributed to the creation of, and distributed, a Modification. (See Section 2)
|
||||
|
||||
Derivative Works: That term as used in this License is defined under U.S. copyright law. (See Section 1(b))
|
||||
|
||||
License: This BitTorrent Open Source License. (See first paragraph of License)
|
||||
|
||||
Licensed Product: Any BitTorrent Product licensed pursuant to this License. The term "Licensed Product" includes all previous Modifications from any Contributor that you receive. (See first paragraph of License and Section 2)
|
||||
|
||||
Licensor: BitTorrent, Inc. (See first paragraph of License)
|
||||
|
||||
Modifications: Any additions to or deletions from the substance or structure of (i) a file containing Licensed Product, or (ii) any new file that contains any part of Licensed Product. (See Section 2)
|
||||
|
||||
Notice: The notice contained in Exhibit A. (See Section 4(e))
|
||||
|
||||
Source Code: The preferred form for making modifications to the Licensed Product, including all modules contained therein, plus any associated interface definition files, scripts used to control compilation and installation of an executable program, or a list of differential comparisons against the Source Code of the Licensed Product. (See Section 1(a))
|
||||
|
||||
You: This term is defined in Section 14 of this License.
|
||||
|
||||
|
||||
EXHIBIT A
|
||||
|
||||
The Notice below must appear in each file of the Source Code of any copy you distribute of the Licensed Product or any hereto. Contributors to any Modifications may add their own copyright notices to identify their own contributions.
|
||||
|
||||
License:
|
||||
|
||||
The contents of this file are subject to the BitTorrent Open Source License Version 1.0 (the License). You may not copy or use this file, in either source code or executable form, except in compliance with the License. You may obtain a copy of the License at http://www.bittorrent.com/license/.
|
||||
|
||||
Software distributed under the License is distributed on an AS IS basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License.
|
||||
|
||||
1
libs/bencode/__init__.py
Normal file
1
libs/bencode/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from bencode import *
|
||||
131
libs/bencode/bencode.py
Normal file
131
libs/bencode/bencode.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# The contents of this file are subject to the BitTorrent Open Source License
|
||||
# Version 1.1 (the License). You may not copy or use this file, in either
|
||||
# source code or executable form, except in compliance with the License. You
|
||||
# may obtain a copy of the License at http://www.bittorrent.com/license/.
|
||||
#
|
||||
# Software distributed under the License is distributed on an AS IS basis,
|
||||
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
# for the specific language governing rights and limitations under the
|
||||
# License.
|
||||
|
||||
# Written by Petru Paler
|
||||
|
||||
from BTL import BTFailure
|
||||
|
||||
|
||||
def decode_int(x, f):
|
||||
f += 1
|
||||
newf = x.index('e', f)
|
||||
n = int(x[f:newf])
|
||||
if x[f] == '-':
|
||||
if x[f + 1] == '0':
|
||||
raise ValueError
|
||||
elif x[f] == '0' and newf != f+1:
|
||||
raise ValueError
|
||||
return (n, newf+1)
|
||||
|
||||
def decode_string(x, f):
|
||||
colon = x.index(':', f)
|
||||
n = int(x[f:colon])
|
||||
if x[f] == '0' and colon != f+1:
|
||||
raise ValueError
|
||||
colon += 1
|
||||
return (x[colon:colon+n], colon+n)
|
||||
|
||||
def decode_list(x, f):
|
||||
r, f = [], f+1
|
||||
while x[f] != 'e':
|
||||
v, f = decode_func[x[f]](x, f)
|
||||
r.append(v)
|
||||
return (r, f + 1)
|
||||
|
||||
def decode_dict(x, f):
|
||||
r, f = {}, f+1
|
||||
while x[f] != 'e':
|
||||
k, f = decode_string(x, f)
|
||||
r[k], f = decode_func[x[f]](x, f)
|
||||
return (r, f + 1)
|
||||
|
||||
decode_func = {}
|
||||
decode_func['l'] = decode_list
|
||||
decode_func['d'] = decode_dict
|
||||
decode_func['i'] = decode_int
|
||||
decode_func['0'] = decode_string
|
||||
decode_func['1'] = decode_string
|
||||
decode_func['2'] = decode_string
|
||||
decode_func['3'] = decode_string
|
||||
decode_func['4'] = decode_string
|
||||
decode_func['5'] = decode_string
|
||||
decode_func['6'] = decode_string
|
||||
decode_func['7'] = decode_string
|
||||
decode_func['8'] = decode_string
|
||||
decode_func['9'] = decode_string
|
||||
|
||||
def bdecode(x):
|
||||
try:
|
||||
r, l = decode_func[x[0]](x, 0)
|
||||
except (IndexError, KeyError, ValueError):
|
||||
raise BTFailure("not a valid bencoded string")
|
||||
if l != len(x):
|
||||
raise BTFailure("invalid bencoded value (data after valid prefix)")
|
||||
return r
|
||||
|
||||
from types import StringType, IntType, LongType, DictType, ListType, TupleType
|
||||
|
||||
|
||||
class Bencached(object):
|
||||
|
||||
__slots__ = ['bencoded']
|
||||
|
||||
def __init__(self, s):
|
||||
self.bencoded = s
|
||||
|
||||
def encode_bencached(x,r):
|
||||
r.append(x.bencoded)
|
||||
|
||||
def encode_int(x, r):
|
||||
r.extend(('i', str(x), 'e'))
|
||||
|
||||
def encode_bool(x, r):
|
||||
if x:
|
||||
encode_int(1, r)
|
||||
else:
|
||||
encode_int(0, r)
|
||||
|
||||
def encode_string(x, r):
|
||||
r.extend((str(len(x)), ':', x))
|
||||
|
||||
def encode_list(x, r):
|
||||
r.append('l')
|
||||
for i in x:
|
||||
encode_func[type(i)](i, r)
|
||||
r.append('e')
|
||||
|
||||
def encode_dict(x,r):
|
||||
r.append('d')
|
||||
ilist = x.items()
|
||||
ilist.sort()
|
||||
for k, v in ilist:
|
||||
r.extend((str(len(k)), ':', k))
|
||||
encode_func[type(v)](v, r)
|
||||
r.append('e')
|
||||
|
||||
encode_func = {}
|
||||
encode_func[Bencached] = encode_bencached
|
||||
encode_func[IntType] = encode_int
|
||||
encode_func[LongType] = encode_int
|
||||
encode_func[StringType] = encode_string
|
||||
encode_func[ListType] = encode_list
|
||||
encode_func[TupleType] = encode_list
|
||||
encode_func[DictType] = encode_dict
|
||||
|
||||
try:
|
||||
from types import BooleanType
|
||||
encode_func[BooleanType] = encode_bool
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def bencode(x):
|
||||
r = []
|
||||
encode_func[type(x)](x, r)
|
||||
return ''.join(r)
|
||||
@@ -1,9 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||
|
||||
This module offers extensions to the standard python 2.3+
|
||||
This module offers extensions to the standard Python
|
||||
datetime module.
|
||||
"""
|
||||
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
|
||||
__license__ = "PSF License"
|
||||
__version__ = "1.5"
|
||||
__author__ = "Tomi Pieviläinen <tomi.pievilainen@iki.fi>"
|
||||
__license__ = "Simplified BSD"
|
||||
__version__ = "2.1"
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""
|
||||
Copyright (c) 2003-2007 Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||
|
||||
This module offers extensions to the standard python 2.3+
|
||||
This module offers extensions to the standard Python
|
||||
datetime module.
|
||||
"""
|
||||
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
|
||||
__license__ = "PSF License"
|
||||
__license__ = "Simplified BSD"
|
||||
|
||||
import datetime
|
||||
|
||||
@@ -52,7 +51,7 @@ def easter(year, method=EASTER_WESTERN):
|
||||
"""
|
||||
|
||||
if not (1 <= method <= 3):
|
||||
raise ValueError, "invalid method"
|
||||
raise ValueError("invalid method")
|
||||
|
||||
# g - Golden year - 1
|
||||
# c - Century
|
||||
@@ -88,5 +87,5 @@ def easter(year, method=EASTER_WESTERN):
|
||||
p = i-j+e
|
||||
d = 1+(p+27+(p+6)//40)%31
|
||||
m = 3+(p+26)//30
|
||||
return datetime.date(int(y),int(m),int(d))
|
||||
return datetime.date(int(y), int(m), int(d))
|
||||
|
||||
|
||||
@@ -2,25 +2,27 @@
|
||||
"""
|
||||
Copyright (c) 2003-2007 Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||
|
||||
This module offers extensions to the standard python 2.3+
|
||||
This module offers extensions to the standard Python
|
||||
datetime module.
|
||||
"""
|
||||
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
|
||||
__license__ = "PSF License"
|
||||
from __future__ import unicode_literals
|
||||
__license__ = "Simplified BSD"
|
||||
|
||||
|
||||
import datetime
|
||||
import string
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import collections
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
from io import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
from io import StringIO
|
||||
|
||||
import relativedelta
|
||||
import tz
|
||||
from six import text_type, binary_type, integer_types
|
||||
|
||||
from . import relativedelta
|
||||
from . import tz
|
||||
|
||||
|
||||
__all__ = ["parse", "parserinfo"]
|
||||
@@ -39,7 +41,7 @@ __all__ = ["parse", "parserinfo"]
|
||||
class _timelex(object):
|
||||
|
||||
def __init__(self, instream):
|
||||
if isinstance(instream, basestring):
|
||||
if isinstance(instream, text_type):
|
||||
instream = StringIO(instream)
|
||||
self.instream = instream
|
||||
self.wordchars = ('abcdfeghijklmnopqrstuvwxyz'
|
||||
@@ -133,12 +135,15 @@ class _timelex(object):
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
def __next__(self):
|
||||
token = self.get_token()
|
||||
if token is None:
|
||||
raise StopIteration
|
||||
return token
|
||||
|
||||
def next(self):
|
||||
return self.__next__() # Python 2.x support
|
||||
|
||||
def split(cls, s):
|
||||
return list(cls(s))
|
||||
split = classmethod(split)
|
||||
@@ -155,7 +160,7 @@ class _resultbase(object):
|
||||
for attr in self.__slots__:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
l.append("%s=%s" % (attr, `value`))
|
||||
l.append("%s=%s" % (attr, repr(value)))
|
||||
return "%s(%s)" % (classname, ", ".join(l))
|
||||
|
||||
def __repr__(self):
|
||||
@@ -167,7 +172,7 @@ class parserinfo(object):
|
||||
# m from a.m/p.m, t from ISO T separator
|
||||
JUMP = [" ", ".", ",", ";", "-", "/", "'",
|
||||
"at", "on", "and", "ad", "m", "t", "of",
|
||||
"st", "nd", "rd", "th"]
|
||||
"st", "nd", "rd", "th"]
|
||||
|
||||
WEEKDAYS = [("Mon", "Monday"),
|
||||
("Tue", "Tuesday"),
|
||||
@@ -176,7 +181,7 @@ class parserinfo(object):
|
||||
("Fri", "Friday"),
|
||||
("Sat", "Saturday"),
|
||||
("Sun", "Sunday")]
|
||||
MONTHS = [("Jan", "January"),
|
||||
MONTHS = [("Jan", "January"),
|
||||
("Feb", "February"),
|
||||
("Mar", "March"),
|
||||
("Apr", "April"),
|
||||
@@ -184,7 +189,7 @@ class parserinfo(object):
|
||||
("Jun", "June"),
|
||||
("Jul", "July"),
|
||||
("Aug", "August"),
|
||||
("Sep", "September"),
|
||||
("Sep", "Sept", "September"),
|
||||
("Oct", "October"),
|
||||
("Nov", "November"),
|
||||
("Dec", "December")]
|
||||
@@ -197,7 +202,7 @@ class parserinfo(object):
|
||||
PERTAIN = ["of"]
|
||||
TZOFFSET = {}
|
||||
|
||||
def __init__(self, dayfirst=False, yearfirst=False):
|
||||
def __init__(self, dayfirst = False, yearfirst = False):
|
||||
self._jump = self._convert(self.JUMP)
|
||||
self._weekdays = self._convert(self.WEEKDAYS)
|
||||
self._months = self._convert(self.MONTHS)
|
||||
@@ -210,7 +215,7 @@ class parserinfo(object):
|
||||
self.yearfirst = yearfirst
|
||||
|
||||
self._year = time.localtime().tm_year
|
||||
self._century = self._year//100*100
|
||||
self._century = self._year // 100 * 100
|
||||
|
||||
def _convert(self, lst):
|
||||
dct = {}
|
||||
@@ -237,7 +242,7 @@ class parserinfo(object):
|
||||
def month(self, name):
|
||||
if len(name) >= 3:
|
||||
try:
|
||||
return self._months[name.lower()]+1
|
||||
return self._months[name.lower()] + 1
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
@@ -268,7 +273,7 @@ class parserinfo(object):
|
||||
def convertyear(self, year):
|
||||
if year < 100:
|
||||
year += self._century
|
||||
if abs(year-self._year) >= 50:
|
||||
if abs(year - self._year) >= 50:
|
||||
if year < self._year:
|
||||
year += 100
|
||||
else:
|
||||
@@ -289,18 +294,18 @@ class parserinfo(object):
|
||||
|
||||
class parser(object):
|
||||
|
||||
def __init__(self, info=None):
|
||||
def __init__(self, info = None):
|
||||
self.info = info or parserinfo()
|
||||
|
||||
def parse(self, timestr, default=None,
|
||||
ignoretz=False, tzinfos=None,
|
||||
def parse(self, timestr, default = None,
|
||||
ignoretz = False, tzinfos = None,
|
||||
**kwargs):
|
||||
if not default:
|
||||
default = datetime.datetime.now().replace(hour=0, minute=0,
|
||||
second=0, microsecond=0)
|
||||
default = datetime.datetime.now().replace(hour = 0, minute = 0,
|
||||
second = 0, microsecond = 0)
|
||||
res = self._parse(timestr, **kwargs)
|
||||
if res is None:
|
||||
raise ValueError, "unknown string format"
|
||||
raise ValueError("unknown string format")
|
||||
repl = {}
|
||||
for attr in ["year", "month", "day", "hour",
|
||||
"minute", "second", "microsecond"]:
|
||||
@@ -309,29 +314,29 @@ class parser(object):
|
||||
repl[attr] = value
|
||||
ret = default.replace(**repl)
|
||||
if res.weekday is not None and not res.day:
|
||||
ret = ret+relativedelta.relativedelta(weekday=res.weekday)
|
||||
ret = ret + relativedelta.relativedelta(weekday = res.weekday)
|
||||
if not ignoretz:
|
||||
if callable(tzinfos) or tzinfos and res.tzname in tzinfos:
|
||||
if callable(tzinfos):
|
||||
if isinstance(tzinfos, collections.Callable) or tzinfos and res.tzname in tzinfos:
|
||||
if isinstance(tzinfos, collections.Callable):
|
||||
tzdata = tzinfos(res.tzname, res.tzoffset)
|
||||
else:
|
||||
tzdata = tzinfos.get(res.tzname)
|
||||
if isinstance(tzdata, datetime.tzinfo):
|
||||
tzinfo = tzdata
|
||||
elif isinstance(tzdata, basestring):
|
||||
elif isinstance(tzdata, text_type):
|
||||
tzinfo = tz.tzstr(tzdata)
|
||||
elif isinstance(tzdata, int):
|
||||
elif isinstance(tzdata, integer_types):
|
||||
tzinfo = tz.tzoffset(res.tzname, tzdata)
|
||||
else:
|
||||
raise ValueError, "offset must be tzinfo subclass, " \
|
||||
"tz string, or int offset"
|
||||
ret = ret.replace(tzinfo=tzinfo)
|
||||
raise ValueError("offset must be tzinfo subclass, " \
|
||||
"tz string, or int offset")
|
||||
ret = ret.replace(tzinfo = tzinfo)
|
||||
elif res.tzname and res.tzname in time.tzname:
|
||||
ret = ret.replace(tzinfo=tz.tzlocal())
|
||||
ret = ret.replace(tzinfo = tz.tzlocal())
|
||||
elif res.tzoffset == 0:
|
||||
ret = ret.replace(tzinfo=tz.tzutc())
|
||||
ret = ret.replace(tzinfo = tz.tzutc())
|
||||
elif res.tzoffset:
|
||||
ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
|
||||
ret = ret.replace(tzinfo = tz.tzoffset(res.tzname, res.tzoffset))
|
||||
return ret
|
||||
|
||||
class _result(_resultbase):
|
||||
@@ -339,7 +344,7 @@ class parser(object):
|
||||
"hour", "minute", "second", "microsecond",
|
||||
"tzname", "tzoffset"]
|
||||
|
||||
def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False):
|
||||
def _parse(self, timestr, dayfirst = None, yearfirst = None, fuzzy = False):
|
||||
info = self.info
|
||||
if dayfirst is None:
|
||||
dayfirst = info.dayfirst
|
||||
@@ -374,14 +379,14 @@ class parser(object):
|
||||
and (i >= len_l or (l[i] != ':' and
|
||||
info.hms(l[i]) is None))):
|
||||
# 19990101T23[59]
|
||||
s = l[i-1]
|
||||
s = l[i - 1]
|
||||
res.hour = int(s[:2])
|
||||
if len_li == 4:
|
||||
res.minute = int(s[2:])
|
||||
elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6):
|
||||
elif len_li == 6 or (len_li > 6 and l[i - 1].find('.') == 6):
|
||||
# YYMMDD or HHMMSS[.ss]
|
||||
s = l[i-1]
|
||||
if not ymd and l[i-1].find('.') == -1:
|
||||
s = l[i - 1]
|
||||
if not ymd and l[i - 1].find('.') == -1:
|
||||
ymd.append(info.convertyear(int(s[:2])))
|
||||
ymd.append(int(s[2:4]))
|
||||
ymd.append(int(s[4:]))
|
||||
@@ -392,13 +397,13 @@ class parser(object):
|
||||
res.second, res.microsecond = _parsems(s[4:])
|
||||
elif len_li == 8:
|
||||
# YYYYMMDD
|
||||
s = l[i-1]
|
||||
s = l[i - 1]
|
||||
ymd.append(int(s[:4]))
|
||||
ymd.append(int(s[4:6]))
|
||||
ymd.append(int(s[6:]))
|
||||
elif len_li in (12, 14):
|
||||
# YYYYMMDDhhmm[ss]
|
||||
s = l[i-1]
|
||||
s = l[i - 1]
|
||||
ymd.append(int(s[:4]))
|
||||
ymd.append(int(s[4:6]))
|
||||
ymd.append(int(s[6:8]))
|
||||
@@ -407,8 +412,8 @@ class parser(object):
|
||||
if len_li == 14:
|
||||
res.second = int(s[12:])
|
||||
elif ((i < len_l and info.hms(l[i]) is not None) or
|
||||
(i+1 < len_l and l[i] == ' ' and
|
||||
info.hms(l[i+1]) is not None)):
|
||||
(i + 1 < len_l and l[i] == ' ' and
|
||||
info.hms(l[i + 1]) is not None)):
|
||||
# HH[ ]h or MM[ ]m or SS[.ss][ ]s
|
||||
if l[i] == ' ':
|
||||
i += 1
|
||||
@@ -416,12 +421,12 @@ class parser(object):
|
||||
while True:
|
||||
if idx == 0:
|
||||
res.hour = int(value)
|
||||
if value%1:
|
||||
res.minute = int(60*(value%1))
|
||||
if value % 1:
|
||||
res.minute = int(60 * (value % 1))
|
||||
elif idx == 1:
|
||||
res.minute = int(value)
|
||||
if value%1:
|
||||
res.second = int(60*(value%1))
|
||||
if value % 1:
|
||||
res.second = int(60 * (value % 1))
|
||||
elif idx == 2:
|
||||
res.second, res.microsecond = \
|
||||
_parsems(value_repr)
|
||||
@@ -441,17 +446,28 @@ class parser(object):
|
||||
newidx = info.hms(l[i])
|
||||
if newidx is not None:
|
||||
idx = newidx
|
||||
elif i+1 < len_l and l[i] == ':':
|
||||
elif i == len_l and l[i - 2] == ' ' and info.hms(l[i - 3]) is not None:
|
||||
# X h MM or X m SS
|
||||
idx = info.hms(l[i - 3]) + 1
|
||||
if idx == 1:
|
||||
res.minute = int(value)
|
||||
if value % 1:
|
||||
res.second = int(60 * (value % 1))
|
||||
elif idx == 2:
|
||||
res.second, res.microsecond = \
|
||||
_parsems(value_repr)
|
||||
i += 1
|
||||
elif i + 1 < len_l and l[i] == ':':
|
||||
# HH:MM[:SS[.ss]]
|
||||
res.hour = int(value)
|
||||
i += 1
|
||||
value = float(l[i])
|
||||
res.minute = int(value)
|
||||
if value%1:
|
||||
res.second = int(60*(value%1))
|
||||
if value % 1:
|
||||
res.second = int(60 * (value % 1))
|
||||
i += 1
|
||||
if i < len_l and l[i] == ':':
|
||||
res.second, res.microsecond = _parsems(l[i+1])
|
||||
res.second, res.microsecond = _parsems(l[i + 1])
|
||||
i += 2
|
||||
elif i < len_l and l[i] in ('-', '/', '.'):
|
||||
sep = l[i]
|
||||
@@ -467,7 +483,7 @@ class parser(object):
|
||||
if value is not None:
|
||||
ymd.append(value)
|
||||
assert mstridx == -1
|
||||
mstridx = len(ymd)-1
|
||||
mstridx = len(ymd) - 1
|
||||
else:
|
||||
return None
|
||||
i += 1
|
||||
@@ -477,18 +493,18 @@ class parser(object):
|
||||
value = info.month(l[i])
|
||||
if value is not None:
|
||||
ymd.append(value)
|
||||
mstridx = len(ymd)-1
|
||||
mstridx = len(ymd) - 1
|
||||
assert mstridx == -1
|
||||
else:
|
||||
ymd.append(int(l[i]))
|
||||
i += 1
|
||||
elif i >= len_l or info.jump(l[i]):
|
||||
if i+1 < len_l and info.ampm(l[i+1]) is not None:
|
||||
if i + 1 < len_l and info.ampm(l[i + 1]) is not None:
|
||||
# 12 am
|
||||
res.hour = int(value)
|
||||
if res.hour < 12 and info.ampm(l[i+1]) == 1:
|
||||
if res.hour < 12 and info.ampm(l[i + 1]) == 1:
|
||||
res.hour += 12
|
||||
elif res.hour == 12 and info.ampm(l[i+1]) == 0:
|
||||
elif res.hour == 12 and info.ampm(l[i + 1]) == 0:
|
||||
res.hour = 0
|
||||
i += 1
|
||||
else:
|
||||
@@ -521,7 +537,7 @@ class parser(object):
|
||||
if value is not None:
|
||||
ymd.append(value)
|
||||
assert mstridx == -1
|
||||
mstridx = len(ymd)-1
|
||||
mstridx = len(ymd) - 1
|
||||
i += 1
|
||||
if i < len_l:
|
||||
if l[i] in ('-', '/'):
|
||||
@@ -535,12 +551,12 @@ class parser(object):
|
||||
i += 1
|
||||
ymd.append(int(l[i]))
|
||||
i += 1
|
||||
elif (i+3 < len_l and l[i] == l[i+2] == ' '
|
||||
and info.pertain(l[i+1])):
|
||||
elif (i + 3 < len_l and l[i] == l[i + 2] == ' '
|
||||
and info.pertain(l[i + 1])):
|
||||
# Jan of 01
|
||||
# In this case, 01 is clearly year
|
||||
try:
|
||||
value = int(l[i+3])
|
||||
value = int(l[i + 3])
|
||||
except ValueError:
|
||||
# Wrong guess
|
||||
pass
|
||||
@@ -585,32 +601,32 @@ class parser(object):
|
||||
|
||||
# Check for a numbered timezone
|
||||
if res.hour is not None and l[i] in ('+', '-'):
|
||||
signal = (-1,1)[l[i] == '+']
|
||||
signal = (-1, 1)[l[i] == '+']
|
||||
i += 1
|
||||
len_li = len(l[i])
|
||||
if len_li == 4:
|
||||
# -0300
|
||||
res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60
|
||||
elif i+1 < len_l and l[i+1] == ':':
|
||||
res.tzoffset = int(l[i][:2]) * 3600 + int(l[i][2:]) * 60
|
||||
elif i + 1 < len_l and l[i + 1] == ':':
|
||||
# -03:00
|
||||
res.tzoffset = int(l[i])*3600+int(l[i+2])*60
|
||||
res.tzoffset = int(l[i]) * 3600 + int(l[i + 2]) * 60
|
||||
i += 2
|
||||
elif len_li <= 2:
|
||||
# -[0]3
|
||||
res.tzoffset = int(l[i][:2])*3600
|
||||
res.tzoffset = int(l[i][:2]) * 3600
|
||||
else:
|
||||
return None
|
||||
i += 1
|
||||
res.tzoffset *= signal
|
||||
|
||||
# Look for a timezone name between parenthesis
|
||||
if (i+3 < len_l and
|
||||
info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and
|
||||
3 <= len(l[i+2]) <= 5 and
|
||||
not [x for x in l[i+2]
|
||||
if (i + 3 < len_l and
|
||||
info.jump(l[i]) and l[i + 1] == '(' and l[i + 3] == ')' and
|
||||
3 <= len(l[i + 2]) <= 5 and
|
||||
not [x for x in l[i + 2]
|
||||
if x not in string.ascii_uppercase]):
|
||||
# -0300 (BRST)
|
||||
res.tzname = l[i+2]
|
||||
res.tzname = l[i + 2]
|
||||
i += 4
|
||||
continue
|
||||
|
||||
@@ -690,7 +706,12 @@ class parser(object):
|
||||
return res
|
||||
|
||||
DEFAULTPARSER = parser()
|
||||
def parse(timestr, parserinfo=None, **kwargs):
|
||||
def parse(timestr, parserinfo = None, **kwargs):
|
||||
# Python 2.x support: datetimes return their string presentation as
|
||||
# bytes in 2.x and unicode in 3.x, so it's reasonable to expect that
|
||||
# the parser will get both kinds. Internally we use unicode only.
|
||||
if isinstance(timestr, binary_type):
|
||||
timestr = timestr.decode()
|
||||
if parserinfo:
|
||||
return parser(parserinfo).parse(timestr, **kwargs)
|
||||
else:
|
||||
@@ -743,7 +764,7 @@ class _tzparser(object):
|
||||
if l[i] in ('+', '-'):
|
||||
# Yes, that's right. See the TZ variable
|
||||
# documentation.
|
||||
signal = (1,-1)[l[i] == '+']
|
||||
signal = (1, -1)[l[i] == '+']
|
||||
i += 1
|
||||
else:
|
||||
signal = -1
|
||||
@@ -751,16 +772,16 @@ class _tzparser(object):
|
||||
if len_li == 4:
|
||||
# -0300
|
||||
setattr(res, offattr,
|
||||
(int(l[i][:2])*3600+int(l[i][2:])*60)*signal)
|
||||
elif i+1 < len_l and l[i+1] == ':':
|
||||
(int(l[i][:2]) * 3600 + int(l[i][2:]) * 60) * signal)
|
||||
elif i + 1 < len_l and l[i + 1] == ':':
|
||||
# -03:00
|
||||
setattr(res, offattr,
|
||||
(int(l[i])*3600+int(l[i+2])*60)*signal)
|
||||
(int(l[i]) * 3600 + int(l[i + 2]) * 60) * signal)
|
||||
i += 2
|
||||
elif len_li <= 2:
|
||||
# -[0]3
|
||||
setattr(res, offattr,
|
||||
int(l[i][:2])*3600*signal)
|
||||
int(l[i][:2]) * 3600 * signal)
|
||||
else:
|
||||
return None
|
||||
i += 1
|
||||
@@ -787,29 +808,29 @@ class _tzparser(object):
|
||||
x.month = int(l[i])
|
||||
i += 2
|
||||
if l[i] == '-':
|
||||
value = int(l[i+1])*-1
|
||||
value = int(l[i + 1]) * -1
|
||||
i += 1
|
||||
else:
|
||||
value = int(l[i])
|
||||
i += 2
|
||||
if value:
|
||||
x.week = value
|
||||
x.weekday = (int(l[i])-1)%7
|
||||
x.weekday = (int(l[i]) - 1) % 7
|
||||
else:
|
||||
x.day = int(l[i])
|
||||
i += 2
|
||||
x.time = int(l[i])
|
||||
i += 2
|
||||
if i < len_l:
|
||||
if l[i] in ('-','+'):
|
||||
signal = (-1,1)[l[i] == "+"]
|
||||
if l[i] in ('-', '+'):
|
||||
signal = (-1, 1)[l[i] == "+"]
|
||||
i += 1
|
||||
else:
|
||||
signal = 1
|
||||
res.dstoffset = (res.stdoffset+int(l[i]))*signal
|
||||
res.dstoffset = (res.stdoffset + int(l[i])) * signal
|
||||
elif (l.count(',') == 2 and l[i:].count('/') <= 2 and
|
||||
not [y for x in l[i:] if x not in (',','/','J','M',
|
||||
'.','-',':')
|
||||
not [y for x in l[i:] if x not in (',', '/', 'J', 'M',
|
||||
'.', '-', ':')
|
||||
for y in x if y not in "0123456789"]):
|
||||
for x in (res.start, res.end):
|
||||
if l[i] == 'J':
|
||||
@@ -829,10 +850,10 @@ class _tzparser(object):
|
||||
i += 1
|
||||
assert l[i] in ('-', '.')
|
||||
i += 1
|
||||
x.weekday = (int(l[i])-1)%7
|
||||
x.weekday = (int(l[i]) - 1) % 7
|
||||
else:
|
||||
# year day (zero based)
|
||||
x.yday = int(l[i])+1
|
||||
x.yday = int(l[i]) + 1
|
||||
|
||||
i += 1
|
||||
|
||||
@@ -842,17 +863,17 @@ class _tzparser(object):
|
||||
len_li = len(l[i])
|
||||
if len_li == 4:
|
||||
# -0300
|
||||
x.time = (int(l[i][:2])*3600+int(l[i][2:])*60)
|
||||
elif i+1 < len_l and l[i+1] == ':':
|
||||
x.time = (int(l[i][:2]) * 3600 + int(l[i][2:]) * 60)
|
||||
elif i + 1 < len_l and l[i + 1] == ':':
|
||||
# -03:00
|
||||
x.time = int(l[i])*3600+int(l[i+2])*60
|
||||
x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60
|
||||
i += 2
|
||||
if i+1 < len_l and l[i+1] == ':':
|
||||
if i + 1 < len_l and l[i + 1] == ':':
|
||||
i += 2
|
||||
x.time += int(l[i])
|
||||
elif len_li <= 2:
|
||||
# -[0]3
|
||||
x.time = (int(l[i][:2])*3600)
|
||||
x.time = (int(l[i][:2]) * 3600)
|
||||
else:
|
||||
return None
|
||||
i += 1
|
||||
@@ -865,7 +886,7 @@ class _tzparser(object):
|
||||
|
||||
except (IndexError, ValueError, AssertionError):
|
||||
return None
|
||||
|
||||
|
||||
return res
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"""
|
||||
Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||
|
||||
This module offers extensions to the standard python 2.3+
|
||||
This module offers extensions to the standard Python
|
||||
datetime module.
|
||||
"""
|
||||
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
|
||||
__license__ = "PSF License"
|
||||
__license__ = "Simplified BSD"
|
||||
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
from six import integer_types
|
||||
|
||||
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
||||
|
||||
class weekday(object):
|
||||
@@ -42,7 +43,7 @@ class weekday(object):
|
||||
|
||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
|
||||
|
||||
class relativedelta:
|
||||
class relativedelta(object):
|
||||
"""
|
||||
The relativedelta type is based on the specification of the excelent
|
||||
work done by M.-A. Lemburg in his mx.DateTime extension. However,
|
||||
@@ -113,10 +114,9 @@ Here is the behavior of operations with relativedelta:
|
||||
yearday=None, nlyearday=None,
|
||||
hour=None, minute=None, second=None, microsecond=None):
|
||||
if dt1 and dt2:
|
||||
if not isinstance(dt1, datetime.date) or \
|
||||
not isinstance(dt2, datetime.date):
|
||||
raise TypeError, "relativedelta only diffs datetime/date"
|
||||
if type(dt1) is not type(dt2):
|
||||
if (not isinstance(dt1, datetime.date)) or (not isinstance(dt2, datetime.date)):
|
||||
raise TypeError("relativedelta only diffs datetime/date")
|
||||
if not type(dt1) == type(dt2): #isinstance(dt1, type(dt2)):
|
||||
if not isinstance(dt1, datetime.datetime):
|
||||
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
||||
elif not isinstance(dt2, datetime.datetime):
|
||||
@@ -172,7 +172,7 @@ Here is the behavior of operations with relativedelta:
|
||||
self.second = second
|
||||
self.microsecond = microsecond
|
||||
|
||||
if type(weekday) is int:
|
||||
if isinstance(weekday, integer_types):
|
||||
self.weekday = weekdays[weekday]
|
||||
else:
|
||||
self.weekday = weekday
|
||||
@@ -185,7 +185,7 @@ Here is the behavior of operations with relativedelta:
|
||||
if yearday > 59:
|
||||
self.leapdays = -1
|
||||
if yday:
|
||||
ydayidx = [31,59,90,120,151,181,212,243,273,304,334,366]
|
||||
ydayidx = [31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 366]
|
||||
for idx, ydays in enumerate(ydayidx):
|
||||
if yday <= ydays:
|
||||
self.month = idx+1
|
||||
@@ -195,7 +195,7 @@ Here is the behavior of operations with relativedelta:
|
||||
self.day = yday-ydayidx[idx-1]
|
||||
break
|
||||
else:
|
||||
raise ValueError, "invalid year day (%d)" % yday
|
||||
raise ValueError("invalid year day (%d)" % yday)
|
||||
|
||||
self._fix()
|
||||
|
||||
@@ -242,9 +242,26 @@ Here is the behavior of operations with relativedelta:
|
||||
else:
|
||||
self.years = 0
|
||||
|
||||
def __radd__(self, other):
|
||||
def __add__(self, other):
|
||||
if isinstance(other, relativedelta):
|
||||
return relativedelta(years=other.years+self.years,
|
||||
months=other.months+self.months,
|
||||
days=other.days+self.days,
|
||||
hours=other.hours+self.hours,
|
||||
minutes=other.minutes+self.minutes,
|
||||
seconds=other.seconds+self.seconds,
|
||||
microseconds=other.microseconds+self.microseconds,
|
||||
leapdays=other.leapdays or self.leapdays,
|
||||
year=other.year or self.year,
|
||||
month=other.month or self.month,
|
||||
day=other.day or self.day,
|
||||
weekday=other.weekday or self.weekday,
|
||||
hour=other.hour or self.hour,
|
||||
minute=other.minute or self.minute,
|
||||
second=other.second or self.second,
|
||||
microsecond=other.microsecond or self.microsecond)
|
||||
if not isinstance(other, datetime.date):
|
||||
raise TypeError, "unsupported type for add operation"
|
||||
raise TypeError("unsupported type for add operation")
|
||||
elif self._has_time and not isinstance(other, datetime.datetime):
|
||||
other = datetime.datetime.fromordinal(other.toordinal())
|
||||
year = (self.year or other.year)+self.years
|
||||
@@ -285,48 +302,31 @@ Here is the behavior of operations with relativedelta:
|
||||
ret += datetime.timedelta(days=jumpdays)
|
||||
return ret
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.__add__(other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.__neg__().__radd__(other)
|
||||
|
||||
def __add__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
raise TypeError, "unsupported type for add operation"
|
||||
return relativedelta(years=other.years+self.years,
|
||||
months=other.months+self.months,
|
||||
days=other.days+self.days,
|
||||
hours=other.hours+self.hours,
|
||||
minutes=other.minutes+self.minutes,
|
||||
seconds=other.seconds+self.seconds,
|
||||
microseconds=other.microseconds+self.microseconds,
|
||||
leapdays=other.leapdays or self.leapdays,
|
||||
year=other.year or self.year,
|
||||
month=other.month or self.month,
|
||||
day=other.day or self.day,
|
||||
weekday=other.weekday or self.weekday,
|
||||
hour=other.hour or self.hour,
|
||||
minute=other.minute or self.minute,
|
||||
second=other.second or self.second,
|
||||
microsecond=other.second or self.microsecond)
|
||||
|
||||
def __sub__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
raise TypeError, "unsupported type for sub operation"
|
||||
return relativedelta(years=other.years-self.years,
|
||||
months=other.months-self.months,
|
||||
days=other.days-self.days,
|
||||
hours=other.hours-self.hours,
|
||||
minutes=other.minutes-self.minutes,
|
||||
seconds=other.seconds-self.seconds,
|
||||
microseconds=other.microseconds-self.microseconds,
|
||||
leapdays=other.leapdays or self.leapdays,
|
||||
year=other.year or self.year,
|
||||
month=other.month or self.month,
|
||||
day=other.day or self.day,
|
||||
weekday=other.weekday or self.weekday,
|
||||
hour=other.hour or self.hour,
|
||||
minute=other.minute or self.minute,
|
||||
second=other.second or self.second,
|
||||
microsecond=other.second or self.microsecond)
|
||||
raise TypeError("unsupported type for sub operation")
|
||||
return relativedelta(years=self.years-other.years,
|
||||
months=self.months-other.months,
|
||||
days=self.days-other.days,
|
||||
hours=self.hours-other.hours,
|
||||
minutes=self.minutes-other.minutes,
|
||||
seconds=self.seconds-other.seconds,
|
||||
microseconds=self.microseconds-other.microseconds,
|
||||
leapdays=self.leapdays or other.leapdays,
|
||||
year=self.year or other.year,
|
||||
month=self.month or other.month,
|
||||
day=self.day or other.day,
|
||||
weekday=self.weekday or other.weekday,
|
||||
hour=self.hour or other.hour,
|
||||
minute=self.minute or other.minute,
|
||||
second=self.second or other.second,
|
||||
microsecond=self.microsecond or other.microsecond)
|
||||
|
||||
def __neg__(self):
|
||||
return relativedelta(years=-self.years,
|
||||
@@ -346,7 +346,7 @@ Here is the behavior of operations with relativedelta:
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __nonzero__(self):
|
||||
def __bool__(self):
|
||||
return not (not self.years and
|
||||
not self.months and
|
||||
not self.days and
|
||||
@@ -366,13 +366,13 @@ Here is the behavior of operations with relativedelta:
|
||||
|
||||
def __mul__(self, other):
|
||||
f = float(other)
|
||||
return relativedelta(years=self.years*f,
|
||||
months=self.months*f,
|
||||
days=self.days*f,
|
||||
hours=self.hours*f,
|
||||
minutes=self.minutes*f,
|
||||
seconds=self.seconds*f,
|
||||
microseconds=self.microseconds*f,
|
||||
return relativedelta(years=int(self.years*f),
|
||||
months=int(self.months*f),
|
||||
days=int(self.days*f),
|
||||
hours=int(self.hours*f),
|
||||
minutes=int(self.minutes*f),
|
||||
seconds=int(self.seconds*f),
|
||||
microseconds=int(self.microseconds*f),
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
@@ -383,6 +383,8 @@ Here is the behavior of operations with relativedelta:
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
return False
|
||||
@@ -415,6 +417,8 @@ Here is the behavior of operations with relativedelta:
|
||||
def __div__(self, other):
|
||||
return self.__mul__(1/float(other))
|
||||
|
||||
__truediv__ = __div__
|
||||
|
||||
def __repr__(self):
|
||||
l = []
|
||||
for attr in ["years", "months", "days", "leapdays",
|
||||
@@ -426,7 +430,7 @@ Here is the behavior of operations with relativedelta:
|
||||
"hour", "minute", "second", "microsecond"]:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
l.append("%s=%s" % (attr, `value`))
|
||||
l.append("%s=%s" % (attr, repr(value)))
|
||||
return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
|
||||
|
||||
# vim:ts=4:sw=4:et
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
"""
|
||||
Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||
|
||||
This module offers extensions to the standard python 2.3+
|
||||
This module offers extensions to the standard Python
|
||||
datetime module.
|
||||
"""
|
||||
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
|
||||
__license__ = "PSF License"
|
||||
__license__ = "Simplified BSD"
|
||||
|
||||
import itertools
|
||||
import datetime
|
||||
import calendar
|
||||
import thread
|
||||
try:
|
||||
import _thread
|
||||
except ImportError:
|
||||
import thread as _thread
|
||||
import sys
|
||||
|
||||
from six import advance_iterator, integer_types
|
||||
|
||||
__all__ = ["rrule", "rruleset", "rrulestr",
|
||||
"YEARLY", "MONTHLY", "WEEKLY", "DAILY",
|
||||
"HOURLY", "MINUTELY", "SECONDLY",
|
||||
@@ -22,15 +26,15 @@ __all__ = ["rrule", "rruleset", "rrulestr",
|
||||
M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30+
|
||||
[7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
|
||||
M365MASK = list(M366MASK)
|
||||
M29, M30, M31 = range(1,30), range(1,31), range(1,32)
|
||||
M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
|
||||
MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
|
||||
MDAY365MASK = list(MDAY366MASK)
|
||||
M29, M30, M31 = range(-29,0), range(-30,0), range(-31,0)
|
||||
M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
|
||||
NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
|
||||
NMDAY365MASK = list(NMDAY366MASK)
|
||||
M366RANGE = (0,31,60,91,121,152,182,213,244,274,305,335,366)
|
||||
M365RANGE = (0,31,59,90,120,151,181,212,243,273,304,334,365)
|
||||
WDAYMASK = [0,1,2,3,4,5,6]*55
|
||||
M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
|
||||
M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
|
||||
WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
|
||||
del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
|
||||
MDAY365MASK = tuple(MDAY365MASK)
|
||||
M365MASK = tuple(M365MASK)
|
||||
@@ -41,7 +45,7 @@ M365MASK = tuple(M365MASK)
|
||||
DAILY,
|
||||
HOURLY,
|
||||
MINUTELY,
|
||||
SECONDLY) = range(7)
|
||||
SECONDLY) = list(range(7))
|
||||
|
||||
# Imported on demand.
|
||||
easter = None
|
||||
@@ -52,7 +56,7 @@ class weekday(object):
|
||||
|
||||
def __init__(self, weekday, n=None):
|
||||
if n == 0:
|
||||
raise ValueError, "Can't create weekday with n == 0"
|
||||
raise ValueError("Can't create weekday with n == 0")
|
||||
self.weekday = weekday
|
||||
self.n = n
|
||||
|
||||
@@ -79,11 +83,11 @@ class weekday(object):
|
||||
|
||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
|
||||
|
||||
class rrulebase:
|
||||
class rrulebase(object):
|
||||
def __init__(self, cache=False):
|
||||
if cache:
|
||||
self._cache = []
|
||||
self._cache_lock = thread.allocate_lock()
|
||||
self._cache_lock = _thread.allocate_lock()
|
||||
self._cache_gen = self._iter()
|
||||
self._cache_complete = False
|
||||
else:
|
||||
@@ -112,7 +116,7 @@ class rrulebase:
|
||||
break
|
||||
try:
|
||||
for j in range(10):
|
||||
cache.append(gen.next())
|
||||
cache.append(advance_iterator(gen))
|
||||
except StopIteration:
|
||||
self._cache_gen = gen = None
|
||||
self._cache_complete = True
|
||||
@@ -133,13 +137,13 @@ class rrulebase:
|
||||
else:
|
||||
return list(itertools.islice(self,
|
||||
item.start or 0,
|
||||
item.stop or sys.maxint,
|
||||
item.stop or sys.maxsize,
|
||||
item.step or 1))
|
||||
elif item >= 0:
|
||||
gen = iter(self)
|
||||
try:
|
||||
for i in range(item+1):
|
||||
res = gen.next()
|
||||
res = advance_iterator(gen)
|
||||
except StopIteration:
|
||||
raise IndexError
|
||||
return res
|
||||
@@ -232,7 +236,7 @@ class rrule(rrulebase):
|
||||
byweekno=None, byweekday=None,
|
||||
byhour=None, byminute=None, bysecond=None,
|
||||
cache=False):
|
||||
rrulebase.__init__(self, cache)
|
||||
super(rrule, self).__init__(cache)
|
||||
global easter
|
||||
if not dtstart:
|
||||
dtstart = datetime.datetime.now().replace(microsecond=0)
|
||||
@@ -250,13 +254,13 @@ class rrule(rrulebase):
|
||||
self._until = until
|
||||
if wkst is None:
|
||||
self._wkst = calendar.firstweekday()
|
||||
elif type(wkst) is int:
|
||||
elif isinstance(wkst, integer_types):
|
||||
self._wkst = wkst
|
||||
else:
|
||||
self._wkst = wkst.weekday
|
||||
if bysetpos is None:
|
||||
self._bysetpos = None
|
||||
elif type(bysetpos) is int:
|
||||
elif isinstance(bysetpos, integer_types):
|
||||
if bysetpos == 0 or not (-366 <= bysetpos <= 366):
|
||||
raise ValueError("bysetpos must be between 1 and 366, "
|
||||
"or between -366 and -1")
|
||||
@@ -280,14 +284,14 @@ class rrule(rrulebase):
|
||||
# bymonth
|
||||
if not bymonth:
|
||||
self._bymonth = None
|
||||
elif type(bymonth) is int:
|
||||
elif isinstance(bymonth, integer_types):
|
||||
self._bymonth = (bymonth,)
|
||||
else:
|
||||
self._bymonth = tuple(bymonth)
|
||||
# byyearday
|
||||
if not byyearday:
|
||||
self._byyearday = None
|
||||
elif type(byyearday) is int:
|
||||
elif isinstance(byyearday, integer_types):
|
||||
self._byyearday = (byyearday,)
|
||||
else:
|
||||
self._byyearday = tuple(byyearday)
|
||||
@@ -295,7 +299,7 @@ class rrule(rrulebase):
|
||||
if byeaster is not None:
|
||||
if not easter:
|
||||
from dateutil import easter
|
||||
if type(byeaster) is int:
|
||||
if isinstance(byeaster, integer_types):
|
||||
self._byeaster = (byeaster,)
|
||||
else:
|
||||
self._byeaster = tuple(byeaster)
|
||||
@@ -305,7 +309,7 @@ class rrule(rrulebase):
|
||||
if not bymonthday:
|
||||
self._bymonthday = ()
|
||||
self._bynmonthday = ()
|
||||
elif type(bymonthday) is int:
|
||||
elif isinstance(bymonthday, integer_types):
|
||||
if bymonthday < 0:
|
||||
self._bynmonthday = (bymonthday,)
|
||||
self._bymonthday = ()
|
||||
@@ -318,7 +322,7 @@ class rrule(rrulebase):
|
||||
# byweekno
|
||||
if byweekno is None:
|
||||
self._byweekno = None
|
||||
elif type(byweekno) is int:
|
||||
elif isinstance(byweekno, integer_types):
|
||||
self._byweekno = (byweekno,)
|
||||
else:
|
||||
self._byweekno = tuple(byweekno)
|
||||
@@ -326,7 +330,7 @@ class rrule(rrulebase):
|
||||
if byweekday is None:
|
||||
self._byweekday = None
|
||||
self._bynweekday = None
|
||||
elif type(byweekday) is int:
|
||||
elif isinstance(byweekday, integer_types):
|
||||
self._byweekday = (byweekday,)
|
||||
self._bynweekday = None
|
||||
elif hasattr(byweekday, "n"):
|
||||
@@ -340,7 +344,7 @@ class rrule(rrulebase):
|
||||
self._byweekday = []
|
||||
self._bynweekday = []
|
||||
for wday in byweekday:
|
||||
if type(wday) is int:
|
||||
if isinstance(wday, integer_types):
|
||||
self._byweekday.append(wday)
|
||||
elif not wday.n or freq > MONTHLY:
|
||||
self._byweekday.append(wday.weekday)
|
||||
@@ -358,7 +362,7 @@ class rrule(rrulebase):
|
||||
self._byhour = (dtstart.hour,)
|
||||
else:
|
||||
self._byhour = None
|
||||
elif type(byhour) is int:
|
||||
elif isinstance(byhour, integer_types):
|
||||
self._byhour = (byhour,)
|
||||
else:
|
||||
self._byhour = tuple(byhour)
|
||||
@@ -368,7 +372,7 @@ class rrule(rrulebase):
|
||||
self._byminute = (dtstart.minute,)
|
||||
else:
|
||||
self._byminute = None
|
||||
elif type(byminute) is int:
|
||||
elif isinstance(byminute, integer_types):
|
||||
self._byminute = (byminute,)
|
||||
else:
|
||||
self._byminute = tuple(byminute)
|
||||
@@ -378,7 +382,7 @@ class rrule(rrulebase):
|
||||
self._bysecond = (dtstart.second,)
|
||||
else:
|
||||
self._bysecond = None
|
||||
elif type(bysecond) is int:
|
||||
elif isinstance(bysecond, integer_types):
|
||||
self._bysecond = (bysecond,)
|
||||
else:
|
||||
self._bysecond = tuple(bysecond)
|
||||
@@ -716,7 +720,7 @@ class _iterinfo(object):
|
||||
# days from last year's last week number in
|
||||
# this year.
|
||||
if -1 not in rr._byweekno:
|
||||
lyearweekday = datetime.date(year-1,1,1).weekday()
|
||||
lyearweekday = datetime.date(year-1, 1, 1).weekday()
|
||||
lno1wkst = (7-lyearweekday+rr._wkst)%7
|
||||
lyearlen = 365+calendar.isleap(year-1)
|
||||
if lno1wkst >= 4:
|
||||
@@ -768,7 +772,7 @@ class _iterinfo(object):
|
||||
self.lastmonth = month
|
||||
|
||||
def ydayset(self, year, month, day):
|
||||
return range(self.yearlen), 0, self.yearlen
|
||||
return list(range(self.yearlen)), 0, self.yearlen
|
||||
|
||||
def mdayset(self, year, month, day):
|
||||
set = [None]*self.yearlen
|
||||
@@ -823,27 +827,38 @@ class _iterinfo(object):
|
||||
|
||||
class rruleset(rrulebase):
|
||||
|
||||
class _genitem:
|
||||
class _genitem(object):
|
||||
def __init__(self, genlist, gen):
|
||||
try:
|
||||
self.dt = gen()
|
||||
self.dt = advance_iterator(gen)
|
||||
genlist.append(self)
|
||||
except StopIteration:
|
||||
pass
|
||||
self.genlist = genlist
|
||||
self.gen = gen
|
||||
|
||||
def next(self):
|
||||
def __next__(self):
|
||||
try:
|
||||
self.dt = self.gen()
|
||||
self.dt = advance_iterator(self.gen)
|
||||
except StopIteration:
|
||||
self.genlist.remove(self)
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.dt, other.dt)
|
||||
next = __next__
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.dt < other.dt
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.dt > other.dt
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.dt == other.dt
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.dt != other.dt
|
||||
|
||||
def __init__(self, cache=False):
|
||||
rrulebase.__init__(self, cache)
|
||||
super(rruleset, self).__init__(cache)
|
||||
self._rrule = []
|
||||
self._rdate = []
|
||||
self._exrule = []
|
||||
@@ -851,7 +866,7 @@ class rruleset(rrulebase):
|
||||
|
||||
def rrule(self, rrule):
|
||||
self._rrule.append(rrule)
|
||||
|
||||
|
||||
def rdate(self, rdate):
|
||||
self._rdate.append(rdate)
|
||||
|
||||
@@ -864,14 +879,14 @@ class rruleset(rrulebase):
|
||||
def _iter(self):
|
||||
rlist = []
|
||||
self._rdate.sort()
|
||||
self._genitem(rlist, iter(self._rdate).next)
|
||||
for gen in [iter(x).next for x in self._rrule]:
|
||||
self._genitem(rlist, iter(self._rdate))
|
||||
for gen in [iter(x) for x in self._rrule]:
|
||||
self._genitem(rlist, gen)
|
||||
rlist.sort()
|
||||
exlist = []
|
||||
self._exdate.sort()
|
||||
self._genitem(exlist, iter(self._exdate).next)
|
||||
for gen in [iter(x).next for x in self._exrule]:
|
||||
self._genitem(exlist, iter(self._exdate))
|
||||
for gen in [iter(x) for x in self._exrule]:
|
||||
self._genitem(exlist, gen)
|
||||
exlist.sort()
|
||||
lastdt = None
|
||||
@@ -880,17 +895,17 @@ class rruleset(rrulebase):
|
||||
ritem = rlist[0]
|
||||
if not lastdt or lastdt != ritem.dt:
|
||||
while exlist and exlist[0] < ritem:
|
||||
exlist[0].next()
|
||||
advance_iterator(exlist[0])
|
||||
exlist.sort()
|
||||
if not exlist or ritem != exlist[0]:
|
||||
total += 1
|
||||
yield ritem.dt
|
||||
lastdt = ritem.dt
|
||||
ritem.next()
|
||||
advance_iterator(ritem)
|
||||
rlist.sort()
|
||||
self._len = total
|
||||
|
||||
class _rrulestr:
|
||||
class _rrulestr(object):
|
||||
|
||||
_freq_map = {"YEARLY": YEARLY,
|
||||
"MONTHLY": MONTHLY,
|
||||
@@ -932,7 +947,7 @@ class _rrulestr:
|
||||
ignoretz=kwargs.get("ignoretz"),
|
||||
tzinfos=kwargs.get("tzinfos"))
|
||||
except ValueError:
|
||||
raise ValueError, "invalid until date"
|
||||
raise ValueError("invalid until date")
|
||||
|
||||
def _handle_WKST(self, rrkwargs, name, value, **kwargs):
|
||||
rrkwargs["wkst"] = self._weekday_map[value]
|
||||
@@ -959,7 +974,7 @@ class _rrulestr:
|
||||
if line.find(':') != -1:
|
||||
name, value = line.split(':')
|
||||
if name != "RRULE":
|
||||
raise ValueError, "unknown parameter name"
|
||||
raise ValueError("unknown parameter name")
|
||||
else:
|
||||
value = line
|
||||
rrkwargs = {}
|
||||
@@ -972,9 +987,9 @@ class _rrulestr:
|
||||
ignoretz=ignoretz,
|
||||
tzinfos=tzinfos)
|
||||
except AttributeError:
|
||||
raise ValueError, "unknown parameter '%s'" % name
|
||||
raise ValueError("unknown parameter '%s'" % name)
|
||||
except (KeyError, ValueError):
|
||||
raise ValueError, "invalid '%s': %s" % (name, value)
|
||||
raise ValueError("invalid '%s': %s" % (name, value))
|
||||
return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
|
||||
|
||||
def _parse_rfc(self, s,
|
||||
@@ -991,7 +1006,7 @@ class _rrulestr:
|
||||
unfold = True
|
||||
s = s.upper()
|
||||
if not s.strip():
|
||||
raise ValueError, "empty string"
|
||||
raise ValueError("empty string")
|
||||
if unfold:
|
||||
lines = s.splitlines()
|
||||
i = 0
|
||||
@@ -1026,36 +1041,36 @@ class _rrulestr:
|
||||
name, value = line.split(':', 1)
|
||||
parms = name.split(';')
|
||||
if not parms:
|
||||
raise ValueError, "empty property name"
|
||||
raise ValueError("empty property name")
|
||||
name = parms[0]
|
||||
parms = parms[1:]
|
||||
if name == "RRULE":
|
||||
for parm in parms:
|
||||
raise ValueError, "unsupported RRULE parm: "+parm
|
||||
raise ValueError("unsupported RRULE parm: "+parm)
|
||||
rrulevals.append(value)
|
||||
elif name == "RDATE":
|
||||
for parm in parms:
|
||||
if parm != "VALUE=DATE-TIME":
|
||||
raise ValueError, "unsupported RDATE parm: "+parm
|
||||
raise ValueError("unsupported RDATE parm: "+parm)
|
||||
rdatevals.append(value)
|
||||
elif name == "EXRULE":
|
||||
for parm in parms:
|
||||
raise ValueError, "unsupported EXRULE parm: "+parm
|
||||
raise ValueError("unsupported EXRULE parm: "+parm)
|
||||
exrulevals.append(value)
|
||||
elif name == "EXDATE":
|
||||
for parm in parms:
|
||||
if parm != "VALUE=DATE-TIME":
|
||||
raise ValueError, "unsupported RDATE parm: "+parm
|
||||
raise ValueError("unsupported RDATE parm: "+parm)
|
||||
exdatevals.append(value)
|
||||
elif name == "DTSTART":
|
||||
for parm in parms:
|
||||
raise ValueError, "unsupported DTSTART parm: "+parm
|
||||
raise ValueError("unsupported DTSTART parm: "+parm)
|
||||
if not parser:
|
||||
from dateutil import parser
|
||||
dtstart = parser.parse(value, ignoretz=ignoretz,
|
||||
tzinfos=tzinfos)
|
||||
else:
|
||||
raise ValueError, "unsupported property: "+name
|
||||
raise ValueError("unsupported property: "+name)
|
||||
if (forceset or len(rrulevals) > 1 or
|
||||
rdatevals or exrulevals or exdatevals):
|
||||
if not parser and (rdatevals or exdatevals):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user