Compare commits

...
47 Commits
Author SHA1 Message Date
rembo10 24165fb5f3 v0.5.8 2015-07-13 15:22:03 -07:00
rembo10 71da960386 Updated changelog for v0.5.8 2015-07-13 15:21:44 -07:00
rembo10 a16c2d0a26 Fix for #2279: What.cd not honoring custom search string 2015-07-13 15:09:01 -07:00
rembo10 53de44a225 typo when excluding similar artist update on an artist scan 2015-07-08 23:15:38 -07:00
rembo10 e32cff60e5 Took out some unneccessary functions if we're just scanning an artist 2015-07-08 22:26:55 -07:00
rembo10 0ebba8d17c Fix for #2271: Scanning an empty directory with DETECT_BITRATE turned on 2015-07-08 21:58:41 -07:00
rembo10 464db79c04 Some cleanup to the scanArtist function 2015-07-08 11:52:00 -07:00
rembo10 65019d8323 Fix for #2260 & #2129 & 2175 & #2163 : Better artist directory scanning 2015-07-08 02:41:17 -07:00
rembo10 f131d316ca Set localhost as default 2015-07-08 00:45:33 -07:00
rembo10 e6663c96b5 Fixed some pushbullet config text 2015-07-07 23:56:17 -07:00
rembo10 5f06d11fe9 Fixed pushbullet notifications 2015-07-07 23:47:38 -07:00
rembo10 6db2836662 Moved plex notifications to requests lib for pre-v12 plex 2015-07-07 22:54:34 -07:00
rembo10 cb209f8d98 Fix for #2264: Metacritic parsing failing with htmlparser 2015-07-06 13:05:51 -07:00
rembo10 5634e17a2e Fix for #2155, local variable response referenced before assignment utorrent 2015-07-06 01:28:48 -07:00
rembo10 7c990edeaf Better fix for #2183 2015-07-06 00:34:16 -07:00
rembo10 9149f62760 Fix for #2173: Uncaught exception in get_age 2015-07-06 00:21:26 -07:00
rembo10 872ec6b11c Fix for #2089 2015-07-06 00:09:43 -07:00
rembo10 064a21a07c Better fix for #2183 2015-07-06 00:02:09 -07:00
rembo10 57c3c7b2e0 Fixed songkick description to prevent spilling over on default fonts 2015-07-05 21:44:37 -07:00
rembo10 6dc75d7a44 Cleaned up the config page a little bit. Took out {option} enabled buttons and just put them in with the option. Alphabetized the notifiers. Fixed checkbox on chrome 2015-07-05 21:40:52 -07:00
rembo10 5f3b7e05c5 Fixed some appearance stuff on the config page (if fonts weren't installed) 2015-07-05 15:04:18 -07:00
rembo10 61d3d8ad00 Fix for history page showing nzbs snatched from hp indexer as unknown 2015-07-05 12:05:04 -07:00
rembo10 e36440f834 Fix for XSS bug when searching 2015-07-04 23:53:03 -07:00
rembo10 4029543d51 Added option to wait until an album's release date before searching 2015-07-04 20:43:06 -07:00
rembo10 8aee9ac91f Fixed plex client url in config 2015-07-04 19:36:39 -07:00
rembo10 6e5e15f06f Fixed plex notifications for version 12 2015-07-04 19:27:27 -07:00
rembo10 a72bd4cb7c Fixed up the appearance of the config page (was kind of broken on Firefox) 2015-07-04 17:26:47 -07:00
rembo10 d062ab553c Merge branch 'master' of https://github.com/texke/headphones into develop 2015-07-04 12:26:05 -07:00
rembo10 a41edde12a Merge branch 'wfriesen-logtypo' into develop 2015-07-04 12:23:57 -07:00
rembo10 7608927b4f Merge branch 'logtypo' of https://github.com/wfriesen/headphones into wfriesen-logtypo 2015-07-04 12:23:29 -07:00
rembo10 24d7647c08 Updated notifymyandroid library 2015-07-04 12:13:40 -07:00
rembo10 a14369cb7d Added option to only include 'official' extras, rather than everything (bootlegs, promos, etc) 2015-07-04 00:47:02 -07:00
rembo10 3e81c4f3b6 Fix for #2183. Check to make sure the response has headers before trying to parse them 2015-07-03 21:01:46 -07:00
rembo10 84a6c19677 Release v0.5.7 2015-07-01 02:18:01 -07:00
rembo10 624a1d5509 Updated changelog for 0.5.7 release 2015-07-01 02:17:51 -07:00
rembo10 8b6d35cfe2 Updated changelog to reflect metacritic changes 2015-07-01 02:08:13 -07:00
rembo10 867e502448 Cache metacritic scores in the artists table so we don't lose them if we can't connect for whatever reason 2015-07-01 02:06:27 -07:00
rembo10 74275b969e Added a useragent to the metacritic request 2015-07-01 00:56:48 -07:00
William Friesen 275dde0630 Fixed typo in postprocessor message 2015-06-28 10:56:48 +10:00
Bas Stottelaar 46b470f30b Merge pull request #2253 from Hellowlol/patch-1
Add getLogs and clearLogs to api
2015-06-26 08:04:05 +02:00
Hellowlol fbfec95a8a Add getLogs and clearLogs to api 2015-06-25 22:53:39 +02:00
rembo10 1e75a18f55 Initial commit to add plex tokens. Plex notifications need to be updated to requests, and the get request to the server might need to be cleaned up - but want to make sure it works first 2015-06-19 17:08:54 -07:00
Jens Rogier a98849bf30 Added Musicbrainz link to "alternate release" window 2015-06-18 17:01:01 +02:00
rembo10 1b998e9d01 Updated pushover to use requests lib 2015-06-12 00:08:35 -07:00
rembo10 ebd6d3f3f9 Added option to keep folders when force post-processing under Manage. Also, added config option to keep original folder globally 2015-06-09 00:31:50 -07:00
rembo10 1204afadbb Convert preferred bitrate to vbr preset for what.cd searching 2015-06-08 21:56:10 -07:00
rembo10 cd08b72df7 Added ability to test pushover 2015-06-08 21:55:14 -07:00
22 changed files with 922 additions and 635 deletions
+32
View File
@@ -1,5 +1,37 @@
# Changelog
## v0.5.8
Released 13 July 2015
Highlights:
* Added: Option to only include official extras
* Added: Option to wait until album release date before searching
* Fixed: NotifyMyAndroid notifications
* Fixed: Plex Notifications
* Fixed: Metacritic parsing
* Fixed: Pushbullet notifications
* Fixed: What.cd not honoring custom search term (#2279)
* Improved: XSS Search bug
* Improved: Config page layout
* Improved: Set localhost as default
* Improved: Better single artist scanning
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.6...v0.5.7).
## v0.5.7
Released 01 July 2015
Highlights:
* Improved: Moved pushover to use requests lib
* Improved: Plex tokens with Plex Home
* Improved: Added getLogs & clearLogs to api
* Improved: Cache MetaCritic scores. Added user-agent header to fix 403 errors
* Improved: Specify whether to delete folders when force post-processing
* Improved: Convert target bitrate to vbr preset for what.cd searching
* Improved: Switched Pushover to requests lib
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.6...v0.5.7).
## v0.5.6
Released 08 June 2015
+2 -1
View File
@@ -35,6 +35,7 @@
%for alternate_album in alternate_albums:
<%
track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=?", [alternate_album['ReleaseID']]))
mb_link = "http://musicbrainz.org/release/" + alternate_album['ReleaseID']
have_track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=? AND Location IS NOT NULL", [alternate_album['ReleaseID']]))
if alternate_album['AlbumID'] == alternate_album['ReleaseID']:
alternate_album_name = "Headphones Default Release (" + str(alternate_album['ReleaseDate']) + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
@@ -42,7 +43,7 @@
alternate_album_name = alternate_album['AlbumTitle'] + " (" + alternate_album['ReleaseCountry'] + ", " + str(alternate_album['ReleaseDate']) + ", " + alternate_album['ReleaseFormat'] + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
%>
<a href="#" onclick="doAjaxCall('switchAlbum?AlbumID=${album['AlbumID']}&ReleaseID=${alternate_album['ReleaseID']}', $(this), 'table');" data-success="Switched release to: ${alternate_album_name}">${alternate_album_name}</a><br>
<a href="#" onclick="doAjaxCall('switchAlbum?AlbumID=${album['AlbumID']}&ReleaseID=${alternate_album['ReleaseID']}', $(this), 'table');" data-success="Switched release to: ${alternate_album_name}">${alternate_album_name}</a><a href="${mb_link}" target="_blank">MB</a><br>
%endfor
%endif
</div>
File diff suppressed because it is too large Load Diff
+20 -1
View File
@@ -325,7 +325,7 @@ form fieldset small.heading {
margin-top: -15px;
}
form .row {
font-family: Helvetica, Arial;
font-family: Helvetica, Arial, sans-serif;
margin-bottom: 10px;
}
form .row label {
@@ -408,6 +408,18 @@ form .checkbox small {
form .indent input {
margin-left: 15px;
}
form .suboptions {
margin-left: 15px;
}
.option{
font-size: 15px;
font-weight: 600;
vertical-align: middle;
}
input.bigcheck[type="checkbox"] {
width: 16px;
height: 16px;
}
ul,
ol {
margin-left: 2em;
@@ -1420,6 +1432,13 @@ div#artistheader h2 a {
line-height: 10px;
height: 10px;
}
.nopad {
margin-bottom: 0px !important;
margin-top: 0px !important;
padding-top: 0px !important;
padding-bottom: 0px !important;
padding: 0px 0px !important;
}
#album_table th#albumname,
#upcoming_table th#artistname,
#wanted_table th#artistname {
+2
View File
@@ -52,6 +52,8 @@
fileid = 'torrent'
if item['URL'].find('rutracker') != -1:
fileid = 'torrent'
if item['URL'].find('codeshy') != -1:
fileid = 'nzb'
folder = 'Folder: ' + item['FolderName']
+69 -22
View File
@@ -137,28 +137,27 @@
No empty artists found.
%endif
</div>
<a href="#" onclick="doAjaxCall('forcePostProcess',$(this))" data-success="Post-Processor is being loaded" data-error="Error during Post-Processing"><i class="fa fa-wrench fa-fw"></i> Force Post-Process Albums in Download Folder</a>
<div id="post_process">
<a href="#" class="btnOpenDialog"><i class="fa fa-wrench fa-fw"></i> Force Post-Process Albums in Download Folder</a>
</div>
</div>
</fieldset>
<form action="forcePostProcess" method="GET">
<fieldset>
<div class="row">
<label>Force Post-Process Albums in Alternate Folder</label>
<input type="text" value="" name="dir" id="dir" size="50" /><input type="submit" />
</div>
</fieldset>
</form>
<form action="forcePostProcess" method="GET">
<fieldset>
<div class="row">
<label>Post-Process Single Folder</label>
<input type="text" value="" name="album_dir" id="album_dir" size="50" /><input type="submit" />
</div>
</fieldset>
</form>
<fieldset>
<div class="row" id="post_process_alternate">
<label>Force Post-Process Albums in Alternate Folder</label>
<input type="text" value="" name="dir" id="dir" size="50" />
<input type="button" class="btnOpenDialog" value="Submit" />
</div>
</fieldset>
<fieldset>
<div class="row" id="post_process_single">
<label>Post-Process Single Folder</label>
<input type="text" value="" name="album_dir" id="album_dir" size="50" />
<input type="button" class="btnOpenDialog" value="Submit" />
</div>
</fieldset>
</div>
@@ -177,8 +176,7 @@
</fieldset>
</div>
<div id="dialog-confirm"></div>
</div>
</%def>
@@ -187,6 +185,55 @@
function addScanAction() {
$('#autoadd').append('<input type="hidden" name="scan" value=1 />');
};
function fnOpenNormalDialog(id) {
if (id === "post_process"){
var url = "forcePostProcess?"
}
if (id === "post_process_alternate"){
var dir = $('#dir').val();
var url = "forcePostProcess?dir=" + dir + "&"
}
if (id === "post_process_single"){
var dir = $('#album_dir').val();
var url = "forcePostProcess?album_dir=" + dir + "&"
}
var t = $('<a data-success="Post-Processor is being loaded" data-error="Error during Post-Processing">');
$("#dialog-confirm").html("Do you want to keep the original folder(s) after post-processing? If you click no, the folders still might be kept depending on your global settings");
// Define the Dialog and its properties.
$("#dialog-confirm").dialog({
resizable: false,
modal: true,
title: "Keep original folder(s)?",
height: 170,
width: 400,
buttons: {
"Yes": function () {
$(this).dialog('close');
doAjaxCall(url + "keep_original_folder=True", t);
},
"No": function () {
$(this).dialog('close');
doAjaxCall(url + "keep_original_folder=False", t);
}
}
});
}
$('.btnOpenDialog').click(function(e){
e.preventDefault();
var parentId = $(this).closest('div').prop('id');
fnOpenNormalDialog(parentId);
});
function callback(value) {
if (value) {
alert("Confirmed");
} else {
alert("Rejected");
}
}
function initThisPage() {
$('#manage_albums').click(function() {
+6 -1
View File
@@ -353,7 +353,7 @@ def dbcheck():
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
c.execute(
'CREATE TABLE IF NOT EXISTS artists (ArtistID TEXT UNIQUE, ArtistName TEXT, ArtistSortName TEXT, DateAdded TEXT, Status TEXT, IncludeExtras INTEGER, LatestAlbum TEXT, ReleaseDate TEXT, AlbumID TEXT, HaveTracks INTEGER, TotalTracks INTEGER, LastUpdated TEXT, ArtworkURL TEXT, ThumbURL TEXT, Extras TEXT, Type TEXT)')
'CREATE TABLE IF NOT EXISTS artists (ArtistID TEXT UNIQUE, ArtistName TEXT, ArtistSortName TEXT, DateAdded TEXT, Status TEXT, IncludeExtras INTEGER, LatestAlbum TEXT, ReleaseDate TEXT, AlbumID TEXT, HaveTracks INTEGER, TotalTracks INTEGER, LastUpdated TEXT, ArtworkURL TEXT, ThumbURL TEXT, Extras TEXT, Type TEXT, MetaCritic TEXT)')
# ReleaseFormat here means CD,Digital,Vinyl, etc. If using the default
# Headphones hybrid release, ReleaseID will equal AlbumID (AlbumID is
# releasegroup id)
@@ -599,6 +599,11 @@ def dbcheck():
except sqlite3.OperationalError:
c.execute('ALTER TABLE artists ADD COLUMN Type TEXT DEFAULT NULL')
try:
c.execute('SELECT MetaCritic from artists')
except sqlite3.OperationalError:
c.execute('ALTER TABLE artists ADD COLUMN MetaCritic TEXT DEFAULT NULL')
conn.commit()
c.close()
+9 -3
View File
@@ -21,8 +21,8 @@ import json
cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSnatched', 'getSimilar', 'getHistory', 'getLogs',
'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist',
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'forceActiveArtistsUpdate',
'getVersion', 'checkGithub','shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt',
'getArtistInfo', 'getAlbumInfo', 'getArtistThumb', 'getAlbumThumb',
'getVersion', 'checkGithub', 'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt',
'getArtistInfo', 'getAlbumInfo', 'getArtistThumb', 'getAlbumThumb', 'clearLogs',
'choose_specific_download', 'download_specific_release']
@@ -176,7 +176,13 @@ class Api(object):
return
def _getLogs(self, **kwargs):
pass
self.data = headphones.LOG_LIST
return
def _clearLogs(self, **kwargs):
headphones.LOG_LIST = []
self.data = 'Cleared log'
return
def _findArtist(self, **kwargs):
if 'name' not in kwargs:
+5 -1
View File
@@ -99,7 +99,7 @@ _CONFIG_DEFINITIONS = {
'HPUSER': (str, 'General', ''),
'HTTPS_CERT': (str, 'General', ''),
'HTTPS_KEY': (str, 'General', ''),
'HTTP_HOST': (str, 'General', '0.0.0.0'),
'HTTP_HOST': (str, 'General', 'localhost'),
'HTTP_PASSWORD': (str, 'General', ''),
'HTTP_PORT': (int, 'General', 8181),
'HTTP_PROXY': (int, 'General', 0),
@@ -154,6 +154,7 @@ _CONFIG_DEFINITIONS = {
'NZBSORG_HASH': (str, 'NZBsorg', ''),
'NZBSORG_UID': (str, 'NZBsorg', ''),
'NZB_DOWNLOADER': (int, 'General', 0),
'OFFICIAL_RELEASES_ONLY': (int, 'General', 0),
'OMGWTFNZBS': (int, 'omgwtfnzbs', 0),
'OMGWTFNZBS_APIKEY': (str, 'omgwtfnzbs', ''),
'OMGWTFNZBS_UID': (str, 'omgwtfnzbs', ''),
@@ -175,6 +176,7 @@ _CONFIG_DEFINITIONS = {
'PLEX_SERVER_HOST': (str, 'Plex', ''),
'PLEX_UPDATE': (int, 'Plex', 0),
'PLEX_USERNAME': (str, 'Plex', ''),
'PLEX_TOKEN': (str, 'Plex', ''),
'PREFERRED_BITRATE': (str, 'General', ''),
'PREFERRED_BITRATE_ALLOW_LOSSLESS': (int, 'General', 0),
'PREFERRED_BITRATE_HIGH_BUFFER': (int, 'General', 0),
@@ -200,6 +202,7 @@ _CONFIG_DEFINITIONS = {
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
'RENAME_FILES': (int, 'General', 0),
'REPLACE_EXISTING_FOLDERS': (int, 'General', 0),
'KEEP_ORIGINAL_FOLDER': (int, 'General', 0),
'REQUIRED_WORDS': (str, 'General', ''),
'RUTRACKER': (int, 'Rutracker', 0),
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
@@ -239,6 +242,7 @@ _CONFIG_DEFINITIONS = {
'UTORRENT_PASSWORD': (str, 'uTorrent', ''),
'UTORRENT_USERNAME': (str, 'uTorrent', ''),
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
'WAIT_UNTIL_RELEASE_DATE' : (int, 'General', 0),
'WAFFLES': (int, 'Waffles', 0),
'WAFFLES_PASSKEY': (str, 'Waffles', ''),
'WAFFLES_RATIO': (str, 'Waffles', ''),
+1 -1
View File
@@ -149,7 +149,7 @@ def get_age(date):
try:
days_old = int(split_date[0]) * 365 + int(split_date[1]) * 30 + int(split_date[2])
except IndexError:
except (IndexError,ValueError):
days_old = False
return days_old
+1 -1
View File
@@ -496,7 +496,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
cache.getThumb(ArtistID=artistid)
logger.info(u"Fetching Metacritic reviews for: %s" % artist['artist_name'])
metacritic.update(artist['artist_name'], artist['releasegroups'])
metacritic.update(artistid, artist['artist_name'], artist['releasegroups'])
if errors:
logger.info("[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (artist['artist_name'], artist['artist_name']))
+7 -5
View File
@@ -23,7 +23,7 @@ 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 and ArtistName.
def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
cron=False):
cron=False, artistScan=False):
if cron and not headphones.CONFIG.LIBRARYSCAN:
return
@@ -36,7 +36,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
# If we're appending a dir, it's coming from the post processor which is
# already bytestring
if not append:
if not append or artistScan:
dir = dir.encode(headphones.SYS_ENCODING)
if not os.path.isdir(dir):
@@ -287,7 +287,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
if not append:
if not append or artistScan:
logger.info('Updating scanned artist track counts')
# Clean up the new artist list
@@ -334,7 +334,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
for artist in artist_list:
myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist])
if headphones.CONFIG.DETECT_BITRATE:
if headphones.CONFIG.DETECT_BITRATE and bitrates:
headphones.CONFIG.PREFERRED_BITRATE = sum(bitrates) / len(bitrates) / 1000
else:
@@ -346,6 +346,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
if not append:
update_album_status()
if not append and not artistScan:
lastfm.getSimilar()
logger.info('Library scan complete')
@@ -374,7 +376,7 @@ def update_album_status(AlbumID=None):
album_completion = 0
logger.info('Album %s does not have any tracks in database' % album['AlbumTitle'])
if album_completion >= headphones.CONFIG.ALBUM_COMPLETION_PCT and album['Status'] == 'Skipped':
if album_completion >= headphones.CONFIG.ALBUM_COMPLETION_PCT:
new_album_status = "Downloaded"
# I don't think we want to change Downloaded->Skipped.....
+6 -4
View File
@@ -466,6 +466,11 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
myDB = db.DBConnection()
results = []
release_status = "official"
if includeExtras and not headphones.CONFIG.OFFICIAL_RELEASES_ONLY:
release_status = []
try:
limit = 100
newResults = None
@@ -474,6 +479,7 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
newResults = musicbrainzngs.browse_releases(
release_group=rgid,
includes=['artist-credits', 'labels', 'recordings', 'release-groups', 'media'],
release_status = release_status,
limit=limit,
offset=len(results))
if 'release-list' not in newResults:
@@ -513,10 +519,6 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
num_new_releases = 0
for releasedata in results:
#releasedata.get will return None if it doesn't have a status
#all official releases should have the Official status included
if not includeExtras and releasedata.get('status') != 'Official':
continue
release = {}
rel_id_check = releasedata['id']
+43 -13
View File
@@ -14,15 +14,17 @@
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
import re
import json
import headphones
from headphones import db, helpers, logger, request
from headphones.common import USER_AGENT
def update(artist_name,release_groups):
def update(artistid, artist_name,release_groups):
""" Pretty simple and crude function to find the artist page on metacritic,
then parse that page to get critic & user scores for albums"""
# First let's modify the artist name to fit the metacritic convention.
# First let's modify the artist name to fit the metacritic convention.
# We could just do a search, then take the top result, but at least this will
# cut down on api calls. If it's ineffective then we'll switch to search
@@ -31,28 +33,56 @@ def update(artist_name,release_groups):
mc_artist_name = mc_artist_name.replace(" ","-")
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'}
url = "http://www.metacritic.com/person/" + mc_artist_name + "?filter-options=music&sort_options=date&num_items=100"
res = request.request_soup(url, parser='html.parser')
res = request.request_soup(url, headers=headers)
rows = None
try:
rows = res.tbody.find_all('tr')
table = res.find("table", class_="credits person_credits")
rows = table.tbody.find_all('tr')
except:
logger.info("Unable to get metacritic scores for: %s" % artist_name)
return
myDB = db.DBConnection()
artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [artistid]).fetchone()
for row in rows:
title = row.a.string
score_list = []
# If we couldn't get anything from MetaCritic for whatever reason,
# let's try to load scores from the db
if not rows:
if artist['MetaCritic']:
score_list = json.loads(artist['MetaCritic'])
else:
return
# If we did get scores, let's update the db with them
else:
for row in rows:
title = row.a.string
scores = row.find_all("span")
critic_score = scores[0].string
user_score = scores[1].string
score_dict = {'title':title,'critic_score':critic_score,'user_score':user_score}
score_list.append(score_dict)
# Save scores to the database
controlValueDict = {"ArtistID": artistid}
newValueDict = {'MetaCritic':json.dumps(score_list)}
myDB.upsert("artists", newValueDict, controlValueDict)
for score in score_list:
title = score['title']
# Iterate through the release groups we got passed to see if we can find
# a match
for rg in release_groups:
if rg['title'].lower() == title.lower():
scores = row.find_all("span")
critic_score = scores[0].string
user_score = scores[1].string
critic_score = score['critic_score']
user_score = score['user_score']
controlValueDict = {"AlbumID": rg['id']}
newValueDict = {'CriticScore':critic_score,'UserScore':user_score}
myDB.upsert("albums", newValueDict, controlValueDict)
+66 -94
View File
@@ -316,34 +316,32 @@ class Plex(object):
self.client_hosts = headphones.CONFIG.PLEX_CLIENT_HOST
self.username = headphones.CONFIG.PLEX_USERNAME
self.password = headphones.CONFIG.PLEX_PASSWORD
self.token = headphones.CONFIG.PLEX_TOKEN
def _sendhttp(self, host, command):
username = self.username
password = self.password
url = host + '/xbmcCmds/xbmcHttp/?' + command
url_command = urllib.urlencode(command)
url = host + '/xbmcCmds/xbmcHttp/?' + url_command
req = urllib2.Request(url)
if password:
base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '')
req.add_header("Authorization", "Basic %s" % base64string)
logger.info('Plex url: %s' % url)
try:
handle = urllib2.urlopen(req)
except Exception as e:
logger.warn('Error opening Plex url: %s' % e)
return
response = handle.read().decode(headphones.SYS_ENCODING)
if self.password:
response = request.request_response(url, auth=(self.username, self.password))
else:
response = request.request_response(url)
return response
def _sendjson(self, host, method, params={}):
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
headers = {'Content-Type': 'application/json'}
url = host + '/jsonrpc'
if self.password:
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers, auth=(self.username, self.password))
else:
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers)
if response:
return response[0]['result']
def update(self):
# From what I read you can't update the music library on a per directory or per path basis
@@ -354,13 +352,15 @@ class Plex(object):
for host in hosts:
logger.info('Sending library update command to Plex Media Server@ ' + host)
url = "%s/library/sections" % host
try:
xml_sections = minidom.parse(urllib.urlopen(url))
except IOError, e:
logger.warn("Error while trying to contact Plex Media Server: %s" % e)
return False
if self.token:
params = {'X-Plex-Token': self.token}
else:
params = False
r = request.request_minidom(url, params=params)
sections = r.getElementsByTagName('Directory')
sections = xml_sections.getElementsByTagName('Directory')
if not sections:
logger.info(u"Plex Media Server not running on: " + host)
return False
@@ -368,11 +368,7 @@ class Plex(object):
for s in sections:
if s.getAttribute('type') == "artist":
url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key'))
try:
urllib.urlopen(url)
except Exception as e:
logger.warn("Error updating library section for Plex Media Server: %s" % e)
return False
request.request_response(url, params=params)
def notify(self, artist, album, albumartpath):
@@ -383,17 +379,24 @@ class Plex(object):
time = "3000" # in ms
for host in hosts:
logger.info('Sending notification command to Plex Media Server @ ' + host)
logger.info('Sending notification command to Plex client @ ' + host)
try:
notification = header + "," + message + "," + time + "," + albumartpath
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'}
request = self._sendhttp(host, notifycommand)
version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version']['major']
if version < 12: #Eden
notification = header + "," + message + "," + time + "," + albumartpath
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'}
request = self._sendhttp(host, notifycommand)
else: #Frodo
params = {'title': header, 'message': message, 'displaytime': int(time), 'image': albumartpath}
request = self._sendjson(host, 'GUI.ShowNotification', params)
if not request:
raise Exception
except:
logger.warn('Error sending notification request to Plex Media Server')
except Exception:
logger.error('Error sending notification request to Plex client @ ' + host)
class NMA(object):
@@ -440,52 +443,30 @@ class PUSHBULLET(object):
self.apikey = headphones.CONFIG.PUSHBULLET_APIKEY
self.deviceid = headphones.CONFIG.PUSHBULLET_DEVICEID
def conf(self, options):
return cherrypy.config['config'].get('PUSHBULLET', options)
def notify(self, message, event):
def notify(self, message):
if not headphones.CONFIG.PUSHBULLET_ENABLED:
return
http_handler = HTTPSConnection("api.pushbullet.com")
url = "https://api.pushbullet.com/v2/pushes"
data = {'type': "note",
'title': "Headphones",
'body': message.encode("utf-8")}
'body': message}
http_handler.request("POST",
"/v2/pushes",
headers={'Content-type': "application/json",
'Authorization': 'Basic %s' % base64.b64encode(headphones.CONFIG.PUSHBULLET_APIKEY + ":")},
body=json.dumps(data))
response = http_handler.getresponse()
request_status = response.status
logger.debug(u"PushBullet response status: %r" % request_status)
logger.debug(u"PushBullet response headers: %r" % response.getheaders())
logger.debug(u"PushBullet response body: %r" % response.read())
if self.deviceid:
data['device_iden'] = self.deviceid
if request_status == 200:
logger.info(u"PushBullet notifications sent.")
return True
elif request_status >= 400 and request_status < 500:
logger.info(u"PushBullet request failed: %s" % response.reason)
return False
headers={'Content-type': "application/json",
'Authorization': 'Bearer ' + headphones.CONFIG.PUSHBULLET_APIKEY}
response = request.request_json(url, method="post", headers=headers, data=json.dumps(data))
if response:
logger.info(u"PushBullet notifications sent.")
return True
else:
logger.info(u"PushBullet notification failed serverside.")
return False
def updateLibrary(self):
#For uniformity reasons not removed
return
def test(self, apikey, deviceid):
self.enabled = True
self.apikey = apikey
self.deviceid = deviceid
self.notify('Main Screen Activate', 'Test Message')
logger.info(u"PushBullet notification failed.")
return False
class PUSHALOT(object):
@@ -583,7 +564,7 @@ class PUSHOVER(object):
if not headphones.CONFIG.PUSHOVER_ENABLED:
return
http_handler = HTTPSConnection("api.pushover.net")
url = "https://api.pushover.net/1/messages.json"
data = {'token': self.application_token,
'user': headphones.CONFIG.PUSHOVER_KEYS,
@@ -591,25 +572,16 @@ class PUSHOVER(object):
'message': message.encode("utf-8"),
'priority': headphones.CONFIG.PUSHOVER_PRIORITY}
http_handler.request("POST",
"/1/messages.json",
headers={'Content-type': "application/x-www-form-urlencoded"},
body=urlencode(data))
response = http_handler.getresponse()
request_status = response.status
logger.debug(u"Pushover response status: %r" % request_status)
logger.debug(u"Pushover response headers: %r" % response.getheaders())
logger.debug(u"Pushover response body: %r" % response.read())
headers = {'Content-type': "application/x-www-form-urlencoded"}
if request_status == 200:
logger.info(u"Pushover notifications sent.")
return True
elif request_status >= 400 and request_status < 500:
logger.info(u"Pushover request failed: %s" % response.reason)
return False
response = request.request_response(url, method="POST", headers=headers, data=data)
if response:
logger.info(u"Pushover notifications sent.")
return True
else:
logger.info(u"Pushover notification failed.")
return False
logger.error(u"Pushover notification failed.")
return False
def updateLibrary(self):
#For uniformity reasons not removed
@@ -871,4 +843,4 @@ class Email(object):
except Exception, e:
logger.warn('Error sending Email: %s' % e)
return False
return False
+16 -16
View File
@@ -59,7 +59,7 @@ def checkFolder():
logger.debug("Checking download folder finished.")
def verify(albumid, albumpath, Kind=None, forced=False):
def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=False):
myDB = db.DBConnection()
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
@@ -216,7 +216,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
logger.debug('Matching metadata album: %s with album name: %s' % (metaalbum, dbalbum))
if metaartist == dbartist and metaalbum == dbalbum:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind)
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, keep_original_folder)
return
# test #2: filenames
@@ -234,7 +234,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
logger.debug('Checking if track title: %s is in file name: %s' % (dbtrack, filetrack))
if dbtrack in filetrack:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind)
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, keep_original_folder)
return
# test #3: number of songs and duration
@@ -266,7 +266,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
logger.debug('Database track duration: %i' % db_track_duration)
delta = abs(downloaded_track_duration - db_track_duration)
if delta < 240:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind)
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, keep_original_folder)
return
logger.warn(u'Could not identify album: %s. It may not be the intended album.' % albumpath.decode(headphones.SYS_ENCODING, 'replace'))
@@ -276,11 +276,11 @@ def verify(albumid, albumpath, Kind=None, forced=False):
renameUnprocessedFolder(albumpath, tag="Unprocessed")
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None):
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None, keep_original_folder=False):
logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle']))
# Check to see if we're preserving the torrent dir
if headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent" and 'headphones-modified' not in albumpath:
if (headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent" and 'headphones-modified' not in albumpath) or headphones.CONFIG.KEEP_ORIGINAL_FOLDER or keep_original_folder:
new_folder = os.path.join(albumpath, 'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
logger.info("Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
try:
@@ -1060,7 +1060,7 @@ def renameUnprocessedFolder(path, tag):
return
def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_original_folder=False):
logger.info('Force checking download folder for completed downloads')
@@ -1136,7 +1136,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
continue
else:
logger.info('Found a match in the database: %s. Verifying to make sure it is the correct album', snatched['Title'])
verify(snatched['AlbumID'], folder, snatched['Kind'])
verify(snatched['AlbumID'], folder, snatched['Kind'], keep_original_folder=keep_original_folder)
continue
# Attempt 2: strip release group id from filename
@@ -1153,10 +1153,10 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
release = myDB.action('SELECT ArtistName, AlbumTitle, AlbumID from albums WHERE AlbumID=?', [rgid]).fetchone()
if release:
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder, forced=True)
verify(release['AlbumID'], folder, forced=True, keep_original_folder=keep_original_folder)
continue
else:
logger.info('Found a (possibly) valid Musicbrainz realse group id in album folder name.')
logger.info('Found a (possibly) valid Musicbrainz release group id in album folder name.')
verify(rgid, folder, forced=True)
continue
@@ -1172,7 +1172,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', [name, album]).fetchone()
if release:
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder)
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
continue
else:
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
@@ -1183,7 +1183,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
rgid = None
if rgid:
verify(rgid, folder)
verify(rgid, folder, keep_original_folder=keep_original_folder)
continue
else:
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
@@ -1207,7 +1207,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', [name, album]).fetchone()
if release:
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder)
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
continue
else:
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
@@ -1218,7 +1218,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
rgid = None
if rgid:
verify(rgid, folder)
verify(rgid, folder, keep_original_folder=keep_original_folder)
continue
else:
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
@@ -1231,7 +1231,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?', [folder_basename]).fetchone()
if release:
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder)
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
continue
else:
logger.info('Querying MusicBrainz for the release group id for: %s', folder_basename)
@@ -1242,7 +1242,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
rgid = None
if rgid:
verify(rgid, folder)
verify(rgid, folder, keep_original_folder=keep_original_folder)
continue
else:
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
+1 -1
View File
@@ -206,7 +206,7 @@ def server_message(response):
message = None
# First attempt is to 'read' the response as HTML
if "text/html" in response.headers.get("content-type"):
if response.headers.get("content-type") and "text/html" in response.headers.get("content-type"):
try:
soup = BeautifulSoup(response.content, "html5lib")
except Exception:
+23 -1
View File
@@ -29,6 +29,7 @@ import string
import shutil
import random
import urllib
import datetime
import headphones
import subprocess
import unicodedata
@@ -189,10 +190,22 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
results = myDB.select('SELECT * from albums WHERE Status="Wanted" OR Status="Wanted Lossless"')
for album in results:
if not album['AlbumTitle'] or not album['ArtistName']:
logger.warn('Skipping release %s. No title available', album['AlbumID'])
continue
if headphones.CONFIG.WAIT_UNTIL_RELEASE_DATE and album['ReleaseDate']:
try:
release_date = datetime.datetime.strptime(album['ReleaseDate'], "%Y-%m-%d")
except:
logger.warn("No valid date for: %s. Skipping automatic search" % album['AlbumTitle'])
continue
if release_date > datetime.datetime.today():
logger.info("Skipping: %s. Waiting for release date of: %s" % (album['AlbumTitle'], album['ReleaseDate']))
continue
new = True
if album['Status'] == "Wanted Lossless":
@@ -1280,6 +1293,12 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
search_formats = [None] # should return all
bitrate = headphones.CONFIG.PREFERRED_BITRATE
if bitrate:
if 225 <= int(bitrate) < 256:
bitrate = 'V0'
elif 200 <= int(bitrate) < 225:
bitrate = 'V1'
elif 175 <= int(bitrate) < 200:
bitrate = 'V2'
for encoding_string in gazelleencoding.ALL_ENCODINGS:
if re.search(bitrate, encoding_string, flags=re.I):
bitrate_string = encoding_string
@@ -1306,7 +1325,10 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
logger.info(u"Searching %s..." % provider)
all_torrents = []
for search_format in search_formats:
all_torrents.extend(gazelle.search_torrents(artistname=semi_clean_artist_term,
if usersearchterm:
all_torrents.extend(gazelle.search_torrents(searchstr=usersearchterm, format=search_format, encoding=bitrate_string)['results'])
else:
all_torrents.extend(gazelle.search_torrents(artistname=semi_clean_artist_term,
groupname=semi_clean_album_term,
format=search_format, encoding=bitrate_string)['results'])
+5
View File
@@ -73,6 +73,7 @@ class utorrentclient(object):
except urllib2.HTTPError as err:
logger.debug('URL: ' + str(url))
logger.debug('Error getting Token. uTorrent responded with error: ' + str(err))
return
match = re.search(utorrentclient.TOKEN_REGEX, response.read())
return match.group(1)
@@ -147,6 +148,10 @@ class utorrentclient(object):
return self._action(params)
def _action(self, params, body=None, content_type=None):
if not self.token:
return
url = self.base_url + '/gui/' + '?token=' + self.token + '&' + urllib.urlencode(params)
request = urllib2.Request(url)
+97 -13
View File
@@ -32,8 +32,10 @@ import random
import urllib
import json
import time
import cgi
import sys
import os
import re
try:
# pylint:disable=E0611
@@ -149,7 +151,7 @@ class WebInterface(object):
searchresults = mb.findRelease(name, limit=100)
else:
searchresults = mb.findSeries(name, limit=100)
return serve_template(templatename="searchresults.html", title='Search Results for: "' + name + '"', searchresults=searchresults, name=name, type=type)
return serve_template(templatename="searchresults.html", title='Search Results for: "' + cgi.escape(name) + '"', searchresults=searchresults, name=cgi.escape(name), type=type)
@cherrypy.expose
def addArtist(self, artistid):
@@ -265,14 +267,72 @@ class WebInterface(object):
@cherrypy.expose
def scanArtist(self, ArtistID):
logger.info(u"Scanning artist: %s", ArtistID)
myDB = db.DBConnection()
artistname = myDB.select('SELECT DISTINCT ArtistName FROM artists WHERE ArtistID=?', [ArtistID])
artistfolder = os.path.join(headphones.CONFIG.DESTINATION_DIR, artistname[0][0])
try:
threading.Thread(target=librarysync.libraryScan(dir=artistfolder)).start()
except Exception as e:
logger.error('Unable to complete the scan: %s', e)
artist_name = myDB.select('SELECT DISTINCT ArtistName FROM artists WHERE ArtistID=?', [ArtistID])[0][0]
logger.info(u"Scanning artist: %s", artist_name)
full_folder_format = headphones.CONFIG.FOLDER_FORMAT
folder_format = re.findall(r'(.*[Aa]rtist?)\.*', full_folder_format)[0]
acceptable_formats = ["$artist","$sortartist","$first/$artist","$first/$sortartist"]
if not folder_format.lower() in acceptable_formats:
logger.info("Can't determine the artist folder from the configured folder_format. Not scanning")
return
# Format the folder to match the settings
artist = artist_name.replace('/', '_')
if headphones.CONFIG.FILE_UNDERSCORES:
artist = artist.replace(' ', '_')
if artist.startswith('The '):
sortname = artist[4:] + ", The"
else:
sortname = artist
if sortname[0].isdigit():
firstchar = u'0-9'
else:
firstchar = sortname[0]
values = {'$Artist': artist,
'$SortArtist': sortname,
'$First': firstchar.upper(),
'$artist': artist.lower(),
'$sortartist': sortname.lower(),
'$first': firstchar.lower(),
}
folder = helpers.replace_all(folder_format.strip(), values, normalize=True)
folder = helpers.replace_illegal_chars(folder, type="folder")
folder = folder.replace('./', '_/').replace('/.', '/_')
if folder.endswith('.'):
folder = folder[:-1] + '_'
if folder.startswith('.'):
folder = '_' + folder[1:]
dirs = []
if headphones.CONFIG.MUSIC_DIR:
dirs.append(headphones.CONFIG.MUSIC_DIR)
if headphones.CONFIG.DESTINATION_DIR:
dirs.append(headphones.CONFIG.DESTINATION_DIR)
if headphones.CONFIG.LOSSLESS_DESTINATION_DIR:
dirs.append(headphones.CONFIG.LOSSLESS_DESTINATION_DIR)
dirs = set(dirs)
for dir in dirs:
artistfolder = os.path.join(dir, folder)
if not os.path.isdir(artistfolder):
logger.debug("Cannot find directory: " + artistfolder)
continue
threading.Thread(target=librarysync.libraryScan, kwargs={"dir":artistfolder, "artistScan":True, "ArtistID":ArtistID, "ArtistName":artist_name}).start()
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
@cherrypy.expose
@@ -740,9 +800,9 @@ class WebInterface(object):
raise cherrypy.HTTPRedirect("home")
@cherrypy.expose
def forcePostProcess(self, dir=None, album_dir=None):
def forcePostProcess(self, dir=None, album_dir=None, keep_original_folder=False):
from headphones import postprocessor
threading.Thread(target=postprocessor.forcePostProcess, kwargs={'dir': dir, 'album_dir': album_dir}).start()
threading.Thread(target=postprocessor.forcePostProcess, kwargs={'dir': dir, 'album_dir': album_dir, 'keep_original_folder':keep_original_folder}).start()
raise cherrypy.HTTPRedirect("home")
@cherrypy.expose
@@ -1066,12 +1126,15 @@ class WebInterface(object):
"embed_album_art": checked(headphones.CONFIG.EMBED_ALBUM_ART),
"embed_lyrics": checked(headphones.CONFIG.EMBED_LYRICS),
"replace_existing_folders": checked(headphones.CONFIG.REPLACE_EXISTING_FOLDERS),
"keep_original_folder" : checked(headphones.CONFIG.KEEP_ORIGINAL_FOLDER),
"destination_dir": headphones.CONFIG.DESTINATION_DIR,
"lossless_destination_dir": headphones.CONFIG.LOSSLESS_DESTINATION_DIR,
"folder_format": headphones.CONFIG.FOLDER_FORMAT,
"file_format": headphones.CONFIG.FILE_FORMAT,
"file_underscores": checked(headphones.CONFIG.FILE_UNDERSCORES),
"include_extras": checked(headphones.CONFIG.INCLUDE_EXTRAS),
"official_releases_only": checked(headphones.CONFIG.OFFICIAL_RELEASES_ONLY),
"wait_until_release_date": checked(headphones.CONFIG.WAIT_UNTIL_RELEASE_DATE),
"autowant_upcoming": checked(headphones.CONFIG.AUTOWANT_UPCOMING),
"autowant_all": checked(headphones.CONFIG.AUTOWANT_ALL),
"autowant_manually_added": checked(headphones.CONFIG.AUTOWANT_MANUALLY_ADDED),
@@ -1120,6 +1183,7 @@ class WebInterface(object):
"plex_client_host": headphones.CONFIG.PLEX_CLIENT_HOST,
"plex_username": headphones.CONFIG.PLEX_USERNAME,
"plex_password": headphones.CONFIG.PLEX_PASSWORD,
"plex_token": headphones.CONFIG.PLEX_TOKEN,
"plex_update": checked(headphones.CONFIG.PLEX_UPDATE),
"plex_notify": checked(headphones.CONFIG.PLEX_NOTIFY),
"nma_enabled": checked(headphones.CONFIG.NMA_ENABLED),
@@ -1216,9 +1280,10 @@ class WebInterface(object):
checked_configs = [
"launch_browser", "enable_https", "api_enabled", "use_blackhole", "headphones_indexer", "use_newznab", "newznab_enabled",
"use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay", "use_mininova", "use_waffles", "use_rutracker",
"use_whatcd", "preferred_bitrate_allow_lossless", "detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files", "rename_files",
"correct_metadata", "cleanup_files", "keep_nfo", "add_album_art", "embed_album_art", "embed_lyrics", "replace_existing_folders",
"file_underscores", "include_extras", "autowant_upcoming", "autowant_all", "autowant_manually_added", "keep_torrent_files", "music_encoder",
"use_whatcd", "preferred_bitrate_allow_lossless", "detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files",
"rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art", "embed_album_art", "embed_lyrics",
"replace_existing_folders", "keep_original_folder", "file_underscores", "include_extras", "official_releases_only",
"wait_until_release_date", "autowant_upcoming", "autowant_all", "autowant_manually_added", "keep_torrent_files", "music_encoder",
"encoderlossless", "encoder_multicore", "delete_lossless_files", "growl_enabled", "growl_onsnatch", "prowl_enabled",
"prowl_onsnatch", "xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled", "plex_enabled", "plex_update", "plex_notify",
"nma_enabled", "nma_onsnatch", "pushalot_enabled", "pushalot_onsnatch", "synoindex_enabled", "pushover_enabled",
@@ -1424,6 +1489,25 @@ class WebInterface(object):
logger.warn(msg)
return msg
@cherrypy.expose
def testPushover(self):
logger.info(u"Sending Pushover notification")
pushover = notifiers.PUSHOVER()
result = pushover.notify("hooray!", "This is a test")
return str(result)
@cherrypy.expose
def testPlex(self):
logger.info(u"Testing plex notifications")
plex = notifiers.Plex()
plex.notify("hellooooo", "test album!", "")
@cherrypy.expose
def testPushbullet(self):
logger.info("Testing Pushbullet notifications")
pushbullet = notifiers.PUSHBULLET()
pushbullet.notify("it works!")
class Artwork(object):
@cherrypy.expose
def index(self):
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
from pynma import PyNMA
from .pynma import PyNMA
Regular → Executable
+44 -33
View File
@@ -1,12 +1,20 @@
#!/usr/bin/python
from xml.dom.minidom import parseString
from httplib import HTTPSConnection
from urllib import urlencode
__version__ = "0.1"
try:
from http.client import HTTPSConnection
except ImportError:
from httplib import HTTPSConnection
API_SERVER = 'nma.usk.bz'
try:
from urllib.parse import urlencode
except ImportError:
from urllib import urlencode
__version__ = "1.0"
API_SERVER = 'www.notifymyandroid.com'
ADD_PATH = '/publicapi/notify'
USER_AGENT="PyNMA/v%s"%__version__
@@ -18,14 +26,14 @@ def uniq_preserve(seq): # Dave Kirby
def uniq(seq):
# Not order preserving
return {}.fromkeys(seq).keys()
return list({}.fromkeys(seq).keys())
class PyNMA(object):
"""PyNMA(apikey=[], developerkey=None)
takes 2 optional arguments:
- (opt) apykey: might me a string containing 1 key or an array of keys
- (opt) developerkey: where you can store your developer key
"""
takes 2 optional arguments:
- (opt) apykey: might me a string containing 1 key or an array of keys
- (opt) developerkey: where you can store your developer key
"""
def __init__(self, apikey=[], developerkey=None):
self._developerkey = None
@@ -60,19 +68,20 @@ class PyNMA(object):
if type(developerkey) == str and len(developerkey) == 48:
self._developerkey = developerkey
def push(self, application="", event="", description="", url="", priority=0, batch_mode=False):
def push(self, application="", event="", description="", url="", contenttype=None, priority=0, batch_mode=False, html=False):
"""Pushes a message on the registered API keys.
takes 5 arguments:
- (req) application: application name [256]
- (req) event: event name [1000]
- (req) description: description [10000]
- (opt) url: url [512]
- (opt) priority: from -2 (lowest) to 2 (highest) (def:0)
- (opt) batch_mode: call API 5 by 5 (def:False)
Warning: using batch_mode will return error only if all API keys are bad
cf: http://nma.usk.bz/api.php
"""
takes 5 arguments:
- (req) application: application name [256]
- (req) event: event name [1000]
- (req) description: description [10000]
- (opt) url: url [512]
- (opt) contenttype: Content Type (act: None (plain text) or text/html)
- (opt) priority: from -2 (lowest) to 2 (highest) (def:0)
- (opt) batch_mode: push to all keys at once (def:False)
- (opt) html: shortcut for contenttype=text/html
Warning: using batch_mode will return error only if all API keys are bad
cf: http://nma.usk.bz/api.php
"""
datas = {
'application': application[:256].encode('utf8'),
'event': event[:1024].encode('utf8'),
@@ -82,7 +91,10 @@ class PyNMA(object):
if url:
datas['url'] = url[:512]
if contenttype == "text/html" or html == True: # Currently only accepted content type
datas['content-type'] = "text/html"
if self._developerkey:
datas['developerkey'] = self._developerkey
@@ -94,12 +106,11 @@ class PyNMA(object):
res = self.callapi('POST', ADD_PATH, datas)
results[key] = res
else:
for i in range(0, len(self._apikey), 5):
datas['apikey'] = ",".join(self._apikey[i:i+5])
res = self.callapi('POST', ADD_PATH, datas)
results[datas['apikey']] = res
datas['apikey'] = ",".join(self._apikey)
res = self.callapi('POST', ADD_PATH, datas)
results[datas['apikey']] = res
return results
def callapi(self, method, path, args):
headers = { 'User-Agent': USER_AGENT }
if method == "POST":
@@ -110,13 +121,13 @@ class PyNMA(object):
try:
res = self._parse_reponse(resp.read())
except Exception, e:
except Exception as e:
res = {'type': "pynmaerror",
'code': 600,
'message': str(e)
}
pass
return res
def _parse_reponse(self, response):
@@ -124,14 +135,14 @@ class PyNMA(object):
for elem in root.childNodes:
if elem.nodeType == elem.TEXT_NODE: continue
if elem.tagName == 'success':
res = dict(elem.attributes.items())
res = dict(list(elem.attributes.items()))
res['message'] = ""
res['type'] = elem.tagName
return res
if elem.tagName == 'error':
res = dict(elem.attributes.items())
res = dict(list(elem.attributes.items()))
res['message'] = elem.firstChild.nodeValue
res['type'] = elem.tagName
return res