Compare commits

...
40 Commits
Author SHA1 Message Date
rembo10 0457c3c30e Merge branch 'develop': OSX & Boxcar notifications, bug fixes 2014-04-17 20:37:45 -07:00
rembo10 945cfbf21b Took out some leftover boxcar 1 stuff 2014-04-17 17:34:42 -07:00
rembo10 5769cb0dee Merge remote-tracking branch 'NovaXeros/patch-1' into develop
Conflicts:
	headphones/notifiers.py
2014-04-17 17:17:25 -07:00
rembo10 efd043e309 Merge remote-tracking branch 'texke/master' into develop 2014-04-17 17:15:35 -07:00
rembo10 fd19563160 Merge remote-tracking branch 'AdeHub/notify' into develop. Replaced boxcar notifications from daktak
Conflicts:
	headphones/__init__.py
	headphones/notifiers.py
	headphones/sab.py
	headphones/webserve.py
2014-04-17 17:14:37 -07:00
rembo10 1a2ed24566 Merge remote-tracking branch 'basilfx/bugfixes4' into develop 2014-04-17 16:57:19 -07:00
rembo10 d2fecc47ec Possible fix for #1560, what.cd choking when preferred bitrate is not one of the what.cd categories 2014-04-17 13:50:27 -07:00
Jens Rogier f9b0a5efb9 Fixed little bug where the trackcount on the index page was screwing up when choosing different release. 2014-04-17 19:41:40 +02:00
Jens Rogier e304e34ac7 Added ability to ignore albums.
Trackcount on homepage now is now excluding ignored albums.
Added "Manage ignored albums" to the Manage function.
2014-04-17 19:23:39 +02:00
Ade ff3826c0c8 More Boxcar2
Added MusicBrainz link to the message
2014-04-17 23:05:30 +12:00
NovaXeros c96b043a19 Update notifiers.py 2014-04-16 21:01:14 +01:00
NovaXeros a1d0f14eb8 Change LMS title in config options.
Squeezebox is an old unused name, and LMS isn't very self-explanitory.
2014-04-16 20:13:12 +01:00
NovaXeros 0234f4dab9 Update notifiers.py 2014-04-16 20:06:19 +01:00
NovaXeros e00411f7ad Cleaned LMS error reporting.
Even upon successful rescan, the code would throw a keyerror back. Not certain why, but LMS sends some really weird unicode JSON back as its result, unlike XBMC, so having response[0]['result'] will always throw back a keyerror.

Removing the key, however, and just asking the return to find 'result' works fine.
2014-04-16 20:04:14 +01:00
Ade 3b2350cd8f Boxcar2, OS X Notifications
- OS X Notifications require an application bundle, allow user to
create a dummy app in a specified location and then use bundle when
notifying
- Boxcar2
- Moved snatched notify logic to searcher so we can pass extra info
incl provider
2014-04-16 21:41:22 +12:00
Bas Stottelaar d490091a9a Bugfixes in helpers.py for MediaFile errors. Pre-detect potential file errors
Conflicts:
	headphones/postprocessor.py
2014-04-15 10:10:10 +02:00
Bas Stottelaar 5be36b517f Delete by ArtistID, not AlbumID 2014-04-15 10:00:33 +02:00
rembo10 dab323e8ff Better fix for album art stretching 2014-04-14 18:56:10 -07:00
rembo10 7e02f86af8 Fixed import for specifying beets config file 2014-04-14 18:51:24 -07:00
rembo10 26ed43fca8 Fix for album art stretching 2014-04-14 18:46:40 -07:00
rembo10 dab4e3d5f8 Updated beets lib to correctly point to config file (thanks to AdeHub) 2014-04-14 17:38:29 -07:00
rembo10 07e8b6b095 Merge remote-tracking branch 'texke/master' into develop 2014-04-14 17:33:33 -07:00
rembo10 52523a5d3c Merge remote-tracking branch 'daktak/master' into develop 2014-04-14 17:28:34 -07:00
rembo10 ded4b7ef6f Merge remote-tracking branch 'basilfx/bugfixes' into develop 2014-04-14 17:27:22 -07:00
Jens Rogier aa9d669a2b Fixed problem where albums with secondary types still got added 2014-04-14 21:28:06 +02:00
daktak 5378884bf8 boxcar notification 2014-04-14 17:43:01 +10:00
Bas Stottelaar 631d947a9a Re-added decoding of strings 2014-04-13 18:25:17 +02:00
rembo10 7280807588 Don't limit extras to 25 2014-04-12 17:44:48 -07:00
Bas Stottelaar ca010ac522 Prevent misleading messages about whitelisting 2014-04-12 20:35:15 +02:00
Bas Stottelaar e2e1ef9bc6 Add clear log option. Improved styling. 2014-04-12 20:27:38 +02:00
Bas Stottelaar a96b793c98 Add color for warnings.
Some distinction between the levels is handy.
2014-04-12 18:59:08 +02:00
Bas Stottelaar f7ba0ff634 All loggers on the same level.
In the issues, a lot of logs do not include DEBUG info, even if started with
verbose turned on. By defaulting all loggers to INFO, but DEBUG if verbose
is 2, I hope this will improve.

