mirror of
https://github.com/rembo10/headphones.git
synced 2026-09-10 00:32:52 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88af84579c | ||
|
|
917dc7695f | ||
|
|
65e3e4faa2 | ||
|
|
9e7ec3a65c | ||
|
|
7ac7b127c6 | ||
|
|
3945753c5c | ||
|
|
599585d57a | ||
|
|
10cf36401f | ||
|
|
fe2834f4a6 | ||
|
|
674617c0f8 | ||
|
|
f92f8928b0 | ||
|
|
45868bc523 | ||
|
|
9b2594355f | ||
|
|
e30cb26200 | ||
|
|
63d3ab1cc1 | ||
|
|
188d907e10 | ||
|
|
e972a86233 | ||
|
|
52a76e87ad | ||
|
|
8ab19f8121 | ||
|
|
6f3c8c68d4 | ||
|
|
98385ff4c8 | ||
|
|
62a6be26af | ||
|
|
5c1169ba9d | ||
|
|
0b47a5ca3a | ||
|
|
9da75a777b | ||
|
|
2fc2190b59 | ||
|
|
e108935430 | ||
|
|
bbde74db8d | ||
|
|
9fce4edf00 | ||
|
|
ea3c6c3278 | ||
|
|
6a0bd397cd | ||
|
|
7bbb137fd9 | ||
|
|
e67d32445f | ||
|
|
83280f3f99 | ||
|
|
9c81fbaa2b | ||
|
|
1d7415b91d | ||
|
|
bc5a042dcc | ||
|
|
e5af0bdd52 | ||
|
|
dc2939a4f9 | ||
|
|
f52ec56a0f | ||
|
|
75b38df3d6 | ||
|
|
0f5aeea345 | ||
|
|
d80fc36b29 | ||
|
|
4ca42c9c11 | ||
|
|
6c760902d6 | ||
|
|
2a0fcc7566 | ||
|
|
b763ccdd08 | ||
|
|
e72a0d1141 | ||
|
|
295e5a1ccf | ||
|
|
497fafb555 | ||
|
|
585f556006 | ||
|
|
7b115126c9 | ||
|
|
1a9165a084 | ||
|
|
4a74d54cdc | ||
|
|
a0755d4b1c | ||
|
|
084181a9a5 | ||
|
|
5c381b9410 | ||
|
|
0be9500518 | ||
|
|
9c2da72dcc | ||
|
|
3f6145cd07 | ||
|
|
e155f19d0d | ||
|
|
0c7ea8a5a0 | ||
|
|
1f3474a154 | ||
|
|
c0c1f9cc61 | ||
|
|
e4382c23a3 | ||
|
|
29b5305ab4 | ||
|
|
758154e87e | ||
|
|
ea8b1815f6 | ||
|
|
ebc0b7205f | ||
|
|
06ee770184 | ||
|
|
900325186b | ||
|
|
3b9816f3b3 | ||
|
|
3e5779e5ce | ||
|
|
66d0cb4a1b | ||
|
|
54c85c7951 | ||
|
|
941da4be1c | ||
|
|
69c94baf18 | ||
|
|
90016f6eda | ||
|
|
06b647e30d | ||
|
|
84ebcb84f9 | ||
|
|
b11610face | ||
|
|
6e90419014 | ||
|
|
0457c3c30e | ||
|
|
945cfbf21b | ||
|
|
5769cb0dee | ||
|
|
efd043e309 | ||
|
|
fd19563160 | ||
|
|
1a2ed24566 | ||
|
|
d2fecc47ec | ||
|
|
f9b0a5efb9 | ||
|
|
e304e34ac7 | ||
|
|
ff3826c0c8 | ||
|
|
c96b043a19 | ||
|
|
a1d0f14eb8 | ||
|
|
0234f4dab9 | ||
|
|
e00411f7ad | ||
|
|
3b2350cd8f | ||
|
|
d490091a9a | ||
|
|
5be36b517f | ||
|
|
dab323e8ff | ||
|
|
7e02f86af8 | ||
|
|
26ed43fca8 | ||
|
|
dab4e3d5f8 | ||
|
|
07e8b6b095 | ||
|
|
52523a5d3c | ||
|
|
ded4b7ef6f | ||
|
|
aa9d669a2b | ||
|
|
5378884bf8 | ||
|
|
631d947a9a | ||
|
|
7280807588 | ||
|
|
ca010ac522 | ||
|
|
e2e1ef9bc6 | ||
|
|
a96b793c98 | ||
|
|
f7ba0ff634 | ||
|
|
a0339d9814 | ||
|
|
eb7a1d7117 | ||
|
|
65fa0e21fa | ||
|
|
60ea86d499 | ||
|
|
0411bb6b44 | ||
|
|
d0198b15ee | ||
|
|
c4e8678d8f | ||
|
|
d6cd9cd7e4 |
+5
-5
@@ -77,16 +77,16 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
headphones.VERBOSE = 2
|
||||
elif args.quiet:
|
||||
headphones.VERBOSE = 0
|
||||
headphones.VERBOSE = True
|
||||
if args.quiet:
|
||||
headphones.QUIET = True
|
||||
|
||||
if args.daemon:
|
||||
if sys.platform == 'win32':
|
||||
print "Daemonize not supported under Windows, starting normally"
|
||||
else:
|
||||
headphones.DAEMON = True
|
||||
headphones.VERBOSE = False
|
||||
headphones.QUIET = True
|
||||
|
||||
if args.pidfile:
|
||||
headphones.PIDFILE = str(args.pidfile)
|
||||
@@ -101,7 +101,7 @@ def main():
|
||||
try:
|
||||
file(headphones.PIDFILE, 'w').write("pid\n")
|
||||
except IOError, e:
|
||||
raise SystemExit("Unable to write PID file: %s [%d]" % (e.strerror, e.errno))
|
||||
raise SystemExit("Unable to write PID file: %s [%d]", e.strerror, e.errno)
|
||||
else:
|
||||
logger.warn("Not running in daemon mode. PID file creation disabled.")
|
||||
|
||||
|
||||
@@ -172,6 +172,14 @@ table.display tr.even.gradeA {
|
||||
background-color: #ddffdd;
|
||||
}
|
||||
|
||||
table.display tr.odd.gradeI {
|
||||
background-color: #bebebe;
|
||||
}
|
||||
|
||||
table.display tr.even.gradeI {
|
||||
background-color: #bebebe;
|
||||
}
|
||||
|
||||
table.display tr.odd.gradeC {
|
||||
background-color: #ddddff;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div id="subhead_menu">
|
||||
|
||||
<a id="menu_link_delete" href="deleteAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Album</a>
|
||||
%if album['Status'] == 'Skipped':
|
||||
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
|
||||
<a id="menu_link_wanted" href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=False', $(this),true)" data-success="'${album['AlbumTitle']}' added to queue"><i class="fa fa-heart"></i> Mark Album as Wanted</a>
|
||||
%elif album['Status'] == 'Wanted':
|
||||
<a id="menu_link_check" href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this));" data-success="Forced checking successful"><i class="fa fa-search"></i> Force Check</a>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('refreshArtist?ArtistID=${artist['ArtistID']}', $(this)),'table'" href="#" data-success="'${artist['ArtistName']}' is being refreshed"><i class="fa fa-refresh"></i> Refresh Artist</a>
|
||||
<a id="menu_link_refresh" onclick="doSimpleAjaxCall('refreshArtist?ArtistID=${artist['ArtistID']}')" href="#"><i class="fa fa-refresh"></i> Refresh Artist</a>
|
||||
<a id="menu_link_delete" href="deleteArtist?ArtistID=${artist['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Artist</a>
|
||||
%if artist['Status'] == 'Paused':
|
||||
<a id="menu_link_resume" href="#" onclick="doAjaxCall('resumeArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} resumed"><i class="fa fa-play"></i> Resume Artist</a>
|
||||
@@ -16,7 +16,7 @@
|
||||
<a id="menu_link_pauze" href="#" onclick="doAjaxCall('pauseArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} paused"><i class="fa fa-pause"></i> Pause Artist</a>
|
||||
%endif
|
||||
%if artist['IncludeExtras']:
|
||||
<a id="menu_link_removeextra" href="#" onclick="doAjaxCall('removeExtras?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="Extras removed for ${artist['ArtistName']}"><i class="fa fa-minus"></i> Remove Extras</a>
|
||||
<a id="menu_link_removeextra" href="#" onclick="doAjaxCall('removeExtras?ArtistID=${artist['ArtistID']}&ArtistName=${artist['ArtistName']}',$(this),'submenu&table')" data-success="Extras removed for ${artist['ArtistName']}"><i class="fa fa-minus"></i> Remove Extras</a>
|
||||
<a class="menu_link_edit" id="menu_link_modifyextra" href="#"><i class="fa fa-pencil"></i> Modify Extras</a>
|
||||
%else:
|
||||
<a id="menu_link_getextra" href="#"><i class="fa fa-plus"></i> Get Extras</a>
|
||||
@@ -42,14 +42,8 @@
|
||||
<div id="artistImg">
|
||||
<img id="artistImage" class="albumArt" alt="" src="artwork/artist/${artist['ArtistID']}"/>
|
||||
</div>
|
||||
<h1>
|
||||
%if artist['Status'] == 'Loading':
|
||||
<i class="fa fa-refresh fa-spin"></i>
|
||||
%endif
|
||||
<a href="http://musicbrainz.org/artist/${artist['ArtistID']}">${artist['ArtistName']}</a>
|
||||
%if artist['Status'] == 'Loading':
|
||||
<h3><i>(Album information for this artist is currently being loaded)</i></h3>
|
||||
%endif
|
||||
<h1 id="artistname">
|
||||
<a href="http://musicbrainz.org/artist/${artist['ArtistID']}" id="artistnamelink">${artist['ArtistName']}</a>
|
||||
</h1>
|
||||
<div id="artistBio"></div>
|
||||
</div>
|
||||
@@ -62,6 +56,7 @@
|
||||
<option value="Wanted">Wanted</option>
|
||||
<option value="WantedNew">Wanted (new only)</option>
|
||||
<option value="Skipped">Skipped</option>
|
||||
<option value="Ignored">Ignored</option>
|
||||
<option value="Downloaded">Downloaded</option>
|
||||
</select>
|
||||
<input type="hidden" value="Go">
|
||||
@@ -89,6 +84,8 @@
|
||||
grade = 'X'
|
||||
elif album['Status'] == 'Snatched':
|
||||
grade = 'C'
|
||||
elif album['Status'] == 'Ignored':
|
||||
grade = 'I'
|
||||
else:
|
||||
grade = 'A'
|
||||
|
||||
@@ -123,12 +120,12 @@
|
||||
%>
|
||||
<tr class="grade${grade}">
|
||||
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></td>
|
||||
<td id="albumart"><img class="albumArt" id="${album['AlbumID']}" src="artwork/thumbs/album/${album['AlbumID']}" height="64" width="64"></td>
|
||||
<td id="albumart"><img class="albumArtnostretch" id="${album['AlbumID']}" src="artwork/thumbs/album/${album['AlbumID']}" height="64" width="64"></td>
|
||||
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||
<td id="reldate">${album['ReleaseDate']}</td>
|
||||
<td id="type">${album['Type']}</td>
|
||||
<td id="status">${album['Status']}
|
||||
%if album['Status'] == 'Skipped':
|
||||
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
|
||||
[<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' added to Wanted list">want</a>]
|
||||
%elif (album['Status'] == 'Wanted' or album['Status'] == 'Wanted Lossless'):
|
||||
[<a href="#" onclick="doAjaxCall('unqueueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' skipped">skip</a>]
|
||||
@@ -153,9 +150,6 @@
|
||||
|
||||
<%def name="headIncludes()">
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
%if artist['Status'] == 'Loading':
|
||||
<meta http-equiv="refresh" content="5">
|
||||
%endif
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
@@ -168,7 +162,6 @@
|
||||
getInfo(elem,id,'artist');
|
||||
}
|
||||
|
||||
|
||||
<%
|
||||
if headphones.SONGKICK_FILTER_ENABLED:
|
||||
songkick_filter_enabled = "true"
|
||||
@@ -237,6 +230,40 @@
|
||||
});
|
||||
}
|
||||
|
||||
var loadingMessage = false;
|
||||
var spinner_active = false;
|
||||
var loadingtext_active = false;
|
||||
|
||||
function checkArtistStatus() {
|
||||
$.getJSON("getArtistjson?ArtistID=${artist['ArtistID']}", function(data) {
|
||||
if (data['Status'] == "Loading"){
|
||||
refreshTable();
|
||||
$('#artistnamelink').text(data["ArtistName"]);
|
||||
if (loadingMessage == false){
|
||||
$("#ajaxMsg").after( "<div id='ajaxMsg2' class='ajaxMsg'></div>" );
|
||||
showArtistMsg("Getting artist information");
|
||||
loadingMessage = true;
|
||||
}
|
||||
if (spinner_active == false){
|
||||
$('#artistname').prepend('<i class="fa fa-refresh fa-spin" id="artistnamespinner"></i>')
|
||||
spinner_active = true;
|
||||
}
|
||||
if (loadingtext_active == false){
|
||||
$('#artistname').append('<h3 id="loadingtext"><i>(Album information for this artist is currently being loaded)</i></h3>')
|
||||
loadingtext_active = true;
|
||||
}
|
||||
}
|
||||
else{
|
||||
$('#artistnamespinner').remove()
|
||||
$('#loadingtext').remove()
|
||||
$('#ajaxMsg2').remove()
|
||||
spinner_active = false
|
||||
loadingtext_active = false
|
||||
loadingMessage = false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initThisPage() {
|
||||
$('#menu_link_getextra').click(function() {
|
||||
$('#dialog').dialog();
|
||||
@@ -245,10 +272,6 @@
|
||||
$('#menu_link_modifyextra').click(function() {
|
||||
$('#dialog').dialog();
|
||||
});
|
||||
|
||||
%if artist['Status'] == 'Loading':
|
||||
showMsg("Getting artist information",true);
|
||||
%endif
|
||||
$('#album_table').dataTable({
|
||||
"bDestroy": true,
|
||||
"aoColumns": [
|
||||
@@ -280,7 +303,7 @@
|
||||
resetFilters("albums");
|
||||
setTimeout(function(){
|
||||
initFancybox();
|
||||
},1500)
|
||||
},1500);
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
@@ -290,6 +313,10 @@
|
||||
if( ${songkick_enabled} ){
|
||||
getArtistsCalendar();
|
||||
}
|
||||
checkArtistStatus();
|
||||
setInterval(function(){
|
||||
checkArtistStatus();
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="ajaxMsg"></div>
|
||||
<div id="ajaxMsg" class="ajaxMsg"></div>
|
||||
% if not headphones.CURRENT_VERSION:
|
||||
<div id="updatebar">
|
||||
You're running an unknown version of Headphones. <a href="update">Update</a> or
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
<td>
|
||||
<fieldset>
|
||||
<legend>Torrents</legend>
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_blackhole" value="0" ${config['torrent_downloader_blackhole']}>Black Hole <input type="radio" name="torrent_downloader" id="torrent_downloader_transmission" value="1" ${config['torrent_downloader_transmission']}> Transmission <!-- <input type="radio" name="torrent_downloader" id="torrent_downloader_utorrent" value="2" ${config['torrent_downloader_utorrent']}> uTorrent -->
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_blackhole" value="0" ${config['torrent_downloader_blackhole']}> Black Hole <input type="radio" name="torrent_downloader" id="torrent_downloader_transmission" value="1" ${config['torrent_downloader_transmission']}> Transmission <input type="radio" name="torrent_downloader" id="torrent_downloader_utorrent" value="2" ${config['torrent_downloader_utorrent']}> uTorrent (Beta)
|
||||
</fieldset>
|
||||
<fieldset id="torrent_blackhole_options">
|
||||
<div class="row">
|
||||
@@ -237,7 +237,11 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>uTorrent Password:</label>
|
||||
<input type="text" name="utorrent_password" value="${config['utorrent_pass']}" size="30">
|
||||
<input type="password" name="utorrent_password" value="${config['utorrent_pass']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>uTorrent Label:</label>
|
||||
<input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="general_torrent_options">
|
||||
@@ -293,7 +297,7 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Newznab</legend>
|
||||
<legend>Custom Newznab Providers</legend>
|
||||
<div class="row checkbox">
|
||||
<input id="usenewznab" type="checkbox" name="newznab" onclick="initConfigCheckbox($(this));" value="1" ${config['use_newznab']} /><label>Use Newznab</label>
|
||||
</div>
|
||||
@@ -466,7 +470,14 @@
|
||||
<div class="row radio clearfix">
|
||||
<input type="radio" name="preferred_quality" id="preferred_quality0" value="0" ${config['pref_qual_0']}><label>Highest Quality excluding Lossless</label>
|
||||
<input type="radio" name="preferred_quality" id="preferred_quality1" value="1" ${config['pref_qual_1']}><label>Highest Quality including Lossless</label>
|
||||
<input type="radio" name="preferred_quality" id="preferred_quality3" value="3" ${config['pref_qual_3']}><label>Lossless Only</label>
|
||||
<input type="radio" name="preferred_quality" id="lossless_only" value="3" ${config['pref_qual_3']}><label>Lossless Only</label>
|
||||
<div id="lossless_only_options">
|
||||
<span style="padding-left: 20px">
|
||||
Reject if target size is not in bitrate range: \
|
||||
<input type="text" class="override-float" name="lossless_bitrate_from" value="${config['lossless_bitrate_from']}" size="4" title="e.g. if album length = 40 mins, from = 350 kbps, then min target size = 102.5 mb, anything less will be rejected">to\
|
||||
<input type="text" class="override-float" name="lossless_bitrate_to" value="${config['lossless_bitrate_to']}" size="5" title="e.g. if album length = 40 mins, to = 2000 kbps, then max target size = 586 mb, anything greater will be rejected">kbps
|
||||
</span>
|
||||
</div>
|
||||
<input type="radio" id="preferred_bitrate" name="preferred_quality" value="2" ${config['pref_qual_2']}>Preferred Bitrate: <input type="text" class="override-float" name="preferred_bitrate" value="${config['pref_bitrate']}" size="3">kbps<br>
|
||||
<div id="preferred_bitrate_options">
|
||||
Reject if <strong>less than</strong> <input type="text" class="override-float" name="preferred_bitrate_low_buffer" value="${config['pref_bitrate_low']}" size="3">% or <strong>more than</strong> <input type="text" class="override-float" name="preferred_bitrate_high_buffer" value="${config['pref_bitrate_high']}" size="3">% of the target size (leave blank for no limit)<br><br>
|
||||
@@ -492,7 +503,7 @@
|
||||
<div class="row">
|
||||
<label>Preferred Words</label>
|
||||
<input type="text" name="preferred_words" value="${config['preferred_words']}" size="50">
|
||||
<small>Results with these words in the title will be preferred over results without them</small>
|
||||
<small>Results with these words in the title will be preferred over results without them (search provider names can also be entered)</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Required Words</label>
|
||||
@@ -678,9 +689,9 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<h3>LMS</h3>
|
||||
<h3>Logitech Media Server</h3>
|
||||
<div class="checkbox row">
|
||||
<input type="checkbox" name="lms_enabled" id="lms" value="1" ${config['lms_enabled']} /><label>Enable Squeezebox Updates</label>
|
||||
<input type="checkbox" name="lms_enabled" id="lms" value="1" ${config['lms_enabled']} /><label>Enable LMS Updates</label>
|
||||
</div>
|
||||
<div id="lmsoptions">
|
||||
<div class="row">
|
||||
@@ -690,8 +701,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<fieldset>
|
||||
<h3>Pushalot</h3>
|
||||
<div class="checkbox row">
|
||||
@@ -708,6 +718,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<fieldset>
|
||||
<h3>Synology NAS</h3>
|
||||
<div class="checkbox row">
|
||||
@@ -776,6 +790,47 @@
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<h3>OS X</h3>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="osx_notify_enabled" id="osx_notify" value="1" ${config['osx_notify_enabled']} /><label>Enable OS X Notifications</label>
|
||||
</div>
|
||||
<div id="osx_notify_options">
|
||||
<div class="row">
|
||||
<small>Enter the path/application name to be registered with the Notification Center, default is /Applications/Headphones</small>
|
||||
<input type="text" id="osx_notify_reg" name="osx_notify_app" value="${config['osx_notify_app']}" size="50"><label>Register Notify App</label>
|
||||
<input type="button" value="Register" id="osxnotifyregister">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="osx_notify_onsnatch" value="1" ${config['osx_notify_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<h3>Boxcar2</h3>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="boxcar_enabled" id="boxcar" value="1" ${config['boxcar_enabled']} /><label>Enable Boxcar2 Notifications</label>
|
||||
</div>
|
||||
<div id="boxcar_options">
|
||||
<div class="row">
|
||||
<label>Access Token</label><input type="text" name="boxcar_token" value="${config['boxcar_token']}" size="35">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="boxcar_onsnatch" value="1" ${config['boxcar_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<h3>MPC</h3>
|
||||
<div class="checkbox row">
|
||||
<input type="checkbox" name="mpc_enabled" id="mpc" value="1" ${config['mpc_enabled']} /><label>Enable MPC Update</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -1427,6 +1482,7 @@
|
||||
$("#pushbulletoptions").hide();
|
||||
}
|
||||
|
||||
|
||||
$("#pushbullet").click(function(){
|
||||
if ($("#pushbullet").is(":checked"))
|
||||
{
|
||||
@@ -1458,6 +1514,46 @@
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#osx_notify").is(":checked"))
|
||||
{
|
||||
$("#osx_notify_options").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#osx_notify_options").hide();
|
||||
}
|
||||
|
||||
$("#osx_notify").click(function(){
|
||||
if ($("#osx_notify").is(":checked"))
|
||||
{
|
||||
$("#osx_notify_options").slideDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#osx_notify_options").slideUp();
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#boxcar").is(":checked"))
|
||||
{
|
||||
$("#boxcar_options").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#boxcar_options").hide();
|
||||
}
|
||||
|
||||
$("#boxcar").click(function(){
|
||||
if ($("#boxcar").is(":checked"))
|
||||
{
|
||||
$("#boxcar_options").slideDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#boxcar_options").slideUp();
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#songkick").is(":checked"))
|
||||
{
|
||||
$("#songkickoptions").show();
|
||||
@@ -1498,6 +1594,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#lossless_only").is(":checked"))
|
||||
{
|
||||
$("#lossless_only_options").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#lossless_only_options").hide();
|
||||
}
|
||||
|
||||
if ($("#preferred_bitrate").is(":checked"))
|
||||
{
|
||||
$("#preferred_bitrate_options").show();
|
||||
@@ -1543,17 +1648,21 @@
|
||||
if ($("#preferred_bitrate").is(":checked"))
|
||||
{
|
||||
$("#preferred_bitrate_options").slideDown("fast");
|
||||
$("#lossless_only_options").slideUp("fast");
|
||||
}
|
||||
if ($("#preferred_quality0").is(":checked"))
|
||||
{
|
||||
$("#preferred_bitrate_options").slideUp("fast");
|
||||
$("#lossless_only_options").slideUp("fast");
|
||||
}
|
||||
if ($("#preferred_quality1").is(":checked"))
|
||||
{
|
||||
$("#preferred_bitrate_options").slideUp("fast");
|
||||
$("#lossless_only_options").slideUp("fast");
|
||||
}
|
||||
if ($("#preferred_quality3").is(":checked"))
|
||||
if ($("#lossless_only").is(":checked"))
|
||||
{
|
||||
$("#lossless_only_options").slideDown("fast");
|
||||
$("#preferred_bitrate_options").slideUp("fast");
|
||||
}
|
||||
if ($("#nzb_downloader_sabnzbd").is(":checked"))
|
||||
@@ -1671,6 +1780,12 @@
|
||||
function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$('#osxnotifyregister').click(function () {
|
||||
var osx_notify_app = $("#osx_notify_reg").val();
|
||||
$.get("/osxnotifyregister", {'app': osx_notify_app}, function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut()
|
||||
})
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
@@ -224,6 +224,14 @@ table.display tr.even.gradeA {
|
||||
background-color: #ddffdd;
|
||||
}
|
||||
|
||||
table.display tr.odd.gradeI {
|
||||
background-color: #bebebe;
|
||||
}
|
||||
|
||||
table.display tr.even.gradeI {
|
||||
background-color: #bebebe;
|
||||
}
|
||||
|
||||
table.display tr.odd.gradeC {
|
||||
background-color: #ebf5ff;
|
||||
}
|
||||
@@ -254,6 +262,13 @@ table.display tr.even.gradeU {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
table.display tr.odd.gradeW {
|
||||
background-color: #ffffaa;
|
||||
}
|
||||
|
||||
table.display tr.even.gradeW {
|
||||
background-color: #ffffaa;
|
||||
}
|
||||
|
||||
table.display tr.odd.gradeZ {
|
||||
background-color: #FAFAFA;
|
||||
@@ -269,6 +284,7 @@ table.display tr.gradeL #status {
|
||||
}
|
||||
table.display tr.gradeA td,
|
||||
table.display tr.gradeC td,
|
||||
table.display tr.gradeI td,
|
||||
table.display tr.gradeX td,
|
||||
table.display tr.gradeU td,
|
||||
table.display tr.gradeZ td {border-bottom: 1px solid #FFF;}
|
||||
|
||||
@@ -163,6 +163,14 @@ img.albumArt {
|
||||
max-height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
img.albumArt-nostretch {
|
||||
float: left;
|
||||
min-height: 64px;
|
||||
min-width: 64px;
|
||||
max-width: 250px;
|
||||
max-height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 10px;
|
||||
@@ -537,7 +545,7 @@ footer {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
}
|
||||
#ajaxMsg {
|
||||
.ajaxMsg {
|
||||
border: 1px solid #cccccc;
|
||||
background-image: -moz-linear-gradient(#ffffff, #eeeeee) !important;
|
||||
background-image: linear-gradient(#ffffff, #eeeeee) !important;
|
||||
@@ -568,16 +576,16 @@ footer {
|
||||
-o-opacity: 0.8 !important;
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
#ajaxMsg .msg {
|
||||
.ajaxMsg .msg {
|
||||
font-family: "Trebuchet MS", Helvetica, Arial, sans-serif;
|
||||
line-height: normal;
|
||||
padding-left: 20px;
|
||||
}
|
||||
#ajaxMsg .loader {
|
||||
.ajaxMsg .loader {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
#ajaxMsg.success {
|
||||
.ajaxMsg .success {
|
||||
background-image: -moz-linear-gradient(#d3ffd7, #c2edc6) !important;
|
||||
background-image: linear-gradient(#d3ffd7, #c2edc6) !important;
|
||||
background-image: -webkit-linear-gradient(#d3ffd7, #c2edc6) !important;
|
||||
@@ -587,7 +595,7 @@ footer {
|
||||
padding: 15px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
#ajaxMsg.error {
|
||||
.ajaxMsg .error {
|
||||
background-image: -moz-linear-gradient(#ffd3d3, #edc4c4) !important;
|
||||
background-image: linear-gradient(#ffd3d3, #edc4c4) !important;
|
||||
background-image: -webkit-linear-gradient(#ffd3d3, #edc4c4) !important;
|
||||
@@ -597,7 +605,7 @@ footer {
|
||||
padding: 15px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
#ajaxMsg .ui-icon {
|
||||
.ajaxMsg .ui-icon {
|
||||
display: inline-block;
|
||||
margin-left: -20px;
|
||||
top: 2px;
|
||||
|
||||
@@ -228,6 +228,21 @@ function showMsg(msg,loader,timeout,ms) {
|
||||
}
|
||||
}
|
||||
|
||||
function showArtistMsg(msg) {
|
||||
var feedback = $("#ajaxMsg2");
|
||||
update = $("#updatebar");
|
||||
if ( update.is(":visible") ) {
|
||||
var height = update.height() + 35;
|
||||
feedback.css("bottom",height + "px");
|
||||
} else {
|
||||
feedback.removeAttr("style");
|
||||
}
|
||||
feedback.fadeIn();
|
||||
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||
feedback.css("padding","14px 10px")
|
||||
$(feedback).prepend(message);
|
||||
}
|
||||
|
||||
function doAjaxCall(url,elem,reload,form) {
|
||||
// Set Message
|
||||
feedback = $("#ajaxMsg");
|
||||
@@ -316,6 +331,10 @@ function doAjaxCall(url,elem,reload,form) {
|
||||
}
|
||||
if ( reload == "tabs") refreshTab();
|
||||
if ( reload == "page") location.reload();
|
||||
if ( reload == "submenu&table") {
|
||||
refreshSubmenu();
|
||||
refreshTable();
|
||||
}
|
||||
if ( form ) {
|
||||
// Change the option to 'choose...'
|
||||
$(formID + " select").children('option[disabled=disabled]').attr('selected','selected');
|
||||
@@ -330,6 +349,10 @@ function doAjaxCall(url,elem,reload,form) {
|
||||
});
|
||||
}
|
||||
|
||||
function doSimpleAjaxCall(url) {
|
||||
$.ajax(url);
|
||||
}
|
||||
|
||||
function resetFilters(text){
|
||||
if ( $(".dataTables_filter").length > 0 ) {
|
||||
$(".dataTables_filter input").attr("placeholder","filter " + text + "");
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
lossless<%inherit file="base.html"/>
|
||||
<%inherit file="base.html"/>
|
||||
<%!
|
||||
from headphones import helpers
|
||||
%>
|
||||
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a class="menu_link_edit" href="clearLogs"><i class="fa fa-trash-o"></i> Clear log</a>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="body()">
|
||||
<div class="title">
|
||||
<div id="paddingheader">
|
||||
<h1 class="clearfix"><i class="fa fa-flag"></i> Logs</h1>
|
||||
</div>
|
||||
<table class="display" id="log_table">
|
||||
@@ -40,6 +48,8 @@ lossless<%inherit file="base.html"/>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
initActions();
|
||||
|
||||
$('#log_table').dataTable( {
|
||||
"bProcessing": true,
|
||||
"bServerSide": true,
|
||||
@@ -49,23 +59,26 @@ $(document).ready(function() {
|
||||
"iDisplayLength": 25,
|
||||
"bStateSave": true,
|
||||
"oLanguage": {
|
||||
"sSearch":"",
|
||||
"sSearch":"Filter:",
|
||||
"sLengthMenu":"Show _MENU_ lines per page",
|
||||
"sEmptyTable": "No log information available",
|
||||
"sInfo":"Showing _START_ to _END_ of _TOTAL_ lines",
|
||||
"sInfoEmpty":"Showing 0 to 0 of 0 lines",
|
||||
"sInfoFiltered":"(filtered from _MAX_ total lines)"},
|
||||
"fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
|
||||
if (aData[1] === "WARNING" || aData[1] === "ERROR")
|
||||
if (aData[1] === "ERROR")
|
||||
{
|
||||
$('td', nRow).closest('tr').addClass("gradeX");
|
||||
}
|
||||
else if (aData[1] === "WARNING")
|
||||
{
|
||||
$('td', nRow).closest('tr').addClass("gradeW");
|
||||
}
|
||||
else
|
||||
{
|
||||
$('td', nRow).closest('tr').addClass("gradeZ");
|
||||
}
|
||||
|
||||
|
||||
return nRow;
|
||||
},
|
||||
"fnServerData": function ( sSource, aoData, fnCallback ) {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<a href="manageAlbums?Status=Snatched"><i class="fa fa-cloud-download fa-fw"></i> </span>Manage Snatched Albums</a><br>
|
||||
<a href="manageAlbums?Status=Upcoming"><i class="fa fa-calendar fa-fw"></i> Manage Upcoming Albums</a><br>
|
||||
<a href="manageAlbums?Status=Wanted"><i class="fa fa-heart fa-fw"></i> </span>Manage Wanted Albums</a><br>
|
||||
<a href="manageAlbums?Status=Ignored"><i class="fa fa-meh-o fa-fw"></i> </span>Manage Ignored Albums</a><br>
|
||||
<br><br>
|
||||
<a href="manageAlbums">Manage All Albums</a>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<option value="WantedNew">Wanted (new only)</option>
|
||||
<option value="WantedLossless">Wanted (lossless)</option>
|
||||
<option value="Skipped">Skipped</option>
|
||||
<option value="Ignored">Ignored</option>
|
||||
<option value="Downloaded">Downloaded</option>
|
||||
</select>
|
||||
<input type="hidden" value="Go">
|
||||
@@ -50,6 +51,8 @@
|
||||
grade = 'Z'
|
||||
elif album['Status'] == 'Wanted':
|
||||
grade = 'X'
|
||||
elif album['Status'] == 'Ignored':
|
||||
grade = 'I'
|
||||
elif album['Status'] == 'Snatched':
|
||||
grade = 'C'
|
||||
else:
|
||||
|
||||
@@ -35,12 +35,12 @@
|
||||
%if type == 'album':
|
||||
<td id="albumname"><a href="${result['albumurl']}">${result['title']}</a></td>
|
||||
%endif
|
||||
<td id="artistname"><a href="${result['url']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
||||
<td id="artistname"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
||||
<td id="score"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></td>
|
||||
%if type == 'album':
|
||||
<td id="add"><a href="addReleaseById?rid=${result['albumid']}"><i class="fa fa-plus"></i> Add this album</a></td>
|
||||
%else:
|
||||
<td id="add"><a href="addArtist?artistid=${result['id']}"><i class="fa fa-plus"></i> Add this artist</a></td>
|
||||
<td id="add"><a href="${result['url']}"></i> View on MusicBrainz</a></td>
|
||||
%endif
|
||||
</tr>
|
||||
%endfor
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<select name="action" onChange="doAjaxCall('markAlbums',$(this),'table',true);" data-error="You didn't select any albums">
|
||||
<option disabled="disabled" selected="selected">Choose...</option>
|
||||
<option value="Skipped">Skipped</option>
|
||||
<option value="Ignored">Ignored</option>
|
||||
<option value="Downloaded">Downloaded</option>
|
||||
</select>
|
||||
<input type="hidden" value="Go">
|
||||
|
||||
+55
-15
@@ -15,20 +15,17 @@
|
||||
|
||||
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os, sys, subprocess
|
||||
|
||||
import threading
|
||||
import webbrowser
|
||||
import sqlite3
|
||||
import itertools
|
||||
import cherrypy
|
||||
|
||||
from lib.apscheduler.scheduler import Scheduler
|
||||
from lib.configobj import ConfigObj
|
||||
|
||||
import cherrypy
|
||||
|
||||
from headphones import versioncheck, logger, version
|
||||
from headphones.common import *
|
||||
|
||||
@@ -41,7 +38,8 @@ SIGNAL = None
|
||||
SYS_PLATFORM = None
|
||||
SYS_ENCODING = None
|
||||
|
||||
VERBOSE = 1
|
||||
QUIET = False
|
||||
VERBOSE = False
|
||||
DAEMON = False
|
||||
CREATEPID = False
|
||||
PIDFILE= None
|
||||
@@ -106,6 +104,8 @@ PREFERRED_BITRATE_HIGH_BUFFER = None
|
||||
PREFERRED_BITRATE_LOW_BUFFER = None
|
||||
PREFERRED_BITRATE_ALLOW_LOSSLESS = False
|
||||
DETECT_BITRATE = False
|
||||
LOSSLESS_BITRATE_FROM = None
|
||||
LOSSLESS_BITRATE_TO = None
|
||||
ADD_ARTISTS = False
|
||||
CORRECT_METADATA = False
|
||||
MOVE_FILES = False
|
||||
@@ -157,6 +157,7 @@ TRANSMISSION_PASSWORD = None
|
||||
UTORRENT_HOST = None
|
||||
UTORRENT_USERNAME = None
|
||||
UTORRENT_PASSWORD = None
|
||||
UTORRENT_LABEL = None
|
||||
|
||||
NEWZNAB = False
|
||||
NEWZNAB_HOST = None
|
||||
@@ -250,7 +251,7 @@ PLEX_UPDATE = False
|
||||
PLEX_NOTIFY = False
|
||||
NMA_ENABLED = False
|
||||
NMA_APIKEY = None
|
||||
NMA_PRIORITY = None
|
||||
NMA_PRIORITY = 0
|
||||
NMA_ONSNATCH = None
|
||||
PUSHALOT_ENABLED = False
|
||||
PUSHALOT_APIKEY = None
|
||||
@@ -270,6 +271,12 @@ TWITTER_ONSNATCH = False
|
||||
TWITTER_USERNAME = None
|
||||
TWITTER_PASSWORD = None
|
||||
TWITTER_PREFIX = None
|
||||
OSX_NOTIFY_ENABLED = False
|
||||
OSX_NOTIFY_ONSNATCH = False
|
||||
OSX_NOTIFY_APP = None
|
||||
BOXCAR_ENABLED = False
|
||||
BOXCAR_ONSNATCH = False
|
||||
BOXCAR_TOKEN = None
|
||||
MIRRORLIST = ["musicbrainz.org","headphones","custom"]
|
||||
MIRROR = None
|
||||
CUSTOMHOST = None
|
||||
@@ -281,12 +288,15 @@ SONGKICK_ENABLED = False
|
||||
SONGKICK_APIKEY = None
|
||||
SONGKICK_LOCATION = None
|
||||
SONGKICK_FILTER_ENABLED = False
|
||||
MPC_ENABLED = False
|
||||
|
||||
CACHE_SIZEMB = 32
|
||||
JOURNAL_MODE = None
|
||||
|
||||
UMASK = None
|
||||
|
||||
VERIFY_SSL_CERT = True
|
||||
|
||||
def CheckSection(sec):
|
||||
""" Check if INI section exists, if not create it """
|
||||
try:
|
||||
@@ -333,7 +343,7 @@ def initialize():
|
||||
|
||||
with INIT_LOCK:
|
||||
|
||||
global __INITIALIZED__, FULL_PATH, PROG_DIR, VERBOSE, DAEMON, SYS_PLATFORM, DATA_DIR, CONFIG_FILE, CFG, CONFIG_VERSION, LOG_DIR, CACHE_DIR, \
|
||||
global __INITIALIZED__, FULL_PATH, PROG_DIR, VERBOSE, QUIET, DAEMON, SYS_PLATFORM, DATA_DIR, CONFIG_FILE, CFG, CONFIG_VERSION, LOG_DIR, CACHE_DIR, \
|
||||
HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, HTTP_PROXY, LAUNCH_BROWSER, API_ENABLED, API_KEY, GIT_PATH, GIT_USER, GIT_BRANCH, DO_NOT_OVERRIDE_GIT_BRANCH, \
|
||||
CURRENT_VERSION, LATEST_VERSION, CHECK_GITHUB, CHECK_GITHUB_ON_STARTUP, CHECK_GITHUB_INTERVAL, MUSIC_DIR, DESTINATION_DIR, \
|
||||
LOSSLESS_DESTINATION_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, ADD_ARTISTS, CORRECT_METADATA, MOVE_FILES, \
|
||||
@@ -343,19 +353,19 @@ def initialize():
|
||||
RUTRACKER, RUTRACKER_USER, RUTRACKER_PASSWORD, WHATCD, WHATCD_USERNAME, WHATCD_PASSWORD, DOWNLOAD_TORRENT_DIR, \
|
||||
LIBRARYSCAN, LIBRARYSCAN_INTERVAL, DOWNLOAD_SCAN_INTERVAL, UPDATE_DB_INTERVAL, MB_IGNORE_AGE, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, \
|
||||
NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_HOST, HEADPHONES_INDEXER, NZBMATRIX, TRANSMISSION_HOST, TRANSMISSION_USERNAME, TRANSMISSION_PASSWORD, \
|
||||
UTORRENT_HOST, UTORRENT_USERNAME, UTORRENT_PASSWORD, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, NEWZNAB_ENABLED, EXTRA_NEWZNABS, \
|
||||
UTORRENT_HOST, UTORRENT_USERNAME, UTORRENT_PASSWORD, UTORRENT_LABEL, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, NEWZNAB_ENABLED, EXTRA_NEWZNABS, \
|
||||
NZBSORG, NZBSORG_UID, NZBSORG_HASH, NZBSRUS, NZBSRUS_UID, NZBSRUS_APIKEY, OMGWTFNZBS, OMGWTFNZBS_UID, OMGWTFNZBS_APIKEY, \
|
||||
NZB_DOWNLOADER, TORRENT_DOWNLOADER, PREFERRED_WORDS, REQUIRED_WORDS, IGNORED_WORDS, LASTFM_USERNAME, \
|
||||
INTERFACE, FOLDER_PERMISSIONS, FILE_PERMISSIONS, ENCODERFOLDER, ENCODER_PATH, ENCODER, XLDPROFILE, BITRATE, SAMPLINGFREQUENCY, \
|
||||
MUSIC_ENCODER, ADVANCEDENCODER, ENCODEROUTPUTFORMAT, ENCODERQUALITY, ENCODERVBRCBR, ENCODERLOSSLESS, ENCODER_MULTICORE, ENCODER_MULTICORE_COUNT, DELETE_LOSSLESS_FILES, \
|
||||
GROWL_ENABLED, GROWL_HOST, GROWL_PASSWORD, GROWL_ONSNATCH, PROWL_ENABLED, PROWL_PRIORITY, PROWL_KEYS, PROWL_ONSNATCH, PUSHOVER_ENABLED, PUSHOVER_PRIORITY, PUSHOVER_KEYS, PUSHOVER_ONSNATCH, PUSHOVER_APITOKEN, MIRRORLIST, \
|
||||
TWITTER_ENABLED, TWITTER_ONSNATCH, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, \
|
||||
TWITTER_ENABLED, TWITTER_ONSNATCH, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, OSX_NOTIFY_ENABLED, OSX_NOTIFY_ONSNATCH, OSX_NOTIFY_APP, BOXCAR_ENABLED, BOXCAR_ONSNATCH, BOXCAR_TOKEN, \
|
||||
PUSHBULLET_ENABLED, PUSHBULLET_APIKEY, PUSHBULLET_DEVICEID, PUSHBULLET_ONSNATCH, \
|
||||
MIRROR, CUSTOMHOST, CUSTOMPORT, CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, \
|
||||
XBMC_NOTIFY, LMS_ENABLED, LMS_HOST, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \
|
||||
PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \
|
||||
PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, LOSSLESS_BITRATE_FROM, LOSSLESS_BITRATE_TO, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \
|
||||
PLEX_ENABLED, PLEX_SERVER_HOST, PLEX_CLIENT_HOST, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE, PLEX_NOTIFY, PUSHALOT_ENABLED, PUSHALOT_APIKEY, \
|
||||
PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED
|
||||
PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED, VERIFY_SSL_CERT
|
||||
|
||||
|
||||
if __INITIALIZED__:
|
||||
@@ -386,6 +396,8 @@ def initialize():
|
||||
CheckSection('Pushalot')
|
||||
CheckSection('Synoindex')
|
||||
CheckSection('Twitter')
|
||||
CheckSection('OSX_Notify')
|
||||
CheckSection('Boxcar')
|
||||
CheckSection('Songkick')
|
||||
CheckSection('Advanced')
|
||||
|
||||
@@ -431,6 +443,8 @@ def initialize():
|
||||
PREFERRED_BITRATE_LOW_BUFFER = check_setting_int(CFG, 'General', 'preferred_bitrate_low_buffer', '')
|
||||
PREFERRED_BITRATE_ALLOW_LOSSLESS = bool(check_setting_int(CFG, 'General', 'preferred_bitrate_allow_lossless', 0))
|
||||
DETECT_BITRATE = bool(check_setting_int(CFG, 'General', 'detect_bitrate', 0))
|
||||
LOSSLESS_BITRATE_FROM = check_setting_int(CFG, 'General', 'lossless_bitrate_from', '')
|
||||
LOSSLESS_BITRATE_TO = check_setting_int(CFG, 'General', 'lossless_bitrate_to', '')
|
||||
ADD_ARTISTS = bool(check_setting_int(CFG, 'General', 'auto_add_artists', 1))
|
||||
CORRECT_METADATA = bool(check_setting_int(CFG, 'General', 'correct_metadata', 0))
|
||||
MOVE_FILES = bool(check_setting_int(CFG, 'General', 'move_files', 0))
|
||||
@@ -506,6 +520,7 @@ def initialize():
|
||||
UTORRENT_HOST = check_setting_str(CFG, 'uTorrent', 'utorrent_host', '')
|
||||
UTORRENT_USERNAME = check_setting_str(CFG, 'uTorrent', 'utorrent_username', '')
|
||||
UTORRENT_PASSWORD = check_setting_str(CFG, 'uTorrent', 'utorrent_password', '')
|
||||
UTORRENT_LABEL = check_setting_str(CFG, 'uTorrent', 'utorrent_label', '')
|
||||
|
||||
NEWZNAB = bool(check_setting_int(CFG, 'Newznab', 'newznab', 0))
|
||||
NEWZNAB_HOST = check_setting_str(CFG, 'Newznab', 'newznab_host', '')
|
||||
@@ -610,6 +625,14 @@ def initialize():
|
||||
TWITTER_PASSWORD = check_setting_str(CFG, 'Twitter', 'twitter_password', '')
|
||||
TWITTER_PREFIX = check_setting_str(CFG, 'Twitter', 'twitter_prefix', 'Headphones')
|
||||
|
||||
OSX_NOTIFY_ENABLED = bool(check_setting_int(CFG, 'OSX_Notify', 'osx_notify_enabled', 0))
|
||||
OSX_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'OSX_Notify', 'osx_notify_onsnatch', 0))
|
||||
OSX_NOTIFY_APP = check_setting_str(CFG, 'OSX_Notify', 'osx_notify_app', '/Applications/Headphones')
|
||||
|
||||
BOXCAR_ENABLED = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_enabled', 0))
|
||||
BOXCAR_ONSNATCH = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_onsnatch', 0))
|
||||
BOXCAR_TOKEN = check_setting_str(CFG, 'Boxcar', 'boxcar_token', '')
|
||||
|
||||
SONGKICK_ENABLED = bool(check_setting_int(CFG, 'Songkick', 'songkick_enabled', 1))
|
||||
SONGKICK_APIKEY = check_setting_str(CFG, 'Songkick', 'songkick_apikey', 'nd1We7dFW2RqxPw8')
|
||||
SONGKICK_LOCATION = check_setting_str(CFG, 'Songkick', 'songkick_location', '')
|
||||
@@ -627,6 +650,8 @@ def initialize():
|
||||
|
||||
ALBUM_COMPLETION_PCT = check_setting_int(CFG, 'Advanced', 'album_completion_pct', 80)
|
||||
|
||||
VERIFY_SSL_CERT = bool(check_setting_int(CFG, 'Advanced', 'verify_ssl_cert', 1))
|
||||
|
||||
# update folder formats in the config & bump up config version
|
||||
if CONFIG_VERSION == '0':
|
||||
from headphones.helpers import replace_all
|
||||
@@ -697,8 +722,9 @@ def initialize():
|
||||
if VERBOSE:
|
||||
sys.stderr.write('Unable to create the log directory. Logging to screen only.\n')
|
||||
|
||||
# Start the logger, silence console logging if we need to
|
||||
logger.initLogger(verbose=VERBOSE)
|
||||
# Start the logger, disable console if needed
|
||||
logger.initLogger(console=not QUIET, verbose=VERBOSE)
|
||||
logger.initLogger(console=not QUIET, verbose=False)
|
||||
|
||||
if not CACHE_DIR:
|
||||
# Put the cache dir in the data dir for now
|
||||
@@ -846,6 +872,8 @@ def config_write():
|
||||
new_config['General']['preferred_bitrate_low_buffer'] = PREFERRED_BITRATE_LOW_BUFFER
|
||||
new_config['General']['preferred_bitrate_allow_lossless'] = int(PREFERRED_BITRATE_ALLOW_LOSSLESS)
|
||||
new_config['General']['detect_bitrate'] = int(DETECT_BITRATE)
|
||||
new_config['General']['lossless_bitrate_from'] = LOSSLESS_BITRATE_FROM
|
||||
new_config['General']['lossless_bitrate_to'] = LOSSLESS_BITRATE_TO
|
||||
new_config['General']['auto_add_artists'] = int(ADD_ARTISTS)
|
||||
new_config['General']['correct_metadata'] = int(CORRECT_METADATA)
|
||||
new_config['General']['move_files'] = int(MOVE_FILES)
|
||||
@@ -928,6 +956,7 @@ def config_write():
|
||||
new_config['uTorrent']['utorrent_host'] = UTORRENT_HOST
|
||||
new_config['uTorrent']['utorrent_username'] = UTORRENT_USERNAME
|
||||
new_config['uTorrent']['utorrent_password'] = UTORRENT_PASSWORD
|
||||
new_config['uTorrent']['utorrent_label'] = UTORRENT_LABEL
|
||||
|
||||
new_config['Newznab'] = {}
|
||||
new_config['Newznab']['newznab'] = int(NEWZNAB)
|
||||
@@ -997,7 +1026,7 @@ def config_write():
|
||||
new_config['NMA'] = {}
|
||||
new_config['NMA']['nma_enabled'] = int(NMA_ENABLED)
|
||||
new_config['NMA']['nma_apikey'] = NMA_APIKEY
|
||||
new_config['NMA']['nma_priority'] = NMA_PRIORITY
|
||||
new_config['NMA']['nma_priority'] = int(NMA_PRIORITY)
|
||||
new_config['NMA']['nma_onsnatch'] = int(NMA_ONSNATCH)
|
||||
|
||||
new_config['Pushalot'] = {}
|
||||
@@ -1025,6 +1054,16 @@ def config_write():
|
||||
new_config['Twitter']['twitter_password'] = TWITTER_PASSWORD
|
||||
new_config['Twitter']['twitter_prefix'] = TWITTER_PREFIX
|
||||
|
||||
new_config['OSX_Notify'] = {}
|
||||
new_config['OSX_Notify']['osx_notify_enabled'] = int(OSX_NOTIFY_ENABLED)
|
||||
new_config['OSX_Notify']['osx_notify_onsnatch'] = int(OSX_NOTIFY_ONSNATCH)
|
||||
new_config['OSX_Notify']['osx_notify_app'] = OSX_NOTIFY_APP
|
||||
|
||||
new_config['Boxcar'] = {}
|
||||
new_config['Boxcar']['boxcar_enabled'] = int(BOXCAR_ENABLED)
|
||||
new_config['Boxcar']['boxcar_onsnatch'] = int(BOXCAR_ONSNATCH)
|
||||
new_config['Boxcar']['boxcar_token'] = BOXCAR_TOKEN
|
||||
|
||||
new_config['Songkick'] = {}
|
||||
new_config['Songkick']['songkick_enabled'] = int(SONGKICK_ENABLED)
|
||||
new_config['Songkick']['songkick_apikey'] = SONGKICK_APIKEY
|
||||
@@ -1065,6 +1104,7 @@ def config_write():
|
||||
new_config['Advanced']['album_completion_pct'] = ALBUM_COMPLETION_PCT
|
||||
new_config['Advanced']['cache_sizemb'] = CACHE_SIZEMB
|
||||
new_config['Advanced']['journal_mode'] = JOURNAL_MODE
|
||||
new_config['Advanced']['verify_ssl_cert'] = int(VERIFY_SSL_CERT)
|
||||
|
||||
new_config.write()
|
||||
|
||||
@@ -1093,7 +1133,7 @@ def start():
|
||||
started = True
|
||||
|
||||
def sig_handler(signum=None, frame=None):
|
||||
if type(signum) != type(None):
|
||||
if signum is not None:
|
||||
logger.info("Signal %i caught, saving and exiting...", signum)
|
||||
shutdown()
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ def switch(AlbumID, ReleaseID):
|
||||
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID])
|
||||
|
||||
# Update have track counts on index
|
||||
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [newalbumdata['ArtistID']]))
|
||||
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [newalbumdata['ArtistID']]))
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [newalbumdata['ArtistID']]))
|
||||
|
||||
controlValueDict = {"ArtistID": newalbumdata['ArtistID']}
|
||||
|
||||
+11
-11
@@ -274,7 +274,7 @@ def expand_subfolders(f):
|
||||
difference = max(path_depths) - min(path_depths)
|
||||
|
||||
if difference > 0:
|
||||
logger.info("Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder" % (len(media_folders), difference))
|
||||
logger.info("Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder.", len(media_folders), difference)
|
||||
|
||||
# While already failed, advice the user what he could try. We assume the
|
||||
# directory may contain separate CD's and maybe some extra's. The
|
||||
@@ -283,7 +283,7 @@ def expand_subfolders(f):
|
||||
extra_media_folders = [ media_folder[:min(path_depths)] for media_folder in media_folders if len(media_folder) > min(path_depths) ]
|
||||
extra_media_folders = list(set([ os.path.join(*media_folder) for media_folder in extra_media_folders ]))
|
||||
|
||||
logger.info("Please look at the following folder(s), since they cause the depth difference: %s" % extra_media_folders)
|
||||
logger.info("Please look at the following folder(s), since they cause the depth difference: %s", extra_media_folders)
|
||||
return
|
||||
|
||||
# Convert back to paths and remove duplicates, which may be there after
|
||||
@@ -297,7 +297,7 @@ def expand_subfolders(f):
|
||||
logger.debug("Did not expand subfolder, as it resulted in one folder.")
|
||||
return
|
||||
|
||||
logger.debug("Expanded subfolders in folder: " % media_folders)
|
||||
logger.debug("Expanded subfolders in folder: %s", media_folders)
|
||||
return media_folders
|
||||
|
||||
def extract_data(s):
|
||||
@@ -360,7 +360,7 @@ def extract_metadata(f):
|
||||
# Try to read the file info
|
||||
try:
|
||||
media_file = MediaFile(os.path.join(root, file))
|
||||
except FileTypeError, UnreadableFileError:
|
||||
except (FileTypeError, UnreadableFileError):
|
||||
# Probably not a media file
|
||||
continue
|
||||
|
||||
@@ -374,14 +374,14 @@ def extract_metadata(f):
|
||||
|
||||
# Verify results
|
||||
if len(results) == 0:
|
||||
logger.info("No metadata in media files found, ignoring")
|
||||
logger.info("No metadata in media files found, ignoring.")
|
||||
return (None, None, None)
|
||||
|
||||
# Require that some percentage of files have tags
|
||||
count_ratio = 0.75
|
||||
|
||||
if count < (count_ratio * len(results)):
|
||||
logger.info("Counted %d media files, but only %d have tags, ignoring" % (count, len(results)))
|
||||
logger.info("Counted %d media files, but only %d have tags, ignoring.", count, len(results))
|
||||
return (None, None, None)
|
||||
|
||||
# Count distinct values
|
||||
@@ -399,7 +399,7 @@ def extract_metadata(f):
|
||||
old_album = new_albums[index]
|
||||
new_albums[index] = RE_CD_ALBUM.sub("", album).strip()
|
||||
|
||||
logger.debug("Stripped albumd number identifier: %s -> %s" % (old_album, new_albums[index]))
|
||||
logger.debug("Stripped albumd number identifier: %s -> %s", old_album, new_albums[index])
|
||||
|
||||
# Remove duplicates
|
||||
new_albums = list(set(new_albums))
|
||||
@@ -417,7 +417,7 @@ def extract_metadata(f):
|
||||
if len(artists) > 1 and len(albums) == 1:
|
||||
split_artists = [ RE_FEATURING.split(artist) for artist in artists ]
|
||||
featurings = [ len(split_artist) - 1 for split_artist in split_artists ]
|
||||
logger.info("Album seem to feature %d different artists" % sum(featurings))
|
||||
logger.info("Album seem to feature %d different artists", sum(featurings))
|
||||
|
||||
if sum(featurings) > 0:
|
||||
# Find the artist of which the least splits have been generated.
|
||||
@@ -429,8 +429,8 @@ def extract_metadata(f):
|
||||
return (artist, albums[0], years[0])
|
||||
|
||||
# Not sure what to do here.
|
||||
logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring" % (len(artists), len(albums), len(years)))
|
||||
logger.debug("Artists: %s, Albums: %s, Years: %s" % (artists, albums, years))
|
||||
logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring", len(artists), len(albums), len(years))
|
||||
logger.debug("Artists: %s, Albums: %s, Years: %s", artists, albums, years)
|
||||
|
||||
return (None, None, None)
|
||||
|
||||
@@ -486,7 +486,7 @@ def smartMove(src, dest, delete=True):
|
||||
filename = os.path.basename(src)
|
||||
|
||||
if os.path.isfile(os.path.join(dest, filename)):
|
||||
logger.info('Destination file exists: %s', os.path.join(dest, filename).decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info('Destination file exists: %s', os.path.join(dest, filename))
|
||||
title = os.path.splitext(filename)[0]
|
||||
ext = os.path.splitext(filename)[1]
|
||||
i = 1
|
||||
|
||||
+60
-43
@@ -210,6 +210,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [items['AlbumID']])
|
||||
logger.info("[%s] Removing all references to release group %s to reflect MusicBrainz" % (artist['artist_name'], items['AlbumID']))
|
||||
force_repackage = 1
|
||||
elif extrasonly:
|
||||
# Not really sure what we're doing here but don't want to log the message below if we're fetching extras only
|
||||
pass
|
||||
else:
|
||||
logger.info("[%s] There was either an error pulling data from MusicBrainz or there might not be any releases for this category" % artist['artist_name'])
|
||||
|
||||
@@ -224,25 +227,36 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
#Make a user configurable variable to skip update of albums with release dates older than this date (in days)
|
||||
pause_delta = headphones.MB_IGNORE_AGE
|
||||
|
||||
check_release_date = myDB.action("SELECT ReleaseDate, Status from albums WHERE ArtistID=? AND AlbumTitle=?", (artistid, al_title)).fetchone()
|
||||
|
||||
#Skip update if Status set
|
||||
if check_release_date and check_release_date[1]:
|
||||
logger.info("[%s] Not updating: %s (Status is %s, skipping)" % (artist['artist_name'], rg['title'], check_release_date[1]))
|
||||
continue
|
||||
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
|
||||
|
||||
if not forcefull:
|
||||
if check_release_date:
|
||||
if check_release_date[0] is None:
|
||||
|
||||
new_release_group = False
|
||||
|
||||
try:
|
||||
check_release_date = rg_exists['ReleaseDate']
|
||||
except TypeError:
|
||||
check_release_date = None
|
||||
new_release_group = True
|
||||
|
||||
|
||||
if new_release_group:
|
||||
|
||||
logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title']))
|
||||
new_releases = mb.get_new_releases(rgid,includeExtras)
|
||||
|
||||
else:
|
||||
|
||||
if check_release_date is None or check_release_date == u"None":
|
||||
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
|
||||
new_releases = mb.get_new_releases(rgid,includeExtras,True)
|
||||
else:
|
||||
if len(check_release_date[0]) == 10:
|
||||
release_date = check_release_date[0]
|
||||
elif len(check_release_date[0]) == 7:
|
||||
release_date = check_release_date[0]+"-31"
|
||||
elif len(check_release_date[0]) == 4:
|
||||
release_date = check_release_date[0]+"-12-31"
|
||||
if len(check_release_date) == 10:
|
||||
release_date = check_release_date
|
||||
elif len(check_release_date) == 7:
|
||||
release_date = check_release_date+"-31"
|
||||
elif len(check_release_date) == 4:
|
||||
release_date = check_release_date+"-12-31"
|
||||
else:
|
||||
release_date = today
|
||||
if helpers.get_age(today) - helpers.get_age(release_date) < pause_delta:
|
||||
@@ -252,9 +266,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
logger.info("[%s] Skipping: %s (Release Date >%s Days)" % (artist['artist_name'], rg['title'], pause_delta))
|
||||
skip_log = 1
|
||||
new_releases = 0
|
||||
else:
|
||||
logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title']))
|
||||
new_releases = mb.get_new_releases(rgid,includeExtras)
|
||||
|
||||
if force_repackage == 1:
|
||||
new_releases = -1
|
||||
@@ -265,11 +276,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
logger.info("[%s] Now adding/updating: %s (Comprehensive Force)" % (artist['artist_name'], rg['title']))
|
||||
new_releases = mb.get_new_releases(rgid,includeExtras,forcefull)
|
||||
|
||||
#What this does is adds new releases per artist to the allalbums + alltracks databases
|
||||
#new_releases = mb.get_new_releases(rgid,includeExtras)
|
||||
#print al_title
|
||||
#print new_releases
|
||||
|
||||
if new_releases != 0:
|
||||
#Dump existing hybrid release since we're repackaging/replacing it
|
||||
myDB.action("DELETE from albums WHERE ReleaseID=?", [rg['id']])
|
||||
@@ -378,7 +384,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
# If there's no release in the main albums tables, add the default (hybrid)
|
||||
# If there is a release, check the ReleaseID against the AlbumID to see if they differ (user updated)
|
||||
# check if the album already exists
|
||||
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
|
||||
|
||||
if not rg_exists:
|
||||
releaseid = rg['id']
|
||||
else:
|
||||
@@ -399,8 +405,11 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
"ReleaseFormat": album['ReleaseFormat']
|
||||
}
|
||||
|
||||
if not rg_exists:
|
||||
if rg_exists:
|
||||
newValueDict['DateAdded'] = rg_exists['DateAdded']
|
||||
newValueDict['Status'] = rg_exists['Status']
|
||||
|
||||
else:
|
||||
today = helpers.today()
|
||||
|
||||
newValueDict['DateAdded'] = today
|
||||
@@ -469,10 +478,34 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
if skip_log == 0:
|
||||
logger.info(u"[%s] No new releases, so no changes made to %s" % (artist['artist_name'], rg['title']))
|
||||
|
||||
finalize_update(artistid, artist['artist_name'], errors)
|
||||
|
||||
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
|
||||
cache.getThumb(ArtistID=artistid)
|
||||
|
||||
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']))
|
||||
else:
|
||||
myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']])
|
||||
logger.info(u"Updating complete for: %s" % artist['artist_name'])
|
||||
|
||||
# Start searching for newly added albums
|
||||
if album_searches:
|
||||
from headphones import searcher
|
||||
logger.info("Start searching for %d albums.", len(album_searches))
|
||||
|
||||
for album_search in album_searches:
|
||||
searcher.searchforalbum(albumid=album_search)
|
||||
|
||||
def finalize_update(artistid, artistname, errors=False):
|
||||
# Moving this little bit to it's own function so we can update have tracks & latest album when deleting extras
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
latestalbum = myDB.action('SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC', [artistid]).fetchone()
|
||||
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid]))
|
||||
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [artistid]))
|
||||
#havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['artist_name']]))
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist['artist_name']]))
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artistname]))
|
||||
|
||||
controlValueDict = {"ArtistID": artistid}
|
||||
|
||||
@@ -493,23 +526,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
|
||||
cache.getThumb(ArtistID=artistid)
|
||||
|
||||
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']))
|
||||
else:
|
||||
myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']])
|
||||
logger.info(u"Updating complete for: %s" % artist['artist_name'])
|
||||
|
||||
# Start searching for newly added albums
|
||||
if album_searches:
|
||||
from headphones import searcher
|
||||
logger.info("Start searching for %d albums.", len(album_searches))
|
||||
|
||||
for album_search in album_searches:
|
||||
searcher.searchforalbum(albumid=album_search)
|
||||
|
||||
def addReleaseById(rid):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
@@ -573,6 +589,7 @@ def addReleaseById(rid):
|
||||
controlValueDict = {"AlbumID": rgid}
|
||||
|
||||
newValueDict = {"ArtistID": release_dict['artist_id'],
|
||||
"ReleaseID": rgid,
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
"AlbumTitle": release_dict['rg_title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
|
||||
@@ -328,8 +328,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [ArtistID])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [ArtistName]))
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
|
||||
|
||||
update_album_status()
|
||||
if not append:
|
||||
update_album_status()
|
||||
lastfm.getSimilar()
|
||||
logger.info('Library scan complete')
|
||||
|
||||
@@ -355,13 +355,21 @@ 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.ALBUM_COMPLETION_PCT:
|
||||
if album_completion >= headphones.ALBUM_COMPLETION_PCT and album['Status'] == 'Skipped':
|
||||
new_album_status = "Downloaded"
|
||||
else:
|
||||
if album['Status'] == "Skipped" or album['Status'] == "Downloaded":
|
||||
new_album_status = "Skipped"
|
||||
|
||||
# I don't think we want to change Downloaded->Skipped.....
|
||||
# I think we can only automatically change Skipped->Downloaded when updating
|
||||
# There was a bug report where this was causing infinite downloads if the album was
|
||||
# recent, but matched to less than 80%. It would go Downloaded->Skipped->Wanted->Downloaded->Skipped->Wanted->etc....
|
||||
#else:
|
||||
# if album['Status'] == "Skipped" or album['Status'] == "Downloaded":
|
||||
# new_album_status = "Skipped"
|
||||
# else:
|
||||
# new_album_status = album['Status']
|
||||
else:
|
||||
new_album_status = album['Status']
|
||||
|
||||
myDB.upsert("albums", {'Status' : new_album_status}, {'AlbumID' : album['AlbumID']})
|
||||
if new_album_status != album['Status']:
|
||||
logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status))
|
||||
|
||||
+18
-9
@@ -43,19 +43,32 @@ class LogListHandler(logging.Handler):
|
||||
|
||||
headphones.LOG_LIST.insert(0, (helpers.now(), message, record.levelname, record.threadName))
|
||||
|
||||
def initLogger(verbose=1):
|
||||
def initLogger(console=False, verbose=False):
|
||||
"""
|
||||
Setup logging for Headphones. It uses the logger instance with the name
|
||||
'headphones'. Three log handlers are added:
|
||||
|
||||
* RotatingFileHandler: for the file headphones.log
|
||||
* LogListHandler: for Web UI
|
||||
* StreamHandler: for console (if verbose > 0)
|
||||
* StreamHandler: for console (if console)
|
||||
|
||||
Console logging is only enabled if console is set to True.
|
||||
"""
|
||||
|
||||
# Close and remove old handlers. This is required to reinit the loggers
|
||||
# at runtime
|
||||
for handler in logger.handlers[:]:
|
||||
# Just make sure it is cleaned up.
|
||||
if isinstance(handler, handlers.RotatingFileHandler):
|
||||
handler.close()
|
||||
elif isinstance(handler, logging.StreamHandler):
|
||||
handler.flush()
|
||||
|
||||
logger.removeHandler(handler)
|
||||
|
||||
# Configure the logger to accept all messages
|
||||
logger.propagate = False
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
||||
|
||||
# Setup file logger
|
||||
filename = os.path.join(headphones.LOG_DIR, FILENAME)
|
||||
@@ -69,19 +82,15 @@ def initLogger(verbose=1):
|
||||
|
||||
# Add list logger
|
||||
loglist_handler = LogListHandler()
|
||||
loglist_handler.setLevel(logging.INFO)
|
||||
loglist_handler.setLevel(logging.DEBUG)
|
||||
|
||||
logger.addHandler(loglist_handler)
|
||||
|
||||
# Setup console logger
|
||||
if verbose:
|
||||
if console:
|
||||
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(console_formatter)
|
||||
|
||||
if verbose == 1:
|
||||
console_handler.setLevel(logging.INFO)
|
||||
elif verbose == 2:
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
+8
-8
@@ -13,8 +13,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import time
|
||||
import threading
|
||||
|
||||
@@ -200,7 +198,7 @@ def getArtist(artistid, extrasonly=False):
|
||||
|
||||
if not extrasonly:
|
||||
for rg in artist['release-group-list']:
|
||||
if rg['type'] != 'Album': #only add releases without a secondary type
|
||||
if "secondary-type-list" in rg.keys(): #only add releases without a secondary type
|
||||
continue
|
||||
releasegroups.append({
|
||||
'title': unicode(rg['title']),
|
||||
@@ -234,17 +232,19 @@ def getArtist(artistid, extrasonly=False):
|
||||
|
||||
for include in includes:
|
||||
|
||||
artist = None
|
||||
mb_extras_list = []
|
||||
|
||||
try:
|
||||
artist = musicbrainzngs.get_artist_by_id(artistid,includes=["releases","release-groups"],release_status=['official'],release_type=include)['artist']
|
||||
limit = 200
|
||||
newRgs = None
|
||||
while newRgs == None or len(newRgs) >= limit:
|
||||
newRgs = musicbrainzngs.browse_release_groups(artistid,release_type=include,offset=len(mb_extras_list),limit=limit)['release-group-list']
|
||||
mb_extras_list += newRgs
|
||||
except WebServiceError, e:
|
||||
logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e)))
|
||||
time.sleep(5)
|
||||
|
||||
if not artist:
|
||||
continue
|
||||
for rg in artist['release-group-list']:
|
||||
for rg in mb_extras_list:
|
||||
releasegroups.append({
|
||||
'title': unicode(rg['title']),
|
||||
'id': unicode(rg['id']),
|
||||
|
||||
+133
-34
@@ -22,11 +22,12 @@ import simplejson
|
||||
import os.path
|
||||
import subprocess
|
||||
import gntp.notifier
|
||||
import time
|
||||
|
||||
from xml.dom import minidom
|
||||
from httplib import HTTPSConnection
|
||||
from urllib import urlencode
|
||||
|
||||
from lib.pynma import pynma
|
||||
import lib.oauth2 as oauth
|
||||
import lib.pythontwitter as twitter
|
||||
|
||||
@@ -168,6 +169,17 @@ class PROWL:
|
||||
|
||||
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
|
||||
|
||||
class MPC:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
pass
|
||||
|
||||
def notify( self ):
|
||||
|
||||
subprocess.call( ["mpc", "update"] )
|
||||
|
||||
|
||||
class XBMC:
|
||||
|
||||
def __init__(self):
|
||||
@@ -191,9 +203,9 @@ class XBMC:
|
||||
url = host + '/jsonrpc'
|
||||
|
||||
if self.password:
|
||||
response = request.request_json(url, method="POST", data=simplejson.dumps(data), headers=headers, auth=(self.username, self.password))
|
||||
response = request.request_json(url, method="post", data=simplejson.dumps(data), headers=headers, auth=(self.username, self.password))
|
||||
else:
|
||||
response = request.request_json(url, method="POST", data=simplejson.dumps(data), headers=headers)
|
||||
response = request.request_json(url, method="post", data=simplejson.dumps(data), headers=headers)
|
||||
|
||||
if response:
|
||||
return response[0]['result']
|
||||
@@ -238,6 +250,7 @@ class XBMC:
|
||||
|
||||
except:
|
||||
logger.warn('Error sending notification request to XBMC')
|
||||
|
||||
class LMS:
|
||||
|
||||
#Class for updating a Logitech Media Server
|
||||
@@ -247,10 +260,10 @@ class LMS:
|
||||
self.hosts = headphones.LMS_HOST
|
||||
|
||||
def _sendjson(self, host):
|
||||
data = {'id': 1, 'method': 'slim.request', 'params': ["",["rescan"]]} #Had a lot of trouble with simplejson, but this works.
|
||||
data = {'id': 1, 'method': 'slim.request', 'params': ["",["rescan"]]}
|
||||
data = simplejson.JSONEncoder().encode(data)
|
||||
|
||||
content = {'Content-Type': 'application/json', 'Content-Length': len(data)}
|
||||
content = {'Content-Type': 'application/json'}
|
||||
|
||||
req = urllib2.Request(host+'/jsonrpc.js', data, content)
|
||||
|
||||
@@ -261,20 +274,15 @@ class LMS:
|
||||
return
|
||||
|
||||
response = simplejson.JSONDecoder().decode(handle.read())
|
||||
server_result = simplejson.dumps(response)
|
||||
|
||||
try:
|
||||
return response[0]['result']
|
||||
return response['result']
|
||||
except:
|
||||
logger.warn('LMS returned error: %s' % response[0]['error'])
|
||||
logger.warn('LMS returned error: %s' % response['error'])
|
||||
return
|
||||
|
||||
def update(self):
|
||||
|
||||
#Send the ["rescan"] command to an LMS server.
|
||||
#Note that the command must be prefixed with the 'player' that the command is aimed at,
|
||||
#But with this being a request for the server to update its library, the player is blank, so ""
|
||||
|
||||
hosts = [x.strip() for x in self.hosts.split(',')]
|
||||
|
||||
for host in hosts:
|
||||
@@ -372,34 +380,40 @@ class Plex:
|
||||
logger.warn('Error sending notification request to Plex Media Server')
|
||||
|
||||
class NMA:
|
||||
def notify(self, artist=None, album=None, snatched=None):
|
||||
title = 'Headphones'
|
||||
api = headphones.NMA_APIKEY
|
||||
nma_priority = headphones.NMA_PRIORITY
|
||||
|
||||
def __init__(self):
|
||||
logger.debug(u"NMA title: " + title)
|
||||
logger.debug(u"NMA API: " + api)
|
||||
logger.debug(u"NMA Priority: " + str(nma_priority))
|
||||
|
||||
self.apikey = headphones.NMA_APIKEY
|
||||
self.priority = headphones.NMA_PRIORITY
|
||||
|
||||
def _send(self, data):
|
||||
return request.request_content('https://www.notifymyandroid.com/publicapi/notify', data=data)
|
||||
|
||||
def notify(self, artist=None, album=None, snatched_nzb=None):
|
||||
|
||||
apikey = self.apikey
|
||||
priority = self.priority
|
||||
|
||||
if snatched_nzb:
|
||||
event = snatched_nzb + " snatched!"
|
||||
description = "Headphones has snatched: " + snatched_nzb + " and has sent it to SABnzbd+"
|
||||
if snatched:
|
||||
event = snatched + " snatched!"
|
||||
message = "Headphones has snatched: " + snatched
|
||||
else:
|
||||
event = artist + ' - ' + album + ' complete!'
|
||||
description = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']'
|
||||
message = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']'
|
||||
|
||||
data = { 'apikey': apikey, 'application':'Headphones', 'event': event, 'description': description, 'priority': priority}
|
||||
logger.debug(u"NMA event: " + event)
|
||||
logger.debug(u"NMA message: " + message)
|
||||
|
||||
logger.info('Sending notification request to NotifyMyAndroid')
|
||||
request = self._send(data)
|
||||
batch = False
|
||||
|
||||
if not request:
|
||||
logger.warn('Error sending notification request to NotifyMyAndroid')
|
||||
p = pynma.PyNMA()
|
||||
keys = api.split(',')
|
||||
p.addkey(keys)
|
||||
|
||||
if len(keys) > 1: batch = True
|
||||
|
||||
response = p.push(title, event, message, priority=nma_priority, batch_mode=batch)
|
||||
|
||||
if not response[api][u'code'] == u'200':
|
||||
logger.error(u'Could not send notification to NotifyMyAndroid')
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
class PUSHBULLET:
|
||||
|
||||
@@ -454,7 +468,6 @@ class PUSHBULLET:
|
||||
|
||||
self.notify('Main Screen Activate', 'Test Message')
|
||||
|
||||
|
||||
class PUSHALOT:
|
||||
|
||||
def notify(self, message, event):
|
||||
@@ -529,6 +542,7 @@ class Synoindex:
|
||||
if isinstance(path_list, list):
|
||||
for path in path_list:
|
||||
self.notify(path)
|
||||
|
||||
class PUSHOVER:
|
||||
|
||||
application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz"
|
||||
@@ -694,3 +708,88 @@ class TwitterNotifier:
|
||||
return self._send_tweet(prefix+": "+message)
|
||||
|
||||
notifier = TwitterNotifier
|
||||
|
||||
class OSX_NOTIFY:
|
||||
|
||||
objc = None
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
self.objc = __import__("objc")
|
||||
except:
|
||||
return False
|
||||
|
||||
def swizzle(self, cls, SEL, func):
|
||||
old_IMP = cls.instanceMethodForSelector_(SEL)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
return func(self, old_IMP, *args, **kwargs)
|
||||
new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector,
|
||||
signature=old_IMP.signature)
|
||||
self.objc.classAddMethod(cls, SEL, new_IMP)
|
||||
|
||||
def notify(self, title, subtitle=None, text=None, sound=True):
|
||||
|
||||
try:
|
||||
self.swizzle(self.objc.lookUpClass('NSBundle'),
|
||||
b'bundleIdentifier',
|
||||
self.swizzled_bundleIdentifier)
|
||||
|
||||
NSUserNotification = self.objc.lookUpClass('NSUserNotification')
|
||||
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter')
|
||||
NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')
|
||||
|
||||
if not NSUserNotification or not NSUserNotificationCenter:
|
||||
return False
|
||||
|
||||
pool = NSAutoreleasePool.alloc().init()
|
||||
|
||||
notification = NSUserNotification.alloc().init()
|
||||
notification.setTitle_(title)
|
||||
if subtitle:
|
||||
notification.setSubtitle_(subtitle)
|
||||
if text:
|
||||
notification.setInformativeText_(text)
|
||||
if sound:
|
||||
notification.setSoundName_("NSUserNotificationDefaultSoundName")
|
||||
notification.setHasActionButton_(False)
|
||||
|
||||
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
||||
notification_center.deliverNotification_(notification)
|
||||
|
||||
del pool
|
||||
return True
|
||||
|
||||
except Exception, e:
|
||||
logger.warn('Error sending OS X Notification: %s' % e)
|
||||
return False
|
||||
|
||||
def swizzled_bundleIdentifier(self, original, swizzled):
|
||||
return 'ade.headphones.osxnotify'
|
||||
|
||||
class BOXCAR:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.url = 'https://new.boxcar.io/api/notifications'
|
||||
|
||||
def notify(self, title, message, rgid=None):
|
||||
|
||||
try:
|
||||
if rgid:
|
||||
message += '<br></br><a href="http://musicbrainz.org/release-group/%s">MusicBrainz</a>' % rgid
|
||||
|
||||
data = urllib.urlencode({
|
||||
'user_credentials': headphones.BOXCAR_TOKEN,
|
||||
'notification[title]': title.encode('utf-8'),
|
||||
'notification[long_message]': message.encode('utf-8'),
|
||||
'notification[sound]': "done"
|
||||
})
|
||||
|
||||
req = urllib2.Request(self.url)
|
||||
handle = urllib2.urlopen(req, data)
|
||||
handle.close()
|
||||
return True
|
||||
|
||||
except urllib2.URLError, e:
|
||||
logger.warn('Error sending Boxcar2 Notification: %s' % e)
|
||||
return False
|
||||
|
||||
+86
-64
@@ -13,22 +13,20 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
import music_encoder
|
||||
import shutil, re
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from headphones import notifiers
|
||||
import beets
|
||||
from beets import autotag
|
||||
from beets.mediafile import MediaFile
|
||||
|
||||
import threading
|
||||
import headphones
|
||||
from headphones import db, albumart, librarysync, lyrics, logger, helpers, request
|
||||
from headphones.helpers import sab_replace_dots, sab_replace_spaces
|
||||
|
||||
from beets import autotag
|
||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||
|
||||
from headphones import notifiers
|
||||
from headphones import db, albumart, librarysync, lyrics
|
||||
from headphones import logger, helpers, request, mb, music_encoder
|
||||
|
||||
postprocessor_lock = threading.Lock()
|
||||
|
||||
@@ -44,30 +42,18 @@ def checkFolder():
|
||||
if album['FolderName']:
|
||||
|
||||
if album['Kind'] == 'nzb':
|
||||
# We're now checking sab config options after sending to determine renaming - but we'll keep the
|
||||
# iterations in just in case we can't read the config for some reason
|
||||
download_dir = headphones.DOWNLOAD_DIR
|
||||
else:
|
||||
download_dir = headphones.DOWNLOAD_TORRENT_DIR
|
||||
|
||||
nzb_album_possibilities = [ album['FolderName'],
|
||||
sab_replace_dots(album['FolderName']),
|
||||
sab_replace_spaces(album['FolderName']),
|
||||
sab_replace_spaces(sab_replace_dots(album['FolderName']))
|
||||
]
|
||||
album_path = os.path.join(download_dir, album['FolderName']).encode(headphones.SYS_ENCODING,'replace')
|
||||
logger.info("Checking if %s exists" % album_path)
|
||||
if os.path.exists(album_path):
|
||||
logger.info('Found "' + album['FolderName'] + '" in ' + album['Kind'] + ' download folder. Verifying....')
|
||||
verify(album['AlbumID'], album_path, album['Kind'])
|
||||
|
||||
for nzb_folder_name in nzb_album_possibilities:
|
||||
|
||||
nzb_album_path = os.path.join(headphones.DOWNLOAD_DIR, nzb_folder_name).encode(headphones.SYS_ENCODING, 'replace')
|
||||
|
||||
if os.path.exists(nzb_album_path):
|
||||
logger.debug('Found %s in NZB download folder. Verifying....' % album['FolderName'])
|
||||
verify(album['AlbumID'], nzb_album_path, 'nzb')
|
||||
|
||||
if album['Kind'] == 'torrent':
|
||||
|
||||
torrent_album_path = os.path.join(headphones.DOWNLOAD_TORRENT_DIR, album['FolderName']).encode(headphones.SYS_ENCODING,'replace')
|
||||
|
||||
if os.path.exists(torrent_album_path):
|
||||
logger.debug('Found %s in torrent download folder. Verifying....' % album['FolderName'])
|
||||
verify(album['AlbumID'], torrent_album_path, 'torrent')
|
||||
else:
|
||||
logger.info("No folder name found for " + album['Title'])
|
||||
|
||||
def verify(albumid, albumpath, Kind=None, forced=False):
|
||||
|
||||
@@ -80,18 +66,16 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
||||
#from an RSS feed or etc
|
||||
#TODO: This should be a call to a class method.. copied it out of importer with only minor changes
|
||||
#TODO: odd things can happen when there are diacritic characters in the folder name, need to translate them?
|
||||
import mb
|
||||
|
||||
release_list = None
|
||||
|
||||
try:
|
||||
release_list = mb.getReleaseGroup(albumid)
|
||||
except Exception, e:
|
||||
logger.info('Unable to get release information for manual album with rgid: %s. Error: %s' % (albumid, e))
|
||||
logger.error('Unable to get release information for manual album with rgid: %s. Error: %s' % (albumid, e))
|
||||
return
|
||||
|
||||
if not release_list:
|
||||
logger.info('Unable to get release information for manual album with rgid: %s' % albumid)
|
||||
logger.error('Unable to get release information for manual album with rgid: %s' % albumid)
|
||||
return
|
||||
|
||||
# Since we're just using this to create the bare minimum information to insert an artist/album combo, use the first release
|
||||
@@ -99,6 +83,10 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
||||
|
||||
release_dict = mb.getRelease(releaseid)
|
||||
|
||||
if not release_dict:
|
||||
logger.error('Unable to get release information for manual album with rgid: %s. Cannot continue' % albumid)
|
||||
return
|
||||
|
||||
logger.info(u"Now adding/updating artist: " + release_dict['artist_name'])
|
||||
|
||||
if release_dict['artist_name'].startswith('The '):
|
||||
@@ -350,6 +338,27 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
downloaded_track_list.append(os.path.join(r, files))
|
||||
elif files.lower().endswith('.cue'):
|
||||
downloaded_cuecount += 1
|
||||
|
||||
# Check if files are valid media files and are writeable, before the steps
|
||||
# below are executed. This simplifies errors and prevents unfinished steps.
|
||||
for downloaded_track in downloaded_track_list:
|
||||
try:
|
||||
media_file = MediaFile(downloaded_track)
|
||||
except (FileTypeError, UnreadableFileError):
|
||||
logger.error("Track file is not a valid media file: %s. Not continuing.", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
return
|
||||
|
||||
# Not sure if line(s) below are needed, since it is possible to not
|
||||
# touch any files.
|
||||
if headphones.EMBED_ALBUM_ART or headphones.CLEANUP_FILES or \
|
||||
headphones.ADD_ALBUM_ART or headphones.CORRECT_METADATA or \
|
||||
headphones.EMBED_LYRICS or headphones.RENAME_FILES or \
|
||||
headphones.MOVE_FILES:
|
||||
|
||||
if not os.access(downloaded_track, os.W_OK):
|
||||
logger.error("Track file is not writeable, which is required for some post processing steps: %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
return
|
||||
|
||||
#start encoding
|
||||
if headphones.MUSIC_ENCODER:
|
||||
downloaded_track_list=music_encoder.encode(albumpath)
|
||||
@@ -474,6 +483,21 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
twitter = notifiers.TwitterNotifier()
|
||||
twitter.notify_download(pushmessage)
|
||||
|
||||
if headphones.OSX_NOTIFY_ENABLED:
|
||||
logger.info(u"Sending OS X notification")
|
||||
osx_notify = notifiers.OSX_NOTIFY()
|
||||
osx_notify.notify(release['ArtistName'], release['AlbumTitle'], "Download and Postprocessing completed")
|
||||
|
||||
if headphones.BOXCAR_ENABLED:
|
||||
pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle']
|
||||
logger.info(u"Sending Boxcar2 notification")
|
||||
boxcar = notifiers.BOXCAR()
|
||||
boxcar.notify('Headphones processed: ' + pushmessage, "Download and Postprocessing completed", release['AlbumID'])
|
||||
|
||||
if headphones.MPC_ENABLED:
|
||||
mpc = notifiers.MPC()
|
||||
mpc.notify()
|
||||
|
||||
def embedAlbumArt(artwork, downloaded_track_list):
|
||||
logger.info('Embedding album art')
|
||||
|
||||
@@ -632,7 +656,7 @@ def moveFiles(albumpath, release, tracks):
|
||||
try:
|
||||
shutil.rmtree(lossless_destination_path)
|
||||
except Exception, e:
|
||||
logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossless_destination_path.decode(headphones.SYS_ENCODING, 'replace'), str(e)))
|
||||
logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossless_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
create_duplicate_folder = True
|
||||
|
||||
if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
|
||||
@@ -665,7 +689,7 @@ def moveFiles(albumpath, release, tracks):
|
||||
try:
|
||||
shutil.rmtree(lossy_destination_path)
|
||||
except Exception, e:
|
||||
logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossy_destination_path.decode(headphones.SYS_ENCODING, 'replace'), str(e)))
|
||||
logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossy_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
create_duplicate_folder = True
|
||||
|
||||
if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
|
||||
@@ -746,13 +770,13 @@ def moveFiles(albumpath, release, tracks):
|
||||
try:
|
||||
os.chmod(os.path.normpath(temp_f).encode(headphones.SYS_ENCODING, 'replace'), int(headphones.FOLDER_PERMISSIONS, 8))
|
||||
except Exception, e:
|
||||
logger.error("Error trying to change permissions on folder: %s" % temp_f.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.error("Error trying to change permissions on folder: %s. %s", temp_f, e)
|
||||
|
||||
# If we failed to move all the files out of the directory, this will fail too
|
||||
try:
|
||||
shutil.rmtree(albumpath)
|
||||
except Exception, e:
|
||||
logger.error('Could not remove directory: %s. %s' % (albumpath.decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
logger.error('Could not remove directory: %s. %s', albumpath, e)
|
||||
|
||||
destination_paths = []
|
||||
|
||||
@@ -779,10 +803,10 @@ def correctMetadata(albumid, release, downloaded_track_list):
|
||||
elif any(downloaded_track.lower().endswith('.' + x.lower()) for x in headphones.LOSSY_MEDIA_FORMATS):
|
||||
lossy_items.append(beets.library.Item.from_path(downloaded_track))
|
||||
else:
|
||||
logger.warn("Skipping: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " because it is not a mutagen friendly file format")
|
||||
logger.warn("Skipping: %s because it is not a mutagen friendly file format", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
except Exception, e:
|
||||
|
||||
logger.error("Beets couldn't create an Item from: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " - not a media file?" + str(e))
|
||||
logger.error("Beets couldn't create an Item from: %s - not a media file? %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e))
|
||||
|
||||
for items in [lossy_items, lossless_items]:
|
||||
|
||||
@@ -792,16 +816,16 @@ def correctMetadata(albumid, release, downloaded_track_list):
|
||||
try:
|
||||
cur_artist, cur_album, candidates, rec = autotag.tag_album(items, search_artist=helpers.latinToAscii(release['ArtistName']), search_album=helpers.latinToAscii(release['AlbumTitle']))
|
||||
except Exception, e:
|
||||
logger.error('Error getting recommendation: %s. Not writing metadata' % e)
|
||||
logger.error('Error getting recommendation: %s. Not writing metadata', e)
|
||||
return
|
||||
if str(rec) == 'recommendation.none':
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata' % (release['ArtistName'], release['AlbumTitle']))
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
|
||||
return
|
||||
|
||||
if candidates:
|
||||
dist, info, mapping, extra_items, extra_tracks = candidates[0]
|
||||
else:
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata' % (release['ArtistName'], release['AlbumTitle']))
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
|
||||
return
|
||||
|
||||
logger.info('Beets recommendation for tagging items: %s' % rec)
|
||||
@@ -813,9 +837,9 @@ def correctMetadata(albumid, release, downloaded_track_list):
|
||||
for item in items:
|
||||
try:
|
||||
item.write()
|
||||
logger.info("Successfully applied metadata to: " + item.path.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info("Successfully applied metadata to: %s", item.path.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
except Exception, e:
|
||||
logger.warn("Error writing metadata to " + item.path.decode(headphones.SYS_ENCODING, 'replace') + ": " + str(e))
|
||||
logger.warn("Error writing metadata to '%s': %s", item.path.decode(headphones.SYS_ENCODING, 'replace'), str(e))
|
||||
|
||||
def embedLyrics(downloaded_track_list):
|
||||
logger.info('Adding lyrics')
|
||||
@@ -827,7 +851,7 @@ def embedLyrics(downloaded_track_list):
|
||||
try:
|
||||
f = MediaFile(downloaded_track)
|
||||
except:
|
||||
logger.error('Could not read %s. Not checking lyrics' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.error('Could not read %s. Not checking lyrics', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
continue
|
||||
|
||||
if f.albumartist and f.title:
|
||||
@@ -835,16 +859,16 @@ def embedLyrics(downloaded_track_list):
|
||||
elif f.artist and f.title:
|
||||
metalyrics = lyrics.getLyrics(f.artist, f.title)
|
||||
else:
|
||||
logger.info('No artist/track metadata found for track: %s. Not fetching lyrics' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info('No artist/track metadata found for track: %s. Not fetching lyrics', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
metalyrics = None
|
||||
|
||||
if lyrics:
|
||||
logger.debug('Adding lyrics to: %s' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.debug('Adding lyrics to: %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
f.lyrics = metalyrics
|
||||
try:
|
||||
f.save()
|
||||
except:
|
||||
logger.error('Cannot save lyrics to: %s. Skipping' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.error('Cannot save lyrics to: %s. Skipping', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
continue
|
||||
|
||||
def renameFiles(albumpath, downloaded_track_list, release):
|
||||
@@ -859,7 +883,7 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
try:
|
||||
f = MediaFile(downloaded_track)
|
||||
except:
|
||||
logger.info("MediaFile couldn't parse: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info("MediaFile couldn't parse: %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
continue
|
||||
|
||||
if not f.disc:
|
||||
@@ -928,11 +952,11 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
logger.debug("Renaming for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " is not neccessary")
|
||||
continue
|
||||
|
||||
logger.debug('Renaming %s ---> %s' % (downloaded_track.decode(headphones.SYS_ENCODING,'replace'), new_file_name.decode(headphones.SYS_ENCODING,'replace')))
|
||||
logger.debug('Renaming %s ---> %s', downloaded_track.decode(headphones.SYS_ENCODING,'replace'), new_file_name.decode(headphones.SYS_ENCODING,'replace'))
|
||||
try:
|
||||
os.rename(downloaded_track, new_file)
|
||||
except Exception, e:
|
||||
logger.error('Error renaming file: %s. Error: %s' % (downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
logger.error('Error renaming file: %s. Error: %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
continue
|
||||
|
||||
def updateFilePermissions(albumpaths):
|
||||
@@ -1044,7 +1068,6 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
|
||||
from headphones import mb
|
||||
try:
|
||||
rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album))
|
||||
except:
|
||||
@@ -1072,7 +1095,6 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
|
||||
from headphones import mb
|
||||
try:
|
||||
rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album))
|
||||
except:
|
||||
@@ -1108,17 +1130,18 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
|
||||
continue
|
||||
|
||||
# Attempt 4: Hail mary. Just assume the folder name is the album name if it doesn't have a separator in it
|
||||
if '-' not in folder:
|
||||
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?', [folder]).fetchone()
|
||||
logger.debug('Attempt to extract album name by assuming it is the folder name')
|
||||
|
||||
if '-' not in folder_basename:
|
||||
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)
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s', folder)
|
||||
from headphones import mb
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s', folder_basename)
|
||||
try:
|
||||
rgid = mb.findAlbumID(album=helpers.latinToAscii(folder))
|
||||
rgid = mb.findAlbumID(album=helpers.latinToAscii(folder_basename))
|
||||
except:
|
||||
logger.error('Can not get release information for this album')
|
||||
rgid = None
|
||||
@@ -1128,4 +1151,3 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
|
||||
continue
|
||||
else:
|
||||
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
||||
|
||||
|
||||
+19
-3
@@ -5,6 +5,7 @@ from bs4 import BeautifulSoup
|
||||
|
||||
import requests
|
||||
import feedparser
|
||||
import headphones
|
||||
|
||||
def request_response(url, method="get", auto_raise=True, whitelist_status_code=None, **kwargs):
|
||||
"""
|
||||
@@ -17,9 +18,13 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N
|
||||
if whitelist_status_code and type(whitelist_status_code) != list:
|
||||
whitelist_status_code = [whitelist_status_code]
|
||||
|
||||
# Disable verification of SSL certificates if requested. Note: this could
|
||||
# pose a security issue!
|
||||
kwargs["verify"] = headphones.VERIFY_SSL_CERT
|
||||
|
||||
# Map method to the request.XXX method. This is a simple hack, but it allows
|
||||
# requests to apply more magic per method. See lib/requests/api.py.
|
||||
request_method = getattr(requests, method)
|
||||
request_method = getattr(requests, method.lower())
|
||||
|
||||
try:
|
||||
# Request the URL
|
||||
@@ -30,8 +35,11 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N
|
||||
# is white listed.
|
||||
if whitelist_status_code and auto_raise:
|
||||
if response.status_code not in whitelist_status_code:
|
||||
logger.debug("Response status code %d is not white listed, raising exception", response.status_code)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except:
|
||||
logger.debug("Response status code %d is not white listed, raised exception", response.status_code)
|
||||
raise
|
||||
elif auto_raise:
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -42,7 +50,15 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N
|
||||
logger.error("Request timed out.")
|
||||
except requests.HTTPError, e:
|
||||
if e.response is not None:
|
||||
logger.error("Request raise HTTP error with status code: %d", e.response.status_code)
|
||||
if e.response.status_code >= 500:
|
||||
cause = "remote server error"
|
||||
elif e.response.status_code >= 400:
|
||||
cause = "local request error"
|
||||
else:
|
||||
# I don't think we will end up here, but for completeness
|
||||
cause = "unknown"
|
||||
|
||||
logger.error("Request raise HTTP error with status code %d (%s).", e.response.status_code, cause)
|
||||
else:
|
||||
logger.error("Request raised HTTP error.")
|
||||
except requests.RequestException, e:
|
||||
|
||||
@@ -119,35 +119,6 @@ def sendNZB(nzb):
|
||||
|
||||
if sabText == "ok":
|
||||
logger.info(u"NZB sent to SAB successfully")
|
||||
if headphones.GROWL_ENABLED and headphones.GROWL_ONSNATCH:
|
||||
logger.info(u"Sending Growl notification")
|
||||
growl = notifiers.GROWL()
|
||||
growl.notify(nzb.name,"Download started")
|
||||
if headphones.PROWL_ENABLED and headphones.PROWL_ONSNATCH:
|
||||
logger.info(u"Sending Prowl notification")
|
||||
prowl = notifiers.PROWL()
|
||||
prowl.notify(nzb.name,"Download started")
|
||||
if headphones.PUSHOVER_ENABLED and headphones.PUSHOVER_ONSNATCH:
|
||||
logger.info(u"Sending Pushover notification")
|
||||
prowl = notifiers.PUSHOVER()
|
||||
prowl.notify(nzb.name,"Download started")
|
||||
if headphones.PUSHBULLET_ENABLED and headphones.PUSHBULLET_ONSNATCH:
|
||||
logger.info(u"Sending PushBullet notification")
|
||||
pushbullet = notifiers.PUSHBULLET()
|
||||
pushbullet.notify(nzb.name + " has been snatched!", "Download started")
|
||||
if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH:
|
||||
logger.info(u"Sending Twitter notification")
|
||||
twitter = notifiers.TwitterNotifier()
|
||||
twitter.notify_snatch(nzb.name)
|
||||
if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH:
|
||||
logger.debug(u"Sending NMA notification")
|
||||
nma = notifiers.NMA()
|
||||
nma.notify(snatched_nzb=nzb.name)
|
||||
if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH:
|
||||
logger.info(u"Sending Pushalot notification")
|
||||
pushalot = notifiers.PUSHALOT()
|
||||
pushalot.notify(nzb.name,"Download started")
|
||||
|
||||
return True
|
||||
elif sabText == "Missing authentication":
|
||||
logger.info(u"Incorrect username/password sent to SAB, NZB not sent")
|
||||
|
||||
+155
-19
@@ -21,6 +21,8 @@ from lib.pygazelle import encoding as gazelleencoding
|
||||
from lib.pygazelle import format as gazelleformat
|
||||
from lib.pygazelle import media as gazellemedia
|
||||
from xml.dom import minidom
|
||||
from base64 import b16encode, b32decode
|
||||
from hashlib import sha1
|
||||
|
||||
import os, re, time
|
||||
import string
|
||||
@@ -28,12 +30,13 @@ import shutil
|
||||
import requests
|
||||
import subprocess
|
||||
|
||||
|
||||
import headphones
|
||||
from headphones.common import USER_AGENT
|
||||
from headphones import logger, db, helpers, classes, sab, nzbget, request
|
||||
from headphones import transmission
|
||||
from headphones import utorrent, transmission, notifiers
|
||||
|
||||
import lib.bencode as bencode
|
||||
from lib.bencode import bencode as bencode, bdecode
|
||||
|
||||
import headphones.searcher_rutracker as rutrackersearch
|
||||
rutracker = rutrackersearch.Rutracker()
|
||||
@@ -148,11 +151,20 @@ def sort_search_results(resultlist, album, new):
|
||||
|
||||
# Add a priority if it has any of the preferred words
|
||||
temp_list = []
|
||||
preferred_words = None
|
||||
if headphones.PREFERRED_WORDS:
|
||||
preferred_words = helpers.split_string(headphones.PREFERRED_WORDS)
|
||||
for result in resultlist:
|
||||
if headphones.PREFERRED_WORDS and any(word.lower() in result[0].lower() for word in helpers.split_string(headphones.PREFERRED_WORDS)):
|
||||
temp_list.append((result[0],result[1],result[2],result[3],result[4],1))
|
||||
else:
|
||||
temp_list.append((result[0],result[1],result[2],result[3],result[4],0))
|
||||
priority = 0
|
||||
if preferred_words:
|
||||
if any(word.lower() in result[0].lower() for word in preferred_words):
|
||||
priority = 1
|
||||
# add a search provider priority (weighted based on position)
|
||||
i = next((i for i, word in enumerate(preferred_words) if word in result[3].lower()), None)
|
||||
if i != None:
|
||||
priority += round((len(preferred_words) - i) / float(len(preferred_words)),2)
|
||||
|
||||
temp_list.append((result[0],result[1],result[2],result[3],result[4],priority))
|
||||
|
||||
resultlist = temp_list
|
||||
|
||||
@@ -215,6 +227,34 @@ def sort_search_results(resultlist, album, new):
|
||||
|
||||
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
|
||||
|
||||
# lossless - ignore results if target size outside bitrate range
|
||||
elif headphones.PREFERRED_QUALITY == 3 and (headphones.LOSSLESS_BITRATE_FROM or headphones.LOSSLESS_BITRATE_TO):
|
||||
|
||||
finallist = []
|
||||
tracks = myDB.select('SELECT TrackDuration from tracks WHERE AlbumID=?', [album['AlbumID']])
|
||||
|
||||
if len(tracks):
|
||||
|
||||
albumlength = sum([pair[0] for pair in tracks])
|
||||
mintargetsize = 0
|
||||
maxtargetsize = 0
|
||||
if headphones.LOSSLESS_BITRATE_FROM:
|
||||
mintargetsize = albumlength/1000 * int(headphones.LOSSLESS_BITRATE_FROM) * 128
|
||||
if headphones.LOSSLESS_BITRATE_TO:
|
||||
maxtargetsize = albumlength/1000 * int(headphones.LOSSLESS_BITRATE_TO) * 128
|
||||
|
||||
if mintargetsize > 0 or maxtargetsize > 0:
|
||||
for i, result in reversed(list(enumerate(resultlist))):
|
||||
if int(result[1]) < mintargetsize and mintargetsize > 0 or int(result[1]) > maxtargetsize and maxtargetsize > 0:
|
||||
if int(result[1]) < mintargetsize:
|
||||
logger.info("%s is too small for this album - not considering it. (Size: %s, Minsize: %s)", result[0], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(mintargetsize))
|
||||
else:
|
||||
logger.info("%s is too large for this album - not considering it. (Size: %s, Maxsize: %s)", result[0], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(maxtargetsize))
|
||||
del resultlist[i]
|
||||
|
||||
if len (resultlist):
|
||||
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
|
||||
|
||||
else:
|
||||
|
||||
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
|
||||
@@ -307,13 +347,13 @@ def searchNZB(album, new=False, losslessOnly=False):
|
||||
params = {
|
||||
"t": "search",
|
||||
"cat": categories,
|
||||
"apikey": '89edf227c1de9b3de50383fff11466c6',
|
||||
"apikey": '964d601959918a578a670984bdee9357',
|
||||
"maxage": headphones.USENET_RETENTION,
|
||||
"q": term
|
||||
}
|
||||
|
||||
data = request.request_feed(
|
||||
url="http://headphones.codeshy.com/newznab/api",
|
||||
url="http://indexer.codeshy.com/api",
|
||||
params=params, headers=headers,
|
||||
auth=(headphones.HPUSER, headphones.HPPASS)
|
||||
)
|
||||
@@ -359,6 +399,9 @@ def searchNZB(album, new=False, losslessOnly=False):
|
||||
logger.info("Album type is audiobook/spokenword. Using audiobook category")
|
||||
|
||||
for newznab_host in newznab_hosts:
|
||||
|
||||
provider = newznab_host[0]
|
||||
|
||||
# Add a little mod for kere.ws
|
||||
if newznab_host[0] == "http://kere.ws":
|
||||
if categories == "3040":
|
||||
@@ -531,8 +574,7 @@ def searchNZB(album, new=False, losslessOnly=False):
|
||||
|
||||
data = request.request_json(
|
||||
url='http://api.omgwtfnzbs.org/json/',
|
||||
params=params, headers=headers,
|
||||
validator=lambda x: type(x) == dict
|
||||
params=params, headers=headers
|
||||
)
|
||||
|
||||
# Parse response
|
||||
@@ -573,14 +615,16 @@ def send_to_downloader(data, bestqual, album):
|
||||
nzb = classes.NZBDataSearchResult()
|
||||
nzb.extraInfo.append(data)
|
||||
nzb.name = folder_name
|
||||
nzbget.sendNZB(nzb)
|
||||
if not nzbget.sendNZB(nzb):
|
||||
return
|
||||
|
||||
elif headphones.NZB_DOWNLOADER == 0:
|
||||
|
||||
nzb = classes.NZBDataSearchResult()
|
||||
nzb.extraInfo.append(data)
|
||||
nzb.name = folder_name
|
||||
sab.sendNZB(nzb)
|
||||
if not sab.sendNZB(nzb):
|
||||
return
|
||||
|
||||
# If we sent the file to sab, we can check how it was renamed and insert that into the snatched table
|
||||
(replace_spaces, replace_dots) = sab.checkConfig()
|
||||
@@ -654,7 +698,7 @@ def send_to_downloader(data, bestqual, album):
|
||||
#Open the fresh torrent file again so we can extract the proper torrent name
|
||||
#Used later in post-processing.
|
||||
with open(download_path, 'rb') as fp:
|
||||
torrent_info = bencode.bdecode(fp.read())
|
||||
torrent_info = bdecode(fp.read())
|
||||
|
||||
folder_name = torrent_info['info'].get('name', '')
|
||||
logger.info('Torrent folder name: %s' % folder_name)
|
||||
@@ -691,10 +735,84 @@ def send_to_downloader(data, bestqual, album):
|
||||
except Exception, e:
|
||||
logger.exception("Unhandled exception")
|
||||
|
||||
else:# if headphones.TORRENT_DOWNLOADER == 2:
|
||||
logger.info("Sending torrent to uTorrent")
|
||||
|
||||
# rutracker needs cookies to be set, pass the .torrent file instead of url
|
||||
if bestqual[3] == 'rutracker.org':
|
||||
file_or_url = rutracker.get_torrent(bestqual[2])
|
||||
else:
|
||||
file_or_url = bestqual[2]
|
||||
|
||||
_hash = CalculateTorrentHash(file_or_url, data)
|
||||
|
||||
folder_name = utorrent.addTorrent(file_or_url, _hash)
|
||||
|
||||
if folder_name:
|
||||
logger.info('Torrent folder name: %s' % folder_name)
|
||||
else:
|
||||
logger.error('Torrent folder name could not be determined')
|
||||
return
|
||||
|
||||
# remove temp .torrent file created above
|
||||
if bestqual[3] == 'rutracker.org':
|
||||
try:
|
||||
shutil.rmtree(os.path.split(file_or_url)[0])
|
||||
except Exception, e:
|
||||
logger.exception("Unhandled exception")
|
||||
|
||||
myDB = db.DBConnection()
|
||||
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']])
|
||||
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name, kind])
|
||||
|
||||
# notify
|
||||
artist = album[1]
|
||||
albumname = album[2]
|
||||
rgid = album[6]
|
||||
title = artist + ' - ' + albumname
|
||||
provider = bestqual[3]
|
||||
if provider.startswith(("http://", "https://")):
|
||||
provider = provider.split("//")[1]
|
||||
name = folder_name if folder_name else None
|
||||
|
||||
if headphones.GROWL_ENABLED and headphones.GROWL_ONSNATCH:
|
||||
logger.info(u"Sending Growl notification")
|
||||
growl = notifiers.GROWL()
|
||||
growl.notify(name,"Download started")
|
||||
if headphones.PROWL_ENABLED and headphones.PROWL_ONSNATCH:
|
||||
logger.info(u"Sending Prowl notification")
|
||||
prowl = notifiers.PROWL()
|
||||
prowl.notify(name,"Download started")
|
||||
if headphones.PUSHOVER_ENABLED and headphones.PUSHOVER_ONSNATCH:
|
||||
logger.info(u"Sending Pushover notification")
|
||||
prowl = notifiers.PUSHOVER()
|
||||
prowl.notify(name,"Download started")
|
||||
if headphones.PUSHBULLET_ENABLED and headphones.PUSHBULLET_ONSNATCH:
|
||||
logger.info(u"Sending PushBullet notification")
|
||||
pushbullet = notifiers.PUSHBULLET()
|
||||
pushbullet.notify(name + " has been snatched!", "Download started")
|
||||
if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH:
|
||||
logger.info(u"Sending Twitter notification")
|
||||
twitter = notifiers.TwitterNotifier()
|
||||
twitter.notify_snatch(name)
|
||||
if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH:
|
||||
logger.info(u"Sending NMA notification")
|
||||
nma = notifiers.NMA()
|
||||
nma.notify(snatched=name)
|
||||
if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH:
|
||||
logger.info(u"Sending Pushalot notification")
|
||||
pushalot = notifiers.PUSHALOT()
|
||||
pushalot.notify(name,"Download started")
|
||||
if headphones.OSX_NOTIFY_ENABLED and headphones.OSX_NOTIFY_ONSNATCH:
|
||||
logger.info(u"Sending OS X notification")
|
||||
osx_notify = notifiers.OSX_NOTIFY()
|
||||
osx_notify.notify(artist, albumname, 'Snatched: ' + provider + '. ' + name)
|
||||
if headphones.BOXCAR_ENABLED and headphones.BOXCAR_ONSNATCH:
|
||||
logger.info(u"Sending Boxcar2 notification")
|
||||
b2msg = 'From ' + provider + '<br></br>' + name
|
||||
boxcar = notifiers.BOXCAR()
|
||||
boxcar.notify('Headphones snatched: ' + title, b2msg, rgid)
|
||||
|
||||
def verifyresult(title, artistterm, term, lossless):
|
||||
|
||||
title = re.sub('[\.\-\/\_]', ' ', title)
|
||||
@@ -862,7 +980,7 @@ def searchTorrent(album, new=False, losslessOnly=False):
|
||||
minimumseeders = int(headphones.NUMBEROFSEEDERS) - 1
|
||||
|
||||
if headphones.KAT:
|
||||
provider = "Kick Ass Torrent"
|
||||
provider = "Kick Ass Torrents"
|
||||
providerurl = url_fix("http://kickass.to/usearch/" + term)
|
||||
if headphones.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||
categories = "7" #music
|
||||
@@ -1051,7 +1169,7 @@ def searchTorrent(album, new=False, losslessOnly=False):
|
||||
if re.search(bitrate, encoding_string, flags=re.I):
|
||||
bitrate_string = encoding_string
|
||||
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
||||
raise Exception("Preferred bitrate %s not recognized by %s" % (bitrate_string, provider))
|
||||
logger.info(u"Your preferred bitrate is not one of the available What.cd filters, so not using it as a search parameter.")
|
||||
maxsize = 10000000000
|
||||
elif headphones.PREFERRED_QUALITY == 1: # Highest quality including lossless
|
||||
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
||||
@@ -1179,6 +1297,8 @@ def searchTorrent(album, new=False, losslessOnly=False):
|
||||
continue
|
||||
else:
|
||||
url = item.findAll("a")[3]['href']
|
||||
if url.lower().startswith("//"):
|
||||
url = "http:" + url
|
||||
formatted_size = re.search('Size (.*),', unicode(item)).group(1).replace(u'\xa0', ' ')
|
||||
size = helpers.piratesize(formatted_size)
|
||||
if size < maxsize and minimumseeders < seeds and url != None:
|
||||
@@ -1321,10 +1441,10 @@ def preprocess(resultlist):
|
||||
for result in resultlist:
|
||||
|
||||
if result[4] == 'torrent':
|
||||
#Get out of here if we're using Transmission or uTorrent
|
||||
if headphones.TORRENT_DOWNLOADER != 0:
|
||||
#Get out of here if we're using Transmission
|
||||
if headphones.TORRENT_DOWNLOADER == 1: ## if not a magnet link still need the .torrent to generate hash... uTorrent support labeling
|
||||
return True, result
|
||||
# get outta here if rutracker or piratebay
|
||||
# get outta here if rutracker
|
||||
if result[3] == 'rutracker.org':
|
||||
return True, result
|
||||
# Get out of here if it's a magnet link
|
||||
@@ -1334,7 +1454,7 @@ def preprocess(resultlist):
|
||||
# Download the torrent file
|
||||
headers = {}
|
||||
|
||||
if result[3] == 'Kick Ass Torrent':
|
||||
if result[3] == 'Kick Ass Torrents':
|
||||
headers['Referer'] = 'http://kat.ph/'
|
||||
elif result[3] == 'What.cd':
|
||||
headers['User-Agent'] = 'Headphones'
|
||||
@@ -1372,3 +1492,19 @@ def preprocess(resultlist):
|
||||
continue
|
||||
|
||||
return (None, None)
|
||||
|
||||
|
||||
|
||||
def CalculateTorrentHash(link, data):
|
||||
|
||||
if link.startswith('magnet'):
|
||||
tor_hash = re.findall('urn:btih:([\w]{32,40})', link)[0]
|
||||
if len(tor_hash) == 32:
|
||||
tor_hash = b16encode(b32decode(tor_hash)).lower()
|
||||
else:
|
||||
info = bdecode(data)["info"]
|
||||
tor_hash = sha1(bencode(info)).hexdigest()
|
||||
|
||||
logger.debug('Torrent Hash: ' + str(tor_hash))
|
||||
|
||||
return tor_hash
|
||||
@@ -18,6 +18,7 @@ from tempfile import mkdtemp
|
||||
class Rutracker():
|
||||
|
||||
logged_in = False
|
||||
|
||||
# Stores a number of login attempts to prevent recursion.
|
||||
#login_counter = 0
|
||||
|
||||
@@ -51,7 +52,6 @@ class Rutracker():
|
||||
pass
|
||||
|
||||
# Check if we're logged in
|
||||
|
||||
for cookie in self.cookiejar:
|
||||
if cookie.name == 'bb_data':
|
||||
self.logged_in = True
|
||||
@@ -64,7 +64,6 @@ class Rutracker():
|
||||
"""
|
||||
|
||||
# Build search url
|
||||
|
||||
searchterm = ''
|
||||
if artist != 'Various Artists':
|
||||
searchterm = artist
|
||||
@@ -83,7 +82,6 @@ class Rutracker():
|
||||
format = '+mp3||aac'
|
||||
|
||||
# sort by size, descending.
|
||||
|
||||
sort = '&o=7&s=2'
|
||||
|
||||
searchurl = "%s?nm=%s%s%s" % (providerurl, urllib.quote(searchterm), format, sort)
|
||||
@@ -111,25 +109,21 @@ class Rutracker():
|
||||
#logger.debug (soup.prettify())
|
||||
|
||||
# Title
|
||||
|
||||
for link in soup.find_all('a', attrs={'class' : 'med tLink hl-tags bold'}):
|
||||
title = link.get_text()
|
||||
titles.append(title)
|
||||
|
||||
# Download URL
|
||||
|
||||
for link in soup.find_all('a', attrs={'class' : 'small tr-dl dl-stub'}):
|
||||
url = link.get('href')
|
||||
urls.append(url)
|
||||
|
||||
# Seeders
|
||||
|
||||
for link in soup.find_all('b', attrs={'class' : 'seedmed'}):
|
||||
seeder = link.get_text()
|
||||
seeders.append(seeder)
|
||||
|
||||
# Size
|
||||
|
||||
for link in soup.find_all('td', attrs={'class' : 'row4 small nowrap tor-size'}):
|
||||
size = link.u.string
|
||||
sizes.append(size)
|
||||
@@ -138,16 +132,19 @@ class Rutracker():
|
||||
pass
|
||||
|
||||
# Combine lists
|
||||
|
||||
torrentlist = zip(titles, urls, seeders, sizes)
|
||||
|
||||
# return if nothing found
|
||||
|
||||
if not torrentlist:
|
||||
return False
|
||||
|
||||
# get headphones track count for album, return if not found
|
||||
# don't bother checking track counts anymore, let searcher filter instead
|
||||
# leave code in just in case
|
||||
check_track_count = False
|
||||
|
||||
if check_track_count:
|
||||
|
||||
# get headphones track count for album, return if not found
|
||||
myDB = db.DBConnection()
|
||||
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
|
||||
hptrackcount = len(tracks)
|
||||
@@ -170,13 +167,18 @@ class Rutracker():
|
||||
seeders = torrent[2]
|
||||
size = torrent[3]
|
||||
|
||||
title = returntitle.lower()
|
||||
|
||||
if int(size) <= maxsize and int(seeders) >= minseeders:
|
||||
|
||||
# Check torrent info
|
||||
|
||||
#Torrent topic page
|
||||
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
|
||||
topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id
|
||||
|
||||
# add to list
|
||||
if not check_track_count:
|
||||
valid = True
|
||||
else:
|
||||
|
||||
# Check torrent info
|
||||
self.cookiejar.set_cookie(cookielib.Cookie(version=0, name='bb_dl', value=torrent_id, port=None, port_specified=False, domain='.rutracker.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False))
|
||||
|
||||
# Debug
|
||||
@@ -195,7 +197,6 @@ class Rutracker():
|
||||
return False
|
||||
|
||||
# get torrent track count and check for cue
|
||||
|
||||
trackcount = 0
|
||||
cuecount = 0
|
||||
|
||||
@@ -208,9 +209,7 @@ class Rutracker():
|
||||
if '.cue' in file:
|
||||
cuecount += 1
|
||||
|
||||
#Torrent topic page
|
||||
|
||||
topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id
|
||||
title = returntitle.lower()
|
||||
logger.debug ('torrent title: %s' % title)
|
||||
logger.debug ('headphones trackcount: %s' % hptrackcount)
|
||||
logger.debug ('rutracker trackcount: %s' % trackcount)
|
||||
@@ -218,7 +217,6 @@ class Rutracker():
|
||||
# If torrent track count less than headphones track count, and there's a cue, then attempt to get track count from log(s)
|
||||
# This is for the case where we have a single .flac/.wav which can be split by cue
|
||||
# Not great, but shouldn't be doing this too often
|
||||
|
||||
totallogcount = 0
|
||||
if trackcount < hptrackcount and cuecount > 0 and cuecount < hptrackcount:
|
||||
page = self.opener.open(topicurl, timeout=60)
|
||||
@@ -247,7 +245,6 @@ class Rutracker():
|
||||
# If torrent track count = hp track count then return torrent,
|
||||
# if greater, check for deluxe/special/foreign editions
|
||||
# if less, then allow if it's a single track with a cue
|
||||
|
||||
valid = False
|
||||
|
||||
if trackcount == hptrackcount:
|
||||
@@ -257,17 +254,14 @@ class Rutracker():
|
||||
valid = True
|
||||
|
||||
# Add to list
|
||||
|
||||
if valid:
|
||||
rulist.append((returntitle, size, topicurl))
|
||||
else:
|
||||
if topicurl:
|
||||
logger.info(u'<a href="%s">Torrent</a> found with %s tracks but the selected headphones release has %s tracks, skipping for rutracker.org' % (topicurl, trackcount, hptrackcount))
|
||||
|
||||
else:
|
||||
logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (returntitle, int(size), int(seeders)))
|
||||
|
||||
|
||||
return rulist
|
||||
|
||||
def get_torrent(self, url, savelocation=None):
|
||||
|
||||
@@ -30,6 +30,13 @@ from headphones import logger, notifiers, request
|
||||
|
||||
def addTorrent(link):
|
||||
method = 'torrent-add'
|
||||
|
||||
if link.endswith('.torrent'):
|
||||
f = open(link,'rb')
|
||||
metainfo = str(base64.b64encode(f.read()))
|
||||
f.close()
|
||||
arguments = {'metainfo': metainfo, 'download-dir':headphones.DOWNLOAD_TORRENT_DIR}
|
||||
else:
|
||||
arguments = {'filename': link, 'download-dir': headphones.DOWNLOAD_TORRENT_DIR}
|
||||
|
||||
response = torrentAction(method,arguments)
|
||||
@@ -49,31 +56,6 @@ def addTorrent(link):
|
||||
retid = False
|
||||
|
||||
logger.info(u"Torrent sent to Transmission successfully")
|
||||
if headphones.GROWL_ENABLED and headphones.GROWL_ONSNATCH:
|
||||
logger.info(u"Sending Growl notification")
|
||||
growl = notifiers.GROWL()
|
||||
growl.notify(name,"Download started")
|
||||
if headphones.PROWL_ENABLED and headphones.PROWL_ONSNATCH:
|
||||
logger.info(u"Sending Prowl notification")
|
||||
prowl = notifiers.PROWL()
|
||||
prowl.notify(name,"Download started")
|
||||
if headphones.PUSHOVER_ENABLED and headphones.PUSHOVER_ONSNATCH:
|
||||
logger.info(u"Sending Pushover notification")
|
||||
pushover = notifiers.PUSHOVER()
|
||||
pushover.notify(name,"Download started")
|
||||
if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH:
|
||||
logger.info(u"Sending Twitter notification")
|
||||
twitter = notifiers.TwitterNotifier()
|
||||
twitter.notify_snatch(nzb.name)
|
||||
if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH:
|
||||
logger.info(u"Sending NMA notification")
|
||||
nma = notifiers.NMA()
|
||||
nma.notify(snatched_nzb=name)
|
||||
if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH:
|
||||
logger.info(u"Sending Pushalot notification")
|
||||
pushalot = notifiers.PUSHALOT()
|
||||
pushalot.notify(name,"Download started")
|
||||
|
||||
return retid
|
||||
|
||||
else:
|
||||
|
||||
@@ -12,3 +12,155 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import urllib, urllib2, urlparse, cookielib
|
||||
import json, re, os, time
|
||||
|
||||
import headphones
|
||||
|
||||
from headphones import logger
|
||||
|
||||
class utorrentclient(object):
|
||||
TOKEN_REGEX = "<div id='token' style='display:none;'>([^<>]+)</div>"
|
||||
|
||||
def __init__(self, base_url = None, username = None, password = None,):
|
||||
|
||||
host = headphones.UTORRENT_HOST
|
||||
if not host.startswith('http'):
|
||||
host = 'http://' + host
|
||||
|
||||
if host.endswith('/'):
|
||||
host = host[:-1]
|
||||
|
||||
if host.endswith('/gui'):
|
||||
host = host[:-4]
|
||||
|
||||
self.base_url = host
|
||||
self.username = headphones.UTORRENT_USERNAME
|
||||
self.password = headphones.UTORRENT_PASSWORD
|
||||
self.opener = self._make_opener('uTorrent', self.base_url, self.username, self.password)
|
||||
self.token = self._get_token()
|
||||
#TODO refresh token, when necessary
|
||||
|
||||
def _make_opener(self, realm, base_url, username, password):
|
||||
"""uTorrent API need HTTP Basic Auth and cookie support for token verify."""
|
||||
auth = urllib2.HTTPBasicAuthHandler()
|
||||
auth.add_password(realm=realm,uri=base_url,user=username,passwd=password)
|
||||
opener = urllib2.build_opener(auth)
|
||||
urllib2.install_opener(opener)
|
||||
|
||||
cookie_jar = cookielib.CookieJar()
|
||||
cookie_handler = urllib2.HTTPCookieProcessor(cookie_jar)
|
||||
|
||||
handlers = [auth, cookie_handler]
|
||||
opener = urllib2.build_opener(*handlers)
|
||||
return opener
|
||||
|
||||
def _get_token(self):
|
||||
url = urlparse.urljoin(self.base_url, 'gui/token.html')
|
||||
try:
|
||||
response = self.opener.open(url)
|
||||
except urllib2.HTTPError as err:
|
||||
logger.debug('URL: ' + str(url))
|
||||
logger.debug('Error getting Token. uTorrent responded with error: ' + str(err))
|
||||
match = re.search(utorrentclient.TOKEN_REGEX, response.read())
|
||||
return match.group(1)
|
||||
|
||||
def list(self, **kwargs):
|
||||
params = [('list', '1')]
|
||||
params += kwargs.items()
|
||||
return self._action(params)
|
||||
|
||||
def add_url(self, url):
|
||||
#can receive magnet or normal .torrent link
|
||||
params = [('action', 'add-url'), ('s', url)]
|
||||
return self._action(params)
|
||||
|
||||
def start(self, *hashes):
|
||||
params = [('action', 'start'), ]
|
||||
for hash in hashes:
|
||||
params.append(('hash', hash))
|
||||
return self._action(params)
|
||||
|
||||
def stop(self, *hashes):
|
||||
params = [('action', 'stop'), ]
|
||||
for hash in hashes:
|
||||
params.append(('hash', hash))
|
||||
return self._action(params)
|
||||
|
||||
def pause(self, *hashes):
|
||||
params = [('action', 'pause'), ]
|
||||
for hash in hashes:
|
||||
params.append(('hash', hash))
|
||||
return self._action(params)
|
||||
|
||||
def forcestart(self, *hashes):
|
||||
params = [('action', 'forcestart'), ]
|
||||
for hash in hashes:
|
||||
params.append(('hash', hash))
|
||||
return self._action(params)
|
||||
|
||||
def getfiles(self, hash):
|
||||
params = [('action', 'getfiles'), ('hash', hash)]
|
||||
return self._action(params)
|
||||
|
||||
def getprops(self, hash):
|
||||
params = [('action', 'getprops'), ('hash', hash)]
|
||||
return self._action(params)
|
||||
|
||||
def setprops(self, hash, s, v):
|
||||
params = [('action', 'setprops'), ('hash', hash), ("s", s), ("v", v)]
|
||||
return self._action(params)
|
||||
|
||||
def setprio(self, hash, priority, *files):
|
||||
params = [('action', 'setprio'), ('hash', hash), ('p', str(priority))]
|
||||
for file_index in files:
|
||||
params.append(('f', str(file_index)))
|
||||
|
||||
return self._action(params)
|
||||
|
||||
def _action(self, params, body=None, content_type=None):
|
||||
url = self.base_url + '/gui/' + '?token=' + self.token + '&' + urllib.urlencode(params)
|
||||
request = urllib2.Request(url)
|
||||
|
||||
if body:
|
||||
request.add_data(body)
|
||||
request.add_header('Content-length', len(body))
|
||||
if content_type:
|
||||
request.add_header('Content-type', content_type)
|
||||
|
||||
try:
|
||||
response = self.opener.open(request)
|
||||
return response.code, json.loads(response.read())
|
||||
except urllib2.HTTPError as err:
|
||||
logger.debug('URL: ' + str(url))
|
||||
logger.debug('uTorrent webUI raised the following error: ' + str(err))
|
||||
|
||||
|
||||
def labelTorrent(hash):
|
||||
label = headphones.UTORRENT_LABEL
|
||||
uTorrentClient = utorrentclient()
|
||||
settinglabel = True
|
||||
while settinglabel:
|
||||
torrentList = uTorrentClient.list()
|
||||
for torrent in torrentList[1].get('torrents'):
|
||||
if (torrent[0].lower() == hash):
|
||||
uTorrentClient.setprops(hash,'label',label)
|
||||
settinglabel = False
|
||||
return True
|
||||
|
||||
|
||||
def dirTorrent(hash):
|
||||
uTorrentClient = utorrentclient()
|
||||
torrentList = uTorrentClient.list()
|
||||
for torrent in torrentList[1].get('torrents'):
|
||||
if (torrent[0].lower() == hash):
|
||||
return torrent[26]
|
||||
return False
|
||||
|
||||
|
||||
def addTorrent(link, hash):
|
||||
uTorrentClient = utorrentclient()
|
||||
uTorrentClient.add_url(link)
|
||||
labelTorrent(hash)
|
||||
return dirTorrent(hash)
|
||||
|
||||
@@ -76,7 +76,7 @@ def getVersion():
|
||||
logger.error('Couldn\'t find latest installed version.')
|
||||
cur_commit_hash = None
|
||||
|
||||
cur_commit_hash = output
|
||||
cur_commit_hash = str(output)
|
||||
|
||||
if not re.match('^[a-z0-9]+$', cur_commit_hash):
|
||||
logger.error('Output doesn\'t look like a hash, not using it')
|
||||
@@ -136,6 +136,10 @@ def checkGithub():
|
||||
logger.info('You are running an unknown version of Headphones. Run the updater to identify your version')
|
||||
return headphones.LATEST_VERSION
|
||||
|
||||
if headphones.LATEST_VERSION == headphones.CURRENT_VERSION:
|
||||
logger.info('Headphones is up to date')
|
||||
return headphones.LATEST_VERSION
|
||||
|
||||
logger.info('Comparing currently installed version with latest GitHub version')
|
||||
url = 'https://api.github.com/repos/%s/headphones/compare/%s...%s' % (headphones.GIT_USER, headphones.LATEST_VERSION, headphones.CURRENT_VERSION)
|
||||
commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict)
|
||||
|
||||
+77
-10
@@ -163,7 +163,7 @@ class WebInterface(object):
|
||||
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
|
||||
getExtras.exposed = True
|
||||
|
||||
def removeExtras(self, ArtistID):
|
||||
def removeExtras(self, ArtistID, ArtistName):
|
||||
myDB = db.DBConnection()
|
||||
controlValueDict = {'ArtistID': ArtistID}
|
||||
newValueDict = {'IncludeExtras': 0}
|
||||
@@ -174,7 +174,8 @@ class WebInterface(object):
|
||||
myDB.action('DELETE from albums WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']])
|
||||
myDB.action('DELETE from allalbums WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']])
|
||||
myDB.action('DELETE from alltracks WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']])
|
||||
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', album['AlbumID'])
|
||||
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [album['AlbumID']])
|
||||
importer.finalize_update(ArtistID, ArtistName)
|
||||
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
|
||||
removeExtras.exposed = True
|
||||
|
||||
@@ -268,6 +269,11 @@ class WebInterface(object):
|
||||
searcher.searchforalbum(mbid, new=True)
|
||||
if action == 'WantedLossless':
|
||||
searcher.searchforalbum(mbid, lossless=True)
|
||||
if ArtistID:
|
||||
ArtistIDT = ArtistID
|
||||
else:
|
||||
ArtistIDT = myDB.action('SELECT ArtistID FROM albums WHERE AlbumID=?', [mbid]).fetchone()[0]
|
||||
myDB.action('UPDATE artists SET TotalTracks=(SELECT COUNT(*) FROM tracks WHERE ArtistID = ? AND AlbumTitle IN (SELECT AlbumTitle FROM albums WHERE Status != "Ignored")) WHERE ArtistID = ?', [ArtistIDT, ArtistIDT])
|
||||
if ArtistID:
|
||||
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
|
||||
else:
|
||||
@@ -629,8 +635,8 @@ class WebInterface(object):
|
||||
for rgid in rgids:
|
||||
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rgid['ReleaseGroupID']])
|
||||
|
||||
myDB.action('DELETE from allalbums WHERE AlbumID=?', [AlbumID])
|
||||
myDB.action('DELETE from alltracks WHERE AlbumID=?', [AlbumID])
|
||||
myDB.action('DELETE from allalbums WHERE ArtistID=?', [ArtistID])
|
||||
myDB.action('DELETE from alltracks WHERE ArtistID=?', [ArtistID])
|
||||
myDB.action('INSERT OR REPLACE into blacklist VALUES (?)', [ArtistID])
|
||||
elif action == 'pause':
|
||||
controlValueDict = {'ArtistID': ArtistID}
|
||||
@@ -724,6 +730,19 @@ class WebInterface(object):
|
||||
return serve_template(templatename="logs.html", title="Log", lineList=headphones.LOG_LIST)
|
||||
logs.exposed = True
|
||||
|
||||
def clearLogs(self):
|
||||
headphones.LOG_LIST = []
|
||||
logger.info("Web logs cleared")
|
||||
raise cherrypy.HTTPRedirect("logs")
|
||||
clearLogs.exposed = True
|
||||
|
||||
def toggleVerbose(self):
|
||||
headphones.VERBOSE = not headphones.VERBOSE
|
||||
logger.initLogger(not headphones.QUIET, headphones.VERBOSE)
|
||||
logger.info("Verbose toggled, set to %s", headphones.VERBOSE)
|
||||
logger.debug("If you read this message, debug logging is available")
|
||||
raise cherrypy.HTTPRedirect("logs")
|
||||
toggleVerbose.exposed = True
|
||||
|
||||
def getLog(self,iDisplayStart=0,iDisplayLength=100,iSortCol_0=0,sSortDir_0="desc",sSearch="",**kwargs):
|
||||
|
||||
@@ -843,6 +862,16 @@ class WebInterface(object):
|
||||
return json_albums
|
||||
getAlbumsByArtist_json.exposed=True
|
||||
|
||||
def getArtistjson(self, ArtistID, **kwargs):
|
||||
myDB = db.DBConnection()
|
||||
artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [ArtistID]).fetchone()
|
||||
artist_json = json.dumps({
|
||||
'ArtistName': artist['ArtistName'],
|
||||
'Status': artist['Status']
|
||||
})
|
||||
return artist_json
|
||||
getArtistjson.exposed=True
|
||||
|
||||
def clearhistory(self, type=None, date_added=None, title=None):
|
||||
myDB = db.DBConnection()
|
||||
if type:
|
||||
@@ -924,6 +953,7 @@ class WebInterface(object):
|
||||
"utorrent_host" : headphones.UTORRENT_HOST,
|
||||
"utorrent_user" : headphones.UTORRENT_USERNAME,
|
||||
"utorrent_pass" : headphones.UTORRENT_PASSWORD,
|
||||
"utorrent_label" : headphones.UTORRENT_LABEL,
|
||||
"nzb_downloader_sabnzbd" : radio(headphones.NZB_DOWNLOADER, 0),
|
||||
"nzb_downloader_nzbget" : radio(headphones.NZB_DOWNLOADER, 1),
|
||||
"nzb_downloader_blackhole" : radio(headphones.NZB_DOWNLOADER, 2),
|
||||
@@ -978,6 +1008,8 @@ class WebInterface(object):
|
||||
"pref_bitrate_low" : headphones.PREFERRED_BITRATE_LOW_BUFFER,
|
||||
"pref_bitrate_allow_lossless" : checked(headphones.PREFERRED_BITRATE_ALLOW_LOSSLESS),
|
||||
"detect_bitrate" : checked(headphones.DETECT_BITRATE),
|
||||
"lossless_bitrate_from" : headphones.LOSSLESS_BITRATE_FROM,
|
||||
"lossless_bitrate_to" : headphones.LOSSLESS_BITRATE_TO,
|
||||
"move_files" : checked(headphones.MOVE_FILES),
|
||||
"rename_files" : checked(headphones.RENAME_FILES),
|
||||
"correct_metadata" : checked(headphones.CORRECT_METADATA),
|
||||
@@ -1059,6 +1091,12 @@ class WebInterface(object):
|
||||
"pushbullet_deviceid": headphones.PUSHBULLET_DEVICEID,
|
||||
"twitter_enabled": checked(headphones.TWITTER_ENABLED),
|
||||
"twitter_onsnatch": checked(headphones.TWITTER_ONSNATCH),
|
||||
"osx_notify_enabled": checked(headphones.OSX_NOTIFY_ENABLED),
|
||||
"osx_notify_onsnatch": checked(headphones.OSX_NOTIFY_ONSNATCH),
|
||||
"osx_notify_app": headphones.OSX_NOTIFY_APP,
|
||||
"boxcar_enabled": checked(headphones.BOXCAR_ENABLED),
|
||||
"boxcar_onsnatch": checked(headphones.BOXCAR_ONSNATCH),
|
||||
"boxcar_token": headphones.BOXCAR_TOKEN,
|
||||
"mirror_list": headphones.MIRRORLIST,
|
||||
"mirror": headphones.MIRROR,
|
||||
"customhost": headphones.CUSTOMHOST,
|
||||
@@ -1072,7 +1110,8 @@ class WebInterface(object):
|
||||
"songkick_filter_enabled": checked(headphones.SONGKICK_FILTER_ENABLED),
|
||||
"cache_sizemb": headphones.CACHE_SIZEMB,
|
||||
"file_permissions": headphones.FILE_PERMISSIONS,
|
||||
"folder_permissions": headphones.FOLDER_PERMISSIONS
|
||||
"folder_permissions": headphones.FOLDER_PERMISSIONS,
|
||||
"mpc_enabled": checked(headphones.MPC_ENABLED)
|
||||
}
|
||||
|
||||
# Need to convert EXTRAS to a dictionary we can pass to the config: it'll come in as a string like 2,5,6,8
|
||||
@@ -1095,7 +1134,7 @@ class WebInterface(object):
|
||||
def configUpdate(self, http_host='0.0.0.0', http_username=None, http_port=8181, http_password=None, launch_browser=0, api_enabled=0, api_key=None,
|
||||
download_scan_interval=None, update_db_interval=None, mb_ignore_age=None, nzb_search_interval=None, libraryscan_interval=None, sab_host=None, sab_username=None, sab_apikey=None, sab_password=None,
|
||||
sab_category=None, nzbget_host=None, nzbget_username=None, nzbget_password=None, nzbget_category=None, transmission_host=None, transmission_username=None, transmission_password=None,
|
||||
utorrent_host=None, utorrent_username=None, utorrent_password=None, nzb_downloader=0, torrent_downloader=0, download_dir=None, blackhole_dir=None, usenet_retention=None,
|
||||
utorrent_host=None, utorrent_username=None, utorrent_password=None, utorrent_label=None,nzb_downloader=0, torrent_downloader=0, download_dir=None, blackhole_dir=None, usenet_retention=None,
|
||||
use_headphones_indexer=0, newznab=0, newznab_host=None, newznab_apikey=None, newznab_enabled=0, nzbsorg=0, nzbsorg_uid=None, nzbsorg_hash=None, nzbsrus=0, nzbsrus_uid=None, nzbsrus_apikey=None, omgwtfnzbs=0, omgwtfnzbs_uid=None, omgwtfnzbs_apikey=None,
|
||||
preferred_words=None, required_words=None, ignored_words=None, preferred_quality=0, preferred_bitrate=None, detect_bitrate=0, move_files=0, torrentblackhole_dir=None, download_torrent_dir=None,
|
||||
numberofseeders=None, use_piratebay=0, piratebay_proxy_url=None, use_isohunt=0, use_kat=0, use_mininova=0, waffles=0, waffles_uid=None, waffles_passkey=None, whatcd=0, whatcd_username=None, whatcd_password=None,
|
||||
@@ -1105,10 +1144,11 @@ class WebInterface(object):
|
||||
bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0,
|
||||
delete_lossless_files=0, growl_enabled=0, growl_onsnatch=0, growl_host=None, growl_password=None, prowl_enabled=0, prowl_onsnatch=0, prowl_keys=None, prowl_priority=0, xbmc_enabled=0, xbmc_host=None, xbmc_username=None, xbmc_password=None,
|
||||
xbmc_update=0, xbmc_notify=0, nma_enabled=False, nma_apikey=None, nma_priority=0, nma_onsnatch=0, pushalot_enabled=False, pushalot_apikey=None, pushalot_onsnatch=0, synoindex_enabled=False, lms_enabled=0, lms_host=None,
|
||||
pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, pushover_apitoken=None, pushbullet_enabled=0, pushbullet_onsnatch=0, pushbullet_apikey=None, pushbullet_deviceid=None, twitter_enabled=0, twitter_onsnatch=0, mirror=None, customhost=None, customport=None,
|
||||
customsleep=None, hpuser=None, hppass=None, preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, cache_sizemb=None,
|
||||
enable_https=0, https_cert=None, https_key=None, file_permissions=None, folder_permissions=None, plex_enabled=0, plex_server_host=None, plex_client_host=None, plex_username=None,
|
||||
plex_password=None, plex_update=0, plex_notify=0, songkick_enabled=0, songkick_apikey=None, songkick_location=None, songkick_filter_enabled=0, encoder_multicore=False, encoder_multicore_count=0, **kwargs):
|
||||
pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, pushover_apitoken=None, pushbullet_enabled=0, pushbullet_onsnatch=0, pushbullet_apikey=None, pushbullet_deviceid=None, twitter_enabled=0, twitter_onsnatch=0,
|
||||
osx_notify_enabled=0, osx_notify_onsnatch=0, osx_notify_app=None, boxcar_enabled=0, boxcar_onsnatch=0, boxcar_token=None, mirror=None, customhost=None, customport=None, customsleep=None, hpuser=None, hppass=None,
|
||||
preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, lossless_bitrate_from=None, lossless_bitrate_to=None, cache_sizemb=None, enable_https=0, https_cert=None, https_key=None,
|
||||
file_permissions=None, folder_permissions=None, plex_enabled=0, plex_server_host=None, plex_client_host=None, plex_username=None, plex_password=None, plex_update=0, plex_notify=0,
|
||||
songkick_enabled=0, songkick_apikey=None, songkick_location=None, songkick_filter_enabled=0, encoder_multicore=False, encoder_multicore_count=0, mpc_enabled=False, **kwargs ):
|
||||
|
||||
headphones.HTTP_HOST = http_host
|
||||
headphones.HTTP_PORT = http_port
|
||||
@@ -1140,6 +1180,7 @@ class WebInterface(object):
|
||||
headphones.UTORRENT_HOST = utorrent_host
|
||||
headphones.UTORRENT_USERNAME = utorrent_username
|
||||
headphones.UTORRENT_PASSWORD = utorrent_password
|
||||
headphones.UTORRENT_LABEL = utorrent_label
|
||||
headphones.NZB_DOWNLOADER = int(nzb_downloader)
|
||||
headphones.TORRENT_DOWNLOADER = int(torrent_downloader)
|
||||
headphones.DOWNLOAD_DIR = download_dir
|
||||
@@ -1185,6 +1226,8 @@ class WebInterface(object):
|
||||
headphones.PREFERRED_BITRATE_LOW_BUFFER = preferred_bitrate_low_buffer
|
||||
headphones.PREFERRED_BITRATE_ALLOW_LOSSLESS = preferred_bitrate_allow_lossless
|
||||
headphones.DETECT_BITRATE = detect_bitrate
|
||||
headphones.LOSSLESS_BITRATE_FROM = lossless_bitrate_from
|
||||
headphones.LOSSLESS_BITRATE_TO = lossless_bitrate_to
|
||||
headphones.MOVE_FILES = move_files
|
||||
headphones.CORRECT_METADATA = correct_metadata
|
||||
headphones.RENAME_FILES = rename_files
|
||||
@@ -1268,6 +1311,17 @@ class WebInterface(object):
|
||||
headphones.SONGKICK_FILTER_ENABLED = songkick_filter_enabled
|
||||
headphones.TWITTER_ENABLED = twitter_enabled
|
||||
headphones.TWITTER_ONSNATCH = twitter_onsnatch
|
||||
|
||||
headphones.OSX_NOTIFY_ENABLED = osx_notify_enabled
|
||||
headphones.OSX_NOTIFY_ONSNATCH = osx_notify_onsnatch
|
||||
headphones.OSX_NOTIFY_APP = osx_notify_app
|
||||
|
||||
headphones.BOXCAR_ENABLED = boxcar_enabled
|
||||
headphones.BOXCAR_ONSNATCH = boxcar_onsnatch
|
||||
headphones.BOXCAR_TOKEN = boxcar_token
|
||||
|
||||
headphones.MPC_ENABLED = mpc_enabled
|
||||
|
||||
headphones.MIRROR = mirror
|
||||
headphones.CUSTOMHOST = customhost
|
||||
headphones.CUSTOMPORT = customport
|
||||
@@ -1435,6 +1489,19 @@ class WebInterface(object):
|
||||
return "Error sending tweet"
|
||||
testTwitter.exposed = True
|
||||
|
||||
def osxnotifyregister(self, app):
|
||||
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
|
||||
from lib.osxnotify import registerapp as osxnotify
|
||||
result, msg = osxnotify.registerapp(app)
|
||||
if result:
|
||||
osx_notify = notifiers.OSX_NOTIFY()
|
||||
osx_notify.notify('Registered', result, 'Success :-)')
|
||||
logger.info('Registered %s, to re-register a different app, delete this app first' % result)
|
||||
else:
|
||||
logger.warn(msg)
|
||||
return msg
|
||||
osxnotifyregister.exposed = True
|
||||
|
||||
class Artwork(object):
|
||||
def index(self):
|
||||
return "Artwork"
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
# This particular version has been slightly modified to work with headphones
|
||||
# https://github.com/rembo10/headphones
|
||||
import os
|
||||
|
||||
__version__ = '1.3.4'
|
||||
__author__ = 'Adrian Sampson <adrian@radbox.org>'
|
||||
@@ -23,4 +24,4 @@ from beets.util import confit
|
||||
|
||||
Library = beets.library.Library
|
||||
|
||||
config = confit.LazyConfig('beets', __name__)
|
||||
config = confit.LazyConfig(os.path.dirname(__file__), __name__)
|
||||
|
||||
Executable
Executable
BIN
Binary file not shown.
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import shutil
|
||||
import os
|
||||
import stat
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
def registerapp(app):
|
||||
|
||||
# don't do any of this unless >= 10.8
|
||||
v, _, _ = platform.mac_ver()
|
||||
v = float('.'.join(v.split('.')[:2]))
|
||||
if v < 10.8:
|
||||
return None, 'Registering requires OS X version >= 10.8'
|
||||
|
||||
app_path = None
|
||||
|
||||
# check app bundle doesn't already exist
|
||||
app_path = subprocess.check_output(['/usr/bin/mdfind', 'kMDItemCFBundleIdentifier == "ade.headphones.osxnotify"']).strip()
|
||||
if app_path:
|
||||
return app_path, 'App previously registered'
|
||||
|
||||
# check app doesn't already exist
|
||||
app = app.strip()
|
||||
if not app:
|
||||
return None, 'Path/Application not entered'
|
||||
if os.path.splitext(app)[1] == ".app":
|
||||
app_path = app
|
||||
else:
|
||||
app_path = app + '.app'
|
||||
if os.path.exists(app_path):
|
||||
return None, 'App %s already exists, choose a different name' % app_path
|
||||
|
||||
# generate app
|
||||
try:
|
||||
os.mkdir(app_path)
|
||||
os.mkdir(app_path + "/Contents")
|
||||
os.mkdir(app_path + "/Contents/MacOS")
|
||||
os.mkdir(app_path + "/Contents/Resources")
|
||||
shutil.copy(os.path.join(os.path.dirname(__file__), "appIcon.icns"), app_path + "/Contents/Resources/")
|
||||
|
||||
version = "1.0.0"
|
||||
bundleName = "OSXNotify"
|
||||
bundleIdentifier = "ade.headphones.osxnotify"
|
||||
|
||||
f = open(app_path + "/Contents/Info.plist", "w")
|
||||
f.write("""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>main.py</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>%s</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>appIcon.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>%s</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>%s</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>%s</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>%s</string>
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<string>YES</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
""" % (bundleName + " " + version, bundleIdentifier, bundleName, bundleName + " " + version, version))
|
||||
f.close()
|
||||
|
||||
f = open(app_path + "/Contents/PkgInfo", "w")
|
||||
f.write("APPL????")
|
||||
f.close()
|
||||
|
||||
f = open(app_path + "/Contents/MacOS/main.py", "w")
|
||||
f.write("""#!/usr/bin/python
|
||||
|
||||
objc = None
|
||||
|
||||
def swizzle(cls, SEL, func):
|
||||
old_IMP = cls.instanceMethodForSelector_(SEL)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
return func(self, old_IMP, *args, **kwargs)
|
||||
new_IMP = objc.selector(wrapper, selector=old_IMP.selector,
|
||||
signature=old_IMP.signature)
|
||||
objc.classAddMethod(cls, SEL, new_IMP)
|
||||
|
||||
def notify(title, subtitle=None, text=None, sound=True):
|
||||
global objc
|
||||
objc = __import__("objc")
|
||||
swizzle(objc.lookUpClass('NSBundle'),
|
||||
b'bundleIdentifier',
|
||||
swizzled_bundleIdentifier)
|
||||
NSUserNotification = objc.lookUpClass('NSUserNotification')
|
||||
NSUserNotificationCenter = objc.lookUpClass('NSUserNotificationCenter')
|
||||
NSAutoreleasePool = objc.lookUpClass('NSAutoreleasePool')
|
||||
pool = NSAutoreleasePool.alloc().init()
|
||||
notification = NSUserNotification.alloc().init()
|
||||
notification.setTitle_(title)
|
||||
notification.setSubtitle_(subtitle)
|
||||
notification.setInformativeText_(text)
|
||||
notification.setSoundName_("NSUserNotificationDefaultSoundName")
|
||||
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
||||
notification_center.deliverNotification_(notification)
|
||||
del pool
|
||||
|
||||
def swizzled_bundleIdentifier(self, original):
|
||||
return 'ade.headphones.osxnotify'
|
||||
|
||||
if __name__ == '__main__':
|
||||
notify('Half Man Half Biscuit', 'Back in the DHSS', '99% Of Gargoyles Look Like Bob Todd')
|
||||
""")
|
||||
f.close()
|
||||
|
||||
oldmode = os.stat(app_path + "/Contents/MacOS/main.py").st_mode
|
||||
os.chmod(app_path + "/Contents/MacOS/main.py", oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
|
||||
return app_path, 'App registered'
|
||||
|
||||
except Exception, e:
|
||||
return None, 'Error creating App %s. %s' % (app_path, e)
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from pynma import PyNMA
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from xml.dom.minidom import parseString
|
||||
from httplib import HTTPSConnection
|
||||
from urllib import urlencode
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
API_SERVER = 'nma.usk.bz'
|
||||
ADD_PATH = '/publicapi/notify'
|
||||
|
||||
USER_AGENT="PyNMA/v%s"%__version__
|
||||
|
||||
def uniq_preserve(seq): # Dave Kirby
|
||||
# Order preserving
|
||||
seen = set()
|
||||
return [x for x in seq if x not in seen and not seen.add(x)]
|
||||
|
||||
def uniq(seq):
|
||||
# Not order preserving
|
||||
return {}.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
|
||||
"""
|
||||
|
||||
def __init__(self, apikey=[], developerkey=None):
|
||||
self._developerkey = None
|
||||
self.developerkey(developerkey)
|
||||
if apikey:
|
||||
if type(apikey) == str:
|
||||
apikey = [apikey]
|
||||
self._apikey = uniq(apikey)
|
||||
|
||||
def addkey(self, key):
|
||||
"Add a key (register ?)"
|
||||
if type(key) == str:
|
||||
if not key in self._apikey:
|
||||
self._apikey.append(key)
|
||||
elif type(key) == list:
|
||||
for k in key:
|
||||
if not k in self._apikey:
|
||||
self._apikey.append(k)
|
||||
|
||||
def delkey(self, key):
|
||||
"Removes a key (unregister ?)"
|
||||
if type(key) == str:
|
||||
if key in self._apikey:
|
||||
self._apikey.remove(key)
|
||||
elif type(key) == list:
|
||||
for k in key:
|
||||
if key in self._apikey:
|
||||
self._apikey.remove(k)
|
||||
|
||||
def developerkey(self, developerkey):
|
||||
"Sets the developer key (and check it has the good length)"
|
||||
if type(developerkey) == str and len(developerkey) == 48:
|
||||
self._developerkey = developerkey
|
||||
|
||||
def push(self, application="", event="", description="", url="", priority=0, batch_mode=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
|
||||
"""
|
||||
datas = {
|
||||
'application': application[:256].encode('utf8'),
|
||||
'event': event[:1024].encode('utf8'),
|
||||
'description': description[:10000].encode('utf8'),
|
||||
'priority': priority
|
||||
}
|
||||
|
||||
if url:
|
||||
datas['url'] = url[:512]
|
||||
|
||||
if self._developerkey:
|
||||
datas['developerkey'] = self._developerkey
|
||||
|
||||
results = {}
|
||||
|
||||
if not batch_mode:
|
||||
for key in self._apikey:
|
||||
datas['apikey'] = key
|
||||
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
|
||||
return results
|
||||
|
||||
def callapi(self, method, path, args):
|
||||
headers = { 'User-Agent': USER_AGENT }
|
||||
if method == "POST":
|
||||
headers['Content-type'] = "application/x-www-form-urlencoded"
|
||||
http_handler = HTTPSConnection(API_SERVER)
|
||||
http_handler.request(method, path, urlencode(args), headers)
|
||||
resp = http_handler.getresponse()
|
||||
|
||||
try:
|
||||
res = self._parse_reponse(resp.read())
|
||||
except Exception, e:
|
||||
res = {'type': "pynmaerror",
|
||||
'code': 600,
|
||||
'message': str(e)
|
||||
}
|
||||
pass
|
||||
|
||||
return res
|
||||
|
||||
def _parse_reponse(self, response):
|
||||
root = parseString(response).firstChild
|
||||
for elem in root.childNodes:
|
||||
if elem.nodeType == elem.TEXT_NODE: continue
|
||||
if elem.tagName == 'success':
|
||||
res = dict(elem.attributes.items())
|
||||
res['message'] = ""
|
||||
res['type'] = elem.tagName
|
||||
return res
|
||||
if elem.tagName == 'error':
|
||||
res = dict(elem.attributes.items())
|
||||
res['message'] = elem.firstChild.nodeValue
|
||||
res['type'] = elem.tagName
|
||||
return res
|
||||
|
||||
|
||||
Reference in New Issue
Block a user