@@ -199,18 +199,54 @@ m<%inherit file="base.html"/>
-
-
+
+
+ <%
+ 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 = $("");
+ $("#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['AlbumID']}]() |
- ${album['ArtistName']} |
+ ${album['ArtistName']} |
${album['AlbumTitle']} |
${album['ReleaseDate']} |
${album['Type']} |
@@ -71,7 +71,7 @@
%for album in upcoming:
![${album['AlbumID']}]() |
- ${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()