Compare commits

...
89 Commits
Author SHA1 Message Date
rembo10 e67d32445f Merge branch 'develop' 2014-05-05 23:49:47 -07:00
rembo10 83280f3f99 Fix for #1603, typo in utorrent 2014-05-05 23:43:32 -07:00
rembo10 1d7415b91d Catch errors if we can't grab release info in post processor (#1602) 2014-05-05 18:56:47 -07:00
rembo10 bc5a042dcc Merge branch 'master' into develop 2014-05-05 17:46:25 -07:00
rembo10 e5af0bdd52 Merge branch 'develop' 2014-05-05 17:44:26 -07:00
rembo10 dc2939a4f9 Merge branch 'artist-page-refresh' into develop 2014-05-05 17:38:30 -07:00
rembo10 f52ec56a0f Couple fixes to get everything looking nice 2014-05-05 17:38:18 -07:00
rembo10 75b38df3d6 Typo 2014-05-05 16:58:31 -07:00
rembo10 0f5aeea345 some fixes 2014-05-05 16:58:31 -07:00
rembo10 d80fc36b29 Move to json so we can get the artist name as well 2014-05-05 16:58:31 -07:00
rembo10 4ca42c9c11 Initial changes to stop artist page from refreshing when artist is loading 2014-05-05 16:58:31 -07:00
rembo10 6c760902d6 Merge branch 'develop' 2014-05-05 16:49:58 -07:00
rembo10 2a0fcc7566 Fix for importer failing on adding new release groups 2014-05-05 16:49:43 -07:00
rembo10 b763ccdd08 Don't update all artist statuses when appending a single dir from the postprocessor. Also, don't change any other status but Skipped->Downloaded 2014-05-05 16:41:55 -07:00
rembo10 e72a0d1141 Merge pull request #1599 from delphiactual/master
Fix version check
2014-05-05 16:05:28 -07:00
rembo10 295e5a1ccf Merge branch 'develop': update Headphones Indexer url, fix for #1598, artist updates not working 2014-05-05 16:01:36 -07:00
rembo10 497fafb555 Don't need to query the db twice 2014-05-05 15:09:58 -07:00
rembo10 585f556006 Fix for #1598: Active artist update no longer updates any existing release groups 2014-05-05 14:26:44 -07:00
delphiactual 7b115126c9 Fix version check 2014-05-03 23:10:57 -06:00
rembo10 1a9165a084 Merge remote-tracking branch 'delphiactual/develop' into develop 2014-05-03 18:41:14 -07:00
rembo10 4a74d54cdc Merge remote-tracking branch 'tocsick/master' into develop 2014-05-03 18:40:11 -07:00
rembo10 a0755d4b1c Headphones indexer changes 2014-05-03 18:38:16 -07:00
delphiactual 084181a9a5 password field 2014-05-02 00:46:20 -06:00
delphiactual 5c381b9410 Added Labeling based off of hash for utorrent and fixed a typo in postprocessor.py 2014-05-01 21:58:05 -06:00
rembo10 0be9500518 Merge branch 'utorrent' into develop 2014-05-01 01:10:46 -07:00
rembo10 9c2da72dcc Bug fixes to get utorrent working 2014-05-01 01:09:26 -07:00
rembo10 3f6145cd07 Took out debug logging, put the torrent checking in the right place 2014-05-01 00:28:50 -07:00
rembo10 e155f19d0d Debug logging 2014-05-01 00:14:10 -07:00
rembo10 0c7ea8a5a0 typo 2014-05-01 00:09:45 -07:00
rembo10 1f3474a154 bug fix 2014-05-01 00:06:25 -07:00
rembo10 c0c1f9cc61 Some changes. Getting close 2014-05-01 00:02:29 -07:00
rembo10 e4382c23a3 fixin some bugs 2014-04-30 22:38:20 -07:00
rembo10 29b5305ab4 Slowly making progress 2014-04-30 21:58:46 -07:00
rembo10 758154e87e Some initial changes for utorrent 2014-04-30 21:00:30 -07:00
Kane Wallmann ea8b1815f6 Fixed tab indent to match file 2014-04-30 20:18:48 +10:00
Kane Wallmann ebc0b7205f Added MPC notifier, calls 'mpc update' on local machine 2014-04-30 19:53:59 +10:00
rembo10 06ee770184 Bug fix for postprocessor fix 2014-04-28 20:08:22 -07:00
rembo10 900325186b Url fix for pirate bay (credit to dsm1212) 2014-04-28 20:03:02 -07:00
rembo10 3b9816f3b3 Took out logging messages. Took out nzb possibilities from checkFolder since we're doing it in searcher.py anyways 2014-04-28 19:52:25 -07:00
rembo10 3e5779e5ce more log messages for testing #1575 issue 2014-04-28 19:22:28 -07:00
rembo10 66d0cb4a1b Added some more logging to see why postprocessor is not automatically running 2014-04-28 15:56:52 -07:00
rembo10 54c85c7951 Added some logging to see why postprocessor is not automatically running 2014-04-28 15:55:42 -07:00
rembo10 941da4be1c Merge remote-tracking branch 'sildur/master' into develop 2014-04-28 15:26:13 -07:00
rembo10 69c94baf18 Merge remote-tracking branch 'texke/master' into develop 2014-04-28 15:25:40 -07:00
Jens Rogier 90016f6eda Cleaned up query some more 2014-04-20 19:23:10 +02:00
Roberto Romero 06b647e30d Fix error: AttributeError: 'module' object has no attribute 'POST' 2014-04-19 01:41:23 -03:00
rembo10 84ebcb84f9 Changed 'newznab' -> 'custom newznab providers' to hopefully get rid of some confusion 2014-04-18 11:04:52 -07:00
Jens Rogier b11610face Fixed major mistake where tracklisting on the index would count the total tracks of the whole database when updating albums of the particular artist. 2014-04-18 10:40:15 +02:00
rembo10 6e90419014 Don't need to get commits behind if current version == latest version (versioncheck.py) 2014-04-17 20:44:28 -07:00
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
31 changed files with 984 additions and 342 deletions
+8
View File
@@ -172,6 +172,14 @@ table.display tr.even.gradeA {
background-color: #ddffdd;
}
table.display tr.odd.gradeI {
background-color: #bebebe;
}
table.display tr.even.gradeI {
background-color: #bebebe;
}
table.display tr.odd.gradeC {
background-color: #ddddff;
}
+1 -1
View File
@@ -13,7 +13,7 @@
<div id="subhead_menu">
<a id="menu_link_delete" href="deleteAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Album</a>
%if album['Status'] == 'Skipped':
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
<a id="menu_link_wanted" href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=False', $(this),true)" data-success="'${album['AlbumTitle']}' added to queue"><i class="fa fa-heart"></i> Mark Album as Wanted</a>
%elif album['Status'] == 'Wanted':
<a id="menu_link_check" href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this));" data-success="Forced checking successful"><i class="fa fa-search"></i> Force Check</a>
+49 -22
View File
@@ -8,7 +8,7 @@
<%def name="headerIncludes()">
<div id="subhead_container">
<div id="subhead_menu">
<a id="menu_link_refresh" onclick="doAjaxCall('refreshArtist?ArtistID=${artist['ArtistID']}', $(this)),'table'" href="#" data-success="'${artist['ArtistName']}' is being refreshed"><i class="fa fa-refresh"></i> Refresh Artist</a>
<a id="menu_link_refresh" onclick="doSimpleAjaxCall('refreshArtist?ArtistID=${artist['ArtistID']}')" href="#"><i class="fa fa-refresh"></i> Refresh Artist</a>
<a id="menu_link_delete" href="deleteArtist?ArtistID=${artist['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Artist</a>
%if artist['Status'] == 'Paused':
<a id="menu_link_resume" href="#" onclick="doAjaxCall('resumeArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} resumed"><i class="fa fa-play"></i> Resume Artist</a>
@@ -16,7 +16,7 @@
<a id="menu_link_pauze" href="#" onclick="doAjaxCall('pauseArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} paused"><i class="fa fa-pause"></i> Pause Artist</a>
%endif
%if artist['IncludeExtras']:
<a id="menu_link_removeextra" href="#" onclick="doAjaxCall('removeExtras?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="Extras removed for ${artist['ArtistName']}"><i class="fa fa-minus"></i> Remove Extras</a>
<a id="menu_link_removeextra" href="#" onclick="doAjaxCall('removeExtras?ArtistID=${artist['ArtistID']}&ArtistName=${artist['ArtistName']}',$(this),'submenu&table')" data-success="Extras removed for ${artist['ArtistName']}"><i class="fa fa-minus"></i> Remove Extras</a>
<a class="menu_link_edit" id="menu_link_modifyextra" href="#"><i class="fa fa-pencil"></i> Modify Extras</a>
%else:
<a id="menu_link_getextra" href="#"><i class="fa fa-plus"></i> Get Extras</a>
@@ -42,14 +42,8 @@
<div id="artistImg">
<img id="artistImage" class="albumArt" alt="" src="artwork/artist/${artist['ArtistID']}"/>
</div>
<h1>
%if artist['Status'] == 'Loading':
<i class="fa fa-refresh fa-spin"></i>
%endif
<a href="http://musicbrainz.org/artist/${artist['ArtistID']}">${artist['ArtistName']}</a>
%if artist['Status'] == 'Loading':
<h3><i>(Album information for this artist is currently being loaded)</i></h3>
%endif
<h1 id="artistname">
<a href="http://musicbrainz.org/artist/${artist['ArtistID']}" id="artistnamelink">${artist['ArtistName']}</a>
</h1>
<div id="artistBio"></div>
</div>
@@ -62,6 +56,7 @@
<option value="Wanted">Wanted</option>
<option value="WantedNew">Wanted (new only)</option>
<option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option>
</select>
<input type="hidden" value="Go">
@@ -89,6 +84,8 @@
grade = 'X'
elif album['Status'] == 'Snatched':
grade = 'C'
elif album['Status'] == 'Ignored':
grade = 'I'
else:
grade = 'A'
@@ -123,12 +120,12 @@
%>
<tr class="grade${grade}">
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></td>
<td id="albumart"><img class="albumArt" id="${album['AlbumID']}" src="artwork/thumbs/album/${album['AlbumID']}" height="64" width="64"></td>
<td id="albumart"><img class="albumArtnostretch" id="${album['AlbumID']}" src="artwork/thumbs/album/${album['AlbumID']}" height="64" width="64"></td>
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
<td id="reldate">${album['ReleaseDate']}</td>
<td id="type">${album['Type']}</td>
<td id="status">${album['Status']}
%if album['Status'] == 'Skipped':
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
[<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' added to Wanted list">want</a>]
%elif (album['Status'] == 'Wanted' or album['Status'] == 'Wanted Lossless'):
[<a href="#" onclick="doAjaxCall('unqueueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' skipped">skip</a>]
@@ -153,9 +150,6 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
%if artist['Status'] == 'Loading':
<meta http-equiv="refresh" content="5">
%endif
</%def>
<%def name="javascriptIncludes()">
@@ -168,7 +162,6 @@
getInfo(elem,id,'artist');
}
<%
if headphones.SONGKICK_FILTER_ENABLED:
songkick_filter_enabled = "true"
@@ -237,6 +230,40 @@
});
}
var loadingMessage = false;
var spinner_active = false;
var loadingtext_active = false;
function checkArtistStatus() {
$.getJSON("getArtistjson?ArtistID=${artist['ArtistID']}", function(data) {
if (data['Status'] == "Loading"){
refreshTable();
$('#artistnamelink').text(data["ArtistName"]);
if (loadingMessage == false){
showMsg("Getting artist information",true);
loadingMessage = true;
}
if (spinner_active == false){
$('#artistname').prepend('<i class="fa fa-refresh fa-spin" id="artistnamespinner"></i>')
spinner_active = true;
}
if (loadingtext_active == false){
$('#artistname').append('<h3 id="loadingtext"><i>(Album information for this artist is currently being loaded)</i></h3>')
loadingtext_active = true;
}
}
else{
$('#artistnamespinner').remove()
$('#loadingtext').remove()
$('#ajaxMsg').empty()
$('#ajaxMsg').removeAttr('style')
spinner_active = false
loadingtext_active = false
loadingMessage = false
}
});
}
function initThisPage() {
$('#menu_link_getextra').click(function() {
$('#dialog').dialog();
@@ -245,10 +272,6 @@
$('#menu_link_modifyextra').click(function() {
$('#dialog').dialog();
});
%if artist['Status'] == 'Loading':
showMsg("Getting artist information",true);
%endif
$('#album_table').dataTable({
"bDestroy": true,
"aoColumns": [
@@ -288,8 +311,12 @@
initThisPage();
getArtistBio();
if( ${songkick_enabled} ){
getArtistsCalendar();
}
getArtistsCalendar();
}
checkArtistStatus();
setInterval(function(){
checkArtistStatus();
}, 1500);
});
</script>
+106 -11
View File
@@ -124,7 +124,7 @@
<td>
<fieldset>
<legend>Usenet</legend>
<input type="radio" name="nzb_downloader" id="nzb_downloader_sabnzbd" value="0" ${config['nzb_downloader_sabnzbd']}>Sabnzbd <input type="radio" name="nzb_downloader" id="nzb_downloader_nzbget" value="1" ${config['nzb_downloader_nzbget']}> NZBget <input type="radio" name="nzb_downloader" id="nzb_downloader_blackhole" value="2" ${config['nzb_downloader_blackhole']}>Black Hole
<input type="radio" name="nzb_downloader" id="nzb_downloader_sabnzbd" value="0" ${config['nzb_downloader_sabnzbd']}> Sabnzbd <input type="radio" name="nzb_downloader" id="nzb_downloader_nzbget" value="1" ${config['nzb_downloader_nzbget']}> NZBget <input type="radio" name="nzb_downloader" id="nzb_downloader_blackhole" value="2" ${config['nzb_downloader_blackhole']}> Black Hole
</fieldset>
<fieldset id="sabnzbd_options">
<div class="row">
@@ -192,7 +192,7 @@
<td>
<fieldset>
<legend>Torrents</legend>
<input type="radio" name="torrent_downloader" id="torrent_downloader_blackhole" value="0" ${config['torrent_downloader_blackhole']}>Black Hole <input type="radio" name="torrent_downloader" id="torrent_downloader_transmission" value="1" ${config['torrent_downloader_transmission']}> Transmission <!-- <input type="radio" name="torrent_downloader" id="torrent_downloader_utorrent" value="2" ${config['torrent_downloader_utorrent']}> uTorrent -->
<input type="radio" name="torrent_downloader" id="torrent_downloader_blackhole" value="0" ${config['torrent_downloader_blackhole']}> Black Hole <input type="radio" name="torrent_downloader" id="torrent_downloader_transmission" value="1" ${config['torrent_downloader_transmission']}> Transmission <input type="radio" name="torrent_downloader" id="torrent_downloader_utorrent" value="2" ${config['torrent_downloader_utorrent']}> uTorrent (Beta)
</fieldset>
<fieldset id="torrent_blackhole_options">
<div class="row">
@@ -237,7 +237,11 @@
</div>
<div class="row">
<label>uTorrent Password:</label>
<input type="text" name="utorrent_password" value="${config['utorrent_pass']}" size="30">
<input type="password" name="utorrent_password" value="${config['utorrent_pass']}" size="30">
</div>
<div class="row">
<label>uTorrent Label:</label>
<input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30">
</div>
</fieldset>
<fieldset id="general_torrent_options">
@@ -293,7 +297,7 @@
</div>
</fieldset>
<fieldset>
<legend>Newznab</legend>
<legend>Custom Newznab Providers</legend>
<div class="row checkbox">
<input id="usenewznab" type="checkbox" name="newznab" onclick="initConfigCheckbox($(this));" value="1" ${config['use_newznab']} /><label>Use Newznab</label>
</div>
@@ -678,9 +682,9 @@
</div>
</fieldset>
<fieldset>
<h3>LMS</h3>
<h3>Logitech Media Server</h3>
<div class="checkbox row">
<input type="checkbox" name="lms_enabled" id="lms" value="1" ${config['lms_enabled']} /><label>Enable Squeezebox Updates</label>
<input type="checkbox" name="lms_enabled" id="lms" value="1" ${config['lms_enabled']} /><label>Enable LMS Updates</label>
</div>
<div id="lmsoptions">
<div class="row">
@@ -690,8 +694,7 @@
</div>
</div>
</fieldset>
</td>
<td>
<fieldset>
<h3>Pushalot</h3>
<div class="checkbox row">
@@ -708,6 +711,10 @@
</div>
</div>
</fieldset>
</td>
<td>
<fieldset>
<h3>Synology NAS</h3>
<div class="checkbox row">
@@ -776,7 +783,48 @@
</div>
</div>
</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>
<fieldset>
<h3>MPC</h3>
<div class="checkbox row">
<input type="checkbox" name="mpc_enabled" id="mpc" value="1" ${config['mpc_enabled']} /><label>Enable MPC Update</label>
</div>
</fieldset>
<fieldset>
</td>
</tr>
</table>
<input type="button" class="configsubmit" value="Save Changes" onclick="doAjaxCall('configUpdate',$(this),'tabs',true);return false;" data-success="Changes saved successfully">
@@ -1427,6 +1475,7 @@
$("#pushbulletoptions").hide();
}
$("#pushbullet").click(function(){
if ($("#pushbullet").is(":checked"))
{
@@ -1457,8 +1506,48 @@
$("#twitteroptions").slideUp();
}
});
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();
}
@@ -1671,6 +1760,12 @@
function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
});
$('#osxnotifyregister').click(function () {
var osx_notify_app = $("#osx_notify_reg").val();
$.get("/osxnotifyregister", {'app': osx_notify_app}, function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut()
})
}
$(document).ready(function() {
@@ -224,6 +224,14 @@ table.display tr.even.gradeA {
background-color: #ddffdd;
}
table.display tr.odd.gradeI {
background-color: #bebebe;
}
table.display tr.even.gradeI {
background-color: #bebebe;
}
table.display tr.odd.gradeC {
background-color: #ebf5ff;
}
@@ -254,6 +262,13 @@ table.display tr.even.gradeU {
background-color: #eee;
}
table.display tr.odd.gradeW {
background-color: #ffffaa;
}
table.display tr.even.gradeW {
background-color: #ffffaa;
}
table.display tr.odd.gradeZ {
background-color: #FAFAFA;
@@ -269,6 +284,7 @@ table.display tr.gradeL #status {
}
table.display tr.gradeA td,
table.display tr.gradeC td,
table.display tr.gradeI td,
table.display tr.gradeX td,
table.display tr.gradeU td,
table.display tr.gradeZ td {border-bottom: 1px solid #FFF;}
+8
View File
@@ -163,6 +163,14 @@ img.albumArt {
max-height: 300px;
position: relative;
}
img.albumArt-nostretch {
float: left;
min-height: 64px;
min-width: 64px;
max-width: 250px;
max-height: 300px;
position: relative;
}
.title {
margin-bottom: 20px;
margin-top: 10px;
+8
View File
@@ -316,6 +316,10 @@ function doAjaxCall(url,elem,reload,form) {
}
if ( reload == "tabs") refreshTab();
if ( reload == "page") location.reload();
if ( reload == "submenu&table") {
refreshSubmenu();
refreshTable();
}
if ( form ) {
// Change the option to 'choose...'
$(formID + " select").children('option[disabled=disabled]').attr('selected','selected');
@@ -330,6 +334,10 @@ function doAjaxCall(url,elem,reload,form) {
});
}
function doSimpleAjaxCall(url) {
$.ajax(url);
}
function resetFilters(text){
if ( $(".dataTables_filter").length > 0 ) {
$(".dataTables_filter input").attr("placeholder","filter " + text + "");
+19 -6
View File
@@ -1,10 +1,18 @@
lossless<%inherit file="base.html"/>
<%inherit file="base.html"/>
<%!
from headphones import helpers
%>
<%def name="headerIncludes()">
<div id="subhead_container">
<div id="subhead_menu">
<a class="menu_link_edit" href="clearLogs"><i class="fa fa-trash-o"></i> Clear log</a>
</div>
</div>
</%def>
<%def name="body()">
<div class="title">
<div id="paddingheader">
<h1 class="clearfix"><i class="fa fa-flag"></i> Logs</h1>
</div>
<table class="display" id="log_table">
@@ -40,6 +48,8 @@ lossless<%inherit file="base.html"/>
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function() {
initActions();
$('#log_table').dataTable( {
"bProcessing": true,
"bServerSide": true,
@@ -49,23 +59,26 @@ $(document).ready(function() {
"iDisplayLength": 25,
"bStateSave": true,
"oLanguage": {
"sSearch":"",
"sSearch":"Filter:",
"sLengthMenu":"Show _MENU_ lines per page",
"sEmptyTable": "No log information available",
"sInfo":"Showing _START_ to _END_ of _TOTAL_ lines",
"sInfoEmpty":"Showing 0 to 0 of 0 lines",
"sInfoFiltered":"(filtered from _MAX_ total lines)"},
"fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
if (aData[1] === "WARNING" || aData[1] === "ERROR")
if (aData[1] === "ERROR")
{
$('td', nRow).closest('tr').addClass("gradeX");
}
else if (aData[1] === "WARNING")
{
$('td', nRow).closest('tr').addClass("gradeW");
}
else
{
$('td', nRow).closest('tr').addClass("gradeZ");
}
return nRow;
},
"fnServerData": function ( sSource, aoData, fnCallback ) {
+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=Upcoming"><i class="fa fa-calendar fa-fw"></i> Manage Upcoming Albums</a><br>
<a href="manageAlbums?Status=Wanted"><i class="fa fa-heart fa-fw"></i> </span>Manage Wanted Albums</a><br>
<a href="manageAlbums?Status=Ignored"><i class="fa fa-meh-o fa-fw"></i> </span>Manage Ignored Albums</a><br>
<br><br>
<a href="manageAlbums">Manage All Albums</a>
</div>
@@ -25,6 +25,7 @@
<option value="WantedNew">Wanted (new only)</option>
<option value="WantedLossless">Wanted (lossless)</option>
<option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option>
</select>
<input type="hidden" value="Go">
@@ -50,6 +51,8 @@
grade = 'Z'
elif album['Status'] == 'Wanted':
grade = 'X'
elif album['Status'] == 'Ignored':
grade = 'I'
elif album['Status'] == 'Snatched':
grade = 'C'
else:
+1
View File
@@ -20,6 +20,7 @@
<select name="action" onChange="doAjaxCall('markAlbums',$(this),'table',true);" data-error="You didn't select any albums">
<option disabled="disabled" selected="selected">Choose...</option>
<option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option>
</select>
<input type="hidden" value="Go">
+36 -9
View File
@@ -15,20 +15,17 @@
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
from __future__ import with_statement
import os, sys, subprocess
import threading
import webbrowser
import sqlite3
import itertools
import cherrypy
from lib.apscheduler.scheduler import Scheduler
from lib.configobj import ConfigObj
import cherrypy
from headphones import versioncheck, logger, version
from headphones.common import *
@@ -157,6 +154,7 @@ TRANSMISSION_PASSWORD = None
UTORRENT_HOST = None
UTORRENT_USERNAME = None
UTORRENT_PASSWORD = None
UTORRENT_LABEL = None
NEWZNAB = False
NEWZNAB_HOST = None
@@ -270,6 +268,12 @@ TWITTER_ONSNATCH = False
TWITTER_USERNAME = None
TWITTER_PASSWORD = None
TWITTER_PREFIX = None
OSX_NOTIFY_ENABLED = False
OSX_NOTIFY_ONSNATCH = False
OSX_NOTIFY_APP = None
BOXCAR_ENABLED = False
BOXCAR_ONSNATCH = False
BOXCAR_TOKEN = None
MIRRORLIST = ["musicbrainz.org","headphones","custom"]
MIRROR = None
CUSTOMHOST = None
@@ -281,6 +285,7 @@ SONGKICK_ENABLED = False
SONGKICK_APIKEY = None
SONGKICK_LOCATION = None
SONGKICK_FILTER_ENABLED = False
MPC_ENABLED = False
CACHE_SIZEMB = 32
JOURNAL_MODE = None
@@ -343,13 +348,13 @@ def initialize():
RUTRACKER, RUTRACKER_USER, RUTRACKER_PASSWORD, WHATCD, WHATCD_USERNAME, WHATCD_PASSWORD, DOWNLOAD_TORRENT_DIR, \
LIBRARYSCAN, LIBRARYSCAN_INTERVAL, DOWNLOAD_SCAN_INTERVAL, UPDATE_DB_INTERVAL, MB_IGNORE_AGE, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, \
NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_HOST, HEADPHONES_INDEXER, NZBMATRIX, TRANSMISSION_HOST, TRANSMISSION_USERNAME, TRANSMISSION_PASSWORD, \
UTORRENT_HOST, UTORRENT_USERNAME, UTORRENT_PASSWORD, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, NEWZNAB_ENABLED, EXTRA_NEWZNABS, \
UTORRENT_HOST, UTORRENT_USERNAME, UTORRENT_PASSWORD, UTORRENT_LABEL, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, NEWZNAB_ENABLED, EXTRA_NEWZNABS, \
NZBSORG, NZBSORG_UID, NZBSORG_HASH, NZBSRUS, NZBSRUS_UID, NZBSRUS_APIKEY, OMGWTFNZBS, OMGWTFNZBS_UID, OMGWTFNZBS_APIKEY, \
NZB_DOWNLOADER, TORRENT_DOWNLOADER, PREFERRED_WORDS, REQUIRED_WORDS, IGNORED_WORDS, LASTFM_USERNAME, \
INTERFACE, FOLDER_PERMISSIONS, FILE_PERMISSIONS, ENCODERFOLDER, ENCODER_PATH, ENCODER, XLDPROFILE, BITRATE, SAMPLINGFREQUENCY, \
MUSIC_ENCODER, ADVANCEDENCODER, ENCODEROUTPUTFORMAT, ENCODERQUALITY, ENCODERVBRCBR, ENCODERLOSSLESS, ENCODER_MULTICORE, ENCODER_MULTICORE_COUNT, DELETE_LOSSLESS_FILES, \
GROWL_ENABLED, GROWL_HOST, GROWL_PASSWORD, GROWL_ONSNATCH, PROWL_ENABLED, PROWL_PRIORITY, PROWL_KEYS, PROWL_ONSNATCH, PUSHOVER_ENABLED, PUSHOVER_PRIORITY, PUSHOVER_KEYS, PUSHOVER_ONSNATCH, PUSHOVER_APITOKEN, MIRRORLIST, \
TWITTER_ENABLED, TWITTER_ONSNATCH, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, \
TWITTER_ENABLED, TWITTER_ONSNATCH, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, OSX_NOTIFY_ENABLED, OSX_NOTIFY_ONSNATCH, OSX_NOTIFY_APP, BOXCAR_ENABLED, BOXCAR_ONSNATCH, BOXCAR_TOKEN, \
PUSHBULLET_ENABLED, PUSHBULLET_APIKEY, PUSHBULLET_DEVICEID, PUSHBULLET_ONSNATCH, \
MIRROR, CUSTOMHOST, CUSTOMPORT, CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, \
XBMC_NOTIFY, LMS_ENABLED, LMS_HOST, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \
@@ -386,6 +391,8 @@ def initialize():
CheckSection('Pushalot')
CheckSection('Synoindex')
CheckSection('Twitter')
CheckSection('OSX_Notify')
CheckSection('Boxcar')
CheckSection('Songkick')
CheckSection('Advanced')
@@ -506,6 +513,7 @@ def initialize():
UTORRENT_HOST = check_setting_str(CFG, 'uTorrent', 'utorrent_host', '')
UTORRENT_USERNAME = check_setting_str(CFG, 'uTorrent', 'utorrent_username', '')
UTORRENT_PASSWORD = check_setting_str(CFG, 'uTorrent', 'utorrent_password', '')
UTORRENT_LABEL = check_setting_str(CFG, 'uTorrent', 'utorrent_label', '')
NEWZNAB = bool(check_setting_int(CFG, 'Newznab', 'newznab', 0))
NEWZNAB_HOST = check_setting_str(CFG, 'Newznab', 'newznab_host', '')
@@ -609,7 +617,15 @@ def initialize():
TWITTER_USERNAME = check_setting_str(CFG, 'Twitter', 'twitter_username', '')
TWITTER_PASSWORD = check_setting_str(CFG, 'Twitter', 'twitter_password', '')
TWITTER_PREFIX = check_setting_str(CFG, 'Twitter', 'twitter_prefix', 'Headphones')
OSX_NOTIFY_ENABLED = bool(check_setting_int(CFG, 'OSX_Notify', 'osx_notify_enabled', 0))
OSX_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'OSX_Notify', 'osx_notify_onsnatch', 0))
OSX_NOTIFY_APP = check_setting_str(CFG, 'OSX_Notify', 'osx_notify_app', '/Applications/Headphones')
BOXCAR_ENABLED = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_enabled', 0))
BOXCAR_ONSNATCH = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_onsnatch', 0))
BOXCAR_TOKEN = check_setting_str(CFG, 'Boxcar', 'boxcar_token', '')
SONGKICK_ENABLED = bool(check_setting_int(CFG, 'Songkick', 'songkick_enabled', 1))
SONGKICK_APIKEY = check_setting_str(CFG, 'Songkick', 'songkick_apikey', 'nd1We7dFW2RqxPw8')
SONGKICK_LOCATION = check_setting_str(CFG, 'Songkick', 'songkick_location', '')
@@ -928,6 +944,7 @@ def config_write():
new_config['uTorrent']['utorrent_host'] = UTORRENT_HOST
new_config['uTorrent']['utorrent_username'] = UTORRENT_USERNAME
new_config['uTorrent']['utorrent_password'] = UTORRENT_PASSWORD
new_config['uTorrent']['utorrent_label'] = UTORRENT_LABEL
new_config['Newznab'] = {}
new_config['Newznab']['newznab'] = int(NEWZNAB)
@@ -1025,6 +1042,16 @@ def config_write():
new_config['Twitter']['twitter_password'] = TWITTER_PASSWORD
new_config['Twitter']['twitter_prefix'] = TWITTER_PREFIX
new_config['OSX_Notify'] = {}
new_config['OSX_Notify']['osx_notify_enabled'] = int(OSX_NOTIFY_ENABLED)
new_config['OSX_Notify']['osx_notify_onsnatch'] = int(OSX_NOTIFY_ONSNATCH)
new_config['OSX_Notify']['osx_notify_app'] = OSX_NOTIFY_APP
new_config['Boxcar'] = {}
new_config['Boxcar']['boxcar_enabled'] = int(BOXCAR_ENABLED)
new_config['Boxcar']['boxcar_onsnatch'] = int(BOXCAR_ONSNATCH)
new_config['Boxcar']['boxcar_token'] = BOXCAR_TOKEN
new_config['Songkick'] = {}
new_config['Songkick']['songkick_enabled'] = int(SONGKICK_ENABLED)
new_config['Songkick']['songkick_apikey'] = SONGKICK_APIKEY
@@ -1085,7 +1112,7 @@ def start():
if CHECK_GITHUB:
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.start()
@@ -1093,7 +1120,7 @@ def start():
started = True
def sig_handler(signum=None, frame=None):
if type(signum) != type(None):
if signum is not None:
logger.info("Signal %i caught, saving and exiting...", signum)
shutdown()
+1 -1
View File
@@ -71,7 +71,7 @@ def switch(AlbumID, ReleaseID):
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID])
# Update have track counts on index
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [newalbumdata['ArtistID']]))
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [newalbumdata['ArtistID']]))
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [newalbumdata['ArtistID']]))
controlValueDict = {"ArtistID": newalbumdata['ArtistID']}
+11 -11
View File
@@ -274,7 +274,7 @@ def expand_subfolders(f):
difference = max(path_depths) - min(path_depths)
if difference > 0:
logger.info("Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder" % (len(media_folders), difference))
logger.info("Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder.", len(media_folders), difference)
# While already failed, advice the user what he could try. We assume the
# directory may contain separate CD's and maybe some extra's. The
@@ -283,7 +283,7 @@ def expand_subfolders(f):
extra_media_folders = [ media_folder[:min(path_depths)] for media_folder in media_folders if len(media_folder) > min(path_depths) ]
extra_media_folders = list(set([ os.path.join(*media_folder) for media_folder in extra_media_folders ]))
logger.info("Please look at the following folder(s), since they cause the depth difference: %s" % extra_media_folders)
logger.info("Please look at the following folder(s), since they cause the depth difference: %s", extra_media_folders)
return
# Convert back to paths and remove duplicates, which may be there after
@@ -297,7 +297,7 @@ def expand_subfolders(f):
logger.debug("Did not expand subfolder, as it resulted in one folder.")
return
logger.debug("Expanded subfolders in folder: " % media_folders)
logger.debug("Expanded subfolders in folder: %s", media_folders)
return media_folders
def extract_data(s):
@@ -360,7 +360,7 @@ def extract_metadata(f):
# Try to read the file info
try:
media_file = MediaFile(os.path.join(root, file))
except FileTypeError, UnreadableFileError:
except (FileTypeError, UnreadableFileError):
# Probably not a media file
continue
@@ -374,14 +374,14 @@ def extract_metadata(f):
# Verify results
if len(results) == 0:
logger.info("No metadata in media files found, ignoring")
logger.info("No metadata in media files found, ignoring.")
return (None, None, None)
# Require that some percentage of files have tags
count_ratio = 0.75
if count < (count_ratio * len(results)):
logger.info("Counted %d media files, but only %d have tags, ignoring" % (count, len(results)))
logger.info("Counted %d media files, but only %d have tags, ignoring.", count, len(results))
return (None, None, None)
# Count distinct values
@@ -399,7 +399,7 @@ def extract_metadata(f):
old_album = new_albums[index]
new_albums[index] = RE_CD_ALBUM.sub("", album).strip()
logger.debug("Stripped albumd number identifier: %s -> %s" % (old_album, new_albums[index]))
logger.debug("Stripped albumd number identifier: %s -> %s", old_album, new_albums[index])
# Remove duplicates
new_albums = list(set(new_albums))
@@ -417,7 +417,7 @@ def extract_metadata(f):
if len(artists) > 1 and len(albums) == 1:
split_artists = [ RE_FEATURING.split(artist) for artist in artists ]
featurings = [ len(split_artist) - 1 for split_artist in split_artists ]
logger.info("Album seem to feature %d different artists" % sum(featurings))
logger.info("Album seem to feature %d different artists", sum(featurings))
if sum(featurings) > 0:
# Find the artist of which the least splits have been generated.
@@ -429,8 +429,8 @@ def extract_metadata(f):
return (artist, albums[0], years[0])
# Not sure what to do here.
logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring" % (len(artists), len(albums), len(years)))
logger.debug("Artists: %s, Albums: %s, Years: %s" % (artists, albums, years))
logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring", len(artists), len(albums), len(years))
logger.debug("Artists: %s, Albums: %s, Years: %s", artists, albums, years)
return (None, None, None)
@@ -486,7 +486,7 @@ def smartMove(src, dest, delete=True):
filename = os.path.basename(src)
if os.path.isfile(os.path.join(dest, filename)):
logger.info('Destination file exists: %s', os.path.join(dest, filename).decode(headphones.SYS_ENCODING, 'replace'))
logger.info('Destination file exists: %s', os.path.join(dest, filename))
title = os.path.splitext(filename)[0]
ext = os.path.splitext(filename)[1]
i = 1
+43 -34
View File
@@ -210,6 +210,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [items['AlbumID']])
logger.info("[%s] Removing all references to release group %s to reflect MusicBrainz" % (artist['artist_name'], items['AlbumID']))
force_repackage = 1
elif extrasonly:
# Not really sure what we're doing here but don't want to log the message below if we're fetching extras only
pass
else:
logger.info("[%s] There was either an error pulling data from MusicBrainz or there might not be any releases for this category" % artist['artist_name'])
@@ -223,15 +226,16 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
skip_log = 0
#Make a user configurable variable to skip update of albums with release dates older than this date (in days)
pause_delta = headphones.MB_IGNORE_AGE
check_release_date = myDB.action("SELECT ReleaseDate, Status from albums WHERE ArtistID=? AND AlbumTitle=?", (artistid, al_title)).fetchone()
#Skip update if Status set
if check_release_date and check_release_date[1]:
logger.info("[%s] Not updating: %s (Status is %s, skipping)" % (artist['artist_name'], rg['title'], check_release_date[1]))
continue
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
if not forcefull:
try:
check_release_date = rg_exists['ReleaseDate']
except TypeError:
check_release_date = None
if check_release_date:
if check_release_date[0] is None:
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
@@ -265,11 +269,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
logger.info("[%s] Now adding/updating: %s (Comprehensive Force)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid,includeExtras,forcefull)
#What this does is adds new releases per artist to the allalbums + alltracks databases
#new_releases = mb.get_new_releases(rgid,includeExtras)
#print al_title
#print new_releases
if new_releases != 0:
#Dump existing hybrid release since we're repackaging/replacing it
myDB.action("DELETE from albums WHERE ReleaseID=?", [rg['id']])
@@ -378,7 +377,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
# If there's no release in the main albums tables, add the default (hybrid)
# If there is a release, check the ReleaseID against the AlbumID to see if they differ (user updated)
# check if the album already exists
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
if not rg_exists:
releaseid = rg['id']
else:
@@ -399,11 +398,14 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
"ReleaseFormat": album['ReleaseFormat']
}
if not rg_exists:
if rg_exists:
newValueDict['DateAdded'] = rg_exists['DateAdded']
newValueDict['Status'] = rg_exists['Status']
else:
today = helpers.today()
newValueDict['DateAdded']= today
newValueDict['DateAdded'] = today
if headphones.AUTOWANT_ALL:
newValueDict['Status'] = "Wanted"
@@ -469,10 +471,34 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
if skip_log == 0:
logger.info(u"[%s] No new releases, so no changes made to %s" % (artist['artist_name'], rg['title']))
finalize_update(artistid, artist['artist_name'], errors)
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
cache.getThumb(ArtistID=artistid)
if errors:
logger.info("[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (artist['artist_name'], artist['artist_name']))
else:
myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']])
logger.info(u"Updating complete for: %s" % artist['artist_name'])
# Start searching for newly added albums
if album_searches:
from headphones import searcher
logger.info("Start searching for %d albums.", len(album_searches))
for album_search in album_searches:
searcher.searchforalbum(albumid=album_search)
def finalize_update(artistid, artistname, errors=False):
# Moving this little bit to it's own function so we can update have tracks & latest album when deleting extras
myDB = db.DBConnection()
latestalbum = myDB.action('SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC', [artistid]).fetchone()
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid]))
#havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['artist_name']]))
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist['artist_name']]))
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artistname]))
controlValueDict = {"ArtistID": artistid}
@@ -493,23 +519,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
myDB.upsert("artists", newValueDict, controlValueDict)
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
cache.getThumb(ArtistID=artistid)
if errors:
logger.info("[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (artist['artist_name'], artist['artist_name']))
else:
myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']])
logger.info(u"Updating complete for: %s" % artist['artist_name'])
# Start searching for newly added albums
if album_searches:
from headphones import searcher
logger.info("Start searching for %d albums.", len(album_searches))
for album_search in album_searches:
searcher.searchforalbum(albumid=album_search)
def addReleaseById(rid):
myDB = db.DBConnection()
+15 -7
View File
@@ -328,12 +328,12 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [ArtistID])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [ArtistName]))
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
update_album_status()
if not append:
update_album_status()
lastfm.getSimilar()
logger.info('Library scan complete')
#ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED
#ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED
def update_album_status(AlbumID=None):
myDB = db.DBConnection()
logger.info('Counting matched tracks to mark albums as skipped/downloaded')
@@ -355,13 +355,21 @@ def update_album_status(AlbumID=None):
album_completion = 0
logger.info('Album %s does not have any tracks in database' % album['AlbumTitle'])
if album_completion >= headphones.ALBUM_COMPLETION_PCT:
if album_completion >= headphones.ALBUM_COMPLETION_PCT and album['Status'] == 'Skipped':
new_album_status = "Downloaded"
# I don't think we want to change Downloaded->Skipped.....
# I think we can only automatically change Skipped->Downloaded when updating
# There was a bug report where this was causing infinite downloads if the album was
# recent, but matched to less than 80%. It would go Downloaded->Skipped->Wanted->Downloaded->Skipped->Wanted->etc....
#else:
# if album['Status'] == "Skipped" or album['Status'] == "Downloaded":
# new_album_status = "Skipped"
# else:
# new_album_status = album['Status']
else:
if album['Status'] == "Skipped" or album['Status'] == "Downloaded":
new_album_status = "Skipped"
else:
new_album_status = album['Status']
new_album_status = album['Status']
myDB.upsert("albums", {'Status' : new_album_status}, {'AlbumID' : album['AlbumID']})
if new_album_status != album['Status']:
logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status))
+3 -7
View File
@@ -55,7 +55,7 @@ def initLogger(verbose=1):
# Configure the logger to accept all messages
logger.propagate = False
logger.setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG if verbose == 2 else logging.INFO)
# Setup file logger
filename = os.path.join(headphones.LOG_DIR, FILENAME)
@@ -69,7 +69,7 @@ def initLogger(verbose=1):
# Add list logger
loglist_handler = LogListHandler()
loglist_handler.setLevel(logging.INFO)
loglist_handler.setLevel(logging.DEBUG)
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_handler = logging.StreamHandler()
console_handler.setFormatter(console_formatter)
if verbose == 1:
console_handler.setLevel(logging.INFO)
elif verbose == 2:
console_handler.setLevel(logging.DEBUG)
console_handler.setLevel(logging.DEBUG)
logger.addHandler(console_handler)
+11 -11
View File
@@ -13,8 +13,6 @@
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from __future__ import with_statement
import time
import threading
@@ -200,7 +198,7 @@ def getArtist(artistid, extrasonly=False):
if not extrasonly:
for rg in artist['release-group-list']:
if rg['type'] != 'Album': #only add releases without a secondary type
if "secondary-type-list" in rg.keys(): #only add releases without a secondary type
continue
releasegroups.append({
'title': unicode(rg['title']),
@@ -233,18 +231,20 @@ def getArtist(artistid, extrasonly=False):
i += 1
for include in includes:
artist = None
mb_extras_list = []
try:
artist = musicbrainzngs.get_artist_by_id(artistid,includes=["releases","release-groups"],release_status=['official'],release_type=include)['artist']
limit = 200
newRgs = None
while newRgs == None or len(newRgs) >= limit:
newRgs = musicbrainzngs.browse_release_groups(artistid,release_type=include,offset=len(mb_extras_list),limit=limit)['release-group-list']
mb_extras_list += newRgs
except WebServiceError, e:
logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e)))
time.sleep(5)
if not artist:
continue
for rg in artist['release-group-list']:
for rg in mb_extras_list:
releasegroups.append({
'title': unicode(rg['title']),
'id': unicode(rg['id']),
+170 -77
View File
@@ -22,6 +22,7 @@ import simplejson
import os.path
import subprocess
import gntp.notifier
import time
from xml.dom import minidom
from httplib import HTTPSConnection
@@ -132,7 +133,7 @@ class PROWL:
return
http_handler = HTTPSConnection("api.prowlapp.com")
data = {'apikey': headphones.PROWL_KEYS,
'application': 'Headphones',
'event': event,
@@ -167,7 +168,18 @@ class PROWL:
self.priority = priority
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
class MPC:
def __init__(self):
pass
def notify( self ):
subprocess.call( ["mpc", "update"] )
class XBMC:
def __init__(self):
@@ -191,9 +203,9 @@ class XBMC:
url = host + '/jsonrpc'
if self.password:
response = request.request_json(url, method="POST", data=simplejson.dumps(data), headers=headers, auth=(self.username, self.password))
response = request.request_json(url, method="post", data=simplejson.dumps(data), headers=headers, auth=(self.username, self.password))
else:
response = request.request_json(url, method="POST", data=simplejson.dumps(data), headers=headers)
response = request.request_json(url, method="post", data=simplejson.dumps(data), headers=headers)
if response:
return response[0]['result']
@@ -207,10 +219,10 @@ class XBMC:
for host in hosts:
logger.info('Sending library update command to XBMC @ '+host)
request = self._sendjson(host, 'AudioLibrary.Scan')
if not request:
logger.warn('Error sending update request to XBMC')
def notify(self, artist, album, albumartpath):
hosts = [x.strip() for x in self.hosts.split(',')]
@@ -238,19 +250,20 @@ class XBMC:
except:
logger.warn('Error sending notification request to XBMC')
class LMS:
#Class for updating a Logitech Media Server
def __init__(self):
self.hosts = headphones.LMS_HOST
def _sendjson(self, host):
data = {'id': 1, 'method': 'slim.request', 'params': ["",["rescan"]]} #Had a lot of trouble with simplejson, but this works.
data = {'id': 1, 'method': 'slim.request', 'params': ["",["rescan"]]}
data = simplejson.JSONEncoder().encode(data)
content = {'Content-Type': 'application/json', 'Content-Length': len(data)}
content = {'Content-Type': 'application/json'}
req = urllib2.Request(host+'/jsonrpc.js', data, content)
@@ -261,33 +274,28 @@ class LMS:
return
response = simplejson.JSONDecoder().decode(handle.read())
server_result = simplejson.dumps(response)
try:
return response[0]['result']
return response['result']
except:
logger.warn('LMS returned error: %s' % response[0]['error'])
logger.warn('LMS returned error: %s' % response['error'])
return
def update(self):
#Send the ["rescan"] command to an LMS server.
#Note that the command must be prefixed with the 'player' that the command is aimed at,
#But with this being a request for the server to update its library, the player is blank, so ""
hosts = [x.strip() for x in self.hosts.split(',')]
for host in hosts:
logger.info('Sending library rescan command to LMS @ '+host)
request = self._sendjson(host)
if not request:
logger.warn('Error sending rescan request to LMS')
class Plex:
def __init__(self):
self.server_hosts = headphones.PLEX_SERVER_HOST
self.client_hosts = headphones.PLEX_CLIENT_HOST
self.username = headphones.PLEX_USERNAME
@@ -297,31 +305,31 @@ class Plex:
username = self.username
password = self.password
url_command = urllib.urlencode(command)
url = host + '/xbmcCmds/xbmcHttp/?' + url_command
req = urllib2.Request(url)
if password:
base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '')
req.add_header("Authorization", "Basic %s" % base64string)
logger.info('Plex url: %s' % url)
try:
handle = urllib2.urlopen(req)
except Exception, e:
logger.warn('Error opening Plex url: %s' % e)
return
response = handle.read().decode(headphones.SYS_ENCODING)
return response
def update(self):
# From what I read you can't update the music library on a per directory or per path basis
# so need to update the whole thing
@@ -349,7 +357,7 @@ class Plex:
except Exception, e:
logger.warn("Error updating library section for Plex Media Server: %s" % e)
return False
def notify(self, artist, album, albumartpath):
hosts = [x.strip() for x in self.client_hosts.split(',')]
@@ -361,12 +369,12 @@ class Plex:
for host in hosts:
logger.info('Sending notification command to Plex Media Server @ '+host)
try:
notification = header + "," + message + "," + time + "," + albumartpath
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification('+notification+')'}
request = self._sendhttp(host, notifycommand)
notification = header + "," + message + "," + time + "," + albumartpath
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification('+notification+')'}
request = self._sendhttp(host, notifycommand)
if not request:
raise Exception
if not request:
raise Exception
except:
logger.warn('Error sending notification request to Plex Media Server')
@@ -374,30 +382,30 @@ class Plex:
class NMA:
def __init__(self):
self.apikey = headphones.NMA_APIKEY
self.priority = headphones.NMA_PRIORITY
def _send(self, data):
return request.request_content('https://www.notifymyandroid.com/publicapi/notify', data=data)
def notify(self, artist=None, album=None, snatched_nzb=None):
apikey = self.apikey
priority = self.priority
if snatched_nzb:
event = snatched_nzb + " snatched!"
description = "Headphones has snatched: " + snatched_nzb + " and has sent it to SABnzbd+"
else:
event = artist + ' - ' + album + ' complete!'
description = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']'
data = { 'apikey': apikey, 'application':'Headphones', 'event': event, 'description': description, 'priority': priority}
logger.info('Sending notification request to NotifyMyAndroid')
request = self._send(data)
if not request:
logger.warn('Error sending notification request to NotifyMyAndroid')
@@ -415,7 +423,7 @@ class PUSHBULLET:
return
http_handler = HTTPSConnection("api.pushbullet.com")
data = {'device_iden': headphones.PUSHBULLET_DEVICEID,
'type': "note",
'title': "Headphones",
@@ -454,21 +462,20 @@ class PUSHBULLET:
self.notify('Main Screen Activate', 'Test Message')
class PUSHALOT:
def notify(self, message, event):
if not headphones.PUSHALOT_ENABLED:
return
pushalot_authorizationtoken = headphones.PUSHALOT_APIKEY
pushalot_authorizationtoken = headphones.PUSHALOT_APIKEY
logger.debug(u"Pushalot event: " + event)
logger.debug(u"Pushalot message: " + message)
logger.debug(u"Pushalot api: " + pushalot_authorizationtoken)
logger.debug(u"Pushalot event: " + event)
logger.debug(u"Pushalot message: " + message)
logger.debug(u"Pushalot api: " + pushalot_authorizationtoken)
http_handler = HTTPSConnection("pushalot.com")
data = {'AuthorizationToken': pushalot_authorizationtoken,
'Title': event.encode('utf-8'),
'Body': message.encode("utf-8") }
@@ -529,6 +536,7 @@ class Synoindex:
if isinstance(path_list, list):
for path in path_list:
self.notify(path)
class PUSHOVER:
application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz"
@@ -551,7 +559,7 @@ class PUSHOVER:
return
http_handler = HTTPSConnection("api.pushover.net")
data = {'token': self.application_token,
'user': headphones.PUSHOVER_KEYS,
'title': event,
@@ -589,17 +597,17 @@ class PUSHOVER:
self.priority = priority
self.notify('Main Screen Activate', 'Test Message')
class TwitterNotifier:
consumer_key = "oYKnp2ddX5gbARjqX8ZAAg"
consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk"
REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token'
ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token'
AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize'
SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate'
def notify_snatch(self, title):
if headphones.TWITTER_ONSNATCH:
self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH]+': '+title+' at '+helpers.now())
@@ -612,37 +620,37 @@ class TwitterNotifier:
return self._notifyTwitter("This is a test notification from Headphones at "+helpers.now(), force=True)
def _get_authorization(self):
signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
oauth_client = oauth.Client(oauth_consumer)
logger.info('Requesting temp token from Twitter')
resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET')
if resp['status'] != '200':
logger.info('Invalid respond from Twitter requesting temp token: %s' % resp['status'])
else:
request_token = dict(parse_qsl(content))
headphones.TWITTER_USERNAME = request_token['oauth_token']
headphones.TWITTER_PASSWORD = request_token['oauth_token_secret']
return self.AUTHORIZATION_URL+"?oauth_token="+ request_token['oauth_token']
def _get_credentials(self, key):
request_token = {}
request_token['oauth_token'] = headphones.TWITTER_USERNAME
request_token['oauth_token_secret'] = headphones.TWITTER_PASSWORD
request_token['oauth_callback_confirmed'] = 'true'
token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret'])
token.set_verifier(key)
logger.info('Generating and signing request for an access token using key '+key)
signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
logger.info('oauth_consumer: '+str(oauth_consumer))
@@ -650,10 +658,10 @@ class TwitterNotifier:
logger.info('oauth_client: '+str(oauth_client))
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key)
logger.info('resp, content: '+str(resp)+','+str(content))
access_token = dict(parse_qsl(content))
logger.info('access_token: '+str(access_token))
logger.info('resp[status] = '+str(resp['status']))
if resp['status'] != '200':
logger.info('The request for a token with did not succeed: '+str(resp['status']), logger.ERROR)
@@ -664,33 +672,118 @@ class TwitterNotifier:
headphones.TWITTER_USERNAME = access_token['oauth_token']
headphones.TWITTER_PASSWORD = access_token['oauth_token_secret']
return True
def _send_tweet(self, message=None):
username=self.consumer_key
password=self.consumer_secret
access_token_key=headphones.TWITTER_USERNAME
access_token_secret=headphones.TWITTER_PASSWORD
logger.info(u"Sending tweet: "+message)
api = twitter.Api(username, password, access_token_key, access_token_secret)
try:
api.PostUpdate(message)
except Exception, e:
logger.info(u"Error Sending Tweet: %s" % e)
return False
return True
def _notifyTwitter(self, message='', force=False):
prefix = headphones.TWITTER_PREFIX
if not headphones.TWITTER_ENABLED and not force:
return False
return self._send_tweet(prefix+": "+message)
notifier = TwitterNotifier
class OSX_NOTIFY:
objc = None
def __init__(self):
try:
self.objc = __import__("objc")
except:
return False
def swizzle(self, cls, SEL, func):
old_IMP = cls.instanceMethodForSelector_(SEL)
def wrapper(self, *args, **kwargs):
return func(self, old_IMP, *args, **kwargs)
new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector,
signature=old_IMP.signature)
self.objc.classAddMethod(cls, SEL, new_IMP)
def notify(self, title, subtitle=None, text=None, sound=True):
try:
self.swizzle(self.objc.lookUpClass('NSBundle'),
b'bundleIdentifier',
self.swizzled_bundleIdentifier)
NSUserNotification = self.objc.lookUpClass('NSUserNotification')
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter')
NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')
if not NSUserNotification or not NSUserNotificationCenter:
return False
pool = NSAutoreleasePool.alloc().init()
notification = NSUserNotification.alloc().init()
notification.setTitle_(title)
if subtitle:
notification.setSubtitle_(subtitle)
if text:
notification.setInformativeText_(text)
if sound:
notification.setSoundName_("NSUserNotificationDefaultSoundName")
notification.setHasActionButton_(False)
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
notification_center.deliverNotification_(notification)
del pool
return True
except Exception, e:
logger.warn('Error sending OS X Notification: %s' % e)
return False
def swizzled_bundleIdentifier(self, original, swizzled):
return 'ade.headphones.osxnotify'
class BOXCAR:
def __init__(self):
self.url = 'https://new.boxcar.io/api/notifications'
def notify(self, title, message, rgid=None):
try:
if rgid:
message += '<br></br><a href="http://musicbrainz.org/release-group/%s">MusicBrainz</a>' % rgid
data = urllib.urlencode({
'user_credentials': headphones.BOXCAR_TOKEN,
'notification[title]': title.encode('utf-8'),
'notification[long_message]': message.encode('utf-8'),
'notification[sound]': "done"
})
req = urllib2.Request(self.url)
handle = urllib2.urlopen(req, data)
handle.close()
return True
except urllib2.URLError, e:
logger.warn('Error sending Boxcar2 Notification: %s' % e)
return False
+89 -67
View File
@@ -13,27 +13,25 @@
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from __future__ import with_statement
import os
import time
import threading
import music_encoder
import shutil, re
import re
import shutil
import uuid
from headphones import notifiers
import beets
from beets import autotag
from beets.mediafile import MediaFile
import threading
import headphones
from headphones import db, albumart, librarysync, lyrics, logger, helpers, request
from headphones.helpers import sab_replace_dots, sab_replace_spaces
from beets import autotag
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
from headphones import notifiers
from headphones import db, albumart, librarysync, lyrics
from headphones import logger, helpers, request, mb, music_encoder
postprocessor_lock = threading.Lock()
def checkFolder():
with postprocessor_lock:
myDB = db.DBConnection()
@@ -44,30 +42,18 @@ def checkFolder():
if album['FolderName']:
if album['Kind'] == 'nzb':
# We're now checking sab config options after sending to determine renaming - but we'll keep the
# iterations in just in case we can't read the config for some reason
download_dir = headphones.DOWNLOAD_DIR
else:
download_dir = headphones.DOWNLOAD_TORRENT_DIR
nzb_album_possibilities = [ album['FolderName'],
sab_replace_dots(album['FolderName']),
sab_replace_spaces(album['FolderName']),
sab_replace_spaces(sab_replace_dots(album['FolderName']))
]
for nzb_folder_name in nzb_album_possibilities:
nzb_album_path = os.path.join(headphones.DOWNLOAD_DIR, nzb_folder_name).encode(headphones.SYS_ENCODING, 'replace')
if os.path.exists(nzb_album_path):
logger.debug('Found %s in NZB download folder. Verifying....' % album['FolderName'])
verify(album['AlbumID'], nzb_album_path, 'nzb')
if album['Kind'] == 'torrent':
album_path = os.path.join(download_dir, album['FolderName']).encode(headphones.SYS_ENCODING,'replace')
logger.info("Checking if %s exists" % album_path)
if os.path.exists(album_path):
logger.info('Found "' + album['FolderName'] + '" in ' + album['Kind'] + ' download folder. Verifying....')
verify(album['AlbumID'], album_path, album['Kind'])
torrent_album_path = os.path.join(headphones.DOWNLOAD_TORRENT_DIR, album['FolderName']).encode(headphones.SYS_ENCODING,'replace')
if os.path.exists(torrent_album_path):
logger.debug('Found %s in torrent download folder. Verifying....' % album['FolderName'])
verify(album['AlbumID'], torrent_album_path, 'torrent')
else:
logger.info("No folder name found for " + album['Title'])
def verify(albumid, albumpath, Kind=None, forced=False):
@@ -80,24 +66,26 @@ def verify(albumid, albumpath, Kind=None, forced=False):
#from an RSS feed or etc
#TODO: This should be a call to a class method.. copied it out of importer with only minor changes
#TODO: odd things can happen when there are diacritic characters in the folder name, need to translate them?
import mb
release_list = None
try:
release_list = mb.getReleaseGroup(albumid)
except Exception, e:
logger.info('Unable to get release information for manual album with rgid: %s. Error: %s' % (albumid, e))
logger.error('Unable to get release information for manual album with rgid: %s. Error: %s' % (albumid, e))
return
if not release_list:
logger.info('Unable to get release information for manual album with rgid: %s' % albumid)
logger.error('Unable to get release information for manual album with rgid: %s' % albumid)
return
# Since we're just using this to create the bare minimum information to insert an artist/album combo, use the first release
releaseid = release_list[0]['id']
release_dict = mb.getRelease(releaseid)
if not release_dict:
logger.error('Unable to get release information for manual album with rgid: %s. Cannot continue' % albumid)
return
logger.info(u"Now adding/updating artist: " + release_dict['artist_name'])
@@ -350,6 +338,27 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
downloaded_track_list.append(os.path.join(r, files))
elif files.lower().endswith('.cue'):
downloaded_cuecount += 1
# Check if files are valid media files and are writeable, before the steps
# below are executed. This simplifies errors and prevents unfinished steps.
for downloaded_track in downloaded_track_list:
try:
media_file = MediaFile(downloaded_track)
except (FileTypeError, UnreadableFileError):
logger.error("Track file is not a valid media file: %s. Not continuing.", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
return
# Not sure if line(s) below are needed, since it is possible to not
# touch any files.
if headphones.EMBED_ALBUM_ART or headphones.CLEANUP_FILES or \
headphones.ADD_ALBUM_ART or headphones.CORRECT_METADATA or \
headphones.EMBED_LYRICS or headphones.RENAME_FILES or \
headphones.MOVE_FILES:
if not os.access(downloaded_track, os.W_OK):
logger.error("Track file is not writeable, which is required for some post processing steps: %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
return
#start encoding
if headphones.MUSIC_ENCODER:
downloaded_track_list=music_encoder.encode(albumpath)
@@ -410,7 +419,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
librarysync.libraryScan(dir=albumpath, append=True, ArtistID=release['ArtistID'], ArtistName=release['ArtistName'])
logger.info(u'Post-processing for %s - %s complete' % (release['ArtistName'], release['AlbumTitle']))
if headphones.GROWL_ENABLED:
pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle']
logger.info(u"Growl request")
@@ -473,7 +482,22 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
logger.info(u"Sending Twitter notification")
twitter = notifiers.TwitterNotifier()
twitter.notify_download(pushmessage)
if headphones.OSX_NOTIFY_ENABLED:
logger.info(u"Sending OS X notification")
osx_notify = notifiers.OSX_NOTIFY()
osx_notify.notify(release['ArtistName'], release['AlbumTitle'], "Download and Postprocessing completed")
if headphones.BOXCAR_ENABLED:
pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle']
logger.info(u"Sending Boxcar2 notification")
boxcar = notifiers.BOXCAR()
boxcar.notify('Headphones processed: ' + pushmessage, "Download and Postprocessing completed", release['AlbumID'])
if headphones.MPC_ENABLED:
mpc = notifiers.MPC()
mpc.notify()
def embedAlbumArt(artwork, downloaded_track_list):
logger.info('Embedding album art')
@@ -632,7 +656,7 @@ def moveFiles(albumpath, release, tracks):
try:
shutil.rmtree(lossless_destination_path)
except Exception, e:
logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossless_destination_path.decode(headphones.SYS_ENCODING, 'replace'), str(e)))
logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossless_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
create_duplicate_folder = True
if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
@@ -665,7 +689,7 @@ def moveFiles(albumpath, release, tracks):
try:
shutil.rmtree(lossy_destination_path)
except Exception, e:
logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossy_destination_path.decode(headphones.SYS_ENCODING, 'replace'), str(e)))
logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossy_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
create_duplicate_folder = True
if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
@@ -746,13 +770,13 @@ def moveFiles(albumpath, release, tracks):
try:
os.chmod(os.path.normpath(temp_f).encode(headphones.SYS_ENCODING, 'replace'), int(headphones.FOLDER_PERMISSIONS, 8))
except Exception, e:
logger.error("Error trying to change permissions on folder: %s" % temp_f.decode(headphones.SYS_ENCODING, 'replace'))
logger.error("Error trying to change permissions on folder: %s. %s", temp_f, e)
# If we failed to move all the files out of the directory, this will fail too
try:
shutil.rmtree(albumpath)
except Exception, e:
logger.error('Could not remove directory: %s. %s' % (albumpath.decode(headphones.SYS_ENCODING, 'replace'), e))
logger.error('Could not remove directory: %s. %s', albumpath, e)
destination_paths = []
@@ -779,10 +803,10 @@ def correctMetadata(albumid, release, downloaded_track_list):
elif any(downloaded_track.lower().endswith('.' + x.lower()) for x in headphones.LOSSY_MEDIA_FORMATS):
lossy_items.append(beets.library.Item.from_path(downloaded_track))
else:
logger.warn("Skipping: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " because it is not a mutagen friendly file format")
logger.warn("Skipping: %s because it is not a mutagen friendly file format", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
except Exception, e:
logger.error("Beets couldn't create an Item from: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " - not a media file?" + str(e))
logger.error("Beets couldn't create an Item from: %s - not a media file? %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e))
for items in [lossy_items, lossless_items]:
@@ -792,16 +816,16 @@ def correctMetadata(albumid, release, downloaded_track_list):
try:
cur_artist, cur_album, candidates, rec = autotag.tag_album(items, search_artist=helpers.latinToAscii(release['ArtistName']), search_album=helpers.latinToAscii(release['AlbumTitle']))
except Exception, e:
logger.error('Error getting recommendation: %s. Not writing metadata' % e)
logger.error('Error getting recommendation: %s. Not writing metadata', e)
return
if str(rec) == 'recommendation.none':
logger.warn('No accurate album match found for %s, %s - not writing metadata' % (release['ArtistName'], release['AlbumTitle']))
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
return
if candidates:
dist, info, mapping, extra_items, extra_tracks = candidates[0]
else:
logger.warn('No accurate album match found for %s, %s - not writing metadata' % (release['ArtistName'], release['AlbumTitle']))
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
return
logger.info('Beets recommendation for tagging items: %s' % rec)
@@ -813,9 +837,9 @@ def correctMetadata(albumid, release, downloaded_track_list):
for item in items:
try:
item.write()
logger.info("Successfully applied metadata to: " + item.path.decode(headphones.SYS_ENCODING, 'replace'))
logger.info("Successfully applied metadata to: %s", item.path.decode(headphones.SYS_ENCODING, 'replace'))
except Exception, e:
logger.warn("Error writing metadata to " + item.path.decode(headphones.SYS_ENCODING, 'replace') + ": " + str(e))
logger.warn("Error writing metadata to '%s': %s", item.path.decode(headphones.SYS_ENCODING, 'replace'), str(e))
def embedLyrics(downloaded_track_list):
logger.info('Adding lyrics')
@@ -827,7 +851,7 @@ def embedLyrics(downloaded_track_list):
try:
f = MediaFile(downloaded_track)
except:
logger.error('Could not read %s. Not checking lyrics' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
logger.error('Could not read %s. Not checking lyrics', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
continue
if f.albumartist and f.title:
@@ -835,16 +859,16 @@ def embedLyrics(downloaded_track_list):
elif f.artist and f.title:
metalyrics = lyrics.getLyrics(f.artist, f.title)
else:
logger.info('No artist/track metadata found for track: %s. Not fetching lyrics' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
logger.info('No artist/track metadata found for track: %s. Not fetching lyrics', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
metalyrics = None
if lyrics:
logger.debug('Adding lyrics to: %s' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
logger.debug('Adding lyrics to: %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
f.lyrics = metalyrics
try:
f.save()
except:
logger.error('Cannot save lyrics to: %s. Skipping' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
logger.error('Cannot save lyrics to: %s. Skipping', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
continue
def renameFiles(albumpath, downloaded_track_list, release):
@@ -859,7 +883,7 @@ def renameFiles(albumpath, downloaded_track_list, release):
try:
f = MediaFile(downloaded_track)
except:
logger.info("MediaFile couldn't parse: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
logger.info("MediaFile couldn't parse: %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
continue
if not f.disc:
@@ -928,11 +952,11 @@ def renameFiles(albumpath, downloaded_track_list, release):
logger.debug("Renaming for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " is not neccessary")
continue
logger.debug('Renaming %s ---> %s' % (downloaded_track.decode(headphones.SYS_ENCODING,'replace'), new_file_name.decode(headphones.SYS_ENCODING,'replace')))
logger.debug('Renaming %s ---> %s', downloaded_track.decode(headphones.SYS_ENCODING,'replace'), new_file_name.decode(headphones.SYS_ENCODING,'replace'))
try:
os.rename(downloaded_track, new_file)
except Exception, e:
logger.error('Error renaming file: %s. Error: %s' % (downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e))
logger.error('Error renaming file: %s. Error: %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e)
continue
def updateFilePermissions(albumpaths):
@@ -1044,7 +1068,6 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
continue
else:
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
from headphones import mb
try:
rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album))
except:
@@ -1072,7 +1095,6 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
continue
else:
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
from headphones import mb
try:
rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album))
except:
@@ -1108,17 +1130,18 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
continue
# Attempt 4: Hail mary. Just assume the folder name is the album name if it doesn't have a separator in it
if '-' not in folder:
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?', [folder]).fetchone()
logger.debug('Attempt to extract album name by assuming it is the folder name')
if '-' not in folder_basename:
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?', [folder_basename]).fetchone()
if release:
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder)
continue
else:
logger.info('Querying MusicBrainz for the release group id for: %s', folder)
from headphones import mb
logger.info('Querying MusicBrainz for the release group id for: %s', folder_basename)
try:
rgid = mb.findAlbumID(album=helpers.latinToAscii(folder))
rgid = mb.findAlbumID(album=helpers.latinToAscii(folder_basename))
except:
logger.error('Can not get release information for this album')
rgid = None
@@ -1128,4 +1151,3 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
continue
else:
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
+5 -2
View File
@@ -30,8 +30,11 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N
# is white listed.
if whitelist_status_code and auto_raise:
if response.status_code not in whitelist_status_code:
logger.debug("Response status code %d is not white listed, raising exception", response.status_code)
response.raise_for_status()
try:
response.raise_for_status()
except:
logger.debug("Response status code %d is not white listed, raised exception", response.status_code)
raise
elif auto_raise:
response.raise_for_status()
-29
View File
@@ -119,35 +119,6 @@ def sendNZB(nzb):
if sabText == "ok":
logger.info(u"NZB sent to SAB successfully")
if headphones.GROWL_ENABLED and headphones.GROWL_ONSNATCH:
logger.info(u"Sending Growl notification")
growl = notifiers.GROWL()
growl.notify(nzb.name,"Download started")
if headphones.PROWL_ENABLED and headphones.PROWL_ONSNATCH:
logger.info(u"Sending Prowl notification")
prowl = notifiers.PROWL()
prowl.notify(nzb.name,"Download started")
if headphones.PUSHOVER_ENABLED and headphones.PUSHOVER_ONSNATCH:
logger.info(u"Sending Pushover notification")
prowl = notifiers.PUSHOVER()
prowl.notify(nzb.name,"Download started")
if headphones.PUSHBULLET_ENABLED and headphones.PUSHBULLET_ONSNATCH:
logger.info(u"Sending PushBullet notification")
pushbullet = notifiers.PUSHBULLET()
pushbullet.notify(nzb.name + " has been snatched!", "Download started")
if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH:
logger.info(u"Sending Twitter notification")
twitter = notifiers.TwitterNotifier()
twitter.notify_snatch(nzb.name)
if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH:
logger.debug(u"Sending NMA notification")
nma = notifiers.NMA()
nma.notify(snatched_nzb=nzb.name)
if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH:
logger.info(u"Sending Pushalot notification")
pushalot = notifiers.PUSHALOT()
pushalot.notify(nzb.name,"Download started")
return True
elif sabText == "Missing authentication":
logger.info(u"Incorrect username/password sent to SAB, NZB not sent")
+89 -10
View File
@@ -31,7 +31,7 @@ import subprocess
import headphones
from headphones.common import USER_AGENT
from headphones import logger, db, helpers, classes, sab, nzbget, request
from headphones import transmission
from headphones import utorrent, transmission, notifiers
import lib.bencode as bencode
@@ -307,13 +307,13 @@ def searchNZB(album, new=False, losslessOnly=False):
params = {
"t": "search",
"cat": categories,
"apikey": '89edf227c1de9b3de50383fff11466c6',
"apikey": '964d601959918a578a670984bdee9357',
"maxage": headphones.USENET_RETENTION,
"q": term
}
data = request.request_feed(
url="http://headphones.codeshy.com/newznab/api",
url="http://indexer.codeshy.com/api",
params=params, headers=headers,
auth=(headphones.HPUSER, headphones.HPPASS)
)
@@ -359,6 +359,9 @@ def searchNZB(album, new=False, losslessOnly=False):
logger.info("Album type is audiobook/spokenword. Using audiobook category")
for newznab_host in newznab_hosts:
provider = newznab_host[0]
# Add a little mod for kere.ws
if newznab_host[0] == "http://kere.ws":
if categories == "3040":
@@ -573,14 +576,16 @@ def send_to_downloader(data, bestqual, album):
nzb = classes.NZBDataSearchResult()
nzb.extraInfo.append(data)
nzb.name = folder_name
nzbget.sendNZB(nzb)
if not nzbget.sendNZB(nzb):
return
elif headphones.NZB_DOWNLOADER == 0:
nzb = classes.NZBDataSearchResult()
nzb.extraInfo.append(data)
nzb.name = folder_name
sab.sendNZB(nzb)
if not sab.sendNZB(nzb):
return
# If we sent the file to sab, we can check how it was renamed and insert that into the snatched table
(replace_spaces, replace_dots) = sab.checkConfig()
@@ -691,10 +696,82 @@ def send_to_downloader(data, bestqual, album):
except Exception, e:
logger.exception("Unhandled exception")
else:
logger.info("Sending torrent to uTorrent")
# rutracker needs cookies to be set, pass the .torrent file instead of url
if bestqual[3] == 'rutracker.org':
file_or_url = rutracker.get_torrent(bestqual[2])
else:
file_or_url = bestqual[2]
folder_name = utorrent.addTorrent(file_or_url)
if folder_name:
logger.info('Torrent folder name: %s' % folder_name)
else:
logger.error('Torrent folder name could not be determined')
return
# remove temp .torrent file created above
if bestqual[3] == 'rutracker.org':
try:
shutil.rmtree(os.path.split(file_or_url)[0])
except Exception, e:
logger.exception("Unhandled exception")
myDB = db.DBConnection()
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']])
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name, kind])
# notify
artist = album[1]
albumname = album[2]
rgid = album[6]
title = artist + ' - ' + albumname
provider = bestqual[3]
if provider.startswith(("http://", "https://")):
provider = provider.split("//")[1]
name = folder_name if folder_name else None
if headphones.GROWL_ENABLED and headphones.GROWL_ONSNATCH:
logger.info(u"Sending Growl notification")
growl = notifiers.GROWL()
growl.notify(name,"Download started")
if headphones.PROWL_ENABLED and headphones.PROWL_ONSNATCH:
logger.info(u"Sending Prowl notification")
prowl = notifiers.PROWL()
prowl.notify(name,"Download started")
if headphones.PUSHOVER_ENABLED and headphones.PUSHOVER_ONSNATCH:
logger.info(u"Sending Pushover notification")
prowl = notifiers.PUSHOVER()
prowl.notify(name,"Download started")
if headphones.PUSHBULLET_ENABLED and headphones.PUSHBULLET_ONSNATCH:
logger.info(u"Sending PushBullet notification")
pushbullet = notifiers.PUSHBULLET()
pushbullet.notify(name + " has been snatched!", "Download started")
if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH:
logger.info(u"Sending Twitter notification")
twitter = notifiers.TwitterNotifier()
twitter.notify_snatch(name)
if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH:
logger.info(u"Sending NMA notification")
nma = notifiers.NMA()
nma.notify(snatched_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):
title = re.sub('[\.\-\/\_]', ' ', title)
@@ -862,7 +939,7 @@ def searchTorrent(album, new=False, losslessOnly=False):
minimumseeders = int(headphones.NUMBEROFSEEDERS) - 1
if headphones.KAT:
provider = "Kick Ass Torrent"
provider = "Kick Ass Torrents"
providerurl = url_fix("http://kickass.to/usearch/" + term)
if headphones.PREFERRED_QUALITY == 3 or losslessOnly:
categories = "7" #music
@@ -1051,7 +1128,7 @@ def searchTorrent(album, new=False, losslessOnly=False):
if re.search(bitrate, encoding_string, flags=re.I):
bitrate_string = encoding_string
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
raise Exception("Preferred bitrate %s not recognized by %s" % (bitrate_string, provider))
logger.info(u"Your preferred bitrate is not one of the available What.cd filters, so not using it as a search parameter.")
maxsize = 10000000000
elif headphones.PREFERRED_QUALITY == 1: # Highest quality including lossless
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
@@ -1179,6 +1256,8 @@ def searchTorrent(album, new=False, losslessOnly=False):
continue
else:
url = item.findAll("a")[3]['href']
if url.lower().startswith("//"):
url = "http:" + url
formatted_size = re.search('Size (.*),', unicode(item)).group(1).replace(u'\xa0', ' ')
size = helpers.piratesize(formatted_size)
if size < maxsize and minimumseeders < seeds and url != None:
@@ -1324,7 +1403,7 @@ def preprocess(resultlist):
#Get out of here if we're using Transmission or uTorrent
if headphones.TORRENT_DOWNLOADER != 0:
return True, result
# get outta here if rutracker or piratebay
# get outta here if rutracker
if result[3] == 'rutracker.org':
return True, result
# Get out of here if it's a magnet link
@@ -1334,7 +1413,7 @@ def preprocess(resultlist):
# Download the torrent file
headers = {}
if result[3] == 'Kick Ass Torrent':
if result[3] == 'Kick Ass Torrents':
headers['Referer'] = 'http://kat.ph/'
elif result[3] == 'What.cd':
headers['User-Agent'] = 'Headphones'
@@ -1371,4 +1450,4 @@ def preprocess(resultlist):
logger.error("Couldn't retrieve the best nzb. Skipping.")
continue
return (None, None)
return (None, None)
-25
View File
@@ -49,31 +49,6 @@ def addTorrent(link):
retid = False
logger.info(u"Torrent sent to Transmission successfully")
if headphones.GROWL_ENABLED and headphones.GROWL_ONSNATCH:
logger.info(u"Sending Growl notification")
growl = notifiers.GROWL()
growl.notify(name,"Download started")
if headphones.PROWL_ENABLED and headphones.PROWL_ONSNATCH:
logger.info(u"Sending Prowl notification")
prowl = notifiers.PROWL()
prowl.notify(name,"Download started")
if headphones.PUSHOVER_ENABLED and headphones.PUSHOVER_ONSNATCH:
logger.info(u"Sending Pushover notification")
pushover = notifiers.PUSHOVER()
pushover.notify(name,"Download started")
if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH:
logger.info(u"Sending Twitter notification")
twitter = notifiers.TwitterNotifier()
twitter.notify_snatch(nzb.name)
if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH:
logger.info(u"Sending NMA notification")
nma = notifiers.NMA()
nma.notify(snatched_nzb=name)
if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH:
logger.info(u"Sending Pushalot notification")
pushalot = notifiers.PUSHALOT()
pushalot.notify(name,"Download started")
return retid
else:
+84
View File
@@ -12,3 +12,87 @@
#
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
import re
import os
import time
import base64
import headphones
import simplejson as json
from headphones import logger, notifiers, request
# This is just a simple script to send torrents to transmission. The
# intention is to turn this into a class where we can check the state
# of the download, set the download dir, etc.
# TODO: Store the session id so we don't need to make 2 calls
# Store torrent id so we can check up on it
def addTorrent(link):
host = headphones.UTORRENT_HOST
username = headphones.UTORRENT_USERNAME
password = headphones.UTORRENT_PASSWORD
label = headphones.UTORRENT_LABEL
token = ''
if not host.startswith('http'):
host = 'http://' + host
if host.endswith('/'):
host = host[:-1]
if host.endswith('/gui'):
host = host + '/'
else:
host = host + '/gui/'
# Retrieve session id
auth = (username, password) if username and password else None
token_request = request.request_response(host + 'token.html', auth=auth)
token = re.findall('<div.*?>(.*?)</', token_request.content)[0]
guid = token_request.cookies['GUID']
cookies = dict(GUID = guid)
if link.startswith("magnet") or link.startswith("http") or link.endswith(".torrent"):
params = {'action':'add-url', 's':link, 'token':token}
response = request.request_json(host, params=params, auth=auth, cookies=cookies)
else:
params = {'action':'add-file', 'token':token}
files = {'torrent_file':{'music.torrent' : link}}
response = request.request_json(host, method="post", params=params, files=files, auth=auth, cookies=cookies)
if not response:
logger.error("Error sending torrent to uTorrent")
return
if link.startswith('magnet'):
tor_hash = re.findall('urn:btih:([\w]{32,40})', link)[0]
if len(tor_hash) == 32:
tor_hash = b16encode(b32decode(tor_hash)).lower()
else:
info = bdecode(link.content)["info"]
tor_hash = sha1(bencode(info)).hexdigest()
params = {'action':'setprops', 'hash':tor_hash,'s':'label', 'v':label, 'token':token}
response = request.request_json(host, params=params, auth=auth, cookies=cookies)
if not response:
logger.error("Error setting torrent label in uTorrent")
return
# folder info can probably be cleaned up with getprops
folder = None
params = {'list':'1', 'token':token}
response = request.request_json(host, params=params, auth=auth, cookies=cookies)
if not response:
logger.error("Error getting torrent information from uTorrent")
return
for torrent in response['torrents']:
folder = os.path.basename(torrent[26])
return folder
+5 -1
View File
@@ -76,7 +76,7 @@ def getVersion():
logger.error('Couldn\'t find latest installed version.')
cur_commit_hash = None
cur_commit_hash = output
cur_commit_hash = str(output)
if not re.match('^[a-z0-9]+$', cur_commit_hash):
logger.error('Output doesn\'t look like a hash, not using it')
@@ -136,6 +136,10 @@ def checkGithub():
logger.info('You are running an unknown version of Headphones. Run the updater to identify your version')
return headphones.LATEST_VERSION
if headphones.LATEST_VERSION == headphones.CURRENT_VERSION:
logger.info('Headphones is up to date')
return headphones.LATEST_VERSION
logger.info('Comparing currently installed version with latest GitHub version')
url = 'https://api.github.com/repos/%s/headphones/compare/%s...%s' % (headphones.GIT_USER, headphones.LATEST_VERSION, headphones.CURRENT_VERSION)
commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict)
+65 -10
View File
@@ -163,7 +163,7 @@ class WebInterface(object):
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
getExtras.exposed = True
def removeExtras(self, ArtistID):
def removeExtras(self, ArtistID, ArtistName):
myDB = db.DBConnection()
controlValueDict = {'ArtistID': ArtistID}
newValueDict = {'IncludeExtras': 0}
@@ -174,7 +174,8 @@ class WebInterface(object):
myDB.action('DELETE from albums WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']])
myDB.action('DELETE from allalbums WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']])
myDB.action('DELETE from alltracks WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']])
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', album['AlbumID'])
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [album['AlbumID']])
importer.finalize_update(ArtistID, ArtistName)
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
removeExtras.exposed = True
@@ -268,6 +269,11 @@ class WebInterface(object):
searcher.searchforalbum(mbid, new=True)
if action == 'WantedLossless':
searcher.searchforalbum(mbid, lossless=True)
if ArtistID:
ArtistIDT = ArtistID
else:
ArtistIDT = myDB.action('SELECT ArtistID FROM albums WHERE AlbumID=?', [mbid]).fetchone()[0]
myDB.action('UPDATE artists SET TotalTracks=(SELECT COUNT(*) FROM tracks WHERE ArtistID = ? AND AlbumTitle IN (SELECT AlbumTitle FROM albums WHERE Status != "Ignored")) WHERE ArtistID = ?', [ArtistIDT, ArtistIDT])
if ArtistID:
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
else:
@@ -629,8 +635,8 @@ class WebInterface(object):
for rgid in rgids:
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rgid['ReleaseGroupID']])
myDB.action('DELETE from allalbums WHERE AlbumID=?', [AlbumID])
myDB.action('DELETE from alltracks WHERE AlbumID=?', [AlbumID])
myDB.action('DELETE from allalbums WHERE ArtistID=?', [ArtistID])
myDB.action('DELETE from alltracks WHERE ArtistID=?', [ArtistID])
myDB.action('INSERT OR REPLACE into blacklist VALUES (?)', [ArtistID])
elif action == 'pause':
controlValueDict = {'ArtistID': ArtistID}
@@ -724,6 +730,11 @@ class WebInterface(object):
return serve_template(templatename="logs.html", title="Log", lineList=headphones.LOG_LIST)
logs.exposed = True
def clearLogs(self):
headphones.LOG_LIST = []
logger.info("Web logs cleared")
raise cherrypy.HTTPRedirect("logs")
clearLogs.exposed = True
def getLog(self,iDisplayStart=0,iDisplayLength=100,iSortCol_0=0,sSortDir_0="desc",sSearch="",**kwargs):
@@ -843,6 +854,16 @@ class WebInterface(object):
return json_albums
getAlbumsByArtist_json.exposed=True
def getArtistjson(self, ArtistID, **kwargs):
myDB = db.DBConnection()
artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [ArtistID]).fetchone()
artist_json = json.dumps({
'ArtistName': artist['ArtistName'],
'Status': artist['Status']
})
return artist_json
getArtistjson.exposed=True
def clearhistory(self, type=None, date_added=None, title=None):
myDB = db.DBConnection()
if type:
@@ -924,6 +945,7 @@ class WebInterface(object):
"utorrent_host" : headphones.UTORRENT_HOST,
"utorrent_user" : headphones.UTORRENT_USERNAME,
"utorrent_pass" : headphones.UTORRENT_PASSWORD,
"utorrent_label" : headphones.UTORRENT_LABEL,
"nzb_downloader_sabnzbd" : radio(headphones.NZB_DOWNLOADER, 0),
"nzb_downloader_nzbget" : radio(headphones.NZB_DOWNLOADER, 1),
"nzb_downloader_blackhole" : radio(headphones.NZB_DOWNLOADER, 2),
@@ -1059,6 +1081,12 @@ class WebInterface(object):
"pushbullet_deviceid": headphones.PUSHBULLET_DEVICEID,
"twitter_enabled": checked(headphones.TWITTER_ENABLED),
"twitter_onsnatch": checked(headphones.TWITTER_ONSNATCH),
"osx_notify_enabled": checked(headphones.OSX_NOTIFY_ENABLED),
"osx_notify_onsnatch": checked(headphones.OSX_NOTIFY_ONSNATCH),
"osx_notify_app": headphones.OSX_NOTIFY_APP,
"boxcar_enabled": checked(headphones.BOXCAR_ENABLED),
"boxcar_onsnatch": checked(headphones.BOXCAR_ONSNATCH),
"boxcar_token": headphones.BOXCAR_TOKEN,
"mirror_list": headphones.MIRRORLIST,
"mirror": headphones.MIRROR,
"customhost": headphones.CUSTOMHOST,
@@ -1072,7 +1100,8 @@ class WebInterface(object):
"songkick_filter_enabled": checked(headphones.SONGKICK_FILTER_ENABLED),
"cache_sizemb": headphones.CACHE_SIZEMB,
"file_permissions": headphones.FILE_PERMISSIONS,
"folder_permissions": headphones.FOLDER_PERMISSIONS
"folder_permissions": headphones.FOLDER_PERMISSIONS,
"mpc_enabled": checked(headphones.MPC_ENABLED)
}
# Need to convert EXTRAS to a dictionary we can pass to the config: it'll come in as a string like 2,5,6,8
@@ -1095,7 +1124,7 @@ class WebInterface(object):
def configUpdate(self, http_host='0.0.0.0', http_username=None, http_port=8181, http_password=None, launch_browser=0, api_enabled=0, api_key=None,
download_scan_interval=None, update_db_interval=None, mb_ignore_age=None, nzb_search_interval=None, libraryscan_interval=None, sab_host=None, sab_username=None, sab_apikey=None, sab_password=None,
sab_category=None, nzbget_host=None, nzbget_username=None, nzbget_password=None, nzbget_category=None, transmission_host=None, transmission_username=None, transmission_password=None,
utorrent_host=None, utorrent_username=None, utorrent_password=None, nzb_downloader=0, torrent_downloader=0, download_dir=None, blackhole_dir=None, usenet_retention=None,
utorrent_host=None, utorrent_username=None, utorrent_password=None, utorrent_label=None,nzb_downloader=0, torrent_downloader=0, download_dir=None, blackhole_dir=None, usenet_retention=None,
use_headphones_indexer=0, newznab=0, newznab_host=None, newznab_apikey=None, newznab_enabled=0, nzbsorg=0, nzbsorg_uid=None, nzbsorg_hash=None, nzbsrus=0, nzbsrus_uid=None, nzbsrus_apikey=None, omgwtfnzbs=0, omgwtfnzbs_uid=None, omgwtfnzbs_apikey=None,
preferred_words=None, required_words=None, ignored_words=None, preferred_quality=0, preferred_bitrate=None, detect_bitrate=0, move_files=0, torrentblackhole_dir=None, download_torrent_dir=None,
numberofseeders=None, use_piratebay=0, piratebay_proxy_url=None, use_isohunt=0, use_kat=0, use_mininova=0, waffles=0, waffles_uid=None, waffles_passkey=None, whatcd=0, whatcd_username=None, whatcd_password=None,
@@ -1105,10 +1134,11 @@ class WebInterface(object):
bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0,
delete_lossless_files=0, growl_enabled=0, growl_onsnatch=0, growl_host=None, growl_password=None, prowl_enabled=0, prowl_onsnatch=0, prowl_keys=None, prowl_priority=0, xbmc_enabled=0, xbmc_host=None, xbmc_username=None, xbmc_password=None,
xbmc_update=0, xbmc_notify=0, nma_enabled=False, nma_apikey=None, nma_priority=0, nma_onsnatch=0, pushalot_enabled=False, pushalot_apikey=None, pushalot_onsnatch=0, synoindex_enabled=False, lms_enabled=0, lms_host=None,
pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, pushover_apitoken=None, pushbullet_enabled=0, pushbullet_onsnatch=0, pushbullet_apikey=None, pushbullet_deviceid=None, twitter_enabled=0, twitter_onsnatch=0, mirror=None, customhost=None, customport=None,
customsleep=None, hpuser=None, hppass=None, preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, cache_sizemb=None,
enable_https=0, https_cert=None, https_key=None, file_permissions=None, folder_permissions=None, plex_enabled=0, plex_server_host=None, plex_client_host=None, plex_username=None,
plex_password=None, plex_update=0, plex_notify=0, songkick_enabled=0, songkick_apikey=None, songkick_location=None, songkick_filter_enabled=0, encoder_multicore=False, encoder_multicore_count=0, **kwargs):
pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, pushover_apitoken=None, pushbullet_enabled=0, pushbullet_onsnatch=0, pushbullet_apikey=None, pushbullet_deviceid=None, twitter_enabled=0, twitter_onsnatch=0,
osx_notify_enabled=0, osx_notify_onsnatch=0, osx_notify_app=None, boxcar_enabled=0, boxcar_onsnatch=0, boxcar_token=None, mirror=None, customhost=None, customport=None, customsleep=None, hpuser=None, hppass=None,
preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, cache_sizemb=None, enable_https=0, https_cert=None, https_key=None, file_permissions=None, folder_permissions=None,
plex_enabled=0, plex_server_host=None, plex_client_host=None, plex_username=None, plex_password=None, plex_update=0, plex_notify=0,
songkick_enabled=0, songkick_apikey=None, songkick_location=None, songkick_filter_enabled=0, encoder_multicore=False, encoder_multicore_count=0, mpc_enabled=False, **kwargs ):
headphones.HTTP_HOST = http_host
headphones.HTTP_PORT = http_port
@@ -1140,6 +1170,7 @@ class WebInterface(object):
headphones.UTORRENT_HOST = utorrent_host
headphones.UTORRENT_USERNAME = utorrent_username
headphones.UTORRENT_PASSWORD = utorrent_password
headphones.UTORRENT_LABEL = utorrent_label
headphones.NZB_DOWNLOADER = int(nzb_downloader)
headphones.TORRENT_DOWNLOADER = int(torrent_downloader)
headphones.DOWNLOAD_DIR = download_dir
@@ -1268,6 +1299,17 @@ class WebInterface(object):
headphones.SONGKICK_FILTER_ENABLED = songkick_filter_enabled
headphones.TWITTER_ENABLED = twitter_enabled
headphones.TWITTER_ONSNATCH = twitter_onsnatch
headphones.OSX_NOTIFY_ENABLED = osx_notify_enabled
headphones.OSX_NOTIFY_ONSNATCH = osx_notify_onsnatch
headphones.OSX_NOTIFY_APP = osx_notify_app
headphones.BOXCAR_ENABLED = boxcar_enabled
headphones.BOXCAR_ONSNATCH = boxcar_onsnatch
headphones.BOXCAR_TOKEN = boxcar_token
headphones.MPC_ENABLED = mpc_enabled
headphones.MIRROR = mirror
headphones.CUSTOMHOST = customhost
headphones.CUSTOMPORT = customport
@@ -1435,6 +1477,19 @@ class WebInterface(object):
return "Error sending tweet"
testTwitter.exposed = True
def osxnotifyregister(self, app):
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
from lib.osxnotify import registerapp as osxnotify
result, msg = osxnotify.registerapp(app)
if result:
osx_notify = notifiers.OSX_NOTIFY()
osx_notify.notify('Registered', result, 'Success :-)')
logger.info('Registered %s, to re-register a different app, delete this app first' % result)
else:
logger.warn(msg)
return msg
osxnotifyregister.exposed = True
class Artwork(object):
def index(self):
return "Artwork"
+2 -1
View File
@@ -14,6 +14,7 @@
# This particular version has been slightly modified to work with headphones
# https://github.com/rembo10/headphones
import os
__version__ = '1.3.4'
__author__ = 'Adrian Sampson <adrian@radbox.org>'
@@ -23,4 +24,4 @@ from beets.util import confit
Library = beets.library.Library
config = confit.LazyConfig('beets', __name__)
config = confit.LazyConfig(os.path.dirname(__file__), __name__)
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)