Merge pull request #1883 from basilfx/improvements2

Again, a set of improvements
This commit is contained in:
AdeHub
2014-09-15 17:16:17 +12:00
21 changed files with 335 additions and 386 deletions

View File

@@ -63,7 +63,7 @@ def main():
headphones.SYS_ENCODING = 'UTF-8'
# Set up and gather command line arguments
parser = argparse.ArgumentParser(description='Music add-on for SABnzbd+')
parser = argparse.ArgumentParser(description='Music add-on for SABnzbd+, Transmission and more.')
parser.add_argument('-v', '--verbose', action='store_true', help='Increase console logging verbosity')
parser.add_argument('-q', '--quiet', action='store_true', help='Turn off console logging')

View File

@@ -896,8 +896,26 @@
</div>
</fieldset>
<fieldset>
<h3>Subsonic</h3>
<div class="row checkbox">
<input type="checkbox" name="subsonic_enabled" id="subsonic" value="1" ${config['subsonic_enabled']} /><label>Enable Subsonic Updates</label>
</div>
<div id="subsonicoptions">
<div class="row">
<label>Subsonic URL</label><input type="text" name="subsonic_host" value="${config['subsonic_host']}" size="30">
</div>
<div class="row">
<label>Subsonic Username</label><input type="text" name="subsonic_username" value="${config['subsonic_username']}" size="30">
</div>
<div class="row">
<label>Subsonic Password</label><input type="password" name="subsonic_password" value="${config['subsonic_password']}" size="30">
</div>
</div>
</fieldset>
</td>
<td>
<td>
<fieldset>
<h3>Synology NAS</h3>
@@ -1730,6 +1748,26 @@
}
});
if ($("#subsonic").is(":checked"))
{
$("#subsonicoptions").show();
}
else
{
$("#subsonicoptions").hide();
}
$("#subsonic").click(function(){
if ($("#subsonic").is(":checked"))
{
$("#subsonicoptions").slideDown();
}
else
{
$("#subsonicoptions").slideUp();
}
});
if ($("#songkick").is(":checked"))
{
$("#songkickoptions").show();

View File

@@ -283,6 +283,10 @@ OSX_NOTIFY_APP = None
BOXCAR_ENABLED = False
BOXCAR_ONSNATCH = False
BOXCAR_TOKEN = None
SUBSONIC_ENABLED = False
SUBSONIC_HOST = None
SUBSONIC_USERNAME = None
SUBSONIC_PASSWORD = None
MIRRORLIST = ["musicbrainz.org","headphones","custom"]
MIRROR = None
CUSTOMHOST = None
@@ -371,7 +375,7 @@ def initialize():
XBMC_NOTIFY, LMS_ENABLED, LMS_HOST, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \
PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, LOSSLESS_BITRATE_FROM, LOSSLESS_BITRATE_TO, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \
PLEX_ENABLED, PLEX_SERVER_HOST, PLEX_CLIENT_HOST, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE, PLEX_NOTIFY, PUSHALOT_ENABLED, PUSHALOT_APIKEY, \
PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED, VERIFY_SSL_CERT
PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED, SUBSONIC_ENABLED, SUBSONIC_HOST, SUBSONIC_USERNAME, SUBSONIC_PASSWORD, VERIFY_SSL_CERT
if __INITIALIZED__:
@@ -627,6 +631,11 @@ def initialize():
BOXCAR_ONSNATCH = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_onsnatch', 0))
BOXCAR_TOKEN = check_setting_str(CFG, 'Boxcar', 'boxcar_token', '')
SUBSONIC_ENABLED = bool(check_setting_int(CFG, 'Subsonic', 'subsonic_enabled', 0))
SUBSONIC_HOST = check_setting_str(CFG, 'Subsonic', 'subsonic_host', '')
SUBSONIC_USERNAME = check_setting_str(CFG, 'Subsonic', 'subsonic_username', '')
SUBSONIC_PASSWORD = check_setting_str(CFG, 'Subsonic', 'subsonic_password', '')
SONGKICK_ENABLED = bool(check_setting_int(CFG, 'Songkick', 'songkick_enabled', 1))
SONGKICK_APIKEY = check_setting_str(CFG, 'Songkick', 'songkick_apikey', 'nd1We7dFW2RqxPw8')
SONGKICK_LOCATION = check_setting_str(CFG, 'Songkick', 'songkick_location', '')
@@ -1071,6 +1080,12 @@ def config_write():
new_config['Boxcar']['boxcar_onsnatch'] = int(BOXCAR_ONSNATCH)
new_config['Boxcar']['boxcar_token'] = BOXCAR_TOKEN
new_config['Subsonic'] = {}
new_config['Subsonic']['subsonic_enabled'] = int(SUBSONIC_ENABLED)
new_config['Subsonic']['subsonic_host'] = SUBSONIC_HOST
new_config['Subsonic']['subsonic_username'] = SUBSONIC_USERNAME
new_config['Subsonic']['subsonic_password'] = SUBSONIC_PASSWORD
new_config['Songkick'] = {}
new_config['Songkick']['songkick_enabled'] = int(SONGKICK_ENABLED)
new_config['Songkick']['songkick_apikey'] = SONGKICK_APIKEY

View File

@@ -42,24 +42,22 @@ class Cache(object):
path_to_art_cache = os.path.join(headphones.CACHE_DIR, 'artwork')
id = None
id_type = None # 'artist' or 'album' - set automatically depending on whether ArtistID or AlbumID is passed
query_type = None # 'artwork','thumb' or 'info' - set automatically
artwork_files = []
thumb_files = []
artwork_errors = False
artwork_url = None
thumb_errors = False
thumb_url = None
info_summary = None
info_content = None
def __init__(self):
pass
self.id = None
self.id_type = None # 'artist' or 'album' - set automatically depending on whether ArtistID or AlbumID is passed
self.query_type = None # 'artwork','thumb' or 'info' - set automatically
self.artwork_files = []
self.thumb_files = []
self.artwork_errors = False
self.artwork_url = None
self.thumb_errors = False
self.thumb_url = None
self.info_summary = None
self.info_content = None
def _findfilesstartingwith(self,pattern,folder):
files = []
@@ -125,9 +123,9 @@ class Cache(object):
return thumb_url
def get_artwork_from_cache(self, ArtistID=None, AlbumID=None):
'''
"""
Pass a musicbrainz id to this function (either ArtistID or AlbumID)
'''
"""
self.query_type = 'artwork'
@@ -151,9 +149,9 @@ class Cache(object):
return None
def get_thumb_from_cache(self, ArtistID=None, AlbumID=None):
'''
"""
Pass a musicbrainz id to this function (either ArtistID or AlbumID)
'''
"""
self.query_type = 'thumb'
@@ -215,7 +213,7 @@ class Cache(object):
try:
image_url = data['artist']['image'][-1]['#text']
except Exception:
except (KeyError, IndexError):
logger.debug('No artist image found')
image_url = None
@@ -233,7 +231,7 @@ class Cache(object):
try:
image_url = data['album']['image'][-1]['#text']
except Exception:
except (KeyError, IndexError):
logger.debug('No album image found on last.fm')
image_url = None
@@ -260,17 +258,17 @@ class Cache(object):
try:
self.info_summary = data['artist']['bio']['summary']
except Exception:
except KeyError:
logger.debug('No artist bio summary found')
self.info_summary = None
try:
self.info_content = data['artist']['bio']['content']
except Exception:
except KeyError:
logger.debug('No artist bio found')
self.info_content = None
try:
image_url = data['artist']['image'][-1]['#text']
except Exception:
except KeyError:
logger.debug('No artist image found')
image_url = None
@@ -288,17 +286,17 @@ class Cache(object):
try:
self.info_summary = data['album']['wiki']['summary']
except Exception:
except KeyError:
logger.debug('No album summary found')
self.info_summary = None
try:
self.info_content = data['album']['wiki']['content']
except Exception:
except KeyError:
logger.debug('No album infomation found')
self.info_content = None
try:
image_url = data['album']['image'][-1]['#text']
except Exception:
except KeyError:
logger.debug('No album image link found')
image_url = None
@@ -358,10 +356,9 @@ class Cache(object):
artwork_path = os.path.join(self.path_to_art_cache, self.id + '.' + helpers.today() + ext)
try:
f = open(artwork_path, 'wb')
f.write(artwork)
f.close()
except Exception, e:
with open(artwork_path, 'wb') as f:
f.write(artwork)
except IOError as e:
logger.error('Unable to write to the cache dir: %s', e)
self.artwork_errors = True
self.artwork_url = image_url
@@ -375,7 +372,7 @@ class Cache(object):
if not os.path.isdir(self.path_to_art_cache):
try:
os.makedirs(self.path_to_art_cache)
except Exception, e:
except Exception as e:
logger.error('Unable to create artwork cache dir. Error: %s' + e)
self.thumb_errors = True
self.thumb_url = thumb_url
@@ -384,17 +381,16 @@ class Cache(object):
for thumb_file in self.thumb_files:
try:
os.remove(thumb_file)
except:
except Exception as e:
logger.error('Error deleting file from the cache: %s', thumb_file)
ext = os.path.splitext(image_url)[1]
thumb_path = os.path.join(self.path_to_art_cache, 'T_' + self.id + '.' + helpers.today() + ext)
try:
f = open(thumb_path, 'wb')
f.write(artwork)
f.close()
except Exception, e:
with open(thumb_path, 'wb') as f:
f.write(artwork)
except IOError as e:
logger.error('Unable to write to the cache dir: %s', e)
self.thumb_errors = True
self.thumb_url = image_url

View File

@@ -13,29 +13,12 @@
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
def ex(e):
"""
Returns a string from the exception text if it exists.
"""
# sanity check
if not e.args or not e.args[0]:
return ""
e_message = e.args[0]
# if fixStupidEncodings doesn't fix it then maybe it's not a string, in which case we'll try printing it anyway
if not e_message:
try:
e_message = str(e.args[0])
except:
e_message = ""
return e_message
class HeadphonesException(Exception):
"Generic Headphones Exception - should never be thrown, only subclassed"
"""
Generic Headphones Exception - should never be thrown, only subclassed
"""
class NewzbinAPIThrottled(HeadphonesException):
"Newzbin has throttled us, deal with it"
"""
Newzbin has throttled us, deal with it
"""

View File

@@ -603,9 +603,11 @@ def create_https_certificates(ssl_cert, ssl_key):
# Save the key and certificate to disk
try:
open(ssl_key, 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
open(ssl_cert, 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
except Exception, e:
with open(ssl_key, 'w') as f:
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
with open(ssl_cert, 'w') as f:
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
except IOError as e:
logger.error("Error creating SSL key and certificate: %s", e)
return False

View File

@@ -13,24 +13,26 @@
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from lib.pyItunes import *
import time
import threading
import os
from beets.mediafile import MediaFile
import headphones
from headphones import logger, helpers, db, mb, lastfm
blacklisted_special_artist_names = ['[anonymous]','[data]','[no artist]','[traditional]','[unknown]','Various Artists']
blacklisted_special_artists = ['f731ccc4-e22a-43af-a747-64213329e088','33cf029c-63b0-41a0-9855-be2a3665fb3b',\
'314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc','eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61',\
'9be7f096-97ec-4615-8957-8d40b5dcbc41','125ec42a-7229-4250-afc5-e057484327fe',\
'89ad4ac3-39f7-470e-963a-56509c546377']
from beets.mediafile import MediaFile
import os
import time
import threading
import headphones
blacklisted_special_artist_names = ['[anonymous]', '[data]', '[no artist]',
'[traditional]','[unknown]','Various Artists']
blacklisted_special_artists = ['f731ccc4-e22a-43af-a747-64213329e088',
'33cf029c-63b0-41a0-9855-be2a3665fb3b',
'314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc',
'eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61',
'9be7f096-97ec-4615-8957-8d40b5dcbc41',
'125ec42a-7229-4250-afc5-e057484327fe',
'89ad4ac3-39f7-470e-963a-56509c546377']
def is_exists(artistid):
myDB = db.DBConnection()
# See if the artist is already in the database
@@ -56,7 +58,7 @@ def artistlist_to_mbids(artistlist, forced=False):
if not isinstance(artist, unicode):
try:
artist = artist.decode('utf-8', 'replace')
except:
except Exception:
logger.warn("Unable to convert artist to unicode so cannot do a database lookup")
continue
@@ -68,7 +70,6 @@ def artistlist_to_mbids(artistlist, forced=False):
try:
artistid = results[0]['id']
except IndexError:
logger.info('MusicBrainz query turned up no matches for: %s' % artist)
continue
@@ -130,11 +131,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
# We need the current minimal info in the database instantly
# so we don't throw a 500 error when we redirect to the artistPage
controlValueDict = {"ArtistID": artistid}
# Don't replace a known artist name with an "Artist ID" placeholder
dbartist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [artistid]).fetchone()
# Only modify the Include Extras stuff if it's a new artist. We need it early so we know what to fetch
@@ -183,20 +182,20 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
myDB.upsert("artists", newValueDict, controlValueDict)
# See if we need to grab extras. Artist specific extras take precedence over global option
# Global options are set when adding a new artist
myDB = db.DBConnection()
# See if we need to grab extras. Artist specific extras take precedence
# over global option. Global options are set when adding a new artist
try:
db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?', [artistid]).fetchone()
includeExtras = db_artist['IncludeExtras']
except IndexError:
includeExtras = False
#Clean all references to release group in dB that are no longer referenced from the musicbrainz refresh
# Clean all references to release group in dB that are no longer referenced
# from the musicbrainz refresh
group_list = []
force_repackage = 0
#Don't nuke the database if there's a MusicBrainz error
# Don't nuke the database if there's a MusicBrainz error
if len(artist['releasegroups']) != 0:
for groups in artist['releasegroups']:
group_list.append(groups['id'])
@@ -233,7 +232,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
if not forcefull:
new_release_group = False
try:
@@ -244,12 +242,10 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
if new_release_group:
logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid,includeExtras)
else:
if check_release_date is None or check_release_date == u"None":
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid,includeExtras,True)
@@ -263,33 +259,35 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
else:
release_date = today
if helpers.get_age(today) - helpers.get_age(release_date) < pause_delta:
logger.info("[%s] Now updating: %s (Release Date <%s Days) " % (artist['artist_name'], rg['title'], pause_delta))
logger.info("[%s] Now updating: %s (Release Date <%s Days)", artist['artist_name'], rg['title'], pause_delta)
new_releases = mb.get_new_releases(rgid,includeExtras,True)
else:
logger.info("[%s] Skipping: %s (Release Date >%s Days)" % (artist['artist_name'], rg['title'], pause_delta))
logger.info("[%s] Skipping: %s (Release Date >%s Days)", artist['artist_name'], rg['title'], pause_delta)
skip_log = 1
new_releases = 0
if force_repackage == 1:
new_releases = -1
logger.info('[%s] Forcing repackage of %s (Release Group Removed)' % (artist['artist_name'], al_title))
logger.info('[%s] Forcing repackage of %s (Release Group Removed)', artist['artist_name'], al_title)
else:
new_releases = new_releases
else:
logger.info("[%s] Now adding/updating: %s (Comprehensive Force)" % (artist['artist_name'], rg['title']))
logger.info("[%s] Now adding/updating: %s (Comprehensive Force)", artist['artist_name'], rg['title'])
new_releases = mb.get_new_releases(rgid,includeExtras,forcefull)
if new_releases != 0:
#Dump existing hybrid release since we're repackaging/replacing it
# Dump existing hybrid release since we're repackaging/replacing it
myDB.action("DELETE from albums WHERE ReleaseID=?", [rg['id']])
myDB.action("DELETE from allalbums WHERE ReleaseID=?", [rg['id']])
myDB.action("DELETE from tracks WHERE ReleaseID=?", [rg['id']])
myDB.action("DELETE from alltracks WHERE ReleaseID=?", [rg['id']])
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rg['id']])
# This will be used later to build a hybrid release
fullreleaselist = []
#Search for releases within a release group
# Search for releases within a release group
find_hybrid_releases = myDB.action("SELECT * from allalbums WHERE AlbumID=?", [rg['id']])
# Build the dictionary for the fullreleaselist
for items in find_hybrid_releases:
if items['ReleaseID'] != rg['id']: #don't include hybrid information, since that's what we're replacing
@@ -320,14 +318,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
newValueDict['Tracks'] = hybrid_track_array
fullreleaselist.append(newValueDict)
#print fullreleaselist
# Basically just do the same thing again for the hybrid release
# This may end up being called with an empty fullreleaselist
try:
hybridrelease = getHybridRelease(fullreleaselist)
logger.info('[%s] Packaging %s releases into hybrid title' % (artist['artist_name'], rg['title']))
except Exception, e:
except Exception as e:
errors = True
logger.warn('[%s] Unable to get hybrid release information for %s: %s' % (artist['artist_name'],rg['title'],e))
continue
@@ -387,7 +383,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
# If there's no release in the main albums tables, add the default (hybrid)
# If there is a release, check the ReleaseID against the AlbumID to see if they differ (user updated)
# check if the album already exists
if not rg_exists:
releaseid = rg['id']
else:
@@ -445,7 +440,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
continue
for track in tracks:
controlValueDict = {"TrackID": track['TrackID'],
"AlbumID": rg['id']}
@@ -569,7 +563,7 @@ def addReleaseById(rid, rgid=None):
logger.debug("Didn't find releaseID " + rid + " in the cache. Looking up its ReleaseGroupID")
try:
release_dict = mb.getRelease(rid)
except Exception, e:
except Exception as e:
logger.info('Unable to get release information for Release %s: %s', rid, e)
if status == 'Loading':
myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid])
@@ -639,7 +633,6 @@ def addReleaseById(rid, rgid=None):
myDB.action('INSERT INTO releases VALUES( ?, ?)', [rid, release_dict['rgid']])
for track in release_dict['tracks']:
cleanname = helpers.cleanName(release_dict['artist_name'] + ' ' + release_dict['rg_title'] + ' ' + track['title'])
controlValueDict = {"TrackID": track['id'],
@@ -676,7 +669,7 @@ def addReleaseById(rid, rgid=None):
newValueDict = {"Status": "Wanted"}
myDB.upsert("albums", newValueDict, controlValueDict)
#start a search for the album
# Start a search for the album
import searcher
searcher.searchforalbum(rgid, False)
elif not rg_exists and not release_dict:
@@ -718,39 +711,43 @@ def updateFormat():
def getHybridRelease(fullreleaselist):
"""
Returns a dictionary of best group of tracks from the list of releases & earliest release date
Returns a dictionary of best group of tracks from the list of releases and
earliest release date
"""
if len(fullreleaselist) == 0:
raise Exception("getHybridRelease was called with an empty fullreleaselist")
raise ValueError("Empty fullreleaselist")
sortable_release_list = []
formats = {
'2xVinyl': '2',
'Vinyl': '2',
'CD': '0',
'Cassette': '3',
'2xCD': '1',
'Digital Media': '0'
}
countries = {
'US': '0',
'GB': '1',
'JP': '2',
}
for release in fullreleaselist:
formats = {
'2xVinyl': '2',
'Vinyl': '2',
'CD': '0',
'Cassette': '3',
'2xCD': '1',
'Digital Media': '0'
}
countries = {
'US': '0',
'GB': '1',
'JP': '2',
}
# Find values for format and country
try:
format = int(formats[release['Format']])
except:
except (ValueError, KeyError):
format = 3
try:
country = int(countries[release['Country']])
except:
except (ValueError, KeyError):
country = 3
# Create record
release_dict = {
'hasasin': bool(release['AlbumASIN']),
'asin': release['AlbumASIN'],
@@ -760,15 +757,19 @@ def getHybridRelease(fullreleaselist):
'format': format,
'country': country,
'tracks': release['Tracks']
}
}
sortable_release_list.append(release_dict)
#necessary to make dates that miss the month and/or day show up after full dates
# Necessary to make dates that miss the month and/or day show up after full
# dates
def getSortableReleaseDate(releaseDate):
# Change this value to change the sorting behaviour of none, returning
# 'None' will put it at the top which was normal behaviour for pre-ngs
# versions
if releaseDate == None:
return 'None';#change this value to change the sorting behaviour of none, returning 'None' will put it at the top
#which was normal behaviour for pre-ngs versions
return 'None';
if releaseDate.count('-') == 2:
return releaseDate
elif releaseDate.count('-') == 1:

View File

@@ -30,8 +30,8 @@ def request_lastfm(method, **kwargs):
Call a Last.FM API method. Automatically sets the method and API key. Method
will return the result if no error occured.
By default, this method will request the JSON format, since it is lighter
than XML.
By default, this method will request the JSON format, since it is more
lightweight than XML.
"""
# Prepare request
@@ -41,6 +41,8 @@ def request_lastfm(method, **kwargs):
# Send request
logger.debug("Calling Last.FM method: %s", method)
logger.debug("Last.FM call parameters: %s", kwargs)
data = request.request_json(ENTRY_POINT, timeout=TIMEOUT, params=kwargs)
# Parse response and check for errors.
@@ -49,7 +51,7 @@ def request_lastfm(method, **kwargs):
return
if "error" in data:
logger.debug("Last.FM returned an error: %s", data["message"])
logger.error("Last.FM returned an error: %s", data["message"])
return
return data

View File

@@ -15,10 +15,10 @@
import os
import glob
from beets.mediafile import MediaFile
import headphones
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
from headphones import db, logger, helpers, importer, lastfm
# You can scan a single directory and append it to the current library by specifying append=True, ArtistID & ArtistName
@@ -84,8 +84,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
for directory in d[:]:
if directory.startswith("."):
d.remove(directory)
for files in f:
for files in f:
# MEDIA_FORMATS = music file extensions, e.g. mp3, flac, etc
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
@@ -104,9 +104,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
# Try to read the metadata
try:
f = MediaFile(song)
except:
logger.error('Cannot read file: ' + unicode_song_path)
except (FileTypeError, UnreadableFileError):
logger.error("Cannot read file media file '%s'. It may be corrupted or not a media file.", unicode_song_path)
continue
# Grab the bitrates for the auto detect bit rate option

View File

@@ -14,7 +14,6 @@
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from headphones import logger, helpers, common, request
from headphones.exceptions import ex
from xml.dom import minidom
from httplib import HTTPSConnection
@@ -40,7 +39,10 @@ try:
except ImportError:
from cgi import parse_qsl
class GROWL:
class GROWL(object):
"""
Growl notifications, for OS X
"""
def __init__(self):
self.enabled = headphones.GROWL_ENABLED
@@ -90,7 +92,9 @@ class GROWL:
# Send it, including an image
image_file = os.path.join(str(headphones.PROG_DIR), 'data/images/headphoneslogo.png')
image = open(image_file, 'rb').read()
with open(image_file, 'rb') as f:
image = f.read()
try:
growl.notify(
@@ -116,10 +120,10 @@ class GROWL:
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
class PROWL:
keys = []
priority = []
class PROWL(object):
"""
Prowl notifications.
"""
def __init__(self):
self.enabled = headphones.PROWL_ENABLED
@@ -163,25 +167,29 @@ class PROWL:
return
def test(self, keys, priority):
self.enabled = True
self.keys = keys
self.priority = priority
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
class MPC:
class MPC(object):
"""
MPC library update
"""
def __init__(self):
pass
def notify( self ):
subprocess.call( ["mpc", "update"] )
class XBMC:
class XBMC(object):
"""
XBMC notifications
"""
def __init__(self):
@@ -249,15 +257,15 @@ class XBMC:
if not request:
raise Exception
except:
logger.warn('Error sending notification request to XBMC')
except Exception:
logger.error('Error sending notification request to XBMC')
class LMS:
#Class for updating a Logitech Media Server
class LMS(object):
"""
Class for updating a Logitech Media Server
"""
def __init__(self):
self.hosts = headphones.LMS_HOST
def _sendjson(self, host):
@@ -293,8 +301,7 @@ class LMS:
if not request:
logger.warn('Error sending rescan request to LMS')
class Plex:
class Plex(object):
def __init__(self):
self.server_hosts = headphones.PLEX_SERVER_HOST
@@ -380,7 +387,7 @@ class Plex:
except:
logger.warn('Error sending notification request to Plex Media Server')
class NMA:
class NMA(object):
def notify(self, artist=None, album=None, snatched=None):
title = 'Headphones'
api = headphones.NMA_APIKEY
@@ -416,7 +423,7 @@ class NMA:
else:
return True
class PUSHBULLET:
class PUSHBULLET(object):
def __init__(self):
self.apikey = headphones.PUSHBULLET_APIKEY
@@ -469,7 +476,7 @@ class PUSHBULLET:
self.notify('Main Screen Activate', 'Test Message')
class PUSHALOT:
class PUSHALOT(object):
def notify(self, message, event):
if not headphones.PUSHALOT_ENABLED:
@@ -508,7 +515,7 @@ class PUSHALOT:
logger.info(u"Pushalot notification failed.")
return False
class Synoindex:
class Synoindex(object):
def __init__(self, util_loc='/usr/syno/bin/synoindex'):
self.util_loc = util_loc
@@ -544,19 +551,17 @@ class Synoindex:
for path in path_list:
self.notify(path)
class PUSHOVER:
application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz"
keys = []
priority = []
class PUSHOVER(object):
def __init__(self):
self.enabled = headphones.PUSHOVER_ENABLED
self.keys = headphones.PUSHOVER_KEYS
self.priority = headphones.PUSHOVER_PRIORITY
if headphones.PUSHOVER_APITOKEN:
self.application_token = headphones.PUSHOVER_APITOKEN
pass
else:
self.application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz"
def conf(self, options):
return cherrypy.config['config'].get('Pushover', options)
@@ -598,23 +603,23 @@ class PUSHOVER:
return
def test(self, keys, priority):
self.enabled = True
self.keys = keys
self.priority = priority
self.notify('Main Screen Activate', 'Test Message')
class TwitterNotifier:
consumer_key = "oYKnp2ddX5gbARjqX8ZAAg"
consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk"
class TwitterNotifier(object):
REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token'
ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token'
AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize'
SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate'
def __init__(self):
self.consumer_key = "oYKnp2ddX5gbARjqX8ZAAg"
self.consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk"
def notify_snatch(self, title):
if headphones.TWITTER_ONSNATCH:
self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH]+': '+title+' at '+helpers.now())
@@ -708,11 +713,7 @@ class TwitterNotifier:
return self._send_tweet(prefix+": "+message)
notifier = TwitterNotifier
class OSX_NOTIFY:
objc = None
class OSX_NOTIFY(object):
def __init__(self):
try:
@@ -767,14 +768,12 @@ class OSX_NOTIFY:
def swizzled_bundleIdentifier(self, original, swizzled):
return 'ade.headphones.osxnotify'
class BOXCAR:
class BOXCAR(object):
def __init__(self):
self.url = 'https://new.boxcar.io/api/notifications'
def notify(self, title, message, rgid=None):
try:
if rgid:
message += '<br></br><a href="http://musicbrainz.org/release-group/%s">MusicBrainz</a>' % rgid
@@ -791,6 +790,25 @@ class BOXCAR:
handle.close()
return True
except urllib2.URLError, e:
except urllib2.URLError as e:
logger.warn('Error sending Boxcar2 Notification: %s' % e)
return False
class SubSonicNotifier(object):
def __init__(self):
self.host = headphones.SUBSONIC_HOST
self.username = headphones.SUBSONIC_USERNAME
self.password = headphones.SUBSONIC_PASSWORD
def notify(self, albumpaths):
# Correct URL
if not self.host.lower().startswith("http"):
self.host = "http://" + self.host
if not self.host.lower().endswith("/"):
self.host = self.host + "/"
# Invoke request
request.request_response(self.host + "musicFolderSettings.view?scanNow",
auth=(self.username, self.password))

View File

@@ -519,6 +519,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
boxcar.notify('Headphones processed: ' + pushmessage,
statusmessage, release['AlbumID'])
if headphones.SUBSONIC_ENABLED:
logger.info(u"Sending Subsonic update")
subsonic = notifiers.SubSonicNotifier()
subsonic.notify(albumpaths)
if headphones.MPC_ENABLED:
mpc = notifiers.MPC()
mpc.notify()
@@ -569,25 +574,27 @@ def addAlbumArt(artwork, albumpath, release):
album_art_name = album_art_name.replace(".", "_", 1)
try:
file = open(os.path.join(albumpath, album_art_name), 'wb')
file.write(artwork)
file.close()
except Exception, e:
logger.error('Error saving album art: %s' % str(e))
with open(os.path.join(albumpath, album_art_name), 'wb') as f:
f.write(artwork)
except IOError as e:
logger.error('Error saving album art: %s', e)
return
def cleanupFiles(albumpath):
logger.info('Cleaning up files')
for r,d,f in os.walk(albumpath):
for files in f:
if not any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
logger.debug('Removing: %s' % files)
try:
os.remove(os.path.join(r, files))
except Exception, e:
except Exception as e:
logger.error(u'Could not remove file: %s. Error: %s' % (files.decode(headphones.SYS_ENCODING, 'replace'), e))
def renameNFO(albumpath):
logger.info('Renaming NFO')
for r,d,f in os.walk(albumpath):
for file in f:
if file.lower().endswith('.nfo'):
@@ -595,11 +602,10 @@ def renameNFO(albumpath):
try:
new_file_name = os.path.join(r, file)[:-3] + 'orig.nfo'
os.rename(os.path.join(r, file), new_file_name)
except Exception, e:
except Exception as e:
logger.error(u'Could not rename file: %s. Error: %s' % (os.path.join(r, file).decode(headphones.SYS_ENCODING, 'replace'), e))
def moveFiles(albumpath, release, tracks):
try:
year = release['ReleaseDate'][:4]
except TypeError:

View File

@@ -54,7 +54,7 @@ def request_response(url, method="get", auto_raise=True,
try:
response.raise_for_status()
except:
logger.debug("Response status code %d is not white " +
logger.debug("Response status code %d is not white " \
"listed, raised exception", response.status_code)
raise
elif auto_raise:
@@ -62,17 +62,17 @@ def request_response(url, method="get", auto_raise=True,
return response
except requests.ConnectionError:
logger.error("Unable to connect to remote host. Check if the remote " +
logger.error("Unable to connect to remote host. Check if the remote " \
"host is up and running.")
except requests.Timeout:
logger.error("Request timed out. The remote host did not respeond " +
logger.error("Request timed out. The remote host did not respeond " \
"timely.")
except requests.HTTPError as e:
if e.response is not None:
if e.response.status_code >= 500:
cause = "remote server error"
elif e.response.status_code >= 400:
cause = "local request error"
cause = "local client error"
else:
# I don't think we will end up here, but for completeness
cause = "unknown"
@@ -80,25 +80,9 @@ def request_response(url, method="get", auto_raise=True,
logger.error("Request raise HTTP error with status code %d (%s).",
e.response.status_code, cause)
# Some servers return extra information in the result. Try to parse
# it for debugging purpose. Messages are limited to 150 characters,
# since it may return the whole page in case of normal web page URLs
# Debug response
if headphones.VERBOSE:
if e.response.headers.get("content-type") == "text/html":
soup = BeautifulSoup(e.response.content, "html5lib")
message = soup.find("body")
message = message.text if message else soup.text
message = message.strip()
else:
message = e.response.content.strip()
if message:
# Truncate message if it is too long.
if len(message) > 150:
message = message[:150] + "..."
logger.debug("Server responded with message: %s", message)
server_message(e.response)
else:
logger.error("Request raised HTTP error.")
except requests.RequestException as e:
@@ -144,12 +128,16 @@ def request_json(url, **kwargs):
result = response.json()
if validator and not validator(result):
logger.error("JSON validation result vailed")
logger.error("JSON validation result failed")
else:
return result
except ValueError:
logger.error("Response returned invalid JSON data")
# Debug response
if headphones.VERBOSE:
server_message(response)
def request_content(url, **kwargs):
"""
Wrapper for `request_response', which will return the raw content.
@@ -168,4 +156,37 @@ def request_feed(url, **kwargs):
response = request_response(url, **kwargs)
if response is not None:
return feedparser.parse(response.content)
return feedparser.parse(response.content)
def server_message(response):
"""
Extract server message from response and log in to logger with DEBUG level.
Some servers return extra information in the result. Try to parse it for
debugging purpose. Messages are limited to 150 characters, since it may
return the whole page in case of normal web page URLs
"""
message = None
# First attempt is to 'read' the response as HTML
if response.headers.get("content-type") == "text/html":
try:
soup = BeautifulSoup(response.content, "html5lib")
except Exception:
pass
message = soup.find("body")
message = message.text if message else soup.text
message = message.strip()
# Second attempt is to just take the response
if message is None:
message = response.content.strip()
if message:
# Truncate message if it is too long.
if len(message) > 150:
message = message[:150] + "..."
logger.debug("Server responded with message: %s", message)

View File

@@ -51,7 +51,7 @@ class Rutracker():
try:
self.opener.open("http://login.rutracker.org/forum/login.php", params)
except :
except Exception:
pass
# Check if we're logged in
@@ -286,17 +286,17 @@ class Rutracker():
else:
tempdir = mkdtemp(suffix='_rutracker_torrents')
download_path = os.path.join(tempdir, torrent_name)
fp = open (download_path, 'wb')
fp.write (torrent)
fp.close ()
with open(download_path, 'wb') as f:
f.write(torrent)
os.umask(prev)
# Add file to utorrent
if headphones.TORRENT_DOWNLOADER == 2:
self.utorrent_add_file(download_path)
except Exception, e:
logger.error('Error getting torrent: %s' % e)
except Exception as e:
logger.error('Error getting torrent: %s', e)
return False
return download_path, tor_hash
@@ -322,9 +322,10 @@ class Rutracker():
try:
r = session.get(url + 'token.html')
except:
logger.debug('Error getting token')
except Exception:
logger.exception('Error getting token')
return
if r.status_code == '401':
logger.debug('Error reaching utorrent')
return
@@ -336,15 +337,11 @@ class Rutracker():
session.params = {'token': regex.group(1)}
params = {'action': 'add-file'}
f = open(filename, 'rb')
files = {'torrent_file': f}
try:
session.post(url, params=params, files=files)
except:
logger.debug('Error adding file to utorrent')
return
finally:
f.close()
with open(filename, 'rb') as f:
try:
session.post(url, params={'action': 'add-file'},
files={'torrent_file': f})
except Exception:
logger.exception('Error adding file to utorrent')
return

View File

@@ -31,9 +31,8 @@ def addTorrent(link):
method = 'torrent-add'
if link.endswith('.torrent'):
f = open(link,'rb')
metainfo = str(base64.b64encode(f.read()))
f.close()
with open(link, 'rb') as f:
metainfo = str(base64.b64encode(f.read()))
arguments = {'metainfo': metainfo, 'download-dir':headphones.DOWNLOAD_TORRENT_DIR}
else:
arguments = {'filename': link, 'download-dir': headphones.DOWNLOAD_TORRENT_DIR}

View File

@@ -21,7 +21,6 @@ import headphones
import subprocess
from headphones import logger, version, request
from headphones.exceptions import ex
def runGit(args):
@@ -63,7 +62,6 @@ def runGit(args):
def getVersion():
if version.HEADPHONES_VERSION.startswith('win32build'):
headphones.INSTALL_TYPE = 'win'
# Don't have a way to update exe yet, but don't want to set VERSION to None
@@ -109,9 +107,8 @@ def getVersion():
if not os.path.isfile(version_file):
return None, 'master'
fp = open(version_file, 'r')
current_version = fp.read().strip(' \n\r')
fp.close()
with open(version_file, 'r') as f:
current_version = f.read().strip(' \n\r')
if current_version:
return current_version, headphones.GIT_BRANCH
@@ -199,9 +196,8 @@ def update():
tar_download_path = os.path.join(headphones.PROG_DIR, download_name)
# Save tar to disk
f = open(tar_download_path, 'wb')
f.write(data)
f.close()
with open(tar_download_path, 'wb') as f:
f.write(data)
# Extract the tar to update folder
logger.info('Extracting file: ' + tar_download_path)
@@ -233,9 +229,9 @@ def update():
# Update version.txt
try:
ver_file = open(version_path, 'w')
ver_file.write(str(headphones.LATEST_VERSION))
ver_file.close()
except IOError, e:
logger.error("Unable to write current version to version.txt, update not complete: "+ex(e))
with open(version_path, 'w') as f:
f.write(str(headphones.LATEST_VERSION))
except IOError as e:
logger.error("Unable to write current version to version.txt, " \
"update not complete: ", e)
return

View File

@@ -1124,6 +1124,10 @@ class WebInterface(object):
"pushbullet_onsnatch": checked(headphones.PUSHBULLET_ONSNATCH),
"pushbullet_apikey": headphones.PUSHBULLET_APIKEY,
"pushbullet_deviceid": headphones.PUSHBULLET_DEVICEID,
"subsonic_enabled": checked(headphones.SUBSONIC_ENABLED),
"subsonic_host": headphones.SUBSONIC_HOST,
"subsonic_username": headphones.SUBSONIC_USERNAME,
"subsonic_password": headphones.SUBSONIC_PASSWORD,
"twitter_enabled": checked(headphones.TWITTER_ENABLED),
"twitter_onsnatch": checked(headphones.TWITTER_ONSNATCH),
"osx_notify_enabled": checked(headphones.OSX_NOTIFY_ENABLED),
@@ -1181,7 +1185,7 @@ class WebInterface(object):
rutracker=0, rutracker_user=None, rutracker_password=None, rutracker_ratio=None, rename_files=0, correct_metadata=0, cleanup_files=0, keep_nfo=0, add_album_art=0, album_art_format=None, embed_album_art=0, embed_lyrics=0, replace_existing_folders=False,
destination_dir=None, lossless_destination_dir=None, folder_format=None, file_format=None, file_underscores=0, include_extras=0, single=0, ep=0, compilation=0, soundtrack=0, live=0, remix=0, spokenword=0, audiobook=0, other=0, djmix=0, mixtape_street=0, broadcast=0, interview=0, demo=0,
autowant_upcoming=False, autowant_all=False, keep_torrent_files=False, prefer_torrents=0, open_magnet_links=0, interface=None, log_dir=None, cache_dir=None, music_encoder=0, encoder=None, xldprofile=None,
bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0,
bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0, subsonic_enabled=False, subsonic_host=None, subsonic_username=None, subsonic_password=None,
delete_lossless_files=0, growl_enabled=0, growl_onsnatch=0, growl_host=None, growl_password=None, prowl_enabled=0, prowl_onsnatch=0, prowl_keys=None, prowl_priority=0, xbmc_enabled=0, xbmc_host=None, xbmc_username=None, xbmc_password=None,
xbmc_update=0, xbmc_notify=0, nma_enabled=False, nma_apikey=None, nma_priority=0, nma_onsnatch=0, pushalot_enabled=False, pushalot_apikey=None, pushalot_onsnatch=0, synoindex_enabled=False, lms_enabled=0, lms_host=None,
pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, pushover_apitoken=None, pushbullet_enabled=0, pushbullet_onsnatch=0, pushbullet_apikey=None, pushbullet_deviceid=None, twitter_enabled=0, twitter_onsnatch=0,
@@ -1350,6 +1354,10 @@ class WebInterface(object):
headphones.PUSHBULLET_ONSNATCH = pushbullet_onsnatch
headphones.PUSHBULLET_APIKEY = pushbullet_apikey
headphones.PUSHBULLET_DEVICEID = pushbullet_deviceid
headphones.SUBSONIC_ENABLED = subsonic_enabled
headphones.SUBSONIC_HOST = subsonic_host
headphones.SUBSONIC_USERNAME = subsonic_username
headphones.SUBSONIC_PASSWORD = subsonic_password
headphones.SONGKICK_ENABLED = songkick_enabled
headphones.SONGKICK_APIKEY = songkick_apikey
headphones.SONGKICK_LOCATION = songkick_location

View File

@@ -114,7 +114,7 @@ def initialize(options=None):
# Prevent time-outs
cherrypy.engine.timeout_monitor.unsubscribe()
cherrypy.tree.mount(WebInterface(), options['http_root'], config = conf)
cherrypy.tree.mount(WebInterface(), options['http_root'], config=conf)
try:
cherrypy.process.servers.check_port(options['http_host'], options['http_port'])

View File

@@ -1,41 +0,0 @@
from lib.pyItunes.Song import Song
import time
class Library:
def __init__(self,dictionary):
self.songs = self.parseDictionary(dictionary)
def parseDictionary(self,dictionary):
songs = []
format = "%Y-%m-%dT%H:%M:%SZ"
for song,attributes in dictionary.iteritems():
s = Song()
s.name = attributes.get('Name')
s.artist = attributes.get('Artist')
s.album_artist = attributes.get('Album Aritst')
s.composer = attributes.get('Composer')
s.album = attributes.get('Album')
s.genre = attributes.get('Genre')
s.kind = attributes.get('Kind')
if attributes.get('Size'):
s.size = int(attributes.get('Size'))
s.total_time = attributes.get('Total Time')
s.track_number = attributes.get('Track Number')
if attributes.get('Year'):
s.year = int(attributes.get('Year'))
if attributes.get('Date Modified'):
s.date_modified = time.strptime(attributes.get('Date Modified'),format)
if attributes.get('Date Added'):
s.date_added = time.strptime(attributes.get('Date Added'),format)
if attributes.get('Bit Rate'):
s.bit_rate = int(attributes.get('Bit Rate'))
if attributes.get('Sample Rate'):
s.sample_rate = int(attributes.get('Sample Rate'))
s.comments = attributes.get("Comments ")
if attributes.get('Rating'):
s.rating = int(attributes.get('Rating'))
if attributes.get('Play Count'):
s.play_count = int(attributes.get('Play Count'))
if attributes.get('Location'):
s.location = attributes.get('Location')
songs.append(s)
return songs

View File

@@ -1,46 +0,0 @@
class Song:
"""
Song Attributes:
name (String)
artist (String)
album_arist (String)
composer = None (String)
album = None (String)
genre = None (String)
kind = None (String)
size = None (Integer)
total_time = None (Integer)
track_number = None (Integer)
year = None (Integer)
date_modified = None (Time)
date_added = None (Time)
bit_rate = None (Integer)
sample_rate = None (Integer)
comments = None (String)
rating = None (Integer)
album_rating = None (Integer)
play_count = None (Integer)
location = None (String)
"""
name = None
artist = None
album_arist = None
composer = None
album = None
genre = None
kind = None
size = None
total_time = None
track_number = None
year = None
date_modified = None
date_added = None
bit_rate = None
sample_rate = None
comments = None
rating = None
album_rating = None
play_count = None
location = None
#title = property(getTitle,setTitle)

View File

@@ -1,42 +0,0 @@
import re
class XMLLibraryParser:
def __init__(self,xmlLibrary):
f = open(xmlLibrary)
s = f.read()
lines = s.split("\n")
self.dictionary = self.parser(lines)
def getValue(self,restOfLine):
value = re.sub("<.*?>","",restOfLine)
u = unicode(value,"utf-8")
cleanValue = u.encode("ascii","xmlcharrefreplace")
return cleanValue
def keyAndRestOfLine(self,line):
rawkey = re.search('<key>(.*?)</key>',line).group(0)
key = re.sub("</*key>","",rawkey)
restOfLine = re.sub("<key>.*?</key>","",line).strip()
return key,restOfLine
def parser(self,lines):
dicts = 0
songs = {}
inSong = False
for line in lines:
if re.search('<dict>',line):
dicts += 1
if re.search('</dict>',line):
dicts -= 1
inSong = False
songs[songkey] = temp
if dicts == 2 and re.search('<key>(.*?)</key>',line):
rawkey = re.search('<key>(.*?)</key>',line).group(0)
songkey = re.sub("</*key>","",rawkey)
inSong = True
temp = {}
if dicts == 3 and re.search('<key>(.*?)</key>',line):
key,restOfLine = self.keyAndRestOfLine(line)
temp[key] = self.getValue(restOfLine)
if len(songs) > 0 and dicts < 2:
return songs
return songs

View File

@@ -1,3 +0,0 @@
from lib.pyItunes.XMLLibraryParser import XMLLibraryParser
from lib.pyItunes.Library import Library
from lib.pyItunes.Song import Song