diff --git a/CHANGELOG.md b/CHANGELOG.md
index b38d3565..7ae7feca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
# Changelog
+## v0.5.5
+Released 04 May 2015
+
+Highlights:
+* Added: force ID3v2.3 during post processing (#2121)
+* Added: MusicBrainz authentication (#2125)
+* Added: Email notifications (addresses #1045)
+* Fixed: Kickass url updated to kickass.to (#2119)
+* Fixed: Piratebay returning 0 bytes for all files
+* Fixed: Albums stopped automatically refreshing when adding an artist
+* Fixed: Min/max sizes for target bitrate
+* Fixed: Don't filter any results if looking for a specific download
+* Fixed: Sort by size in the specific download table
+* Fixed: Deal with beets recommendation.none correctly
+* Improved: Close dialog window automatically when choosing a specific download
+* Improved: Move some repetitive log messages to debug level
+
+The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.4...v0.5.5).
+
## v0.5.4
Released 05 February 2015
@@ -155,4 +174,4 @@ The full list of commits can be found [here](https://github.com/rembo10/headphon
## v0.1
Released 05 August 2013
-The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/2156e1341405d07c5bcfbe994f6b354b32d94cda...v0.1).
\ No newline at end of file
+The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/2156e1341405d07c5bcfbe994f6b354b32d94cda...v0.1).
diff --git a/data/interfaces/default/album.html b/data/interfaces/default/album.html
index fdabd9d3..0cc868b8 100644
--- a/data/interfaces/default/album.html
+++ b/data/interfaces/default/album.html
@@ -337,6 +337,21 @@
});
}
+ jQuery.extend( jQuery.fn.dataTableExt.oSort, {
+ "title-numeric-pre": function ( a ) {
+ var x = a.match(/title="*(-?[0-9\.]+)/)[1];
+ return parseFloat( x );
+ },
+
+ "title-numeric-asc": function ( a, b ) {
+ return ((a < b) ? -1 : ((a > b) ? 1 : 0));
+ },
+
+ "title-numeric-desc": function ( a, b ) {
+ return ((a < b) ? 1 : ((a > b) ? -1 : 0));
+ }
+ } );
+
$(document).ready(function() {
getAlbumInfo();
initThisPage();
diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html
index 3b72169d..c6925195 100644
--- a/data/interfaces/default/config.html
+++ b/data/interfaces/default/config.html
@@ -786,6 +786,10 @@
Embed lyrics
+
+ Tag using ID3v2.3
+
+
@@ -1104,6 +1108,39 @@
+
+ Email
+
+ Enable Email Notifications
+
+
+
+
@@ -1416,6 +1453,17 @@
Port
+
+ Requires Authentication
+
+
Sleep Interval
@@ -1526,6 +1574,25 @@
$('#api_key').val(data);
});
});
+ if ($("#customauth").is(":checked"))
+ {
+ $("#customauth_options").show();
+ }
+ else
+ {
+ $("#customauth_options").hide();
+ }
+
+ $("#customauth").click(function(){
+ if ($("#customauth").is(":checked"))
+ {
+ $("#customauth_options").slideDown();
+ }
+ else
+ {
+ $("#customauth_options").slideUp();
+ }
+ });
if ($("#enable_https").is(":checked"))
{
$("#https_options").show();
@@ -1866,6 +1933,26 @@
}
});
+ if ($("#email").is(":checked"))
+ {
+ $("#email_options").show();
+ }
+ else
+ {
+ $("#email_options").hide();
+ }
+
+ $("#email").click(function(){
+ if ($("#email").is(":checked"))
+ {
+ $("#email_options").slideDown();
+ }
+ else
+ {
+ $("#email_options").slideUp();
+ }
+ });
+
if ($("#songkick").is(":checked"))
{
$("#songkickoptions").show();
@@ -2074,6 +2161,7 @@
initConfigCheckbox("#use_whatcd");
initConfigCheckbox("#api_enabled");
initConfigCheckbox("#enable_https");
+ initConfigCheckbox("#customauth");
$('#twitterStep1').click(function () {
diff --git a/headphones/config.py b/headphones/config.py
index 4071edd7..b27d48da 100644
--- a/headphones/config.py
+++ b/headphones/config.py
@@ -44,9 +44,12 @@ _CONFIG_DEFINITIONS = {
'CUE_SPLIT': (int, 'General', 1),
'CUE_SPLIT_FLAC_PATH': (str, 'General', ''),
'CUE_SPLIT_SHNTOOL_PATH': (str, 'General', ''),
+ 'CUSTOMAUTH': (int, 'General', 0),
'CUSTOMHOST': (str, 'General', 'localhost'),
+ 'CUSTOMPASS': (str, 'General', ''),
'CUSTOMPORT': (int, 'General', 5000),
'CUSTOMSLEEP': (int, 'General', 1),
+ 'CUSTOMUSER': (str, 'General', ''),
'DELETE_LOSSLESS_FILES': (int, 'General', 1),
'DESTINATION_DIR': (str, 'General', ''),
'DETECT_BITRATE': (int, 'General', 0),
@@ -54,6 +57,15 @@ _CONFIG_DEFINITIONS = {
'DOWNLOAD_SCAN_INTERVAL': (int, 'General', 5),
'DOWNLOAD_TORRENT_DIR': (str, 'General', ''),
'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0),
+ 'EMAIL_ENABLED': (int, 'Email', 0),
+ 'EMAIL_FROM': (str, 'Email', ''),
+ 'EMAIL_TO': (str, 'Email', ''),
+ 'EMAIL_SMTP_SERVER': (str, 'Email', ''),
+ 'EMAIL_SMTP_USER': (str, 'Email', ''),
+ 'EMAIL_SMTP_PASSWORD': (str, 'Email', ''),
+ 'EMAIL_SMTP_PORT': (int, 'Email', 25),
+ 'EMAIL_TLS': (int, 'Email', 0),
+ 'EMAIL_ONSNATCH': (int, 'Email', 0),
'EMBED_ALBUM_ART': (int, 'General', 0),
'EMBED_LYRICS': (int, 'General', 0),
'ENABLE_HTTPS': (int, 'General', 0),
@@ -92,6 +104,7 @@ _CONFIG_DEFINITIONS = {
'HTTP_PROXY': (int, 'General', 0),
'HTTP_ROOT': (str, 'General', '/'),
'HTTP_USERNAME': (str, 'General', ''),
+ 'IDTAG': (int, 'Beets', 0),
'IGNORED_WORDS': (str, 'General', ''),
'IGNORED_FOLDERS': (list, 'Advanced', []),
'IGNORED_FILES': (list, 'Advanced', []),
diff --git a/headphones/helpers.py b/headphones/helpers.py
index ca8c9fdc..1a319ab5 100644
--- a/headphones/helpers.py
+++ b/headphones/helpers.py
@@ -173,15 +173,15 @@ def piratesize(size):
factor = float(split[0])
unit = split[1].upper()
- if unit == 'MiB':
+ if unit == 'MIB':
size = factor * 1048576
elif unit == 'MB':
size = factor * 1000000
- elif unit == 'GiB':
+ elif unit == 'GIB':
size = factor * 1073741824
elif unit == 'GB':
size = factor * 1000000000
- elif unit == 'KiB':
+ elif unit == 'KIB':
size = factor * 1024
elif unit == 'KB':
size = factor * 1000
diff --git a/headphones/mb.py b/headphones/mb.py
index 6caf0090..3a150977 100644
--- a/headphones/mb.py
+++ b/headphones/mb.py
@@ -47,6 +47,8 @@ def startmb():
elif headphones.CONFIG.MIRROR == "custom":
mbhost = headphones.CONFIG.CUSTOMHOST
mbport = int(headphones.CONFIG.CUSTOMPORT)
+ mbuser = headphones.CONFIG.CUSTOMUSER
+ mbpass = headphones.CONFIG.CUSTOMPASS
sleepytime = int(headphones.CONFIG.CUSTOMSLEEP)
elif headphones.CONFIG.MIRROR == "headphones":
mbhost = "144.76.94.239"
@@ -69,12 +71,16 @@ def startmb():
mb_lock.minimum_delta = sleepytime
# Add headphones credentials
- if headphones.CONFIG.MIRROR == "headphones":
- if not mbuser and mbpass:
- logger.warn("No username or password set for VIP server")
+ if headphones.CONFIG.MIRROR == "headphones" or headphones.CONFIG.CUSTOMAUTH:
+ if not mbuser or not mbpass:
+ logger.warn("No username or password set for MusicBrainz server")
else:
musicbrainzngs.hpauth(mbuser, mbpass)
+ # Let us know if we disable custom authentication
+ if not headphones.CONFIG.CUSTOMAUTH and headphones.CONFIG.MIRROR == "custom":
+ musicbrainzngs.disable_hpauth()
+
logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i', mbhost, mbport, sleepytime)
return True
diff --git a/headphones/notifiers.py b/headphones/notifiers.py
index e8503b53..9631660d 100644
--- a/headphones/notifiers.py
+++ b/headphones/notifiers.py
@@ -34,6 +34,11 @@ import json
import oauth2 as oauth
import pythontwitter as twitter
+from email.mime.text import MIMEText
+import smtplib
+import email.utils
+
+
class GROWL(object):
"""
Growl notifications, for OS X.
@@ -827,3 +832,31 @@ class SubSonicNotifier(object):
# Invoke request
request.request_response(self.host + "musicFolderSettings.view?scanNow",
auth=(self.username, self.password))
+
+class Email(object):
+
+ def notify(self, subject, message):
+
+ message = MIMEText(message, 'plain', "utf-8")
+ message['Subject'] = subject
+ message['From'] = email.utils.formataddr(('Headphones', headphones.CONFIG.EMAIL_FROM))
+ message['To'] = headphones.CONFIG.EMAIL_TO
+
+ try:
+ mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER, headphones.CONFIG.EMAIL_SMTP_PORT)
+
+ if (headphones.CONFIG.EMAIL_TLS):
+ mailserver.starttls()
+
+ mailserver.ehlo()
+
+ if headphones.CONFIG.EMAIL_SMTP_USER:
+ mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER, headphones.CONFIG.EMAIL_SMTP_PASSWORD)
+
+ mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, headphones.CONFIG.EMAIL_TO, message.as_string())
+ mailserver.quit()
+ return True
+
+ except Exception, e:
+ logger.warn('Error sending Email: %s' % e)
+ return False
\ No newline at end of file
diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py
index 5bf06d99..d6df01c7 100755
--- a/headphones/postprocessor.py
+++ b/headphones/postprocessor.py
@@ -23,6 +23,7 @@ import itertools
import headphones
from beets import autotag
+from beets import config as beetsconfig
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
from beetsplug import lyrics as beetslyrics
@@ -34,7 +35,7 @@ postprocessor_lock = threading.Lock()
def checkFolder():
- logger.info("Checking download folder for completed downloads (only snatched ones).")
+ logger.debug("Checking download folder for completed downloads (only snatched ones).")
with postprocessor_lock:
myDB = db.DBConnection()
@@ -56,7 +57,7 @@ def checkFolder():
else:
logger.info("No folder name found for " + album['Title'])
- logger.info("Checking download folder finished.")
+ logger.debug("Checking download folder finished.")
def verify(albumid, albumpath, Kind=None, forced=False):
@@ -513,6 +514,12 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
mpc = notifiers.MPC()
mpc.notify()
+ if headphones.CONFIG.EMAIL_ENABLED:
+ logger.info(u"Sending Email notification")
+ email = notifiers.Email()
+ subject = release['ArtistName'] + ' - ' + release['AlbumTitle']
+ email.notify(subject, "Download and Postprocessing completed")
+
def embedAlbumArt(artwork, downloaded_track_list):
logger.info('Embedding album art')
@@ -858,7 +865,7 @@ def correctMetadata(albumid, release, downloaded_track_list):
except Exception as e:
logger.error('Error getting recommendation: %s. Not writing metadata', e)
return
- if str(rec) == 'recommendation.none':
+ if str(rec) == 'Recommendation.none':
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
return
@@ -873,6 +880,14 @@ def correctMetadata(albumid, release, downloaded_track_list):
# TODO: Handle extra_items & extra_tracks
autotag.apply_metadata(info, mapping)
+
+ # Set ID3 tag version
+ if headphones.CONFIG.IDTAG:
+ beetsconfig['id3v23'] = True
+ logger.debug("Using ID3v2.3")
+ else:
+ beetsconfig['id3v23'] = False
+ logger.debug("Using ID3v2.4")
for item in items:
try:
@@ -1053,6 +1068,8 @@ def renameUnprocessedFolder(path, tag):
def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
+ logger.info('Force checking download folder for completed downloads')
+
ignored = 0
if album_dir:
diff --git a/headphones/searcher.py b/headphones/searcher.py
index ea78d744..549e2e05 100644
--- a/headphones/searcher.py
+++ b/headphones/searcher.py
@@ -225,7 +225,7 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
myDB = db.DBConnection()
albumlength = myDB.select('SELECT sum(TrackDuration) from tracks WHERE AlbumID=?', [album['AlbumID']])[0][0]
- if headphones.CONFIG.PREFER_TORRENTS == 0:
+ if headphones.CONFIG.PREFER_TORRENTS == 0 and not choose_specific_download:
if NZB_PROVIDERS and NZB_DOWNLOADERS:
results = searchNZB(album, new, losslessOnly, albumlength)
@@ -233,7 +233,7 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
if not results and TORRENT_PROVIDERS:
results = searchTorrent(album, new, losslessOnly, albumlength)
- elif headphones.CONFIG.PREFER_TORRENTS == 1:
+ elif headphones.CONFIG.PREFER_TORRENTS == 1 and not choose_specific_download:
if TORRENT_PROVIDERS:
results = searchTorrent(album, new, losslessOnly, albumlength)
@@ -247,10 +247,10 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
torrent_results = None
if NZB_PROVIDERS and NZB_DOWNLOADERS:
- nzb_results = searchNZB(album, new, losslessOnly, albumlength)
+ nzb_results = searchNZB(album, new, losslessOnly, albumlength, choose_specific_download)
if TORRENT_PROVIDERS:
- torrent_results = searchTorrent(album, new, losslessOnly, albumlength)
+ torrent_results = searchTorrent(album, new, losslessOnly, albumlength, choose_specific_download)
if not nzb_results:
nzb_results = []
@@ -306,9 +306,9 @@ def more_filtering(results, album, albumlength, new):
targetsize = albumlength / 1000 * int(headphones.CONFIG.PREFERRED_BITRATE) * 128
logger.info('Target size: %s' % helpers.bytes_to_mb(targetsize))
if headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER:
- low_size_limit = targetsize - (targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER) / 100)
+ low_size_limit = targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER) / 100
if headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER:
- high_size_limit = targetsize + (targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER) / 100)
+ high_size_limit = targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER) / 100
if headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS:
allow_lossless = True
@@ -429,7 +429,7 @@ def get_year_from_release_date(release_date):
return year
-def searchNZB(album, new=False, losslessOnly=False, albumlength=None):
+def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_specific_download=False):
reldate = album['ReleaseDate']
year = get_year_from_release_date(reldate)
@@ -691,7 +691,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None):
results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)]
# Additional filtering for size etc
- if results:
+ if results and not choose_specific_download:
results = more_filtering(results, album, albumlength, new)
return results
@@ -946,7 +946,11 @@ def send_to_downloader(data, bestqual, album):
b2msg = 'From ' + provider + ' ' + name
boxcar = notifiers.BOXCAR()
boxcar.notify('Headphones snatched: ' + title, b2msg, rgid)
-
+ if headphones.CONFIG.EMAIL_ENABLED and headphones.CONFIG.EMAIL_ONSNATCH:
+ logger.info(u"Sending Email notification")
+ email = notifiers.Email()
+ message = 'Snatched from ' + provider + '. ' + name
+ email.notify(title, message)
def verifyresult(title, artistterm, term, lossless):
@@ -1014,7 +1018,7 @@ def verifyresult(title, artistterm, term, lossless):
return True
-def searchTorrent(album, new=False, losslessOnly=False, albumlength=None):
+def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose_specific_download=False):
global gazelle # persistent what.cd api object to reduce number of login attempts
# rutracker login
@@ -1092,7 +1096,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None):
if headphones.CONFIG.KAT_PROXY_URL:
providerurl = fix_url(set_proxy(headphones.CONFIG.KAT_PROXY_URL))
else:
- providerurl = fix_url("https://kickass.so")
+ providerurl = fix_url("https://kickass.to")
# Build URL
providerurl = providerurl + "/usearch/" + ka_term
@@ -1521,7 +1525,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None):
results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)]
# Additional filtering for size etc
- if results:
+ if results and not choose_specific_download:
results = more_filtering(results, album, albumlength, new)
return results
diff --git a/headphones/webserve.py b/headphones/webserve.py
index b3b67cac..26b6ac76 100644
--- a/headphones/webserve.py
+++ b/headphones/webserve.py
@@ -374,6 +374,9 @@ class WebInterface(object):
myDB = db.DBConnection()
album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone()
searcher.send_to_downloader(data, bestqual, album)
+ return json.dumps({'result':'success'})
+ else:
+ return json.dumps({'result':'failure'})
@cherrypy.expose
def unqueueAlbum(self, AlbumID, ArtistID):
@@ -877,6 +880,7 @@ class WebInterface(object):
cherrypy.response.headers['Content-type'] = 'application/json'
return json_albums
+ @cherrypy.expose
def getArtistjson(self, ArtistID, **kwargs):
myDB = db.DBConnection()
artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [ArtistID]).fetchone()
@@ -1142,6 +1146,9 @@ class WebInterface(object):
"customhost": headphones.CONFIG.CUSTOMHOST,
"customport": headphones.CONFIG.CUSTOMPORT,
"customsleep": headphones.CONFIG.CUSTOMSLEEP,
+ "customauth": checked(headphones.CONFIG.CUSTOMAUTH),
+ "customuser": headphones.CONFIG.CUSTOMUSER,
+ "custompass": headphones.CONFIG.CUSTOMPASS,
"hpuser": headphones.CONFIG.HPUSER,
"hppass": headphones.CONFIG.HPPASS,
"songkick_enabled": checked(headphones.CONFIG.SONGKICK_ENABLED),
@@ -1151,7 +1158,17 @@ class WebInterface(object):
"cache_sizemb": headphones.CONFIG.CACHE_SIZEMB,
"file_permissions": headphones.CONFIG.FILE_PERMISSIONS,
"folder_permissions": headphones.CONFIG.FOLDER_PERMISSIONS,
- "mpc_enabled": checked(headphones.CONFIG.MPC_ENABLED)
+ "mpc_enabled": checked(headphones.CONFIG.MPC_ENABLED),
+ "email_enabled": checked(headphones.CONFIG.EMAIL_ENABLED),
+ "email_from": headphones.CONFIG.EMAIL_FROM,
+ "email_to": headphones.CONFIG.EMAIL_TO,
+ "email_smtp_server": headphones.CONFIG.EMAIL_SMTP_SERVER,
+ "email_smtp_user": headphones.CONFIG.EMAIL_SMTP_USER,
+ "email_smtp_password": headphones.CONFIG.EMAIL_SMTP_PASSWORD,
+ "email_smtp_port": int(headphones.CONFIG.EMAIL_SMTP_PORT),
+ "email_tls": checked(headphones.CONFIG.EMAIL_TLS),
+ "email_onsnatch": checked(headphones.CONFIG.EMAIL_ONSNATCH),
+ "idtag": checked(headphones.CONFIG.IDTAG)
}
# Need to convert EXTRAS to a dictionary we can pass to the config:
@@ -1196,7 +1213,7 @@ class WebInterface(object):
"nma_enabled", "nma_onsnatch", "pushalot_enabled", "pushalot_onsnatch", "synoindex_enabled", "pushover_enabled",
"pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled", "twitter_enabled", "twitter_onsnatch",
"osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch", "songkick_enabled", "songkick_filter_enabled",
- "mpc_enabled"
+ "mpc_enabled", "email_enabled", "email_tls", "email_onsnatch", "customauth", "idtag"
]
for checked_config in checked_configs:
if checked_config not in kwargs:
diff --git a/lib/musicbrainzngs/musicbrainz.py b/lib/musicbrainzngs/musicbrainz.py
index 875eff6f..d7e5e74f 100644
--- a/lib/musicbrainzngs/musicbrainz.py
+++ b/lib/musicbrainzngs/musicbrainz.py
@@ -271,6 +271,7 @@ user = password = ""
hostname = "musicbrainz.org"
_client = ""
_useragent = ""
+mb_auth = False
def auth(u, p):
"""Set the username and password to be used in subsequent queries to
@@ -284,9 +285,16 @@ def hpauth(u, p):
"""Set the username and password to be used in subsequent queries to
the MusicBrainz XML API that require authentication.
"""
- global hpuser, hppassword
+ global hpuser, hppassword, mb_auth
hpuser = u
hppassword = p
+ mb_auth = True
+
+def disable_hpauth():
+ """Disable the authentication for MusicBrainz XML API
+ """
+ global mb_auth
+ mb_auth = False
def set_useragent(app, version, contact=None):
"""Set the User-Agent to be used for requests to the MusicBrainz webservice.
@@ -635,7 +643,7 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
req.add_header('User-Agent', _useragent)
# Add headphones credentials
- if hostname == '144.76.94.239:8181':
+ if mb_auth:
base64string = base64.encodestring('%s:%s' % (hpuser, hppassword)).replace('\n', '')
req.add_header("Authorization", "Basic %s" % base64string)