diff --git a/data/images/favicon.ico b/data/images/favicon.ico index 6a4728a6..bc5b5d35 100644 Binary files a/data/images/favicon.ico and b/data/images/favicon.ico differ diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index 599759e9..216e4eb6 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -109,14 +109,14 @@ m<%inherit file="base.html"/>
-
+ +
+ + +
-
-
- -
@@ -184,7 +184,7 @@ m<%inherit file="base.html"/>
- +
@@ -199,18 +199,54 @@ m<%inherit file="base.html"/>
-
-
- - - e.g. http://nzb.su -
-
- - -
-
- +
+
+
+ + + e.g. http://nzb.su +
+
+ + +
+
+ +
+
+ <% + newznab_number = 2 + %> + %for newznab in config['extra_newznabs']: + <% + if newznab[2] == '1' or newznab[2] == 1: + newznab_enabled = "checked" + else: + newznab_enabled = "" + %> +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ <% + newznab_number += 1 + %> + %endfor + +
+ @@ -233,12 +269,12 @@ m<%inherit file="base.html"/>
- +
- - + +
@@ -776,6 +812,28 @@ m<%inherit file="base.html"/> $("#mirror").change(handleNewSelection); handleNewSelection.apply($("#mirror")); + + var deletedNewznabs = 0; + + $(".remove").click(function() { + $(this).parent().parent().remove(); + deletedNewznabs = deletedNewznabs + 1; + }); + + $("#add_newznab").click(function() { + var intId = $("#newznab_providers > div").size() + deletedNewznabs + 1; + var formfields = $("
"); + var removeButton = $("
"); + removeButton.click(function() { + $(this).parent().remove(); + deletedNewznabs = deletedNewznabs + 1; + + }); + formfields.append(removeButton); + formfields.append("
"); + $("#add_newznab").before(formfields); + }); + $(function() { $( "#tabs" ).tabs(); }); @@ -789,7 +847,7 @@ m<%inherit file="base.html"/> initConfigCheckbox("#useapi"); } $(document).ready(function() { - initThisPage(); + initThisPage(); }); diff --git a/data/interfaces/default/upcoming.html b/data/interfaces/default/upcoming.html index 69f75c4c..5720f12a 100644 --- a/data/interfaces/default/upcoming.html +++ b/data/interfaces/default/upcoming.html @@ -41,7 +41,7 @@ - ${album['ArtistName']} + ${album['ArtistName']} ${album['AlbumTitle']} ${album['ReleaseDate']} ${album['Type']} @@ -71,7 +71,7 @@ %for album in upcoming: - ${album['ArtistName']} + ${album['ArtistName']} ${album['AlbumTitle']} ${album['ReleaseDate']} ${album['Type']} diff --git a/headphones/__init__.py b/headphones/__init__.py index f3bfc597..265a016e 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -20,6 +20,7 @@ import os, sys, subprocess import threading import webbrowser import sqlite3 +import itertools from lib.apscheduler.scheduler import Scheduler from lib.configobj import ConfigObj @@ -121,6 +122,8 @@ NZBMATRIX_APIKEY = None NEWZNAB = False NEWZNAB_HOST = None NEWZNAB_APIKEY = None +NEWZNAB_ENABLED = False +EXTRA_NEWZNABS = [] NZBSORG = False NZBSORG_UID = None @@ -136,6 +139,8 @@ LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a"] LOSSLESS_MEDIA_FORMATS = ["flac"] MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS +ALBUM_COMPLETION_PCT = None # This is used in importer.py to determine how complete an album needs to be - to be considered "downloaded". Percentage from 0-100 + TORRENTBLACKHOLE_DIR = None NUMBEROFSEEDERS = 10 ISOHUNT = None @@ -225,7 +230,6 @@ def check_setting_str(config, cfg_name, item_name, def_val, log=True): else: logger.debug(item_name + " -> ******") return my_val - def initialize(): @@ -238,11 +242,12 @@ def initialize(): ADD_ALBUM_ART, EMBED_ALBUM_ART, EMBED_LYRICS, DOWNLOAD_DIR, BLACKHOLE, BLACKHOLE_DIR, USENET_RETENTION, SEARCH_INTERVAL, \ TORRENTBLACKHOLE_DIR, NUMBEROFSEEDERS, ISOHUNT, KAT, MININOVA, WAFFLES, WAFFLES_UID, WAFFLES_PASSKEY, DOWNLOAD_TORRENT_DIR, \ LIBRARYSCAN_INTERVAL, DOWNLOAD_SCAN_INTERVAL, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, \ - NZBMATRIX, NZBMATRIX_USERNAME, NZBMATRIX_APIKEY, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, \ + NZBMATRIX, NZBMATRIX_USERNAME, NZBMATRIX_APIKEY, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, NEWZNAB_ENABLED, EXTRA_NEWZNABS,\ NZBSORG, NZBSORG_UID, NZBSORG_HASH, NEWZBIN, NEWZBIN_UID, NEWZBIN_PASSWORD, LASTFM_USERNAME, INTERFACE, FOLDER_PERMISSIONS, \ ENCODERFOLDER, ENCODER, BITRATE, SAMPLINGFREQUENCY, MUSIC_ENCODER, ADVANCEDENCODER, ENCODEROUTPUTFORMAT, ENCODERQUALITY, ENCODERVBRCBR, \ ENCODERLOSSLESS, PROWL_ENABLED, PROWL_PRIORITY, PROWL_KEYS, PROWL_ONSNATCH, MIRRORLIST, MIRROR, CUSTOMHOST, CUSTOMPORT, \ - CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, XBMC_NOTIFY, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, SYNOINDEX_ENABLED + CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, XBMC_NOTIFY, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, SYNOINDEX_ENABLED, \ + ALBUM_COMPLETION_PCT if __INITIALIZED__: return False @@ -259,6 +264,7 @@ def initialize(): CheckSection('XBMC') CheckSection('NMA') CheckSection('Synoindex') + CheckSection('Advanced') # Set global variables based on config file or use defaults CONFIG_VERSION = check_setting_str(CFG, 'General', 'config_version', '0') @@ -336,6 +342,11 @@ def initialize(): NEWZNAB = bool(check_setting_int(CFG, 'Newznab', 'newznab', 0)) NEWZNAB_HOST = check_setting_str(CFG, 'Newznab', 'newznab_host', '') NEWZNAB_APIKEY = check_setting_str(CFG, 'Newznab', 'newznab_apikey', '') + NEWZNAB_ENABLED = bool(check_setting_int(CFG, 'Newznab', 'newznab_enabled', 1)) + + # Need to pack the extra newznabs back into a list of tuples + flattened_newznabs = check_setting_str(CFG, 'Newznab', 'extra_newznabs', [], log=False) + EXTRA_NEWZNABS = list(itertools.izip(*[itertools.islice(flattened_newznabs, i, None, 3) for i in range(3)])) NZBSORG = bool(check_setting_int(CFG, 'NZBsorg', 'nzbsorg', 0)) NZBSORG_UID = check_setting_str(CFG, 'NZBsorg', 'nzbsorg_uid', '') @@ -383,8 +394,10 @@ def initialize(): CUSTOMHOST = check_setting_str(CFG, 'General', 'customhost', 'localhost') CUSTOMPORT = check_setting_int(CFG, 'General', 'customport', 5000) CUSTOMSLEEP = check_setting_int(CFG, 'General', 'customsleep', 1) - HPUSER = check_setting_str(CFG, 'General', 'hpuser', 'username') - HPPASS = check_setting_str(CFG, 'General', 'hppass', 'password') + HPUSER = check_setting_str(CFG, 'General', 'hpuser', '') + HPPASS = check_setting_str(CFG, 'General', 'hppass', '') + + ALBUM_COMPLETION_PCT = check_setting_int(CFG, 'Advanced', 'album_completion_pct', 80) # update folder formats in the config & bump up config version if CONFIG_VERSION == '0': @@ -607,6 +620,14 @@ def config_write(): new_config['Newznab']['newznab'] = int(NEWZNAB) new_config['Newznab']['newznab_host'] = NEWZNAB_HOST new_config['Newznab']['newznab_apikey'] = NEWZNAB_APIKEY + new_config['Newznab']['newznab_enabled'] = int(NEWZNAB_ENABLED) + # Need to unpack the extra newznabs for saving in config.ini + flattened_newznabs = [] + for newznab in EXTRA_NEWZNABS: + for item in newznab: + flattened_newznabs.append(item) + + new_config['Newznab']['extra_newznabs'] = flattened_newznabs new_config['NZBsorg'] = {} new_config['NZBsorg']['nzbsorg'] = int(NZBSORG) @@ -662,6 +683,9 @@ def config_write(): new_config['General']['hpuser'] = HPUSER new_config['General']['hppass'] = HPPASS + new_config['Advanced'] = {} + new_config['Advanced']['album_completion_pct'] = ALBUM_COMPLETION_PCT + new_config.write() diff --git a/headphones/albumart.py b/headphones/albumart.py index 3b80e5f5..5fd6f8a8 100644 --- a/headphones/albumart.py +++ b/headphones/albumart.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . +import urllib2 from headphones import db def getAlbumArt(albumid): @@ -36,7 +37,7 @@ def getCachedArt(albumid): return None if artwork_path.startswith('http://'): - artwork = urllib.urlopen(artwork_path).read() + artwork = urllib2.urlopen(artwork_path, timeout=20).read() return artwork else: artwork = open(artwork_path, "r").read() diff --git a/headphones/importer.py b/headphones/importer.py index 5a5efa75..10d77e52 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -156,7 +156,7 @@ def addArtisttoDB(artistid, extrasonly=False): rgid = rg['id'] # check if the album already exists - rg_exists = myDB.select("SELECT * from albums WHERE AlbumID=?", [rg['id']]) + rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone() try: release_dict = mb.getReleaseGroup(rgid) @@ -180,7 +180,7 @@ def addArtisttoDB(artistid, extrasonly=False): } # Only change the status & add DateAdded if the album is not already in the database - if not len(rg_exists): + if not rg_exists: newValueDict['DateAdded']= helpers.today() @@ -193,6 +193,9 @@ def addArtisttoDB(artistid, extrasonly=False): myDB.upsert("albums", newValueDict, controlValueDict) + # This is used to see how many tracks you have from an album - to mark it as downloaded. Default is 80%, can be set in config as ALBUM_COMPLETION_PCT + total_track_count = len(release_dict['tracks']) + for track in release_dict['tracks']: cleanname = helpers.cleanName(artist['artist_name'] + ' ' + rg['title'] + ' ' + track['title']) @@ -221,12 +224,22 @@ def addArtisttoDB(artistid, extrasonly=False): newValueDict['BitRate'] = match['BitRate'] newValueDict['Format'] = match['Format'] myDB.action('DELETE from have WHERE Location=?', [match['Location']]) - + myDB.upsert("tracks", newValueDict, controlValueDict) - + + # Mark albums as downloaded if they have at least 80% (by default, configurable) of the album + have_track_count = len(myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', [rg['id']])) + + if rg_exists: + if rg_exists['Status'] == 'Skipped' and ((have_track_count/float(total_track_count)) >= (headphones.ALBUM_COMPLETION_PCT/100.0)): + myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', rg['id']]) + else: + if ((have_track_count/float(total_track_count)) >= (headphones.ALBUM_COMPLETION_PCT/100.0)): + myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', rg['id']]) + logger.debug(u"Updating album cache for " + rg['title']) cache.getThumb(AlbumID=rg['id']) - + latestalbum = myDB.action('SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC', [artistid]).fetchone() totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['artist_name']])) diff --git a/headphones/notifiers.py b/headphones/notifiers.py index ae04fe99..3c16b647 100644 --- a/headphones/notifiers.py +++ b/headphones/notifiers.py @@ -197,6 +197,8 @@ class Synoindex: return os.path.exists(self.util_loc) def notify(self, path): + path = os.path.abspath(path) + if not self.util_exists(): logger.warn("Error sending notification: synoindex utility not found at %s" % self.util_loc) return @@ -209,12 +211,12 @@ class Synoindex: logger.warn("Error sending notification: Path passed to synoindex was not a file or folder.") return - cmd = [self.util_loc, cmd_arg, '\"%s\"' % os.path.abspath(path)] - logger.debug("Calling synoindex command: %s" % str(cmd)) + cmd = [self.util_loc, cmd_arg, path] + logger.info("Calling synoindex command: %s" % str(cmd)) try: p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=headphones.PROG_DIR) out, error = p.communicate() - logger.debug("Synoindex result: %s" % str(out)) + #synoindex never returns any codes other than '0', highly irritating except OSError, e: logger.warn("Error sending notification: %s" % str(e)) diff --git a/headphones/searcher.py b/headphones/searcher.py index 95e14a5e..85b699e5 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -216,6 +216,13 @@ def searchNZB(albumid=None, new=False, losslessOnly=False): logger.info(u"No results found from NZBMatrix for %s" % term) if headphones.NEWZNAB: + + newznab_hosts = [(headphones.NEWZNAB_HOST, headphones.NEWZNAB_APIKEY, headphones.NEWZNAB_ENABLED)] + + for newznab_host in headphones.EXTRA_NEWZNABS: + if newznab_host[2] == '1' or newznab_host[2] == 1: + newznab_hosts.append(newznab_host) + provider = "newznab" if headphones.PREFERRED_QUALITY == 3 or losslessOnly: categories = "3040" @@ -227,44 +234,46 @@ def searchNZB(albumid=None, new=False, losslessOnly=False): if albums['Type'] == 'Other': categories = "3030" logger.info("Album type is audiobook/spokenword. Using audiobook category") + + for newznab_host in newznab_hosts: - params = { "t": "search", - "apikey": headphones.NEWZNAB_APIKEY, - "cat": categories, - "maxage": headphones.USENET_RETENTION, - "q": term - } - - searchURL = headphones.NEWZNAB_HOST + '/api?' + urllib.urlencode(params) - - logger.info(u'Parsing results from %s' % (searchURL, headphones.NEWZNAB_HOST)) + params = { "t": "search", + "apikey": newznab_host[1], + "cat": categories, + "maxage": headphones.USENET_RETENTION, + "q": term + } - try: - data = urllib2.urlopen(searchURL, timeout=20).read() - except urllib2.URLError, e: - logger.warn('Error fetching data from %s: %s' % (headphones.NEWZNAB_HOST, e)) - data = False + searchURL = newznab_host[0] + '/api?' + urllib.urlencode(params) + + logger.info(u'Parsing results from %s' % (searchURL, newznab_host[0])) - if data: - - d = feedparser.parse(data) + try: + data = urllib2.urlopen(searchURL, timeout=20).read() + except urllib2.URLError, e: + logger.warn('Error fetching data from %s: %s' % (newznab_host[0], e)) + data = False + + if data: - if not len(d.entries): - logger.info(u"No results found from %s for %s" % (headphones.NEWZNAB_HOST, term)) - pass - - else: - for item in d.entries: - try: - url = item.link - title = item.title - size = int(item.links[1]['length']) + d = feedparser.parse(data) + + if not len(d.entries): + logger.info(u"No results found from %s for %s" % (newznab_host[0], term)) + pass + + else: + for item in d.entries: + try: + url = item.link + title = item.title + size = int(item.links[1]['length']) + + resultlist.append((title, size, url, provider)) + logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) - resultlist.append((title, size, url, provider)) - logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) - - except Exception, e: - logger.error(u"An unknown error occurred trying to parse the feed: %s" % e) + except Exception, e: + logger.error(u"An unknown error occurred trying to parse the feed: %s" % e) if headphones.NZBSORG: provider = "nzbsorg" diff --git a/headphones/webserve.py b/headphones/webserve.py index bd2c11e1..2b2e89fd 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -143,7 +143,7 @@ class WebInterface(object): deleteArtist.exposed = True def refreshArtist(self, ArtistID): - importer.addArtisttoDB(ArtistID) + threading.Thread(target=importer.addArtisttoDB, args=[ArtistID]).start() raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) refreshArtist.exposed=True @@ -214,7 +214,7 @@ class WebInterface(object): def upcoming(self): myDB = db.DBConnection() - upcoming = myDB.select("SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate DESC") + upcoming = myDB.select("SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate ASC") wanted = myDB.select("SELECT * from albums WHERE Status='Wanted'") return serve_template(templatename="upcoming.html", title="Upcoming", upcoming=upcoming, wanted=wanted) upcoming.exposed = True @@ -384,6 +384,8 @@ class WebInterface(object): "use_newznab" : checked(headphones.NEWZNAB), "newznab_host" : headphones.NEWZNAB_HOST, "newznab_api" : headphones.NEWZNAB_APIKEY, + "newznab_enabled" : checked(headphones.NEWZNAB_ENABLED), + "extra_newznabs" : headphones.EXTRA_NEWZNABS, "use_nzbsorg" : checked(headphones.NZBSORG), "nzbsorg_uid" : headphones.NZBSORG_UID, "nzbsorg_hash" : headphones.NZBSORG_HASH, @@ -458,13 +460,13 @@ class WebInterface(object): def configUpdate(self, http_host='0.0.0.0', http_username=None, http_port=8181, http_password=None, launch_browser=0, api_enabled=0, api_key=None, download_scan_interval=None, nzb_search_interval=None, libraryscan_interval=None, sab_host=None, sab_username=None, sab_apikey=None, sab_password=None, sab_category=None, download_dir=None, blackhole=0, blackhole_dir=None, - usenet_retention=None, nzbmatrix=0, nzbmatrix_username=None, nzbmatrix_apikey=None, newznab=0, newznab_host=None, newznab_apikey=None, + usenet_retention=None, nzbmatrix=0, nzbmatrix_username=None, nzbmatrix_apikey=None, newznab=0, newznab_host=None, newznab_apikey=None, newznab_enabled=0, nzbsorg=0, nzbsorg_uid=None, nzbsorg_hash=None, newzbin=0, newzbin_uid=None, newzbin_password=None, preferred_quality=0, preferred_bitrate=None, detect_bitrate=0, move_files=0, torrentblackhole_dir=None, download_torrent_dir=None, numberofseeders=10, use_isohunt=0, use_kat=0, use_mininova=0, waffles=0, waffles_uid=None, waffles_passkey=None, rename_files=0, correct_metadata=0, cleanup_files=0, add_album_art=0, embed_album_art=0, embed_lyrics=0, destination_dir=None, folder_format=None, file_format=None, include_extras=0, autowant_upcoming=False, autowant_all=False, interface=None, log_dir=None, music_encoder=0, encoder=None, bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=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, synoindex_enabled=False, mirror=None, customhost=None, customport=None, customsleep=None, hpuser=None, hppass=None): + nma_enabled=False, nma_apikey=None, nma_priority=0, synoindex_enabled=False, mirror=None, customhost=None, customport=None, customsleep=None, hpuser=None, hppass=None, **kwargs): headphones.HTTP_HOST = http_host headphones.HTTP_PORT = http_port @@ -491,6 +493,7 @@ class WebInterface(object): headphones.NEWZNAB = newznab headphones.NEWZNAB_HOST = newznab_host headphones.NEWZNAB_APIKEY = newznab_apikey + headphones.NEWZNAB_ENABLED = newznab_enabled headphones.NZBSORG = nzbsorg headphones.NZBSORG_UID = nzbsorg_uid headphones.NZBSORG_HASH = nzbsorg_hash @@ -554,6 +557,22 @@ class WebInterface(object): headphones.CUSTOMSLEEP = customsleep headphones.HPUSER = hpuser headphones.HPPASS = hppass + + # Handle the variable config options. Note - keys with False values aren't getting passed + + headphones.EXTRA_NEWZNABS = [] + + for kwarg in kwargs: + if kwarg.startswith('newznab_host'): + newznab_number = kwarg[12:] + newznab_host = kwargs['newznab_host' + newznab_number] + newznab_api = kwargs['newznab_api' + newznab_number] + try: + newznab_enabled = int(kwargs['newznab_enabled' + newznab_number]) + except KeyError: + newznab_enabled = 0 + + headphones.EXTRA_NEWZNABS.append((newznab_host, newznab_api, newznab_enabled)) headphones.config_write()