From 7fbd07beeeb01233a42271c45297976828ab7988 Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Thu, 11 Oct 2012 15:32:50 +0200 Subject: [PATCH 01/13] Fixes issue #864 , better handling of the CACHE_SIZEMB setting --- headphones/db.py | 4 ++++ headphones/webserve.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/headphones/db.py b/headphones/db.py index 871b7c99..f25636be 100644 --- a/headphones/db.py +++ b/headphones/db.py @@ -35,6 +35,10 @@ def dbFilename(filename="headphones.db"): return os.path.join(headphones.DATA_DIR, filename) def getCacheSize(): + #this will protect against typecasting problems produced by empty string and None settings + if not headphones.CACHE_SIZEMB: + #sqlite will work with this (very slowly) + return 0 return int(headphones.CACHE_SIZEMB) class DBConnection: diff --git a/headphones/webserve.py b/headphones/webserve.py index 3a508c51..34315f7b 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -680,7 +680,7 @@ class WebInterface(object): samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0, delete_lossless_files=0, 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, synoindex_enabled=False, mirror=None, customhost=None, customport=None, - customsleep=None, hpuser=None, hppass=None, preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, cache_sizemb=None, **kwargs): + customsleep=None, hpuser=None, hppass=None, preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, cache_sizemb=0, **kwargs): headphones.HTTP_HOST = http_host headphones.HTTP_PORT = http_port @@ -783,7 +783,7 @@ class WebInterface(object): headphones.CUSTOMSLEEP = customsleep headphones.HPUSER = hpuser headphones.HPPASS = hppass - headphones.CACHE_SIZEMB = cache_sizemb + headphones.CACHE_SIZEMB = int(cache_sizemb) # Handle the variable config options. Note - keys with False values aren't getting passed From 27c4ad6ee268c8a7a90f8f035e3a661747fec5c5 Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Sat, 13 Oct 2012 09:53:51 +0200 Subject: [PATCH 02/13] Fix post processing file renaming for some (all?) releases with unicode characters --- headphones/postprocessor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index a78e753e..33caeda9 100644 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -676,7 +676,7 @@ def renameFiles(albumpath, downloaded_track_list, release): logger.debug("Renaming for: " + downloaded_track.decode(headphones.SYS_ENCODING) + " is not neccessary") continue - logger.debug('Renaming %s ---> %s' % (downloaded_track.decode(headphones.SYS_ENCODING), new_file_name.decode(headphones.SYS_ENCODING))) + logger.debug('Renaming %s ---> %s' % (downloaded_track.decode(headphones.SYS_ENCODING,'replace'), new_file_name.decode(headphones.SYS_ENCODING,'replace'))) try: os.rename(downloaded_track, new_file) except Exception, e: From 2614d0140be816c0642981658459e31dca77f5b7 Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Sat, 13 Oct 2012 12:18:03 +0200 Subject: [PATCH 03/13] Write configuration as UTF-8 ,allows unicode characters in configuration settings --- headphones/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headphones/__init__.py b/headphones/__init__.py index a9f9223e..30fa5902 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -584,7 +584,7 @@ def launch_browser(host, port, root): def config_write(): - new_config = ConfigObj() + new_config = ConfigObj(encoding="UTF-8") new_config.filename = CONFIG_FILE new_config['General'] = {} From 8ed253422419eab97761f7d48f714f4d7475a560 Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Sat, 13 Oct 2012 12:41:52 +0200 Subject: [PATCH 04/13] Fixes Issue #835 ,if the album name contains < and > characters replace them with _ before creating the folder (will fail on windows if we don't) * / \ and | are also bad characters on windows and are not yet being handled --- headphones/postprocessor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index a78e753e..89b4f694 100644 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -376,7 +376,7 @@ def moveFiles(albumpath, release, tracks): folder = helpers.replace_all(headphones.FOLDER_FORMAT, values) - folder = folder.replace('./', '_/').replace(':','_').replace('?','_').replace('/.','/_') + folder = folder.replace('./', '_/').replace(':','_').replace('?','_').replace('/.','/_').replace('<','_').replace('>','_') if folder.endswith('.'): folder = folder.replace(folder[len(folder)-1], '_') From 5fa6e426baa9368bfbcbbc929085a9cd3b70d990 Mon Sep 17 00:00:00 2001 From: plww Date: Sun, 14 Oct 2012 10:49:33 +0200 Subject: [PATCH 05/13] added function for getting top artists for tag --- headphones/lastfm.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/headphones/lastfm.py b/headphones/lastfm.py index ce77948b..9546ba78 100644 --- a/headphones/lastfm.py +++ b/headphones/lastfm.py @@ -126,6 +126,41 @@ def getArtists(): for artistid in artistlist: importer.addArtisttoDB(artistid) +def getTagTopArtists(tag, limit=50): + myDB = db.DBConnection() + results = myDB.select('SELECT ArtistID from artists') + + url = 'http://ws.audioscrobbler.com/2.0/?method=tag.gettopartists&limit=%s&tag=%s&api_key=%s' % (limit, tag, api_key) + data = urllib2.urlopen(url, timeout=20).read() + + try: + d = minidom.parseString(data) + except: + logger.error("Could not parse artist list from last.fm data") + return + + artists = d.getElementsByTagName("artist") + + artistlist = [] + + for artist in artists: + mbidnode = artist.getElementsByTagName("mbid")[0].childNodes + + for node in mbidnode: + artist_mbid = node.data + + try: + if not any(artist_mbid in x for x in results): + artistlist.append(artist_mbid) + except: + continue + + from headphones import importer + + for artistid in artistlist: + importet.addArtisttoDB(artistid) + + def getAlbumDescription(rgid, artist, album): myDB = db.DBConnection() From 8358212df9a4dbdc77390864d143b75bae946a6c Mon Sep 17 00:00:00 2001 From: plww Date: Sun, 14 Oct 2012 20:28:36 +0200 Subject: [PATCH 06/13] added interface for importLastFMTag --- data/interfaces/default/manage.html | 22 +++++++++++++++++++++- headphones/lastfm.py | 2 +- headphones/webserve.py | 6 ++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/data/interfaces/default/manage.html b/data/interfaces/default/manage.html index 88e26d8a..db682780 100644 --- a/data/interfaces/default/manage.html +++ b/data/interfaces/default/manage.html @@ -87,7 +87,27 @@ - +
+
+
+ Import Last.FM Tag +