This will disable DEBUG for log file, but I assume verbose is turned on in
case of problems, so it will save disk writings.
2014-04-12 18:56:40 +02:00
Bas Stottelaar a0339d9814 Forgot a parenthesis 2014-04-12 18:23:59 +02:00
Bas Stottelaar eb7a1d7117 Updated methods to log messages new style 2014-04-12 18:23:59 +02:00
Bas Stottelaar 65fa0e21fa Do not decode for logger. 2014-04-12 18:23:59 +02:00
Bas Stottelaar 60ea86d499 Correct indent. Use signum is not None instead of type(signum) != type(None) 2014-04-12 18:23:59 +02:00
Bas Stottelaar 0411bb6b44 Importing with statement is for Python 2.5.
Bases on libraries (e.g. beautifulsoup4), I guess the minimal version for Headphones is at least 2.6
2014-04-12 18:23:58 +02:00
Bas Stottelaar d0198b15ee Restructured imports of post processor 2014-04-12 18:23:58 +02:00
rembo10 c4e8678d8f Fix for removeExtras error 2014-04-12 09:17:52 -07:00
rembo10 d6cd9cd7e4 whitespace 2014-04-11 11:30:51 -07:00
28 changed files with 716 additions and 265 deletions
+8
View File
@@ -172,6 +172,14 @@ table.display tr.even.gradeA {
background-color: #ddffdd; background-color: #ddffdd;
} }
table.display tr.odd.gradeI {
background-color: #bebebe;
}
table.display tr.even.gradeI {
background-color: #bebebe;
}
table.display tr.odd.gradeC { table.display tr.odd.gradeC {
background-color: #ddddff; background-color: #ddddff;
} }
+1 -1
View File
@@ -13,7 +13,7 @@
<div id="subhead_menu"> <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> <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> <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': %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> <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>
+6 -3
View File
@@ -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> <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 %endif
%if artist['IncludeExtras']: %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> <a class="menu_link_edit" id="menu_link_modifyextra" href="#"><i class="fa fa-pencil"></i> Modify Extras</a>
%else: %else:
<a id="menu_link_getextra" href="#"><i class="fa fa-plus"></i> Get Extras</a> <a id="menu_link_getextra" href="#"><i class="fa fa-plus"></i> Get Extras</a>
@@ -62,6 +62,7 @@
<option value="Wanted">Wanted</option> <option value="Wanted">Wanted</option>
<option value="WantedNew">Wanted (new only)</option> <option value="WantedNew">Wanted (new only)</option>
<option value="Skipped">Skipped</option> <option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option> <option value="Downloaded">Downloaded</option>
</select> </select>
<input type="hidden" value="Go"> <input type="hidden" value="Go">
@@ -89,6 +90,8 @@
grade = 'X' grade = 'X'
elif album['Status'] == 'Snatched': elif album['Status'] == 'Snatched':
grade = 'C' grade = 'C'
elif album['Status'] == 'Ignored':
grade = 'I'
else: else:
grade = 'A' grade = 'A'
@@ -123,12 +126,12 @@
%> %>
<tr class="grade${grade}"> <tr class="grade${grade}">
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></td> <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="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
<td id="reldate">${album['ReleaseDate']}</td> <td id="reldate">${album['ReleaseDate']}</td>
<td id="type">${album['Type']}</td> <td id="type">${album['Type']}</td>
<td id="status">${album['Status']} <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>] [<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'): %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>] [<a href="#" onclick="doAjaxCall('unqueueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' skipped">skip</a>]
+89 -6
View File
@@ -678,9 +678,9 @@
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<h3>LMS</h3> <h3>Logitech Media Server</h3>
<div class="checkbox row"> <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>
<div id="lmsoptions"> <div id="lmsoptions">
<div class="row"> <div class="row">
@@ -690,8 +690,7 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
</td>
<td>
<fieldset> <fieldset>
<h3>Pushalot</h3> <h3>Pushalot</h3>
<div class="checkbox row"> <div class="checkbox row">
@@ -708,6 +707,10 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
</td>
<td>
<fieldset> <fieldset>
<h3>Synology NAS</h3> <h3>Synology NAS</h3>
<div class="checkbox row"> <div class="checkbox row">
@@ -776,7 +779,40 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
</td>
<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>
</td>
</tr> </tr>
</table> </table>
<input type="button" class="configsubmit" value="Save Changes" onclick="doAjaxCall('configUpdate',$(this),'tabs',true);return false;" data-success="Changes saved successfully"> <input type="button" class="configsubmit" value="Save Changes" onclick="doAjaxCall('configUpdate',$(this),'tabs',true);return false;" data-success="Changes saved successfully">
@@ -1427,6 +1463,7 @@
$("#pushbulletoptions").hide(); $("#pushbulletoptions").hide();
} }
$("#pushbullet").click(function(){ $("#pushbullet").click(function(){
if ($("#pushbullet").is(":checked")) if ($("#pushbullet").is(":checked"))
{ {
@@ -1458,7 +1495,47 @@
} }
}); });
if ($("#songkick").is(":checked")) 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(); $("#songkickoptions").show();
} }
@@ -1671,6 +1748,12 @@
function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); }); function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); $('#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() { $(document).ready(function() {
@@ -224,6 +224,14 @@ table.display tr.even.gradeA {
background-color: #ddffdd; background-color: #ddffdd;
} }
table.display tr.odd.gradeI {
background-color: #bebebe;
}
table.display tr.even.gradeI {
background-color: #bebebe;
}
table.display tr.odd.gradeC { table.display tr.odd.gradeC {
background-color: #ebf5ff; background-color: #ebf5ff;
} }
@@ -254,6 +262,13 @@ table.display tr.even.gradeU {
background-color: #eee; background-color: #eee;
} }
table.display tr.odd.gradeW {
background-color: #ffffaa;
}
table.display tr.even.gradeW {
background-color: #ffffaa;
}
table.display tr.odd.gradeZ { table.display tr.odd.gradeZ {
background-color: #FAFAFA; background-color: #FAFAFA;
@@ -269,6 +284,7 @@ table.display tr.gradeL #status {
} }
table.display tr.gradeA td, table.display tr.gradeA td,
table.display tr.gradeC td, table.display tr.gradeC td,
table.display tr.gradeI td,
table.display tr.gradeX td, table.display tr.gradeX td,
table.display tr.gradeU td, table.display tr.gradeU td,
table.display tr.gradeZ td {border-bottom: 1px solid #FFF;} table.display tr.gradeZ td {border-bottom: 1px solid #FFF;}
+8
View File
@@ -163,6 +163,14 @@ img.albumArt {
max-height: 300px; max-height: 300px;
position: relative; position: relative;
} }
img.albumArt-nostretch {
float: left;
min-height: 64px;
min-width: 64px;
max-width: 250px;
max-height: 300px;
position: relative;
}
.title { .title {
margin-bottom: 20px; margin-bottom: 20px;
margin-top: 10px; margin-top: 10px;
+4
View File
@@ -316,6 +316,10 @@ function doAjaxCall(url,elem,reload,form) {
} }
if ( reload == "tabs") refreshTab(); if ( reload == "tabs") refreshTab();
if ( reload == "page") location.reload(); if ( reload == "page") location.reload();
if ( reload == "submenu&table") {
refreshSubmenu();
refreshTable();
}
if ( form ) { if ( form ) {
// Change the option to 'choose...' // Change the option to 'choose...'
$(formID + " select").children('option[disabled=disabled]').attr('selected','selected'); $(formID + " select").children('option[disabled=disabled]').attr('selected','selected');
+18 -5
View File
@@ -1,10 +1,18 @@
lossless<%inherit file="base.html"/> <%inherit file="base.html"/>
<%! <%!
from headphones import helpers 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()"> <%def name="body()">
<div class="title"> <div id="paddingheader">
<h1 class="clearfix"><i class="fa fa-flag"></i> Logs</h1> <h1 class="clearfix"><i class="fa fa-flag"></i> Logs</h1>
</div> </div>
<table class="display" id="log_table"> <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 src="js/libs/jquery.dataTables.min.js"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
initActions();
$('#log_table').dataTable( { $('#log_table').dataTable( {
"bProcessing": true, "bProcessing": true,
"bServerSide": true, "bServerSide": true,
@@ -49,23 +59,26 @@ $(document).ready(function() {
"iDisplayLength": 25, "iDisplayLength": 25,
"bStateSave": true, "bStateSave": true,
"oLanguage": { "oLanguage": {
"sSearch":"", "sSearch":"Filter:",
"sLengthMenu":"Show _MENU_ lines per page", "sLengthMenu":"Show _MENU_ lines per page",
"sEmptyTable": "No log information available", "sEmptyTable": "No log information available",
"sInfo":"Showing _START_ to _END_ of _TOTAL_ lines", "sInfo":"Showing _START_ to _END_ of _TOTAL_ lines",
"sInfoEmpty":"Showing 0 to 0 of 0 lines", "sInfoEmpty":"Showing 0 to 0 of 0 lines",
"sInfoFiltered":"(filtered from _MAX_ total lines)"}, "sInfoFiltered":"(filtered from _MAX_ total lines)"},
"fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) { "fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
if (aData[1] === "WARNING" || aData[1] === "ERROR") if (aData[1] === "ERROR")
{ {
$('td', nRow).closest('tr').addClass("gradeX"); $('td', nRow).closest('tr').addClass("gradeX");
} }
else if (aData[1] === "WARNING")
{
$('td', nRow).closest('tr').addClass("gradeW");
}
else else
{ {
$('td', nRow).closest('tr').addClass("gradeZ"); $('td', nRow).closest('tr').addClass("gradeZ");
} }
return nRow; return nRow;
}, },
"fnServerData": function ( sSource, aoData, fnCallback ) { "fnServerData": function ( sSource, aoData, fnCallback ) {
+1
View File
@@ -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=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=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=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> <br><br>
<a href="manageAlbums">Manage All Albums</a> <a href="manageAlbums">Manage All Albums</a>
</div> </div>
@@ -25,6 +25,7 @@
<option value="WantedNew">Wanted (new only)</option> <option value="WantedNew">Wanted (new only)</option>
<option value="WantedLossless">Wanted (lossless)</option> <option value="WantedLossless">Wanted (lossless)</option>
<option value="Skipped">Skipped</option> <option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option> <option value="Downloaded">Downloaded</option>
</select> </select>
<input type="hidden" value="Go"> <input type="hidden" value="Go">
@@ -50,6 +51,8 @@
grade = 'Z' grade = 'Z'
elif album['Status'] == 'Wanted': elif album['Status'] == 'Wanted':
grade = 'X' grade = 'X'
elif album['Status'] == 'Ignored':
grade = 'I'
elif album['Status'] == 'Snatched': elif album['Status'] == 'Snatched':
grade = 'C' grade = 'C'
else: else:
+1
View File
@@ -20,6 +20,7 @@
<select name="action" onChange="doAjaxCall('markAlbums',$(this),'table',true);" data-error="You didn't select any albums"> <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 disabled="disabled" selected="selected">Choose...</option>
<option value="Skipped">Skipped</option> <option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option> <option value="Downloaded">Downloaded</option>
</select> </select>
<input type="hidden" value="Go"> <input type="hidden" value="Go">
+30 -7
View File
@@ -15,20 +15,17 @@
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi # 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 os, sys, subprocess
import threading import threading
import webbrowser import webbrowser
import sqlite3 import sqlite3
import itertools import itertools
import cherrypy
from lib.apscheduler.scheduler import Scheduler from lib.apscheduler.scheduler import Scheduler
from lib.configobj import ConfigObj from lib.configobj import ConfigObj
import cherrypy
from headphones import versioncheck, logger, version from headphones import versioncheck, logger, version
from headphones.common import * from headphones.common import *
@@ -270,6 +267,12 @@ TWITTER_ONSNATCH = False
TWITTER_USERNAME = None TWITTER_USERNAME = None
TWITTER_PASSWORD = None TWITTER_PASSWORD = None
TWITTER_PREFIX = 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"] MIRRORLIST = ["musicbrainz.org","headphones","custom"]
MIRROR = None MIRROR = None
CUSTOMHOST = None CUSTOMHOST = None
@@ -349,7 +352,7 @@ def initialize():
INTERFACE, FOLDER_PERMISSIONS, FILE_PERMISSIONS, ENCODERFOLDER, ENCODER_PATH, ENCODER, XLDPROFILE, BITRATE, SAMPLINGFREQUENCY, \ 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, \ 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, \ 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, \ PUSHBULLET_ENABLED, PUSHBULLET_APIKEY, PUSHBULLET_DEVICEID, PUSHBULLET_ONSNATCH, \
MIRROR, CUSTOMHOST, CUSTOMPORT, CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, \ 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, \ XBMC_NOTIFY, LMS_ENABLED, LMS_HOST, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \
@@ -386,6 +389,8 @@ def initialize():
CheckSection('Pushalot') CheckSection('Pushalot')
CheckSection('Synoindex') CheckSection('Synoindex')
CheckSection('Twitter') CheckSection('Twitter')
CheckSection('OSX_Notify')
CheckSection('Boxcar')
CheckSection('Songkick') CheckSection('Songkick')
CheckSection('Advanced') CheckSection('Advanced')
@@ -610,6 +615,14 @@ def initialize():
TWITTER_PASSWORD = check_setting_str(CFG, 'Twitter', 'twitter_password', '') TWITTER_PASSWORD = check_setting_str(CFG, 'Twitter', 'twitter_password', '')
TWITTER_PREFIX = check_setting_str(CFG, 'Twitter', 'twitter_prefix', 'Headphones') 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_ENABLED = bool(check_setting_int(CFG, 'Songkick', 'songkick_enabled', 1))
SONGKICK_APIKEY = check_setting_str(CFG, 'Songkick', 'songkick_apikey', 'nd1We7dFW2RqxPw8') SONGKICK_APIKEY = check_setting_str(CFG, 'Songkick', 'songkick_apikey', 'nd1We7dFW2RqxPw8')
SONGKICK_LOCATION = check_setting_str(CFG, 'Songkick', 'songkick_location', '') SONGKICK_LOCATION = check_setting_str(CFG, 'Songkick', 'songkick_location', '')
@@ -1025,6 +1038,16 @@ def config_write():
new_config['Twitter']['twitter_password'] = TWITTER_PASSWORD new_config['Twitter']['twitter_password'] = TWITTER_PASSWORD
new_config['Twitter']['twitter_prefix'] = TWITTER_PREFIX 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'] = {}
new_config['Songkick']['songkick_enabled'] = int(SONGKICK_ENABLED) new_config['Songkick']['songkick_enabled'] = int(SONGKICK_ENABLED)
new_config['Songkick']['songkick_apikey'] = SONGKICK_APIKEY new_config['Songkick']['songkick_apikey'] = SONGKICK_APIKEY
@@ -1085,7 +1108,7 @@ def start():
if CHECK_GITHUB: if CHECK_GITHUB:
SCHED.add_interval_job(versioncheck.checkGithub, minutes=CHECK_GITHUB_INTERVAL) SCHED.add_interval_job(versioncheck.checkGithub, minutes=CHECK_GITHUB_INTERVAL)
if DOWNLOAD_SCAN_INTERVAL > 0: if DOWNLOAD_SCAN_INTERVAL > 0:
SCHED.add_interval_job(postprocessor.checkFolder, minutes=DOWNLOAD_SCAN_INTERVAL) SCHED.add_interval_job(postprocessor.checkFolder, minutes=DOWNLOAD_SCAN_INTERVAL)
SCHED.start() SCHED.start()
@@ -1093,7 +1116,7 @@ def start():
started = True started = True
def sig_handler(signum=None, frame=None): 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) logger.info("Signal %i caught, saving and exiting...", signum)
shutdown() shutdown()
+1 -1
View File
@@ -71,7 +71,7 @@ def switch(AlbumID, ReleaseID):
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID]) myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID])
# Update have track counts on index # 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']])) 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']} controlValueDict = {"ArtistID": newalbumdata['ArtistID']}
+11 -11
View File
@@ -274,7 +274,7 @@ def expand_subfolders(f):
difference = max(path_depths) - min(path_depths) difference = max(path_depths) - min(path_depths)
if difference > 0: 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 # 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 # 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 = [ 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 ])) 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 return
# Convert back to paths and remove duplicates, which may be there after # 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.") logger.debug("Did not expand subfolder, as it resulted in one folder.")
return return
logger.debug("Expanded subfolders in folder: " % media_folders) logger.debug("Expanded subfolders in folder: %s", media_folders)
return media_folders return media_folders
def extract_data(s): def extract_data(s):
@@ -360,7 +360,7 @@ def extract_metadata(f):
# Try to read the file info # Try to read the file info
try: try:
media_file = MediaFile(os.path.join(root, file)) media_file = MediaFile(os.path.join(root, file))
except FileTypeError, UnreadableFileError: except (FileTypeError, UnreadableFileError):
# Probably not a media file # Probably not a media file
continue continue
@@ -374,14 +374,14 @@ def extract_metadata(f):
# Verify results # Verify results
if len(results) == 0: 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) return (None, None, None)
# Require that some percentage of files have tags # Require that some percentage of files have tags
count_ratio = 0.75 count_ratio = 0.75
if count < (count_ratio * len(results)): 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) return (None, None, None)
# Count distinct values # Count distinct values
@@ -399,7 +399,7 @@ def extract_metadata(f):
old_album = new_albums[index] old_album = new_albums[index]
new_albums[index] = RE_CD_ALBUM.sub("", album).strip() 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 # Remove duplicates
new_albums = list(set(new_albums)) new_albums = list(set(new_albums))
@@ -417,7 +417,7 @@ def extract_metadata(f):
if len(artists) > 1 and len(albums) == 1: if len(artists) > 1 and len(albums) == 1:
split_artists = [ RE_FEATURING.split(artist) for artist in artists ] split_artists = [ RE_FEATURING.split(artist) for artist in artists ]
featurings = [ len(split_artist) - 1 for split_artist in split_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: if sum(featurings) > 0:
# Find the artist of which the least splits have been generated. # 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]) return (artist, albums[0], years[0])
# Not sure what to do here. # 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.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.debug("Artists: %s, Albums: %s, Years: %s", artists, albums, years)
return (None, None, None) return (None, None, None)
@@ -486,7 +486,7 @@ def smartMove(src, dest, delete=True):
filename = os.path.basename(src) filename = os.path.basename(src)
if os.path.isfile(os.path.join(dest, filename)): 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] title = os.path.splitext(filename)[0]
ext = os.path.splitext(filename)[1] ext = os.path.splitext(filename)[1]
i = 1 i = 1
+28 -18
View File
@@ -210,6 +210,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [items['AlbumID']]) 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'])) logger.info("[%s] Removing all references to release group %s to reflect MusicBrainz" % (artist['artist_name'], items['AlbumID']))
force_repackage = 1 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: 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']) 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'])
@@ -469,10 +472,34 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
if skip_log == 0: if skip_log == 0:
logger.info(u"[%s] No new releases, so no changes made to %s" % (artist['artist_name'], rg['title'])) 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() 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=?', [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 ?', [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} controlValueDict = {"ArtistID": artistid}
@@ -493,23 +520,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
myDB.upsert("artists", newValueDict, controlValueDict) 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): def addReleaseById(rid):
myDB = db.DBConnection() myDB = db.DBConnection()
+3 -7
View File
@@ -55,7 +55,7 @@ def initLogger(verbose=1):
# Configure the logger to accept all messages # Configure the logger to accept all messages
logger.propagate = False logger.propagate = False
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG if verbose == 2 else logging.INFO)
# Setup file logger # Setup file logger
filename = os.path.join(headphones.LOG_DIR, FILENAME) filename = os.path.join(headphones.LOG_DIR, FILENAME)
@@ -69,7 +69,7 @@ def initLogger(verbose=1):
# Add list logger # Add list logger
loglist_handler = LogListHandler() loglist_handler = LogListHandler()
loglist_handler.setLevel(logging.INFO) loglist_handler.setLevel(logging.DEBUG)
logger.addHandler(loglist_handler) logger.addHandler(loglist_handler)
@@ -78,11 +78,7 @@ def initLogger(verbose=1):
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S') console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setFormatter(console_formatter) console_handler.setFormatter(console_formatter)
console_handler.setLevel(logging.DEBUG)
if verbose == 1:
console_handler.setLevel(logging.INFO)
elif verbose == 2:
console_handler.setLevel(logging.DEBUG)
logger.addHandler(console_handler) logger.addHandler(console_handler)
+8 -8
View File
@@ -13,8 +13,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from __future__ import with_statement
import time import time
import threading import threading
@@ -200,7 +198,7 @@ def getArtist(artistid, extrasonly=False):
if not extrasonly: if not extrasonly:
for rg in artist['release-group-list']: 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 continue
releasegroups.append({ releasegroups.append({
'title': unicode(rg['title']), 'title': unicode(rg['title']),
@@ -234,17 +232,19 @@ def getArtist(artistid, extrasonly=False):
for include in includes: for include in includes:
artist = None mb_extras_list = []
try: 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: except WebServiceError, e:
logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e)))
time.sleep(5) time.sleep(5)
if not artist: for rg in mb_extras_list:
continue
for rg in artist['release-group-list']:
releasegroups.append({ releasegroups.append({
'title': unicode(rg['title']), 'title': unicode(rg['title']),
'id': unicode(rg['id']), 'id': unicode(rg['id']),
+101 -19
View File
@@ -22,6 +22,7 @@ import simplejson
import os.path import os.path
import subprocess import subprocess
import gntp.notifier import gntp.notifier
import time
from xml.dom import minidom from xml.dom import minidom
from httplib import HTTPSConnection from httplib import HTTPSConnection
@@ -238,6 +239,7 @@ class XBMC:
except: except:
logger.warn('Error sending notification request to XBMC') logger.warn('Error sending notification request to XBMC')
class LMS: class LMS:
#Class for updating a Logitech Media Server #Class for updating a Logitech Media Server
@@ -247,10 +249,10 @@ class LMS:
self.hosts = headphones.LMS_HOST self.hosts = headphones.LMS_HOST
def _sendjson(self, 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) 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) req = urllib2.Request(host+'/jsonrpc.js', data, content)
@@ -261,20 +263,15 @@ class LMS:
return return
response = simplejson.JSONDecoder().decode(handle.read()) response = simplejson.JSONDecoder().decode(handle.read())
server_result = simplejson.dumps(response)
try: try:
return response[0]['result'] return response['result']
except: except:
logger.warn('LMS returned error: %s' % response[0]['error']) logger.warn('LMS returned error: %s' % response['error'])
return return
def update(self): 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(',')] hosts = [x.strip() for x in self.hosts.split(',')]
for host in hosts: for host in hosts:
@@ -361,12 +358,12 @@ class Plex:
for host in hosts: for host in hosts:
logger.info('Sending notification command to Plex Media Server @ '+host) logger.info('Sending notification command to Plex Media Server @ '+host)
try: try:
notification = header + "," + message + "," + time + "," + albumartpath notification = header + "," + message + "," + time + "," + albumartpath
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification('+notification+')'} notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification('+notification+')'}
request = self._sendhttp(host, notifycommand) request = self._sendhttp(host, notifycommand)
if not request: if not request:
raise Exception raise Exception
except: except:
logger.warn('Error sending notification request to Plex Media Server') logger.warn('Error sending notification request to Plex Media Server')
@@ -454,18 +451,17 @@ class PUSHBULLET:
self.notify('Main Screen Activate', 'Test Message') self.notify('Main Screen Activate', 'Test Message')
class PUSHALOT: class PUSHALOT:
def notify(self, message, event): def notify(self, message, event):
if not headphones.PUSHALOT_ENABLED: if not headphones.PUSHALOT_ENABLED:
return return
pushalot_authorizationtoken = headphones.PUSHALOT_APIKEY pushalot_authorizationtoken = headphones.PUSHALOT_APIKEY
logger.debug(u"Pushalot event: " + event) logger.debug(u"Pushalot event: " + event)
logger.debug(u"Pushalot message: " + message) logger.debug(u"Pushalot message: " + message)
logger.debug(u"Pushalot api: " + pushalot_authorizationtoken) logger.debug(u"Pushalot api: " + pushalot_authorizationtoken)
http_handler = HTTPSConnection("pushalot.com") http_handler = HTTPSConnection("pushalot.com")
@@ -529,6 +525,7 @@ class Synoindex:
if isinstance(path_list, list): if isinstance(path_list, list):
for path in path_list: for path in path_list:
self.notify(path) self.notify(path)
class PUSHOVER: class PUSHOVER:
application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz" application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz"
@@ -694,3 +691,88 @@ class TwitterNotifier:
return self._send_tweet(prefix+": "+message) return self._send_tweet(prefix+": "+message)
notifier = TwitterNotifier 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
+69 -43
View File
@@ -13,22 +13,20 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from __future__ import with_statement
import os import os
import time import re
import threading import shutil
import music_encoder
import shutil, re
import uuid import uuid
from headphones import notifiers
import beets import beets
from beets import autotag import threading
from beets.mediafile import MediaFile
import headphones 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() postprocessor_lock = threading.Lock()
@@ -48,9 +46,9 @@ def checkFolder():
# iterations in just in case we can't read the config for some reason # iterations in just in case we can't read the config for some reason
nzb_album_possibilities = [ album['FolderName'], nzb_album_possibilities = [ album['FolderName'],
sab_replace_dots(album['FolderName']), helpers.sab_replace_dots(album['FolderName']),
sab_replace_spaces(album['FolderName']), helpers.sab_replace_spaces(album['FolderName']),
sab_replace_spaces(sab_replace_dots(album['FolderName'])) helpers.sab_replace_spaces(sab_replace_dots(album['FolderName']))
] ]
for nzb_folder_name in nzb_album_possibilities: for nzb_folder_name in nzb_album_possibilities:
@@ -80,8 +78,6 @@ def verify(albumid, albumpath, Kind=None, forced=False):
#from an RSS feed or etc #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: 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? #TODO: odd things can happen when there are diacritic characters in the folder name, need to translate them?
import mb
release_list = None release_list = None
try: try:
@@ -350,6 +346,27 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
downloaded_track_list.append(os.path.join(r, files)) downloaded_track_list.append(os.path.join(r, files))
elif files.lower().endswith('.cue'): elif files.lower().endswith('.cue'):
downloaded_cuecount += 1 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 equired for some post processing steps: %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
return
#start encoding #start encoding
if headphones.MUSIC_ENCODER: if headphones.MUSIC_ENCODER:
downloaded_track_list=music_encoder.encode(albumpath) downloaded_track_list=music_encoder.encode(albumpath)
@@ -474,6 +491,17 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
twitter = notifiers.TwitterNotifier() twitter = notifiers.TwitterNotifier()
twitter.notify_download(pushmessage) 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'])
def embedAlbumArt(artwork, downloaded_track_list): def embedAlbumArt(artwork, downloaded_track_list):
logger.info('Embedding album art') logger.info('Embedding album art')
@@ -632,7 +660,7 @@ def moveFiles(albumpath, release, tracks):
try: try:
shutil.rmtree(lossless_destination_path) shutil.rmtree(lossless_destination_path)
except Exception, e: 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 create_duplicate_folder = True
if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder: if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
@@ -665,7 +693,7 @@ def moveFiles(albumpath, release, tracks):
try: try:
shutil.rmtree(lossy_destination_path) shutil.rmtree(lossy_destination_path)
except Exception, e: 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 create_duplicate_folder = True
if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder: if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
@@ -746,13 +774,13 @@ def moveFiles(albumpath, release, tracks):
try: try:
os.chmod(os.path.normpath(temp_f).encode(headphones.SYS_ENCODING, 'replace'), int(headphones.FOLDER_PERMISSIONS, 8)) os.chmod(os.path.normpath(temp_f).encode(headphones.SYS_ENCODING, 'replace'), int(headphones.FOLDER_PERMISSIONS, 8))
except Exception, e: 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 # If we failed to move all the files out of the directory, this will fail too
try: try:
shutil.rmtree(albumpath) shutil.rmtree(albumpath)
except Exception, e: 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 = [] destination_paths = []
@@ -779,10 +807,10 @@ def correctMetadata(albumid, release, downloaded_track_list):
elif any(downloaded_track.lower().endswith('.' + x.lower()) for x in headphones.LOSSY_MEDIA_FORMATS): 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)) lossy_items.append(beets.library.Item.from_path(downloaded_track))
else: 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: 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]: for items in [lossy_items, lossless_items]:
@@ -792,16 +820,16 @@ def correctMetadata(albumid, release, downloaded_track_list):
try: try:
cur_artist, cur_album, candidates, rec = autotag.tag_album(items, search_artist=helpers.latinToAscii(release['ArtistName']), search_album=helpers.latinToAscii(release['AlbumTitle'])) 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: except Exception, e:
logger.error('Error getting recommendation: %s. Not writing metadata' % e) logger.error('Error getting recommendation: %s. Not writing metadata', e)
return return
if str(rec) == 'recommendation.none': 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 return
if candidates: if candidates:
dist, info, mapping, extra_items, extra_tracks = candidates[0] dist, info, mapping, extra_items, extra_tracks = candidates[0]
else: 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 return
logger.info('Beets recommendation for tagging items: %s' % rec) logger.info('Beets recommendation for tagging items: %s' % rec)
@@ -813,9 +841,9 @@ def correctMetadata(albumid, release, downloaded_track_list):
for item in items: for item in items:
try: try:
item.write() 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: 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): def embedLyrics(downloaded_track_list):
logger.info('Adding lyrics') logger.info('Adding lyrics')
@@ -827,7 +855,7 @@ def embedLyrics(downloaded_track_list):
try: try:
f = MediaFile(downloaded_track) f = MediaFile(downloaded_track)
except: 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 continue
if f.albumartist and f.title: if f.albumartist and f.title:
@@ -835,16 +863,16 @@ def embedLyrics(downloaded_track_list):
elif f.artist and f.title: elif f.artist and f.title:
metalyrics = lyrics.getLyrics(f.artist, f.title) metalyrics = lyrics.getLyrics(f.artist, f.title)
else: 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 metalyrics = None
if lyrics: 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 f.lyrics = metalyrics
try: try:
f.save() f.save()
except: 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 continue
def renameFiles(albumpath, downloaded_track_list, release): def renameFiles(albumpath, downloaded_track_list, release):
@@ -859,7 +887,7 @@ def renameFiles(albumpath, downloaded_track_list, release):
try: try:
f = MediaFile(downloaded_track) f = MediaFile(downloaded_track)
except: 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 continue
if not f.disc: if not f.disc:
@@ -928,11 +956,11 @@ def renameFiles(albumpath, downloaded_track_list, release):
logger.debug("Renaming for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " is not neccessary") logger.debug("Renaming for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " is not neccessary")
continue 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: try:
os.rename(downloaded_track, new_file) os.rename(downloaded_track, new_file)
except Exception, e: 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 continue
def updateFilePermissions(albumpaths): def updateFilePermissions(albumpaths):
@@ -1044,7 +1072,6 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
continue continue
else: else:
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album) logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
from headphones import mb
try: try:
rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album)) rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album))
except: except:
@@ -1072,7 +1099,6 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
continue continue
else: else:
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album) logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
from headphones import mb
try: try:
rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album)) rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album))
except: except:
@@ -1108,17 +1134,18 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
continue continue
# Attempt 4: Hail mary. Just assume the folder name is the album name if it doesn't have a separator in it # 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: logger.debug('Attempt to extract album name by assuming it is the folder name')
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?', [folder]).fetchone()
if '-' not in folder_basename:
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?', [folder_basename]).fetchone()
if release: 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']) logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder) verify(release['AlbumID'], folder)
continue continue
else: else:
logger.info('Querying MusicBrainz for the release group id for: %s', folder) logger.info('Querying MusicBrainz for the release group id for: %s', folder_basename)
from headphones import mb
try: try:
rgid = mb.findAlbumID(album=helpers.latinToAscii(folder)) rgid = mb.findAlbumID(album=helpers.latinToAscii(folder_basename))
except: except:
logger.error('Can not get release information for this album') logger.error('Can not get release information for this album')
rgid = None rgid = None
@@ -1128,4 +1155,3 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
continue continue
else: else:
logger.info('No match found on MusicBrainz for: %s - %s', name, album) logger.info('No match found on MusicBrainz for: %s - %s', name, album)
+5 -2
View File
@@ -30,8 +30,11 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N
# is white listed. # is white listed.
if whitelist_status_code and auto_raise: if whitelist_status_code and auto_raise:
if response.status_code not in whitelist_status_code: 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() response.raise_for_status()
except:
logger.debug("Response status code %d is not white listed, raised exception", response.status_code)
raise
elif auto_raise: elif auto_raise:
response.raise_for_status() response.raise_for_status()
-29
View File
@@ -119,35 +119,6 @@ def sendNZB(nzb):
if sabText == "ok": if sabText == "ok":
logger.info(u"NZB sent to SAB successfully") 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 return True
elif sabText == "Missing authentication": elif sabText == "Missing authentication":
logger.info(u"Incorrect username/password sent to SAB, NZB not sent") logger.info(u"Incorrect username/password sent to SAB, NZB not sent")
+60 -7
View File
@@ -31,7 +31,7 @@ import subprocess
import headphones import headphones
from headphones.common import USER_AGENT from headphones.common import USER_AGENT
from headphones import logger, db, helpers, classes, sab, nzbget, request from headphones import logger, db, helpers, classes, sab, nzbget, request
from headphones import transmission from headphones import transmission, notifiers
import lib.bencode as bencode import lib.bencode as bencode
@@ -359,6 +359,9 @@ def searchNZB(album, new=False, losslessOnly=False):
logger.info("Album type is audiobook/spokenword. Using audiobook category") logger.info("Album type is audiobook/spokenword. Using audiobook category")
for newznab_host in newznab_hosts: for newznab_host in newznab_hosts:
provider = newznab_host[0]
# Add a little mod for kere.ws # Add a little mod for kere.ws
if newznab_host[0] == "http://kere.ws": if newznab_host[0] == "http://kere.ws":
if categories == "3040": if categories == "3040":
@@ -573,14 +576,16 @@ def send_to_downloader(data, bestqual, album):
nzb = classes.NZBDataSearchResult() nzb = classes.NZBDataSearchResult()
nzb.extraInfo.append(data) nzb.extraInfo.append(data)
nzb.name = folder_name nzb.name = folder_name
nzbget.sendNZB(nzb) if not nzbget.sendNZB(nzb):
return
elif headphones.NZB_DOWNLOADER == 0: elif headphones.NZB_DOWNLOADER == 0:
nzb = classes.NZBDataSearchResult() nzb = classes.NZBDataSearchResult()
nzb.extraInfo.append(data) nzb.extraInfo.append(data)
nzb.name = folder_name 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 # 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() (replace_spaces, replace_dots) = sab.checkConfig()
@@ -695,6 +700,54 @@ def send_to_downloader(data, bestqual, album):
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']]) 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]) 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_nzb=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): def verifyresult(title, artistterm, term, lossless):
title = re.sub('[\.\-\/\_]', ' ', title) title = re.sub('[\.\-\/\_]', ' ', title)
@@ -862,7 +915,7 @@ def searchTorrent(album, new=False, losslessOnly=False):
minimumseeders = int(headphones.NUMBEROFSEEDERS) - 1 minimumseeders = int(headphones.NUMBEROFSEEDERS) - 1
if headphones.KAT: if headphones.KAT:
provider = "Kick Ass Torrent" provider = "Kick Ass Torrents"
providerurl = url_fix("http://kickass.to/usearch/" + term) providerurl = url_fix("http://kickass.to/usearch/" + term)
if headphones.PREFERRED_QUALITY == 3 or losslessOnly: if headphones.PREFERRED_QUALITY == 3 or losslessOnly:
categories = "7" #music categories = "7" #music
@@ -1051,7 +1104,7 @@ def searchTorrent(album, new=False, losslessOnly=False):
if re.search(bitrate, encoding_string, flags=re.I): if re.search(bitrate, encoding_string, flags=re.I):
bitrate_string = encoding_string bitrate_string = encoding_string
if bitrate_string not in gazelleencoding.ALL_ENCODINGS: 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 maxsize = 10000000000
elif headphones.PREFERRED_QUALITY == 1: # Highest quality including lossless elif headphones.PREFERRED_QUALITY == 1: # Highest quality including lossless
search_formats = [gazelleformat.FLAC, gazelleformat.MP3] search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
@@ -1324,7 +1377,7 @@ def preprocess(resultlist):
#Get out of here if we're using Transmission or uTorrent #Get out of here if we're using Transmission or uTorrent
if headphones.TORRENT_DOWNLOADER != 0: if headphones.TORRENT_DOWNLOADER != 0:
return True, result return True, result
# get outta here if rutracker or piratebay # get outta here if rutracker
if result[3] == 'rutracker.org': if result[3] == 'rutracker.org':
return True, result return True, result
# Get out of here if it's a magnet link # Get out of here if it's a magnet link
@@ -1334,7 +1387,7 @@ def preprocess(resultlist):
# Download the torrent file # Download the torrent file
headers = {} headers = {}
if result[3] == 'Kick Ass Torrent': if result[3] == 'Kick Ass Torrents':
headers['Referer'] = 'http://kat.ph/' headers['Referer'] = 'http://kat.ph/'
elif result[3] == 'What.cd': elif result[3] == 'What.cd':
headers['User-Agent'] = 'Headphones' headers['User-Agent'] = 'Headphones'
-25
View File
@@ -49,31 +49,6 @@ def addTorrent(link):
retid = False retid = False
logger.info(u"Torrent sent to Transmission successfully") 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 return retid
else: else:
+44 -8
View File
@@ -163,7 +163,7 @@ class WebInterface(object):
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
getExtras.exposed = True getExtras.exposed = True
def removeExtras(self, ArtistID): def removeExtras(self, ArtistID, ArtistName):
myDB = db.DBConnection() myDB = db.DBConnection()
controlValueDict = {'ArtistID': ArtistID} controlValueDict = {'ArtistID': ArtistID}
newValueDict = {'IncludeExtras': 0} 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 albums WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']])
myDB.action('DELETE from allalbums 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 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) raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
removeExtras.exposed = True removeExtras.exposed = True
@@ -268,6 +269,7 @@ class WebInterface(object):
searcher.searchforalbum(mbid, new=True) searcher.searchforalbum(mbid, new=True)
if action == 'WantedLossless': if action == 'WantedLossless':
searcher.searchforalbum(mbid, lossless=True) searcher.searchforalbum(mbid, lossless=True)
myDB.action('UPDATE artists SET TotalTracks=(SELECT COUNT(*) FROM tracks, artists WHERE tracks.ArtistName = artists.ArtistName AND AlbumTitle IN (SELECT AlbumTitle FROM albums WHERE Status != "Ignored")) WHERE ArtistID=(SELECT ArtistID FROM albums WHERE AlbumID=?)', [mbid])
if ArtistID: if ArtistID:
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
else: else:
@@ -629,8 +631,8 @@ class WebInterface(object):
for rgid in rgids: for rgid in rgids:
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rgid['ReleaseGroupID']]) myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rgid['ReleaseGroupID']])
myDB.action('DELETE from allalbums WHERE AlbumID=?', [AlbumID]) myDB.action('DELETE from allalbums WHERE ArtistID=?', [ArtistID])
myDB.action('DELETE from alltracks WHERE AlbumID=?', [AlbumID]) myDB.action('DELETE from alltracks WHERE ArtistID=?', [ArtistID])
myDB.action('INSERT OR REPLACE into blacklist VALUES (?)', [ArtistID]) myDB.action('INSERT OR REPLACE into blacklist VALUES (?)', [ArtistID])
elif action == 'pause': elif action == 'pause':
controlValueDict = {'ArtistID': ArtistID} controlValueDict = {'ArtistID': ArtistID}
@@ -724,6 +726,11 @@ class WebInterface(object):
return serve_template(templatename="logs.html", title="Log", lineList=headphones.LOG_LIST) return serve_template(templatename="logs.html", title="Log", lineList=headphones.LOG_LIST)
logs.exposed = True logs.exposed = True
def clearLogs(self):
headphones.LOG_LIST = []
logger.info("Web logs cleared")
raise cherrypy.HTTPRedirect("logs")
clearLogs.exposed = True
def getLog(self,iDisplayStart=0,iDisplayLength=100,iSortCol_0=0,sSortDir_0="desc",sSearch="",**kwargs): def getLog(self,iDisplayStart=0,iDisplayLength=100,iSortCol_0=0,sSortDir_0="desc",sSearch="",**kwargs):
@@ -1059,6 +1066,12 @@ class WebInterface(object):
"pushbullet_deviceid": headphones.PUSHBULLET_DEVICEID, "pushbullet_deviceid": headphones.PUSHBULLET_DEVICEID,
"twitter_enabled": checked(headphones.TWITTER_ENABLED), "twitter_enabled": checked(headphones.TWITTER_ENABLED),
"twitter_onsnatch": checked(headphones.TWITTER_ONSNATCH), "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_list": headphones.MIRRORLIST,
"mirror": headphones.MIRROR, "mirror": headphones.MIRROR,
"customhost": headphones.CUSTOMHOST, "customhost": headphones.CUSTOMHOST,
@@ -1105,10 +1118,11 @@ class WebInterface(object):
bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0, 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, 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, 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, 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,
customsleep=None, hpuser=None, hppass=None, preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, cache_sizemb=None, 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,
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, 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_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): 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):
headphones.HTTP_HOST = http_host headphones.HTTP_HOST = http_host
headphones.HTTP_PORT = http_port headphones.HTTP_PORT = http_port
@@ -1268,6 +1282,15 @@ class WebInterface(object):
headphones.SONGKICK_FILTER_ENABLED = songkick_filter_enabled headphones.SONGKICK_FILTER_ENABLED = songkick_filter_enabled
headphones.TWITTER_ENABLED = twitter_enabled headphones.TWITTER_ENABLED = twitter_enabled
headphones.TWITTER_ONSNATCH = twitter_onsnatch 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.MIRROR = mirror headphones.MIRROR = mirror
headphones.CUSTOMHOST = customhost headphones.CUSTOMHOST = customhost
headphones.CUSTOMPORT = customport headphones.CUSTOMPORT = customport
@@ -1435,6 +1458,19 @@ class WebInterface(object):
return "Error sending tweet" return "Error sending tweet"
testTwitter.exposed = True 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): class Artwork(object):
def index(self): def index(self):
return "Artwork" return "Artwork"
+2 -1
View File
@@ -14,6 +14,7 @@
# This particular version has been slightly modified to work with headphones # This particular version has been slightly modified to work with headphones
# https://github.com/rembo10/headphones # https://github.com/rembo10/headphones
import os
__version__ = '1.3.4' __version__ = '1.3.4'
__author__ = 'Adrian Sampson <adrian@radbox.org>' __author__ = 'Adrian Sampson <adrian@radbox.org>'
@@ -23,4 +24,4 @@ from beets.util import confit
Library = beets.library.Library Library = beets.library.Library
config = confit.LazyConfig('beets', __name__) config = confit.LazyConfig(os.path.dirname(__file__), __name__)
View File
BIN
View File
Binary file not shown.
+135
View File
@@ -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)