Merge branch 'develop'

This commit is contained in:
Ade
2017-07-03 11:44:45 +12:00
14 changed files with 174 additions and 27 deletions

View File

@@ -649,6 +649,10 @@
<label>Seed Ratio</label>
<input type="text" class="override-float" name="rutracker_ratio" value="${config['rutracker_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
</div>
<div class="row">
<label>Session Cookie (Optional - Advanced)</label>
<input type="text" class="override-float" name="rutracker_cookie" value="${config['rutracker_cookie']}" size="10" title="bb_session cookie (Advanced)">
</div>
</div>
</fieldset>

View File

@@ -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()

View File

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

View File

@@ -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', ''),

View File

@@ -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)

View File

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

View File

@@ -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']

View File

@@ -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 = {

View File

@@ -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)

View File

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

View File

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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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,