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 +
+
+

Email

+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
@@ -1416,6 +1453,17 @@
+
+ +
+
+
+ +
+
+
+
+
@@ -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)