mirror of
https://github.com/rembo10/headphones.git
synced 2026-09-09 16:22:52 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24165fb5f3 | ||
|
|
71da960386 | ||
|
|
a16c2d0a26 | ||
|
|
53de44a225 | ||
|
|
e32cff60e5 | ||
|
|
0ebba8d17c | ||
|
|
464db79c04 | ||
|
|
65019d8323 | ||
|
|
f131d316ca | ||
|
|
e6663c96b5 | ||
|
|
5f06d11fe9 | ||
|
|
6db2836662 | ||
|
|
cb209f8d98 | ||
|
|
5634e17a2e | ||
|
|
7c990edeaf | ||
|
|
9149f62760 | ||
|
|
872ec6b11c | ||
|
|
064a21a07c | ||
|
|
57c3c7b2e0 | ||
|
|
6dc75d7a44 | ||
|
|
5f3b7e05c5 | ||
|
|
61d3d8ad00 | ||
|
|
e36440f834 | ||
|
|
4029543d51 | ||
|
|
8aee9ac91f | ||
|
|
6e5e15f06f | ||
|
|
a72bd4cb7c | ||
|
|
d062ab553c | ||
|
|
a41edde12a | ||
|
|
7608927b4f | ||
|
|
24d7647c08 | ||
|
|
a14369cb7d | ||
|
|
3e81c4f3b6 | ||
|
|
84a6c19677 | ||
|
|
624a1d5509 | ||
|
|
8b6d35cfe2 | ||
|
|
867e502448 | ||
|
|
74275b969e | ||
|
|
275dde0630 | ||
|
|
46b470f30b | ||
|
|
fbfec95a8a | ||
|
|
1e75a18f55 | ||
|
|
a98849bf30 | ||
|
|
1b998e9d01 | ||
|
|
ebd6d3f3f9 | ||
|
|
1204afadbb | ||
|
|
cd08b72df7 |
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
+466
-423
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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', ''),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']))
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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'])
|
||||
|
||||
|
||||
@@ -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
@@ -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,4 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from pynma import PyNMA
|
||||
from .pynma import PyNMA
|
||||
|
||||
|
||||
Regular → Executable
+44
-33
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user