diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html
index 841df0d1..02f40718 100644
--- a/data/interfaces/default/config.html
+++ b/data/interfaces/default/config.html
@@ -649,6 +649,10 @@
+
+
+
+
diff --git a/headphones/__init__.py b/headphones/__init__.py
index 700caf47..8247e1c2 100644
--- a/headphones/__init__.py
+++ b/headphones/__init__.py
@@ -86,7 +86,7 @@ CURRENT_VERSION = None
LATEST_VERSION = None
COMMITS_BEHIND = None
-LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma"]
+LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma", "opus"]
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff"]
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
@@ -620,6 +620,14 @@ def dbcheck():
c.execute('ALTER TABLE snatched ADD COLUMN TorrentHash TEXT')
c.execute('UPDATE snatched SET TorrentHash = FolderName WHERE Status LIKE "Seed_%"')
+ # One off script to set CleanName to lower case
+ clean_name_mixed = c.execute('SELECT CleanName FROM have ORDER BY Date Desc').fetchone()[0]
+ if clean_name_mixed != clean_name_mixed.lower():
+ logger.info("Updating track clean name, this could take some time...")
+ c.execute('UPDATE tracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
+ c.execute('UPDATE alltracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
+ c.execute('UPDATE have SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
+
conn.commit()
c.close()
diff --git a/headphones/cache.py b/headphones/cache.py
index 202b7802..0e946677 100644
--- a/headphones/cache.py
+++ b/headphones/cache.py
@@ -16,7 +16,7 @@
import os
import headphones
-from headphones import db, helpers, logger, lastfm, request
+from headphones import db, helpers, logger, lastfm, request, mb
LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
@@ -290,6 +290,14 @@ class Cache(object):
data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=LASTFM_API_KEY)
+ # Try with name if not found
+ if not data:
+ dbartist = myDB.action('SELECT ArtistName, Type FROM artists WHERE ArtistID=?', [self.id]).fetchone()
+ if dbartist:
+ data = lastfm.request_lastfm("artist.getinfo",
+ artist=helpers.clean_musicbrainz_name(dbartist['ArtistName']),
+ api_key=LASTFM_API_KEY)
+
if not data:
return
@@ -315,18 +323,31 @@ class Cache(object):
else:
dbalbum = myDB.action(
- 'SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?',
+ 'SELECT ArtistName, AlbumTitle, ReleaseID, Type FROM albums WHERE AlbumID=?',
[self.id]).fetchone()
if dbalbum['ReleaseID'] != self.id:
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'],
api_key=LASTFM_API_KEY)
if not data:
- data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
- album=dbalbum['AlbumTitle'],
+ data = lastfm.request_lastfm("album.getinfo",
+ artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
+ album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
api_key=LASTFM_API_KEY)
else:
- data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
- album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
+ if dbalbum['Type'] != "part of":
+ data = lastfm.request_lastfm("album.getinfo",
+ artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
+ album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
+ api_key=LASTFM_API_KEY)
+ else:
+
+ # Series, use actual artist for the release-group
+ artist = mb.getArtistForReleaseGroup(self.id)
+ if artist:
+ data = lastfm.request_lastfm("album.getinfo",
+ artist=helpers.clean_musicbrainz_name(artist),
+ album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
+ api_key=LASTFM_API_KEY)
if not data:
return
diff --git a/headphones/config.py b/headphones/config.py
index 57af4c16..98b23785 100644
--- a/headphones/config.py
+++ b/headphones/config.py
@@ -248,6 +248,7 @@ _CONFIG_DEFINITIONS = {
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
'RUTRACKER_RATIO': (str, 'Rutracker', ''),
'RUTRACKER_USER': (str, 'Rutracker', ''),
+ 'RUTRACKER_COOKIE': (str, 'Rutracker', ''),
'SAB_APIKEY': (str, 'SABnzbd', ''),
'SAB_CATEGORY': (str, 'SABnzbd', ''),
'SAB_HOST': (str, 'SABnzbd', ''),
diff --git a/headphones/helpers.py b/headphones/helpers.py
index 2ba72101..05f6108b 100644
--- a/headphones/helpers.py
+++ b/headphones/helpers.py
@@ -23,6 +23,10 @@ import sys
import tempfile
import glob
+from beets import logging as beetslogging
+import six
+from contextlib import contextmanager
+
import fnmatch
import re
import os
@@ -257,6 +261,13 @@ _XLATE_SPECIAL = {
u'&': ' and ', # expand & to ' and '
}
+_XLATE_MUSICBRAINZ = {
+ # Translation table for Musicbrainz.
+ u"…": '...', # HORIZONTAL ELLIPSIS (U+2026)
+ u"’": "'", # APOSTROPHE (U+0027)
+ u"‐": "-", # EN DASH (U+2013)
+}
+
def _translate(s, dictionary):
# type: (basestring,Mapping[basestring,basestring])->basestring
@@ -322,9 +333,27 @@ def clean_name(s):
# 6. trim
u = u.strip()
# 7. lowercase
+ u = u.lower()
return u
+def clean_musicbrainz_name(s, return_as_string=True):
+ # type: (basestring)->unicode
+ """Substitute special Musicbrainz characters.
+ :param s: string to clean up, probably unicode.
+ :return: cleaned-up version of input string.
+ """
+ if not isinstance(s, unicode):
+ u = unicode(s, 'ascii', 'replace')
+ else:
+ u = s
+ u = _translate(u, _XLATE_MUSICBRAINZ)
+ if return_as_string:
+ return u.encode('utf-8')
+ else:
+ return u
+
+
def cleanTitle(title):
title = re.sub('[\.\-\/\_]', ' ', title).lower()
@@ -951,3 +980,24 @@ def create_https_certificates(ssl_cert, ssl_key):
return False
return True
+
+
+class BeetsLogCapture(beetslogging.Handler):
+
+ def __init__(self):
+ beetslogging.Handler.__init__(self)
+ self.messages = []
+
+ def emit(self, record):
+ self.messages.append(six.text_type(record.msg))
+
+
+@contextmanager
+def capture_beets_log(logger='beets'):
+ capture = BeetsLogCapture()
+ log = beetslogging.getLogger(logger)
+ log.addHandler(capture)
+ try:
+ yield capture.messages
+ finally:
+ log.removeHandler(capture)
diff --git a/headphones/lastfm.py b/headphones/lastfm.py
index 89ab7448..8f906d5f 100644
--- a/headphones/lastfm.py
+++ b/headphones/lastfm.py
@@ -55,7 +55,7 @@ def request_lastfm(method, **kwargs):
return
if "error" in data:
- logger.error("Last.FM returned an error: %s", data["message"])
+ logger.debug("Last.FM returned an error: %s", data["message"])
return
return data
diff --git a/headphones/mb.py b/headphones/mb.py
index faba6b8a..c087d6b9 100644
--- a/headphones/mb.py
+++ b/headphones/mb.py
@@ -770,3 +770,26 @@ def findAlbumID(artist=None, album=None):
return False
rgid = unicode(results[0]['id'])
return rgid
+
+
+def getArtistForReleaseGroup(rgid):
+ """
+ Returns artist name for a release group
+ Used for series where we store the series instead of the artist
+ """
+ releaseGroup = None
+ try:
+ with mb_lock:
+ releaseGroup = musicbrainzngs.get_release_group_by_id(
+ rgid, ["artists"])
+ releaseGroup = releaseGroup['release-group']
+ except musicbrainzngs.WebServiceError as e:
+ logger.warn(
+ 'Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (
+ rgid, str(e)))
+ mb_lock.snooze(5)
+
+ if not releaseGroup:
+ return False
+ else:
+ return releaseGroup['artist-credit'][0]['artist']['name']
diff --git a/headphones/metacritic.py b/headphones/metacritic.py
index d482786f..4ff20140 100644
--- a/headphones/metacritic.py
+++ b/headphones/metacritic.py
@@ -27,8 +27,9 @@ def update(artistid, artist_name, release_groups):
# cut down on api calls. If it's ineffective then we'll switch to search
replacements = {" & ": " ", ".": ""}
+ mc_artist_name = helpers.clean_musicbrainz_name(artist_name, return_as_string=False)
+ mc_artist_name = mc_artist_name.replace("'", " ")
mc_artist_name = helpers.replace_all(artist_name.lower(), replacements)
-
mc_artist_name = mc_artist_name.replace(" ", "-")
headers = {
diff --git a/headphones/notifiers.py b/headphones/notifiers.py
index f8f1b3f8..54f4f4b5 100644
--- a/headphones/notifiers.py
+++ b/headphones/notifiers.py
@@ -790,7 +790,9 @@ class BOXCAR(object):
'user_credentials': headphones.CONFIG.BOXCAR_TOKEN,
'notification[title]': title.encode('utf-8'),
'notification[long_message]': message.encode('utf-8'),
- 'notification[sound]': "done"
+ 'notification[sound]': "done",
+ 'notification[icon_url]': "https://raw.githubusercontent.com/rembo10/headphones/master/data/images"
+ "/headphoneslogo.png"
})
req = urllib2.Request(self.url)
diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py
index e3deee23..38e4bba5 100755
--- a/headphones/postprocessor.py
+++ b/headphones/postprocessor.py
@@ -24,6 +24,7 @@ import beets
import headphones
from beets import autotag
from beets import config as beetsconfig
+from beets import logging as beetslogging
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
from beetsplug import lyrics as beetslyrics
from headphones import notifiers, utorrent, transmission, deluge, qbittorrent
@@ -953,14 +954,29 @@ def correctMetadata(albumid, release, downloaded_track_list):
if not items:
continue
+ search_ids = []
+ logger.debug('Getting recommendation from beets. Artist: %s. Album: %s. Tracks: %s', release['ArtistName'],
+ release['AlbumTitle'], len(items))
+
+ # Try with specific release, e.g. alternate release selected from albumPage
+ if release['ReleaseID'] != release['AlbumID']:
+ logger.debug('trying beets with specific Release ID: %s', release['ReleaseID'])
+ search_ids = [release['ReleaseID']]
+
try:
- cur_artist, cur_album, prop = autotag.tag_album(items,
- search_artist=helpers.latinToAscii(
- release['ArtistName']),
- search_album=helpers.latinToAscii(
- release['AlbumTitle']))
- candidates = prop.candidates
- rec = prop.recommendation
+ beetslog = beetslogging.getLogger('beets')
+ beetslog.set_global_level(beetslogging.DEBUG) if headphones.VERBOSE else beetslog.set_global_level(
+ beetslogging.CRITICAL)
+ with helpers.capture_beets_log() as logs:
+ cur_artist, cur_album, prop = autotag.tag_album(items,
+ search_artist=release['ArtistName'],
+ search_album=release['AlbumTitle'],
+ search_ids=search_ids)
+ candidates = prop.candidates
+ rec = prop.recommendation
+ for log in logs:
+ logger.debug('Beets: %s', log)
+ beetslog.set_global_level(beetslogging.NOTSET)
except Exception as e:
logger.error('Error getting recommendation: %s. Not writing metadata', e)
return False
diff --git a/headphones/rutracker.py b/headphones/rutracker.py
index 658d490e..7185984d 100644
--- a/headphones/rutracker.py
+++ b/headphones/rutracker.py
@@ -49,7 +49,11 @@ class Rutracker(object):
# try again
if not self.has_bb_session_cookie(r):
time.sleep(10)
- r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
+ if headphones.CONFIG.RUTRACKER_COOKIE:
+ logger.info("Attempting to log in using predefined cookie...")
+ r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False, cookies={'bb_session': headphones.CONFIG.RUTRACKER_COOKIE})
+ else:
+ r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
if self.has_bb_session_cookie(r):
self.loggedin = True
logger.info("Successfully logged in to rutracker")
@@ -94,7 +98,11 @@ class Rutracker(object):
# sort by size, descending.
sort = '&o=7&s=2'
- searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
+ try:
+ searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
+ except:
+ searchterm = searchterm.encode('utf-8')
+ searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
logger.info("Searching rutracker using term: %s", searchterm)
return searchurl
diff --git a/headphones/transmission.py b/headphones/transmission.py
index 54241751..47de2c18 100644
--- a/headphones/transmission.py
+++ b/headphones/transmission.py
@@ -34,7 +34,7 @@ _session_id = None
def addTorrent(link, data=None):
method = 'torrent-add'
- if link.endswith('.torrent') and not link.startswith('http') or data:
+ if link.endswith('.torrent') and not link.startswith(('http', 'magnet')) or data:
if data:
metainfo = str(base64.b64encode(data))
else:
diff --git a/headphones/versioncheck.py b/headphones/versioncheck.py
index 341f7926..11c4e225 100644
--- a/headphones/versioncheck.py
+++ b/headphones/versioncheck.py
@@ -39,14 +39,15 @@ def runGit(args):
try:
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + headphones.PROG_DIR)
- p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True,
+ p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ shell=True,
cwd=headphones.PROG_DIR)
output, err = p.communicate()
output = output.strip()
logger.debug('Git output: ' + output)
- except OSError:
- logger.debug('Command failed: %s', cmd)
+ except OSError as e:
+ logger.debug('Command failed: %s. Error: %s' % (cmd, e))
continue
if 'not found' in output or "not recognized as an internal or external command" in output:
diff --git a/headphones/webserve.py b/headphones/webserve.py
index 855fed24..ee353fe2 100644
--- a/headphones/webserve.py
+++ b/headphones/webserve.py
@@ -252,7 +252,10 @@ class WebInterface(object):
namecheck = myDB.select('SELECT ArtistName from artists where ArtistID=?', [ArtistID])
for name in namecheck:
artistname = name['ArtistName']
- logger.info(u"Deleting all traces of artist: " + artistname)
+ try:
+ logger.info(u"Deleting all traces of artist: " + artistname)
+ except TypeError:
+ logger.info(u"Deleting all traces of artist: null")
myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID])
from headphones import cache
@@ -871,11 +874,19 @@ class WebInterface(object):
def musicScan(self, path, scan=0, redirect=None, autoadd=0, libraryscan=0):
headphones.CONFIG.LIBRARYSCAN = libraryscan
headphones.CONFIG.AUTO_ADD_ARTISTS = autoadd
- headphones.CONFIG.MUSIC_DIR = path
- headphones.CONFIG.write()
+
+ try:
+ params = {}
+ headphones.CONFIG.MUSIC_DIR = path
+ headphones.CONFIG.write()
+ except Exception as e:
+ logger.warn("Cannot save scan directory to config: %s", e)
+ if scan:
+ params = {"dir": path}
+
if scan:
try:
- threading.Thread(target=librarysync.libraryScan).start()
+ threading.Thread(target=librarysync.libraryScan, kwargs=params).start()
except Exception as e:
logger.error('Unable to complete the scan: %s' % e)
if redirect:
@@ -1229,6 +1240,7 @@ class WebInterface(object):
"rutracker_user": headphones.CONFIG.RUTRACKER_USER,
"rutracker_password": headphones.CONFIG.RUTRACKER_PASSWORD,
"rutracker_ratio": headphones.CONFIG.RUTRACKER_RATIO,
+ "rutracker_cookie": headphones.CONFIG.RUTRACKER_COOKIE,
"use_apollo": checked(headphones.CONFIG.APOLLO),
"apollo_username": headphones.CONFIG.APOLLO_USERNAME,
"apollo_password": headphones.CONFIG.APOLLO_PASSWORD,