diff --git a/.pep8 b/.pep8
index 9dc3362c..a9fb14c7 100644
--- a/.pep8
+++ b/.pep8
@@ -1,5 +1,4 @@
[pep8]
-# E111 indentation is not a multiple of four
# E121 continuation line under-indented for hanging indent
# E122 continuation line missing indentation or outdented
# E124 closing bracket does not match visual indentation
@@ -10,8 +9,7 @@
# E261 at least two spaces before inline comment
# E262 inline comment should start with '# '
# E265 block comment should start with '# '
-# E302 expected 2 blank lines, found 1
# E501 line too long (312 > 160 characters)
# E502 the backslash is redundant between brackets
-ignore = E111,E121,E122,E123,E124,E125,E126,E127,E128,E261,E262,E265,E302,E501,E502
-max-line-length = 160
\ No newline at end of file
+ignore = E121,E122,E124,E125,E126,E127,E128,E261,E262,E265,E501,E502
+max-line-length = 160
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5decff36..66fbfdbb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## v0.5.12
+Released 25 February 2016
+
+This is mostly a hotfix update
+
+Highlights:
+* Added: Experimental Deluge Support
+* Fixed: Some pep8 stuff
+* Improved: Use curly braces for pathrender optional variables
+
+The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.11...v0.5.12).
+
## v0.5.11
Released 20 February 2016
diff --git a/README.md b/README.md
index f82e75a5..87eb2f36 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
**Master Branch:** [](https://travis-ci.org/rembo10/headphones)
**Develop Branch:** [](https://travis-ci.org/rembo10/headphones)
-Headphones is an automated music downloader for NZB and Torrent, written in Python. It supports SABnzbd, NZBget, Transmission, µTorrent and Blackhole.
+Headphones is an automated music downloader for NZB and Torrent, written in Python. It supports SABnzbd, NZBget, Transmission, µTorrent, Deluge and Blackhole.
## Support & Discuss
You are free to join the Headphones support community on IRC where you can ask questions, hang around and discuss anything related to HP.
diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html
index 05109089..9ba9f2aa 100644
--- a/data/interfaces/default/config.html
+++ b/data/interfaces/default/config.html
@@ -311,6 +311,7 @@
Black Hole
Transmission
uTorrent (Beta)
+ Deluge (Beta)
+
+
+
@@ -1236,14 +1283,13 @@
- Use: $Artist/$artist, $SortArtist/$sortartist, $Album/$album, $Year/$year, $Type/$type (release type) and $First/$first (first letter in artist name), $OriginalFolder/$originalfolder (downloaded directory name)
- E.g.: $Type/$First/$artist/$album [$year] = Album/G/girl talk/all day [2010]
+ Use: $Artist/$artist, $SortArtist/$sortartist, $Album/$album, $Year/$year, $Type/$type (release type) and $First/$first (first letter in artist name), $OriginalFolder/$originalfolder (downloaded directory name). Put optional variables in square brackets, use single-quote marks to escape square brackets literally ('[', ']'). E.g.: $Type/$First/$artist/$album '['$year']' = Album/G/girl talk/all day [2010]
- Use: $Disc/$disc (disc #), $Track/$track (track #), $Title/$title, $Artist/$artist, $Album/$album and $Year/$year
+ Use: $Disc/$disc (disc #), $Track/$track (track #), $Title/$title, $Artist/$artist, $Album/$album and $Year/$year. Put optional variables in square brackets, use single-quote marks to escape square brackets literally ('[', ']').
@@ -2003,6 +2049,26 @@
}
});
+ if ($("#telegram").is(":checked"))
+ {
+ $("#telegramoptions").show();
+ }
+ else
+ {
+ $("#telegramoptions").hide();
+ }
+
+ $("#telegram").click(function(){
+ if ($("#telegram").is(":checked"))
+ {
+ $("#telegramoptions").slideDown();
+ }
+ else
+ {
+ $("#telegramoptions").slideUp();
+ }
+ });
+
if ($("#osx_notify").is(":checked"))
{
$("#osx_notify_options").show();
@@ -2159,19 +2225,25 @@
if ($("#torrent_downloader_blackhole").is(":checked"))
{
- $("#transmission_options,#utorrent_options").hide();
+ $("#transmission_options,#utorrent_options,#deluge_options").hide();
$("#torrent_blackhole_options").show();
}
if ($("#torrent_downloader_transmission").is(":checked"))
{
- $("#torrent_blackhole_options,#utorrent_options").hide();
+ $("#torrent_blackhole_options,#utorrent_options,#deluge_options").hide();
$("#transmission_options").show();
}
if ($("#torrent_downloader_utorrent").is(":checked"))
{
- $("#torrent_blackhole_options,#transmission_options").hide();
+ $("#torrent_blackhole_options,#transmission_options,#deluge_options").hide();
$("#utorrent_options").show();
}
+ if ($("#torrent_downloader_deluge").is(":checked"))
+ {
+ $("#torrent_blackhole_options,#transmission_options,#utorrent_options").hide();
+ $("#deluge_options").show();
+ }
+
$('input[type=radio]').change(function(){
if ($("#preferred_bitrate").is(":checked"))
@@ -2208,15 +2280,19 @@
}
if ($("#torrent_downloader_blackhole").is(":checked"))
{
- $("#transmission_options,#utorrent_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
+ $("#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
}
if ($("#torrent_downloader_transmission").is(":checked"))
{
- $("#torrent_blackhole_options,#utorrent_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
+ $("#torrent_blackhole_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
}
if ($("#torrent_downloader_utorrent").is(":checked"))
{
- $("#torrent_blackhole_options,#transmission_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
+ $("#torrent_blackhole_options,#transmission_options,#deluge_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
+ }
+ if ($("#torrent_downloader_deluge").is(":checked"))
+ {
+ $("#torrent_blackhole_options,#utorrent_options,#transmission_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
}
});
diff --git a/headphones/__init__.py b/headphones/__init__.py
index 92b274a9..4b36acf7 100644
--- a/headphones/__init__.py
+++ b/headphones/__init__.py
@@ -94,6 +94,7 @@ MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
UMASK = None
+
def initialize(config_file):
with INIT_LOCK:
diff --git a/headphones/albumart_test.py b/headphones/albumart_test.py
index f18b11e3..e1b80c02 100644
--- a/headphones/albumart_test.py
+++ b/headphones/albumart_test.py
@@ -4,6 +4,7 @@ from headphones.unittestcompat import TestCase
import headphones.albumart
+
# no tests...
class AlbumArtTest(TestCase):
def test_nothing(self):
diff --git a/headphones/config.py b/headphones/config.py
index 97675305..dd1ae9b6 100644
--- a/headphones/config.py
+++ b/headphones/config.py
@@ -15,6 +15,7 @@ def bool_int(value):
value = 0
return int(bool(value))
+
class path(str):
"""Internal 'marker' type for paths in config."""
@@ -66,7 +67,12 @@ _CONFIG_DEFINITIONS = {
'CUSTOMSLEEP': (int, 'General', 1),
'CUSTOMUSER': (str, 'General', ''),
'DELETE_LOSSLESS_FILES': (int, 'General', 1),
- 'DESTINATION_DIR': (path, 'General', ''),
+ 'DELUGE_HOST': (str, 'Deluge', ''),
+ 'DELUGE_PASSWORD': (str, 'Deluge', ''),
+ 'DELUGE_LABEL': (str, 'Deluge', ''),
+ 'DELUGE_DONE_DIRECTORY': (str, 'Deluge', ''),
+ 'DELUGE_PAUSED': (int, 'Deluge', 0),
+ 'DESTINATION_DIR': (str, 'General', ''),
'DETECT_BITRATE': (int, 'General', 0),
'DO_NOT_PROCESS_UNMATCHED': (int, 'General', 0),
'DOWNLOAD_DIR': (path, 'General', ''),
@@ -248,7 +254,11 @@ _CONFIG_DEFINITIONS = {
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
'SUBSONIC_USERNAME': (str, 'Subsonic', ''),
'SYNOINDEX_ENABLED': (int, 'Synoindex', 0),
- 'TORRENTBLACKHOLE_DIR': (path, 'General', ''),
+ 'TELEGRAM_TOKEN': (str, 'Telegram', ''),
+ 'TELEGRAM_USERID': (str, 'Telegram', ''),
+ 'TELEGRAM_ENABLED': (int, 'Telegram', 0),
+ 'TELEGRAM_ONSNATCH': (int, 'Telegram', 0),
+ 'TORRENTBLACKHOLE_DIR': (str, 'General', ''),
'TORRENT_DOWNLOADER': (int, 'General', 0),
'TORRENT_REMOVAL_INTERVAL': (int, 'General', 720),
'TORZNAB': (int, 'Torznab', 0),
diff --git a/headphones/config_test.py b/headphones/config_test.py
index e321d4af..1cd5367f 100644
--- a/headphones/config_test.py
+++ b/headphones/config_test.py
@@ -5,6 +5,7 @@ import re
import unittestcompat
from unittestcompat import TestCase, TestArgs
+
class ConfigApiTest(TestCase):
""" Common tests for headphones.Config
diff --git a/headphones/deluge.py b/headphones/deluge.py
new file mode 100644
index 00000000..5f79d29e
--- /dev/null
+++ b/headphones/deluge.py
@@ -0,0 +1,460 @@
+# This file is part of Headphones.
+#
+# Headphones is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Headphones is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Headphones. If not, see .
+
+# Parts of this file are a part of SickRage.
+# Author: Mr_Orange
+# URL: http://code.google.com/p/sickbeard/
+# Adapted for Headphones by
+# URL: https://github.com/noam09
+#
+# SickRage is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# SickRage is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with SickRage. If not, see .
+
+from __future__ import unicode_literals
+
+from headphones import logger
+#from headphones import request
+
+import time
+import re
+import os
+import json
+import headphones
+import requests
+from base64 import b64encode
+import traceback
+
+delugeweb_auth = {}
+delugeweb_url = ''
+
+
+def addTorrent(link, data=None):
+ try:
+ result = {}
+ retid = False
+
+ if link.startswith('magnet:'):
+ logger.debug('Deluge: Got a magnet link: %s' % link)
+ result = {'type': 'magnet',
+ 'url': link}
+ retid = _add_torrent_magnet(result)
+
+ elif link.startswith('http://') or link.startswith('https://'):
+ logger.debug('Deluge: Got a URL: %s' % link)
+ user_agent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
+ headers = {'User-Agent': user_agent}
+ torrentfile = ''
+ logger.debug('Deluge: Trying to download (GET)')
+ try:
+ r = requests.get(link, headers=headers)
+ if r.status_code == 200:
+ logger.debug('Deluge: 200 OK')
+ torrentfile = r.text
+ #for chunk in r.iter_content(chunk_size=1024):
+ # if chunk: # filter out keep-alive new chunks
+ # torrentfile = torrentfile + chunk
+ else:
+ logger.debug('Deluge: Trying to GET %s returned status %d' % (link, r.status_code))
+ return False
+ except Exception as e:
+ logger.debug('Deluge: Download failed: %s' % str(e))
+ if 'announce' not in torrentfile[:40]:
+ logger.debug('Deluge: Contents of %s doesn\'t look like a torrent file' % link)
+ return False
+ # Extract torrent name from .torrent
+ try:
+ logger.debug('Deluge: Getting torrent name length')
+ name_length = int(re.findall('name([0-9]*)\:.*?\:', torrentfile)[0])
+ logger.debug('Deluge: Getting torrent name')
+ name = re.findall('name[0-9]*\:(.*?)\:', torrentfile)[0][:name_length]
+ except Exception as e:
+ logger.debug('Deluge: Could not get torrent name, getting file name')
+ # get last part of link/path (name only)
+ name = link.split('\\')[-1].split('/')[-1]
+ # remove '.torrent' suffix
+ if name[-len('.torrent'):] == '.torrent':
+ name = name[:-len('.torrent')]
+ logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, torrentfile[:40]))
+ result = {'type': 'torrent',
+ 'name': name,
+ 'content': torrentfile}
+ retid = _add_torrent_file(result)
+
+ # elif link.endswith('.torrent') or data:
+ elif not (link.startswith('http://') or link.startswith('https://')):
+ if data:
+ logger.debug('Deluge: Getting .torrent data')
+ torrentfile = data
+ else:
+ logger.debug('Deluge: Getting .torrent file')
+ with open(link, 'rb') as f:
+ torrentfile = f.read()
+ # Extract torrent name from .torrent
+ try:
+ logger.debug('Deluge: Getting torrent name length')
+ name_length = int(re.findall('name([0-9]*)\:.*?\:', torrentfile)[0])
+ logger.debug('Deluge: Getting torrent name')
+ name = re.findall('name[0-9]*\:(.*?)\:', torrentfile)[0][:name_length]
+ except Exception as e:
+ logger.debug('Deluge: Could not get torrent name, getting file name')
+ # get last part of link/path (name only)
+ name = link.split('\\')[-1].split('/')[-1]
+ # remove '.torrent' suffix
+ if name[-len('.torrent'):] == '.torrent':
+ name = name[:-len('.torrent')]
+ logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, torrentfile[:40]))
+ result = {'type': 'torrent',
+ 'name': name,
+ 'content': torrentfile}
+ retid = _add_torrent_file(result)
+
+ else:
+ logger.error('Deluge: Unknown file type: %s' % link)
+
+ if retid:
+ logger.info('Deluge: Torrent sent to Deluge successfully (%s)' % retid)
+ return retid
+ else:
+ logger.info('Deluge returned status %s' % retid)
+ return False
+
+ except Exception as e:
+ logger.error(str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
+
+
+def getTorrentFolder(result):
+ logger.debug('Deluge: Get torrent folder name')
+ if not any(delugeweb_auth):
+ _get_auth()
+
+ try:
+ post_data = json.dumps({"method": "web.get_torrent_status",
+ "params": [
+ result['hash'],
+ ["total_done"]
+ ],
+ "id": 22})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ result['total_done'] = json.loads(response.text)['result']['total_done']
+
+ tries = 0
+ while result['total_done'] == 0 and tries < 10:
+ tries += 1
+ time.sleep(5)
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ result['total_done'] = json.loads(response.text)['result']['total_done']
+
+ post_data = json.dumps({"method": "web.get_torrent_status",
+ "params": [
+ result['hash'],
+ [
+ "name",
+ "save_path",
+ "total_size",
+ "num_files",
+ "message",
+ "tracker",
+ "comment"
+ ]
+ ],
+ "id": 23})
+
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+
+ result['save_path'] = json.loads(response.text)['result']['save_path']
+ result['name'] = json.loads(response.text)['result']['name']
+
+ return json.loads(response.text)['result']['name']
+ except Exception as e:
+ logger.debug('Deluge: Could not get torrent folder name: %s' % str(e))
+
+
+def removeTorrent(torrentid, remove_data=False):
+
+ if not any(delugeweb_auth):
+ _get_auth()
+
+ result = False
+ post_data = json.dumps({"method": "core.remove_torrent",
+ "params": [
+ torrentid,
+ remove_data
+ ],
+ "id": 25})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ result = json.loads(response.text)['result']
+
+ return result
+
+
+def _get_auth():
+ logger.debug('Deluge: Authenticating...')
+ global delugeweb_auth, delugeweb_url
+ delugeweb_auth = {}
+
+ delugeweb_host = headphones.CONFIG.DELUGE_HOST
+ delugeweb_password = headphones.CONFIG.DELUGE_PASSWORD
+
+ if not delugeweb_host.startswith('http'):
+ delugeweb_host = 'http://%s' % delugeweb_host
+
+ if delugeweb_host.endswith('/'):
+ delugeweb_host = delugeweb_host[:-1]
+
+ delugeweb_url = delugeweb_host + '/json'
+
+ post_data = json.dumps({"method": "auth.login",
+ "params": [delugeweb_password],
+ "id": 1})
+ try:
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ # , verify=TORRENT_VERIFY_CERT)
+ except Exception:
+ return None
+
+ auth = json.loads(response.text)["result"]
+ delugeweb_auth = response.cookies
+
+ post_data = json.dumps({"method": "web.connected",
+ "params": [],
+ "id": 10})
+ try:
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ # , verify=TORRENT_VERIFY_CERT)
+ except Exception:
+ return None
+
+ connected = json.loads(response.text)['result']
+
+ if not connected:
+ post_data = json.dumps({"method": "web.get_hosts",
+ "params": [],
+ "id": 11})
+ try:
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ # , verify=TORRENT_VERIFY_CERT)
+ except Exception:
+ return None
+
+ delugeweb_hosts = json.loads(response.text)['result']
+ if len(delugeweb_hosts) == 0:
+ logger.error('Deluge: WebUI does not contain daemons')
+ return None
+
+ post_data = json.dumps({"method": "web.connect",
+ "params": [delugeweb_hosts[0][0]],
+ "id": 11})
+
+ try:
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ # , verify=TORRENT_VERIFY_CERT)
+ except Exception:
+ return None
+
+ post_data = json.dumps({"method": "web.connected",
+ "params": [],
+ "id": 10})
+
+ try:
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ # , verify=TORRENT_VERIFY_CERT)
+ except Exception:
+ return None
+
+ connected = json.loads(response.text)['result']
+
+ if not connected:
+ logger.error('Deluge: WebUI could not connect to daemon')
+ return None
+
+ return auth
+
+
+def _add_torrent_magnet(result):
+ logger.debug('Deluge: Adding magnet')
+ if not any(delugeweb_auth):
+ _get_auth()
+ try:
+ post_data = json.dumps({"method": "core.add_torrent_magnet",
+ "params": [result['url'], {}],
+ "id": 2})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ result['hash'] = json.loads(response.text)['result']
+ logger.debug('Deluge: Response was %s' % str(json.loads(response.text)['result']))
+ return json.loads(response.text)['result']
+ except Exception as e:
+ logger.error('Deluge: Adding torrent magnet failed: %s' % str(e))
+
+'''
+def _add_torrent_url(result):
+ logger.debug('Deluge: Adding URL')
+ if not any(delugeweb_auth):
+ _get_auth()
+ try:
+ post_data = json.dumps({"method": "web.download_torrent_from_url",
+ "params": [result['url'], {}],
+ "id": 2})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ result['hash'] = json.loads(response.text)['result']
+ logger.debug('Deluge: Response was %s' % str(json.loads(response.text)['result']))
+ return json.loads(response.text)['result']
+ except Exception as e:
+ logger.error('Deluge: Adding torrent URL failed: %s' % str(e))
+'''
+
+
+def _add_torrent_file(result):
+ logger.debug('Deluge: Adding file')
+ if not any(delugeweb_auth):
+ _get_auth()
+ try:
+ # content is torrent file contents that needs to be encoded to base64
+ post_data = json.dumps({"method": "core.add_torrent_file",
+ "params": [result['name'] + '.torrent', b64encode(result['content'].encode('utf8')), {}],
+ "id": 2})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ result['hash'] = json.loads(response.text)['result']
+ logger.debug('Deluge: Response was %s' % str(json.loads(response.text)['result']))
+ return json.loads(response.text)['result']
+ except Exception as e:
+ logger.error('Deluge: Adding torrent file failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
+
+
+def setTorrentLabel(result):
+ logger.debug('Deluge: Setting label')
+ label = headphones.CONFIG.DELUGE_LABEL
+
+ if not any(delugeweb_auth):
+ _get_auth()
+
+ if ' ' in label:
+ logger.error('Deluge: Invalid label. Label can\'t contain spaces - replacing with underscores')
+ label = label.replace(' ', '_')
+ if label:
+ # check if label already exists and create it if not
+ post_data = json.dumps({"method": 'label.get_labels',
+ "params": [],
+ "id": 3})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ labels = json.loads(response.text)['result']
+
+ if labels is not None:
+ if label not in labels:
+ try:
+ logger.debug('Deluge: %s label doesn\'t exist in Deluge, let\'s add it' % label)
+ post_data = json.dumps({"method": 'label.add',
+ "params": [label],
+ "id": 4})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ logger.debug('Deluge: %s label added to Deluge' % label)
+ except Exception as e:
+ logger.error('Deluge: Setting label failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
+
+ # add label to torrent
+ post_data = json.dumps({"method": 'label.set_torrent',
+ "params": [result['hash'], label],
+ "id": 5})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ logger.debug('Deluge: %s label added to torrent' % label)
+ else:
+ logger.debug('Deluge: Label plugin not detected')
+ return False
+
+ return not json.loads(response.text)['error']
+
+
+def setSeedRatio(result):
+ logger.debug('Deluge: Setting seed ratio')
+ if not any(delugeweb_auth):
+ _get_auth()
+
+ ratio = None
+ if result['ratio']:
+ ratio = result['ratio']
+
+ if ratio:
+ post_data = json.dumps({"method": "core.set_torrent_stop_at_ratio",
+ "params": [result['hash'], True],
+ "id": 5})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ post_data = json.dumps({"method": "core.set_torrent_stop_ratio",
+ "params": [result['hash'], float(ratio)],
+ "id": 6})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+
+ return not json.loads(response.text)['error']
+
+ return True
+
+
+def setTorrentPath(result):
+ logger.debug('Deluge: Setting download path')
+ if not any(delugeweb_auth):
+ _get_auth()
+
+ if headphones.CONFIG.DELUGE_DONE_DIRECTORY or headphones.CONFIG.DOWNLOAD_TORRENT_DIR:
+ post_data = json.dumps({"method": "core.set_torrent_move_completed",
+ "params": [result['hash'], True],
+ "id": 7})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+
+ if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
+ move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
+ else:
+ move_to = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
+
+ if not os.path.exists(move_to):
+ logger.debug('Deluge: %s directory doesn\'t exist, let\'s create it' % move_to)
+ os.makedirs(move_to)
+ post_data = json.dumps({"method": "core.set_torrent_move_completed_path",
+ "params": [result['hash'], move_to],
+ "id": 8})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+
+ return not json.loads(response.text)['error']
+
+ return True
+
+
+def setTorrentPause(result):
+ logger.debug('Deluge: Pausing torrent')
+ if not any(delugeweb_auth):
+ _get_auth()
+
+ if headphones.CONFIG.DELUGE_PAUSED:
+ post_data = json.dumps({"method": "core.pause_torrent",
+ "params": [[result['hash']]],
+ "id": 9})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+
+ return not json.loads(response.text)['error']
+
+ return True
diff --git a/headphones/exceptions.py b/headphones/exceptions.py
index 107535c5..562dc8c3 100644
--- a/headphones/exceptions.py
+++ b/headphones/exceptions.py
@@ -25,6 +25,7 @@ class NewzbinAPIThrottled(HeadphonesException):
Newzbin has throttled us, deal with it
"""
+
class SoftChrootError(HeadphonesException):
"""
Fatal errors in SoftChroot module
diff --git a/headphones/notifiers.py b/headphones/notifiers.py
index 540107a7..d22303cc 100644
--- a/headphones/notifiers.py
+++ b/headphones/notifiers.py
@@ -856,3 +856,36 @@ class Email(object):
except Exception, e:
logger.warn('Error sending Email: %s' % e)
return False
+
+
+class TELEGRAM(object):
+
+ def notify(self, message, status):
+ if not headphones.CONFIG.TELEGRAM_ENABLED:
+ return
+
+ import requests
+
+ TELEGRAM_API = "https://api.telegram.org/bot%s/%s"
+
+ # Get configuration data
+ token = headphones.CONFIG.TELEGRAM_TOKEN
+ userid = headphones.CONFIG.TELEGRAM_USERID
+
+ # Construct message
+ payload = {'chat_id': userid, 'text': status + ': ' + message}
+
+ # Send message to user using Telegram's Bot API
+ try:
+ response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload)
+ except Exception, e:
+ logger.info(u'Telegram notify failed: ' + str(e))
+
+ # Error logging
+ sent_successfuly = True
+ if not response.status_code == 200:
+ logger.info(u'Could not send notification to TelegramBot (token=%s). Response: [%s]', (token, response.text))
+ sent_successfuly = False
+
+ logger.info(u"Telegram notifications sent.")
+ return sent_successfuly
diff --git a/headphones/pathrender.py b/headphones/pathrender.py
index a00a1192..162c6956 100644
--- a/headphones/pathrender.py
+++ b/headphones/pathrender.py
@@ -25,15 +25,16 @@
* substitution variables, which start with dollar sign ($) and
extend until next non-alphanumeric+underscore character
(like $This and $5_that).
- * optional elements enclosed in square brackets, which render
+ * optional elements enclosed in curly braces, which render
nonempty value only if any variable or optional inside returned
- nonempty value, ignoring literals (like [\'[\'$That\']\' ]).
+ nonempty value, ignoring literals (like {\'[\'$That\']\'}).
'''
from __future__ import print_function
from enum import Enum
__author__ = "Andrzej Ciarkowski "
+
class _PatternElement(object):
'''ABC for hierarchy of path name renderer pattern elements.'''
def render(self, replacement):
@@ -41,11 +42,13 @@ class _PatternElement(object):
'''Format this _PatternElement into string using provided substitution dictionary.'''
raise NotImplementedError()
+
class _Generator(_PatternElement):
# pylint: disable=abstract-method
'''Tagging interface for "content-generating" elements like replacement or optional block.'''
pass
+
class _Replacement(_Generator):
'''Replacement variable, eg. $title.'''
def __init__(self, pattern):
@@ -90,20 +93,23 @@ class _OptionalBlock(_Generator):
return u""
-_OPTIONAL_START = u'['
-_OPTIONAL_END = u']'
+_OPTIONAL_START = u'{'
+_OPTIONAL_END = u'}'
_ESCAPE_CHAR = u'\''
_REPLACEMENT_START = u'$'
+
def _is_replacement_valid(c):
# type: (str) -> bool
return c.isalnum() or c == u'_'
+
class _State(Enum):
LITERAL = 0
ESCAPE = 1
REPLACEMENT = 2
+
def _append_literal(scope, text):
# type: ([_PatternElement], str) -> None
'''Append literal text to the scope BUT ONLY if it's not an empty string.'''
@@ -111,11 +117,13 @@ def _append_literal(scope, text):
return
scope.append(_LiteralText(text))
+
class Warnings(Enum):
'''Pattern parsing warnings, as stored withing warnings property of Pattern object after parsing.'''
UNCLOSED_ESCAPE = 'Warnings.UNCLOSED_ESCAPE'
UNCLOSED_OPTIONAL = 'Warnings.UNCLOSED_OPTIONAL'
+
def _parse_pattern(pattern, warnings):
# type: (str,MutableSet[Warnings]) -> [_PatternElement]
'''Parse path pattern text into list of _PatternElements, put warnings into the provided set.'''
@@ -188,6 +196,7 @@ def _parse_pattern(pattern, warnings):
_append_literal(root_scope, pattern[start:])
return root_scope
+
class Pattern(object):
'''Stores preparsed rename pattern for repeated use.
diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py
index 125b02d7..507a5571 100755
--- a/headphones/postprocessor.py
+++ b/headphones/postprocessor.py
@@ -27,7 +27,7 @@ from beets import autotag
from beets import config as beetsconfig
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
from beetsplug import lyrics as beetslyrics
-from headphones import notifiers, utorrent, transmission
+from headphones import notifiers, utorrent, transmission, deluge
from headphones import db, albumart, librarysync
from headphones import logger, helpers, request, mb, music_encoder
@@ -46,7 +46,10 @@ def checkFolder():
if album['Kind'] == 'nzb':
download_dir = headphones.CONFIG.DOWNLOAD_DIR
else:
- download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
+ if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
+ download_dir = headphones.CONFIG.DELUGE_DONE_DIRECTORY
+ else:
+ download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
album_path = os.path.join(download_dir, album['FolderName']).encode(
headphones.SYS_ENCODING, 'replace')
@@ -454,6 +457,8 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
release['ArtistName'], release['AlbumTitle']))
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
torrent_removed = transmission.removeTorrent(hash, True)
+ elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
+ torrent_removed = deluge.removeTorrent(hash, True)
else:
torrent_removed = utorrent.removeTorrent(hash, True)
@@ -533,6 +538,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
pushbullet = notifiers.PUSHBULLET()
pushbullet.notify(pushmessage, statusmessage)
+ if headphones.CONFIG.TELEGRAM_ENABLED:
+ logger.info(u"Telegram request")
+ telegram = notifiers.TELEGRAM()
+ telegram.notify(pushmessage, statusmessage)
+
if headphones.CONFIG.TWITTER_ENABLED:
logger.info(u"Sending Twitter notification")
twitter = notifiers.TwitterNotifier()
@@ -666,9 +676,9 @@ def renameNFO(albumpath):
def moveFiles(albumpath, release, tracks):
logger.info("Moving files: %s" % albumpath)
try:
- date = release['ReleaseDate']
+ date = release['ReleaseDate']
except TypeError:
- date = u''
+ date = u''
year = date[:4]
artist = release['ArtistName'].replace('/', '_')
album = release['AlbumTitle'].replace('/', '_')
diff --git a/headphones/searcher.py b/headphones/searcher.py
index 91489a23..5c8b4144 100644
--- a/headphones/searcher.py
+++ b/headphones/searcher.py
@@ -33,7 +33,7 @@ from pygazelle import format as gazelleformat
import headphones
from headphones.common import USER_AGENT
from headphones import logger, db, helpers, classes, sab, nzbget, request
-from headphones import utorrent, transmission, notifiers, rutracker
+from headphones import utorrent, transmission, notifiers, rutracker, deluge
from bencode import bencode, bdecode
@@ -851,7 +851,7 @@ def send_to_downloader(data, bestqual, album):
else:
logger.error("Cannot save magnet link in blackhole. " \
"Please switch your torrent downloader to " \
- "Transmission or uTorrent, or allow Headphones " \
+ "Transmission, uTorrent or Deluge, or allow Headphones " \
"to open or convert magnet links")
return
else:
@@ -889,6 +889,48 @@ def send_to_downloader(data, bestqual, album):
if seed_ratio is not None:
transmission.setSeedRatio(torrentid, seed_ratio)
+ elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
+ logger.info("Sending torrent to Deluge")
+
+ try:
+ # Add torrent
+ if bestqual[3] == 'rutracker.org':
+ torrentid = deluge.addTorrent('', data)
+ else:
+ torrentid = deluge.addTorrent(bestqual[2])
+
+ if not torrentid:
+ logger.error("Error sending torrent to Deluge. Are you sure it's running? Maybe the torrent already exists?")
+ return
+
+ # This pauses the torrent right after it is added
+ if headphones.CONFIG.DELUGE_PAUSED:
+ deluge.setTorrentPause({'hash': torrentid})
+
+ # Set Label
+ if headphones.CONFIG.DELUGE_LABEL:
+ deluge.setTorrentLabel({'hash': torrentid})
+
+ # Set Seed Ratio
+ seed_ratio = get_seed_ratio(bestqual[3])
+ if seed_ratio is not None:
+ deluge.setSeedRatio({'hash': torrentid, 'ratio': seed_ratio})
+
+ # Set move-to directory
+ if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
+ deluge.setTorrentPath({'hash': torrentid})
+
+ # I only just realized this function is useless...
+ folder_name = deluge.getTorrentFolder({'hash': torrentid})
+ if folder_name:
+ logger.info('Torrent folder name: %s' % folder_name)
+ else:
+ logger.error('Torrent folder name could not be determined')
+ return
+
+ except Exception as e:
+ logger.error('Error sending torrent to Deluge: %s' % str(e))
+
else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 2:
logger.info("Sending torrent to uTorrent")
@@ -960,6 +1002,10 @@ def send_to_downloader(data, bestqual, album):
logger.info(u"Sending PushBullet notification")
pushbullet = notifiers.PUSHBULLET()
pushbullet.notify(name, "Download started")
+ if headphones.CONFIG.TELEGRAM_ENABLED and headphones.CONFIG.TELEGRAM_ONSNATCH:
+ logger.info(u"Sending Telegram notification")
+ telegram = notifiers.TELEGRAM()
+ telegram.notify(name, "Download started")
if headphones.CONFIG.TWITTER_ENABLED and headphones.CONFIG.TWITTER_ONSNATCH:
logger.info(u"Sending Twitter notification")
twitter = notifiers.TwitterNotifier()
diff --git a/headphones/softchroot.py b/headphones/softchroot.py
index 80878548..3716a035 100644
--- a/headphones/softchroot.py
+++ b/headphones/softchroot.py
@@ -1,6 +1,7 @@
import os
from headphones.exceptions import SoftChrootError
+
class SoftChroot(object):
""" SoftChroot provides SOFT chrooting for UI
diff --git a/headphones/softchroot_test.py b/headphones/softchroot_test.py
index baa56703..474969d7 100644
--- a/headphones/softchroot_test.py
+++ b/headphones/softchroot_test.py
@@ -6,6 +6,7 @@ from headphones.unittestcompat import TestCase, TestArgs
from headphones.softchroot import SoftChroot
from headphones.exceptions import SoftChrootError
+
class SoftChrootTest(TestCase):
def test_create(self):
""" create headphones.SoftChroot """
diff --git a/headphones/transmission.py b/headphones/transmission.py
index 11da8989..b62e2fac 100644
--- a/headphones/transmission.py
+++ b/headphones/transmission.py
@@ -29,6 +29,7 @@ import headphones
_session_id = None
+
def addTorrent(link, data=None):
method = 'torrent-add'
diff --git a/headphones/unittestcompat.py b/headphones/unittestcompat.py
index 12a497be..0bf26c3c 100644
--- a/headphones/unittestcompat.py
+++ b/headphones/unittestcompat.py
@@ -14,6 +14,7 @@ _dummy = False
if sys.version_info[0] == 2 and sys.version_info[1] <= 6:
_dummy = True
+
def _d(f):
def decorate(self, *args, **kw):
if not _dummy:
@@ -92,6 +93,7 @@ class TestCase(TC):
# True indicates, that exception is handled
return True
+
def TestArgs(*parameters):
def tuplify(x):
if not isinstance(x, tuple):
diff --git a/headphones/webserve.py b/headphones/webserve.py
index ee1000de..9253e058 100644
--- a/headphones/webserve.py
+++ b/headphones/webserve.py
@@ -1156,6 +1156,11 @@ class WebInterface(object):
"transmission_host": headphones.CONFIG.TRANSMISSION_HOST,
"transmission_username": headphones.CONFIG.TRANSMISSION_USERNAME,
"transmission_password": headphones.CONFIG.TRANSMISSION_PASSWORD,
+ "deluge_host": headphones.CONFIG.DELUGE_HOST,
+ "deluge_password": headphones.CONFIG.DELUGE_PASSWORD,
+ "deluge_label": headphones.CONFIG.DELUGE_LABEL,
+ "deluge_done_directory": headphones.CONFIG.DELUGE_DONE_DIRECTORY,
+ "deluge_paused": checked(headphones.CONFIG.DELUGE_PAUSED),
"utorrent_host": headphones.CONFIG.UTORRENT_HOST,
"utorrent_username": headphones.CONFIG.UTORRENT_USERNAME,
"utorrent_password": headphones.CONFIG.UTORRENT_PASSWORD,
@@ -1166,6 +1171,7 @@ class WebInterface(object):
"torrent_downloader_blackhole": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 0),
"torrent_downloader_transmission": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 1),
"torrent_downloader_utorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 2),
+ "torrent_downloader_deluge": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 3),
"download_dir": headphones.CONFIG.DOWNLOAD_DIR,
"use_blackhole": checked(headphones.CONFIG.BLACKHOLE),
"blackhole_dir": headphones.CONFIG.BLACKHOLE_DIR,
@@ -1324,6 +1330,10 @@ class WebInterface(object):
"pushbullet_onsnatch": checked(headphones.CONFIG.PUSHBULLET_ONSNATCH),
"pushbullet_apikey": headphones.CONFIG.PUSHBULLET_APIKEY,
"pushbullet_deviceid": headphones.CONFIG.PUSHBULLET_DEVICEID,
+ "telegram_enabled": checked(headphones.CONFIG.TELEGRAM_ENABLED),
+ "telegram_onsnatch": checked(headphones.CONFIG.TELEGRAM_ONSNATCH),
+ "telegram_token": headphones.CONFIG.TELEGRAM_TOKEN,
+ "telegram_userid": headphones.CONFIG.TELEGRAM_USERID,
"subsonic_enabled": checked(headphones.CONFIG.SUBSONIC_ENABLED),
"subsonic_host": headphones.CONFIG.SUBSONIC_HOST,
"subsonic_username": headphones.CONFIG.SUBSONIC_USERNAME,
@@ -1428,10 +1438,11 @@ class WebInterface(object):
"synoindex_enabled", "pushover_enabled",
"pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled",
"twitter_enabled", "twitter_onsnatch",
+ "telegram_enabled", "telegram_onsnatch",
"osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch",
"songkick_enabled", "songkick_filter_enabled",
"mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch",
- "customauth", "idtag"
+ "customauth", "idtag", "deluge_paused"
]
for checked_config in checked_configs:
if checked_config not in kwargs:
@@ -1692,6 +1703,12 @@ class WebInterface(object):
pushbullet = notifiers.PUSHBULLET()
pushbullet.notify("it works!", "Test message")
+ @cherrypy.expose
+ def testTelegram(self):
+ logger.info("Testing Telegram notifications")
+ telegram = notifiers.TELEGRAM()
+ telegram.notify("it works!", "lazers pew pew")
+
class Artwork(object):
@cherrypy.expose