mirror of
https://github.com/rembo10/headphones.git
synced 2026-05-16 16:45:32 +01:00
Merge branch 'develop'
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', ''),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user