Enter tag from which you want import top artists:

+
+
+ + +
+ + +
+
+ +
+ + +
diff --git a/headphones/lastfm.py b/headphones/lastfm.py index 9546ba78..5e0aacaf 100644 --- a/headphones/lastfm.py +++ b/headphones/lastfm.py @@ -158,7 +158,7 @@ def getTagTopArtists(tag, limit=50): from headphones import importer for artistid in artistlist: - importet.addArtisttoDB(artistid) + importer.addArtisttoDB(artistid) def getAlbumDescription(rgid, artist, album): diff --git a/headphones/webserve.py b/headphones/webserve.py index 3a508c51..69c3adc2 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -352,6 +352,12 @@ class WebInterface(object): raise cherrypy.HTTPRedirect("home") importLastFM.exposed = True + def importLastFMTag(self, tag, limit): + threading.Thread(target=lastfm.getTagTopArtists, args=(tag, limit)).start() + time.sleep(10) + raise cherrypy.HTTPRedirect("home") + importLastFMTag.exposed = True + def importItunes(self, path): headphones.PATH_TO_XML = path headphones.config_write() From d57fea0040792e7e8acdbb8034d1af29619c22b8 Mon Sep 17 00:00:00 2001 From: ka2er Date: Wed, 17 Oct 2012 23:45:41 +0200 Subject: [PATCH 07/13] fix various breaks if file name contains non ascii characters (>127) --- headphones/helpers.py | 8 +++++--- headphones/music_encoder.py | 31 ++++++++++++++++++------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/headphones/helpers.py b/headphones/helpers.py index 211bf637..89fb20cc 100644 --- a/headphones/helpers.py +++ b/headphones/helpers.py @@ -226,11 +226,13 @@ def extract_song_data(s): def smartMove(src, dest, delete=True): + from headphones import logger + source_dir = os.path.dirname(src) filename = os.path.basename(src) if os.path.isfile(os.path.join(dest, filename)): - logger.info('Destination file exists: %s' % os.path.join(dest, filename).decode(headphones.SYS_ENCODING)) + logger.info('Destination file exists: %s' % os.path.join(dest, filename).decode(headphones.SYS_ENCODING, 'replace')) title = os.path.splitext(filename)[0] ext = os.path.splitext(filename)[1] i = 1 @@ -244,7 +246,7 @@ def smartMove(src, dest, delete=True): os.rename(src, os.path.join(source_dir, newfile)) filename = newfile except Exception, e: - logger.warn('Error renaming %s: %s' % (src.decode(headphones.SYS_ENCODING), e)) + logger.warn('Error renaming %s: %s' % (src.decode(headphones.SYS_ENCODING, 'replace'), str(e).decode(headphones.SYS_ENCODING, 'replace'))) break try: @@ -254,4 +256,4 @@ def smartMove(src, dest, delete=True): shutil.copy(os.path.join(source_dir, filename), os.path.join(dest, filename)) return True except Exception, e: - logger.warn('Error moving file %s: %s' % (filename.decode(headphones.SYS_ENCODING), e)) + logger.warn('Error moving file %s: %s' % (filename.decode(headphones.SYS_ENCODING, 'replace'), str(e).decode(headphones.SYS_ENCODING, 'replace'))) diff --git a/headphones/music_encoder.py b/headphones/music_encoder.py index 2cff0505..63e44927 100644 --- a/headphones/music_encoder.py +++ b/headphones/music_encoder.py @@ -66,23 +66,23 @@ def encode(albumPath): for music in musicFiles: infoMusic=MediaFile(music) if headphones.ENCODER == 'lame': - if not any(music.lower().endswith('.' + x) for x in ["mp3", "wav"]): + if not any(music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.' + x) for x in ["mp3", "wav"]): logger.warn(u'Lame cant encode "%s" format for "%s", use ffmpeg' % (os.path.splitext(music)[1].decode(headphones.SYS_ENCODING, 'replace'),music.decode(headphones.SYS_ENCODING, 'replace'))) else: - if (music.lower().endswith('.mp3') and (infoMusic.bitrate/1000<=headphones.BITRATE)): + if (music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.mp3') and (int(infoMusic.bitrate/1000)<=headphones.BITRATE)): logger.info('Music "%s" has bitrate<="%skbit" will not be reencoded' % (music.decode(headphones.SYS_ENCODING, 'replace'),headphones.BITRATE)) else: command(encoder,music,musicTempFiles[i],albumPath) ifencoded=1 else: if headphones.ENCODEROUTPUTFORMAT=='ogg': - if music.lower().endswith('.ogg'): + if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.ogg'): logger.warn('Can not reencode .ogg music "%s"' % (music.decode(headphones.SYS_ENCODING, 'replace'))) else: command(encoder,music,musicTempFiles[i],albumPath) ifencoded=1 elif (headphones.ENCODEROUTPUTFORMAT=='mp3' or headphones.ENCODEROUTPUTFORMAT=='m4a'): - if (music.lower().endswith('.'+headphones.ENCODEROUTPUTFORMAT) and (infoMusic.bitrate/1000<=headphones.BITRATE)): + if (music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.'+headphones.ENCODEROUTPUTFORMAT) and (int(infoMusic.bitrate/1000)<=headphones.BITRATE)): logger.info('Music "%s" has bitrate<="%skbit" will not be reencoded' % (music.decode(headphones.SYS_ENCODING, 'replace'),headphones.BITRATE)) else: command(encoder,music,musicTempFiles[i],albumPath) @@ -102,6 +102,7 @@ def encode(albumPath): return musicFinalFiles def command(encoder,musicSource,musicDest,albumPath): + return_code=1 cmd='' startMusicTime=time.time() @@ -134,15 +135,19 @@ def command(encoder,musicSource,musicDest,albumPath): else: cmd=cmd+' '+ headphones.ADVANCEDENCODER cmd=cmd+ ' "' + musicDest + '"' - print cmd - time.sleep(10) - return_code = call(cmd, shell=True) - if (return_code==0) and (os.path.exists(musicDest)): - if headphones.DELETE_LOSSLESS_FILES: - os.remove(musicSource) - shutil.move(musicDest,albumPath) - logger.info('Music "%s" encoded in %s' % (musicSource,getTimeEncode(startMusicTime))) - + + try: + return_code = call(cmd, shell=True) + + if (return_code==0) and (os.path.exists(musicDest)): + if headphones.DELETE_LOSSLESS_FILES: + os.remove(musicSource) + shutil.move(musicDest,albumPath) + logger.info('Music "%s" encoded in %s' % (musicSource,getTimeEncode(startMusicTime))) + + except subprocess.CalledProcessError, e: + logger.warn('Music "%s" encoding error : %s' % (musicSource, e.output)) + def getTimeEncode(start): seconds =int(time.time()-start) hours = seconds / 3600 From 0d35b99a30ef05ecba2a65753f198db0da57cc0d Mon Sep 17 00:00:00 2001 From: ka2er Date: Thu, 18 Oct 2012 16:51:40 +0300 Subject: [PATCH 08/13] replace print cmd with a logger.debug --- headphones/music_encoder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/headphones/music_encoder.py b/headphones/music_encoder.py index 63e44927..04dec8f5 100644 --- a/headphones/music_encoder.py +++ b/headphones/music_encoder.py @@ -136,6 +136,7 @@ def command(encoder,musicSource,musicDest,albumPath): cmd=cmd+' '+ headphones.ADVANCEDENCODER cmd=cmd+ ' "' + musicDest + '"' + logger.debug(cmd) try: return_code = call(cmd, shell=True) From 4cc00d54b35fcf73c2637cedae5c4f6e7d39fc22 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 20 Oct 2012 15:34:23 -0300 Subject: [PATCH 09/13] First changes to fix the 'delete empty artists function to delete artists with no albums in db --- headphones/webserve.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/headphones/webserve.py b/headphones/webserve.py index 0f1e65c3..0edefa4a 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -188,24 +188,32 @@ class WebInterface(object): myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID]) myDB.action('DELETE from albums WHERE ArtistID=?', [ArtistID]) myDB.action('DELETE from tracks WHERE ArtistID=?', [ArtistID]) + myDB.action('DELETE from allalbums WHERE ArtistID=?', [ArtistID]) + myDB.action('DELETE from alltracks WHERE ArtistID=?', [ArtistID]) myDB.action('INSERT OR REPLACE into blacklist VALUES (?)', [ArtistID]) raise cherrypy.HTTPRedirect("home") deleteArtist.exposed = True - + def returnEmptyArtists(self): + mydb = db.DBConnection() + emptyArtistNames = [row['ArtistID'] for row in myDB.select("SELECT ArtistName FROM artists WHERE LatestAlbum == None")] + return EmptyArtistNames + returnEmptyArtists.exposed = True + def deleteEmptyArtists(self): logger.info(u"Deleting all empty artists") myDB = db.DBConnection() - emptyArtistIDs = [row['ArtistID'] for row in myDB.select("SELECT ArtistID FROM artists WHERE HaveTracks == 0")] + emptyArtistIDs = [row['ArtistID'] for row in myDB.select("SELECT ArtistID FROM artists WHERE LatestAlbum == None")] for ArtistID in emptyArtistIDs: logger.info(u"Deleting all traces of artist: " + ArtistID) myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID]) myDB.action('DELETE from albums WHERE ArtistID=?', [ArtistID]) myDB.action('DELETE from tracks WHERE ArtistID=?', [ArtistID]) + myDB.action('DELETE from allalbums WHERE ArtistID=?', [ArtistID]) + myDB.action('DELETE from alltracks WHERE ArtistID=?', [ArtistID]) myDB.action('INSERT OR REPLACE into blacklist VALUES (?)', [ArtistID]) deleteEmptyArtists.exposed = True - - + def refreshArtist(self, ArtistID): threading.Thread(target=importer.addArtisttoDB, args=[ArtistID]).start() raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) From 47291c52288eb9eab4dfad5692aedd6507e2fe37 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 20 Oct 2012 16:13:10 -0300 Subject: [PATCH 10/13] Moved empty artist database query to the main manage page, added a dialog to the ui --- data/interfaces/default/manage.html | 20 ++++++++++++++++++-- headphones/webserve.py | 12 ++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/data/interfaces/default/manage.html b/data/interfaces/default/manage.html index db682780..7a7fb14a 100644 --- a/data/interfaces/default/manage.html +++ b/data/interfaces/default/manage.html @@ -34,7 +34,7 @@
@@ -117,7 +117,19 @@ Force Update Active Artists Force Post-Process Albums in Download Folder Check for Headphones Updates - Delete empty Artists + Delete empty Artists +
@@ -131,6 +143,10 @@ $('#dialog').dialog(); return false; }); + $('#delete_empty_artists').click(function() { + $('#emptyartistdialog').dialog(); + return false; + }); jQuery( "#tabs" ).tabs(); initActions(); }; diff --git a/headphones/webserve.py b/headphones/webserve.py index 0edefa4a..01eb83f0 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -194,16 +194,10 @@ class WebInterface(object): raise cherrypy.HTTPRedirect("home") deleteArtist.exposed = True - def returnEmptyArtists(self): - mydb = db.DBConnection() - emptyArtistNames = [row['ArtistID'] for row in myDB.select("SELECT ArtistName FROM artists WHERE LatestAlbum == None")] - return EmptyArtistNames - returnEmptyArtists.exposed = True - def deleteEmptyArtists(self): logger.info(u"Deleting all empty artists") myDB = db.DBConnection() - emptyArtistIDs = [row['ArtistID'] for row in myDB.select("SELECT ArtistID FROM artists WHERE LatestAlbum == None")] + emptyArtistIDs = [row['ArtistID'] for row in myDB.select("SELECT ArtistID FROM artists WHERE LatestAlbum IS NULL")] for ArtistID in emptyArtistIDs: logger.info(u"Deleting all traces of artist: " + ArtistID) myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID]) @@ -301,7 +295,9 @@ class WebInterface(object): upcoming.exposed = True def manage(self): - return serve_template(templatename="manage.html", title="Manage") + myDB = db.DBConnection() + emptyArtists = myDB.select("SELECT * FROM artists WHERE LatestAlbum IS NULL") + return serve_template(templatename="manage.html", title="Manage", emptyArtists=emptyArtists) manage.exposed = True def manageArtists(self): From f09b0425c6e24fcf0a3a91e4ab75caedc94a7ec5 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 20 Oct 2012 17:35:06 -0300 Subject: [PATCH 11/13] Change music directory without initiating a scan --- data/interfaces/default/manage.html | 3 ++- headphones/webserve.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/data/interfaces/default/manage.html b/data/interfaces/default/manage.html index 7a7fb14a..eaa71963 100644 --- a/data/interfaces/default/manage.html +++ b/data/interfaces/default/manage.html @@ -62,7 +62,8 @@
- + + diff --git a/headphones/webserve.py b/headphones/webserve.py index 01eb83f0..61c252a9 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -370,6 +370,14 @@ class WebInterface(object): raise cherrypy.HTTPRedirect("home") importItunes.exposed = True + def musicSave(self, path, redirect=None, autoadd=0): + headphones.ADD_ARTISTS = autoadd + headphones.MUSIC_DIR = path + headphones.config_write() + if redirect: + raise cherrypy.HTTPRedirect(redirect) + musicSave.exposed = True + def musicScan(self, path, redirect=None, autoadd=0): headphones.ADD_ARTISTS = autoadd headphones.MUSIC_DIR = path From 4f1f05e12b29093c3a41c6dfc091fd722918aa5c Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 20 Oct 2012 20:01:13 -0300 Subject: [PATCH 12/13] Bug fixes for saving music folder/scan options without initiating a scan --- data/interfaces/default/manage.html | 10 +++++++--- headphones/webserve.py | 25 +++++++------------------ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/data/interfaces/default/manage.html b/data/interfaces/default/manage.html index eaa71963..1b833dfe 100644 --- a/data/interfaces/default/manage.html +++ b/data/interfaces/default/manage.html @@ -58,12 +58,12 @@ %endif
- +
- - + + @@ -139,6 +139,10 @@ <%def name="javascriptIncludes()">