Compare commits

..
106 Commits
Author SHA1 Message Date
rembo10 24165fb5f3 v0.5.8 2015-07-13 15:22:03 -07:00
rembo10 71da960386 Updated changelog for v0.5.8 2015-07-13 15:21:44 -07:00
rembo10 a16c2d0a26 Fix for #2279: What.cd not honoring custom search string 2015-07-13 15:09:01 -07:00
rembo10 53de44a225 typo when excluding similar artist update on an artist scan 2015-07-08 23:15:38 -07:00
rembo10 e32cff60e5 Took out some unneccessary functions if we're just scanning an artist 2015-07-08 22:26:55 -07:00
rembo10 0ebba8d17c Fix for #2271: Scanning an empty directory with DETECT_BITRATE turned on 2015-07-08 21:58:41 -07:00
rembo10 464db79c04 Some cleanup to the scanArtist function 2015-07-08 11:52:00 -07:00
rembo10 65019d8323 Fix for #2260 & #2129 & 2175 & #2163 : Better artist directory scanning 2015-07-08 02:41:17 -07:00
rembo10 f131d316ca Set localhost as default 2015-07-08 00:45:33 -07:00
rembo10 e6663c96b5 Fixed some pushbullet config text 2015-07-07 23:56:17 -07:00
rembo10 5f06d11fe9 Fixed pushbullet notifications 2015-07-07 23:47:38 -07:00
rembo10 6db2836662 Moved plex notifications to requests lib for pre-v12 plex 2015-07-07 22:54:34 -07:00
rembo10 cb209f8d98 Fix for #2264: Metacritic parsing failing with htmlparser 2015-07-06 13:05:51 -07:00
rembo10 5634e17a2e Fix for #2155, local variable response referenced before assignment utorrent 2015-07-06 01:28:48 -07:00
rembo10 7c990edeaf Better fix for #2183 2015-07-06 00:34:16 -07:00
rembo10 9149f62760 Fix for #2173: Uncaught exception in get_age 2015-07-06 00:21:26 -07:00
rembo10 872ec6b11c Fix for #2089 2015-07-06 00:09:43 -07:00
rembo10 064a21a07c Better fix for #2183 2015-07-06 00:02:09 -07:00
rembo10 57c3c7b2e0 Fixed songkick description to prevent spilling over on default fonts 2015-07-05 21:44:37 -07:00
rembo10 6dc75d7a44 Cleaned up the config page a little bit. Took out {option} enabled buttons and just put them in with the option. Alphabetized the notifiers. Fixed checkbox on chrome 2015-07-05 21:40:52 -07:00
rembo10 5f3b7e05c5 Fixed some appearance stuff on the config page (if fonts weren't installed) 2015-07-05 15:04:18 -07:00
rembo10 61d3d8ad00 Fix for history page showing nzbs snatched from hp indexer as unknown 2015-07-05 12:05:04 -07:00
rembo10 e36440f834 Fix for XSS bug when searching 2015-07-04 23:53:03 -07:00
rembo10 4029543d51 Added option to wait until an album's release date before searching 2015-07-04 20:43:06 -07:00
rembo10 8aee9ac91f Fixed plex client url in config 2015-07-04 19:36:39 -07:00
rembo10 6e5e15f06f Fixed plex notifications for version 12 2015-07-04 19:27:27 -07:00
rembo10 a72bd4cb7c Fixed up the appearance of the config page (was kind of broken on Firefox) 2015-07-04 17:26:47 -07:00
rembo10 d062ab553c Merge branch 'master' of https://github.com/texke/headphones into develop 2015-07-04 12:26:05 -07:00
rembo10 a41edde12a Merge branch 'wfriesen-logtypo' into develop 2015-07-04 12:23:57 -07:00
rembo10 7608927b4f Merge branch 'logtypo' of https://github.com/wfriesen/headphones into wfriesen-logtypo 2015-07-04 12:23:29 -07:00
rembo10 24d7647c08 Updated notifymyandroid library 2015-07-04 12:13:40 -07:00
rembo10 a14369cb7d Added option to only include 'official' extras, rather than everything (bootlegs, promos, etc) 2015-07-04 00:47:02 -07:00
rembo10 3e81c4f3b6 Fix for #2183. Check to make sure the response has headers before trying to parse them 2015-07-03 21:01:46 -07:00
rembo10 84a6c19677 Release v0.5.7 2015-07-01 02:18:01 -07:00
rembo10 624a1d5509 Updated changelog for 0.5.7 release 2015-07-01 02:17:51 -07:00
rembo10 8b6d35cfe2 Updated changelog to reflect metacritic changes 2015-07-01 02:08:13 -07:00
rembo10 867e502448 Cache metacritic scores in the artists table so we don't lose them if we can't connect for whatever reason 2015-07-01 02:06:27 -07:00
rembo10 74275b969e Added a useragent to the metacritic request 2015-07-01 00:56:48 -07:00
William Friesen 275dde0630 Fixed typo in postprocessor message 2015-06-28 10:56:48 +10:00
Bas Stottelaar 46b470f30b Merge pull request #2253 from Hellowlol/patch-1
Add getLogs and clearLogs to api
2015-06-26 08:04:05 +02:00
Hellowlol fbfec95a8a Add getLogs and clearLogs to api 2015-06-25 22:53:39 +02:00
rembo10 1e75a18f55 Initial commit to add plex tokens. Plex notifications need to be updated to requests, and the get request to the server might need to be cleaned up - but want to make sure it works first 2015-06-19 17:08:54 -07:00
Jens Rogier a98849bf30 Added Musicbrainz link to "alternate release" window 2015-06-18 17:01:01 +02:00
rembo10 1b998e9d01 Updated pushover to use requests lib 2015-06-12 00:08:35 -07:00
rembo10 ebd6d3f3f9 Added option to keep folders when force post-processing under Manage. Also, added config option to keep original folder globally 2015-06-09 00:31:50 -07:00
rembo10 1204afadbb Convert preferred bitrate to vbr preset for what.cd searching 2015-06-08 21:56:10 -07:00
rembo10 cd08b72df7 Added ability to test pushover 2015-06-08 21:55:14 -07:00
rembo10 c5ad7c98e2 Release v0.5.6 2015-06-08 11:42:21 -07:00
rembo10 4b8222d792 Updated changelog 2015-06-08 11:37:51 -07:00
Ade a3524ef8aa OS X Notification fix 2015-06-02 09:02:29 +12:00
rembo10 49ab8aa67d Merge branch 'develop' of https://github.com/rembo10/headphones into develop 2015-05-22 20:26:05 -07:00
rembo10 fa85639efb Hide update bar if github checking is turned off 2015-05-22 20:25:40 -07:00
rembo10 84bca81a9a Update CHANGELOG.md 2015-05-22 20:23:17 -07:00
Ade 206ea72a27 mb demo 2015-05-23 08:59:56 +12:00
rembo10 cc2aceb3d8 Fix for nzbget connection url (#2135) 2015-05-21 19:14:58 -07:00
rembo10 00b95aa6c3 Merge branch 'master' of https://github.com/rembo10/headphones into develop 2015-05-21 18:37:15 -07:00
rembo10 a7632cc1e5 For series, use series name + year as search term 2015-05-21 18:36:58 -07:00
rembo10 39f07025c6 Removed a check from postprocessor normalizedAlbumArtist/artistTitleCount, fixed some logging 2015-05-20 10:26:04 -07:00
rembo10 658cb0403f Added support for series (initial stages - needs some cleanup) 2015-05-19 23:53:14 -07:00
rembo10 db1519bcea Updated musicbrainz library 2015-05-19 20:26:50 -07:00
rembo10 885d1cc77c Updated the codeshy.com links to point to https 2015-05-19 18:15:05 -07:00
rembo10 4005becd0a Updated changelog 2015-05-19 18:06:44 -07:00
rembo10 4c16b53c2a SSL for headphones indexer 2015-05-19 17:50:54 -07:00
rembo10 93dbf13d88 Few metacritic changes. Formatting on artist page, css & fixed replacements in artist name 2015-05-19 16:23:43 -07:00
Bas Stottelaar c5dd1a9002 Merge pull request #2223 from MattTavares/patch-1
Fixed for spelling "writable" in postprocessor.py
2015-05-19 10:12:20 +02:00
rembo10 18af7d6392 Updated changelog 2015-05-17 13:54:02 -07:00
rembo10 dbeb31d4fe Initial commit for fetching metacritic scores. Needs better presentation 2015-05-17 13:53:08 -07:00
rembo10 0fb9234ebc Updated changelog 2015-05-16 20:47:55 -07:00
rembo10 6e4cec829a Updated some sabnzbd logging 2015-05-16 20:40:49 -07:00
rembo10 5a433c42e7 Moved kickass search to json, updated some searcher log messages 2015-05-16 20:26:05 -07:00
rembo10 f808dd131b Fixed a couple log messages in sab.py to make it a little less redundant and fixed some log grammar in requests.py 2015-05-16 19:40:13 -07:00
rembo10 1bbf9a2318 Updated osx init script to prevent crashing on yosemite 2015-05-16 19:32:40 -07:00
rembo10 ccff70d9c4 Capitalized Headphones.py in osx init script 2015-05-15 20:58:09 -07:00
Ade 0347ca1bcc Various
- Email SSL
- History, show Folder Name when hovering over Status
- Cuesplit, allow wav, ape to be split
- mb, possible fix for #2181
2015-05-16 09:02:38 +12:00
Matthew Tavares 23564c14f9 Fixed for spelling "writable" 2015-05-14 10:24:00 -05:00
rembo10 93fca13510 Don't check for status when doing sabnzbd api call 2015-05-11 17:46:24 -07:00
rembo10 1a39066cfc Updated changelog to reflect the sab.py https fix 2015-05-06 00:04:35 -07:00
rembo10 70b1574aa6 Updated sab.py to use the requests library. Should fix #2208 2015-05-06 00:02:11 -07:00
rembo10 898f814dce Update CHANGELOG.md 2015-05-04 21:09:10 -07:00
rembo10 9d425d33ab Added button to toggle verbose/debug logging 2015-05-04 20:58:53 -07:00
rembo10 c6b0f58074 Moved the 'freeze db' option to advanced->misc. tab 2015-05-04 20:55:00 -07:00
rembo10 78b3802f8f Added option to ignore clean/censored releases (#2198) 2015-05-04 20:50:16 -07:00
rembo10 833b90ccff Release v0.5.5 2015-05-04 19:43:37 -07:00
rembo10 c27a381286 Update CHANGELOG.md 2015-05-04 19:37:23 -07:00
rembo10 ad29b43836 Needed to capitalize beets recommendation.none 2015-03-20 17:29:43 -07:00
rembo10 5cd9c51410 Move 'checking folder' message to debug level. Show it if forced 2015-03-15 18:28:38 -07:00
rembo10 6476d42371 Return some json when picking a specific download so we can close the dialog box on success/failure 2015-03-14 00:27:59 -07:00
rembo10 a48d9006ad Fix to get size sorting working again when choosing a specific download 2015-03-14 00:04:14 -07:00
rembo10 c5c2057859 Return everything if they're looking for a specific download 2015-03-13 23:43:29 -07:00
rembo10 1889ab4825 Fix for min/max sizes with target bitrate (wrong calculation) 2015-03-12 14:14:06 -07:00
rembo10 0c81560f2c uggh, indentation 2015-03-09 17:05:28 -07:00
rembo10 26005d6584 expose getArtistjson so we can automatically update albums when updating an artist 2015-03-09 17:03:25 -07:00
rembo10 b0b0bcbdb7 Fix for piratebay returning size as 0 bytes for all results 2015-03-06 10:02:04 -08:00
Bas Stottelaar 546099fa2d Updated Changelog.md 2015-02-19 20:15:08 +01:00
Bas Stottelaar c53fde6dca Merge remote-tracking branch 'origin/master' into develop
* origin/master:
  Release v0.5.4
  Added API call getSnatched
  Potential fix for #2055
2015-02-19 20:12:49 +01:00
Jonny c9aeefc67a Update postprocessor.py
Changed ID3 logging to debug
2015-02-19 19:24:35 +01:00
Jonny 37594d3384 Update postprocessor.py
Set-up of beets config to match headphones config of ID3 tag version before writing metadata
2015-02-19 19:24:25 +01:00
Jonny 00dc5d8c38 Update config.py
Added config option for ID3 tag version select
2015-02-19 19:24:18 +01:00
Jonny ff8f6b1d00 Update webserve.py
Added config option for ID3 tag version select
Conflicts:
	headphones/webserve.py
2015-02-19 19:24:09 +01:00
Jonny aa60ba1fb1 Update config.html
Added option to select using ID3v2.3
2015-02-19 19:22:42 +01:00
Bas Stottelaar 3b8341f249 Merge pull request #2125 from drzoidberg33/develop
Allow authentication on custom MusicBrainz servers
2015-02-19 14:06:29 +01:00
Tim a68bcb6ee1 Fix condition that checks if we should disable custom MusicBrainz server authentication. 2015-02-19 11:48:10 +02:00
Tim 24a52a294c Allow authentication on custom MusicBrainz servers using Basic HTTP Authentication. 2015-02-19 00:54:10 +02:00
Bas Stottelaar 03ea67b20b Fix for #2119 2015-02-15 14:02:49 +01:00
Ade 4f20003f9a Email part 2
small fix up
2015-02-06 21:37:41 +13:00
Ade a92d13652d Email notifications
#1045
2015-02-06 20:28:57 +13:00
34 changed files with 1790 additions and 793 deletions
+72
View File
@@ -1,5 +1,77 @@
# Changelog # Changelog
## v0.5.8
Released 13 July 2015
Highlights:
* Added: Option to only include official extras
* Added: Option to wait until album release date before searching
* Fixed: NotifyMyAndroid notifications
* Fixed: Plex Notifications
* Fixed: Metacritic parsing
* Fixed: Pushbullet notifications
* Fixed: What.cd not honoring custom search term (#2279)
* Improved: XSS Search bug
* Improved: Config page layout
* Improved: Set localhost as default
* Improved: Better single artist scanning
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.6...v0.5.7).
## v0.5.7
Released 01 July 2015
Highlights:
* Improved: Moved pushover to use requests lib
* Improved: Plex tokens with Plex Home
* Improved: Added getLogs & clearLogs to api
* Improved: Cache MetaCritic scores. Added user-agent header to fix 403 errors
* Improved: Specify whether to delete folders when force post-processing
* Improved: Convert target bitrate to vbr preset for what.cd searching
* Improved: Switched Pushover to requests lib
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.6...v0.5.7).
## v0.5.6
Released 08 June 2015
Highlights:
* Added: Metacritic scores
* Added: Series support (e.g. Cafe Del Mar, Now That's What I Call Music, etc)
* Added: Filter out clean/edited/censored releases (#2198)
* Added: Button on the log page to toggle verbose/debug logging
* Fixed: Connecting to SABnzbd over https with python >= 2.7.9
* Fixed: Email Notifications with SSL
* Fixed: Don't limit musicbrainz results to first 100
* Fixed: nzbget url fix
* Fixed: OSX Notifications
* Improved: Cuesplit, allow wav, ape to be split
* Improved: Moved the 'freeze db' option to the advanced->misc. tab
* Improved: Moved kickass searching to json api, so it doesn't throw 404 errors anymore when there are no results
* Improved: SSL for headphones indexer
* Improved: Disable update dialog box if check_github is diabled
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.5...v0.5.6).
## v0.5.5
Released 04 May 2015
Highlights:
* Added: force ID3v2.3 during post processing (#2121)
* Added: MusicBrainz authentication (#2125)
* Added: Email notifications (addresses #1045)
* Fixed: Kickass url updated to kickass.to (#2119)
* Fixed: Piratebay returning 0 bytes for all files
* Fixed: Albums stopped automatically refreshing when adding an artist
* Fixed: Min/max sizes for target bitrate
* Fixed: Don't filter any results if looking for a specific download
* Fixed: Sort by size in the specific download table
* Fixed: Deal with beets recommendation.none correctly
* Improved: Close dialog window automatically when choosing a specific download
* Improved: Move some repetitive log messages to debug level
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.4...v0.5.5).
## v0.5.4 ## v0.5.4
Released 05 February 2015 Released 05 February 2015
+17 -1
View File
@@ -35,6 +35,7 @@
%for alternate_album in alternate_albums: %for alternate_album in alternate_albums:
<% <%
track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=?", [alternate_album['ReleaseID']])) track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=?", [alternate_album['ReleaseID']]))
mb_link = "http://musicbrainz.org/release/" + alternate_album['ReleaseID']
have_track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=? AND Location IS NOT NULL", [alternate_album['ReleaseID']])) have_track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=? AND Location IS NOT NULL", [alternate_album['ReleaseID']]))
if alternate_album['AlbumID'] == alternate_album['ReleaseID']: if alternate_album['AlbumID'] == alternate_album['ReleaseID']:
alternate_album_name = "Headphones Default Release (" + str(alternate_album['ReleaseDate']) + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]" alternate_album_name = "Headphones Default Release (" + str(alternate_album['ReleaseDate']) + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
@@ -42,7 +43,7 @@
alternate_album_name = alternate_album['AlbumTitle'] + " (" + alternate_album['ReleaseCountry'] + ", " + str(alternate_album['ReleaseDate']) + ", " + alternate_album['ReleaseFormat'] + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]" alternate_album_name = alternate_album['AlbumTitle'] + " (" + alternate_album['ReleaseCountry'] + ", " + str(alternate_album['ReleaseDate']) + ", " + alternate_album['ReleaseFormat'] + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
%> %>
<a href="#" onclick="doAjaxCall('switchAlbum?AlbumID=${album['AlbumID']}&ReleaseID=${alternate_album['ReleaseID']}', $(this), 'table');" data-success="Switched release to: ${alternate_album_name}">${alternate_album_name}</a><br> <a href="#" onclick="doAjaxCall('switchAlbum?AlbumID=${album['AlbumID']}&ReleaseID=${alternate_album['ReleaseID']}', $(this), 'table');" data-success="Switched release to: ${alternate_album_name}">${alternate_album_name}</a><a href="${mb_link}" target="_blank">MB</a><br>
%endfor %endfor
%endif %endif
</div> </div>
@@ -337,6 +338,21 @@
}); });
} }
jQuery.extend( jQuery.fn.dataTableExt.oSort, {
"title-numeric-pre": function ( a ) {
var x = a.match(/title="*(-?[0-9\.]+)/)[1];
return parseFloat( x );
},
"title-numeric-asc": function ( a, b ) {
return ((a < b) ? -1 : ((a > b) ? 1 : 0));
},
"title-numeric-desc": function ( a, b ) {
return ((a < b) ? 1 : ((a > b) ? -1 : 0));
}
} );
$(document).ready(function() { $(document).ready(function() {
getAlbumInfo(); getAlbumInfo();
initThisPage(); initThisPage();
+4 -1
View File
@@ -70,6 +70,7 @@
<th id="albumname">Name</th> <th id="albumname">Name</th>
<th id="reldate">Date</th> <th id="reldate">Date</th>
<th id="type">Type</th> <th id="type">Type</th>
<th id="score">Metacritic</th>
<th id="status">Status</th> <th id="status">Status</th>
<th id="have">Have</th> <th id="have">Have</th>
<th id="bitrate">Bitrate</th> <th id="bitrate">Bitrate</th>
@@ -125,6 +126,7 @@
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td> <td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
<td id="reldate">${album['ReleaseDate']}</td> <td id="reldate">${album['ReleaseDate']}</td>
<td id="type">${album['Type']}</td> <td id="type">${album['Type']}</td>
<td id="score">${album['CriticScore']}/${album['UserScore']}</td>
<td id="status">${album['Status']} <td id="status">${album['Status']}
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored': %if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
[<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' added to Wanted list">want</a>] [<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' added to Wanted list">want</a>]
@@ -218,7 +220,7 @@
$("li:gt(4)", this).hide(); /* :gt() is zero-indexed */ $("li:gt(4)", this).hide(); /* :gt() is zero-indexed */
$("li:nth-child(5)", this).after("<br><li class='more'><a href='#'>More...</a></li>"); /* :nth-child() is one-indexed */ $("li:nth-child(5)", this).after("<br><li class='more'><a href='#'>More...</a></li>"); /* :nth-child() is one-indexed */
}); });
$("li.more a").live("click", function() { $("li.more").on("click", 'a', function() {
var li = $(this).parents("li:first"); var li = $(this).parents("li:first");
li.parent().children().show(); li.parent().children().show();
li.remove(); li.remove();
@@ -283,6 +285,7 @@
{ "sType": "date" }, { "sType": "date" },
null, null,
null, null,
null,
{ "sType": "title-numeric"}, { "sType": "title-numeric"},
null, null,
null null
+3 -2
View File
@@ -31,12 +31,12 @@
<body> <body>
<div id="container"> <div id="container">
<div id="ajaxMsg" class="ajaxMsg"></div> <div id="ajaxMsg" class="ajaxMsg"></div>
% if not headphones.CURRENT_VERSION: % if headphones.CONFIG.CHECK_GITHUB and not headphones.CURRENT_VERSION:
<div id="updatebar"> <div id="updatebar">
You're running an unknown version of Headphones. <a href="update">Update</a> or You're running an unknown version of Headphones. <a href="update">Update</a> or
<a href="#" onclick="$('#updatebar').slideUp('slow');">Close</a> <a href="#" onclick="$('#updatebar').slideUp('slow');">Close</a>
</div> </div>
% elif headphones.CURRENT_VERSION != headphones.LATEST_VERSION and headphones.COMMITS_BEHIND > 0 and headphones.INSTALL_TYPE != 'win': % elif headphones.CONFIG.CHECK_GITHUB and headphones.CURRENT_VERSION != headphones.LATEST_VERSION and headphones.COMMITS_BEHIND > 0 and headphones.INSTALL_TYPE != 'win':
<div id="updatebar"> <div id="updatebar">
A <a href="https://github.com/${headphones.CONFIG.GIT_USER}/headphones/compare/${headphones.CURRENT_VERSION}...${headphones.LATEST_VERSION}"> newer version</a> is available. You're ${headphones.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or <a href="#" onclick="$('#updatebar').slideUp('slow');">Close</a> A <a href="https://github.com/${headphones.CONFIG.GIT_USER}/headphones/compare/${headphones.CURRENT_VERSION}...${headphones.LATEST_VERSION}"> newer version</a> is available. You're ${headphones.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or <a href="#" onclick="$('#updatebar').slideUp('slow');">Close</a>
</div> </div>
@@ -62,6 +62,7 @@
<select name="type" id="search_type"> <select name="type" id="search_type">
<option value="artist">Artist</option> <option value="artist">Artist</option>
<option value="album">Album</option> <option value="album">Album</option>
<option value="series">Series</option>
</select> </select>
<input type="submit" value="Search"/> <input type="submit" value="Search"/>
</form> </form>
+369 -227
View File
@@ -38,7 +38,7 @@
HTTP Host HTTP Host
</label> </label>
<input type="text" name="http_host" value="${config['http_host']}" size="30"> <input type="text" name="http_host" value="${config['http_host']}" size="30">
<small>e.g. localhost or an IP, such as 0.0.0.0</small> <small>Use 0.0.0.0 to allow outside connections</small>
</div> </div>
<div class="row"> <div class="row">
<label title="Port to bind web server to. Note that ports below 1024 may require root."> <label title="Port to bind web server to. Note that ports below 1024 may require root.">
@@ -413,10 +413,9 @@
<td> <td>
<legend>NZBs</legend> <legend>NZBs</legend>
<fieldset> <fieldset>
<legend>Headphones Indexer</legend> <div class="row checkbox left">
<div class="row checkbox"> <input id="headphones_indexer" type="checkbox" class="bigcheck" name="headphones_indexer" value="1" ${config['headphones_indexer']} />
<input id="headphones_indexer" type="checkbox" name="headphones_indexer" onclick="initConfigCheckbox($(this));" value="1" ${config['headphones_indexer']} /> <label for="headphones_indexer"><span class="option">Headphones Indexer</span></label>
<label>Use Headphones Indexer</label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
@@ -429,14 +428,13 @@
<input class="hppass" type="password" value="${config['hppass']}" size="20"> <input class="hppass" type="password" value="${config['hppass']}" size="20">
</div> </div>
<div class="row"> <div class="row">
<a href="http://headphones.codeshy.com/vip" id="vipserver" target="_blank">Don't have an account? Sign up!</a> <a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Don't have an account? Sign up!</a>
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Custom Newznab Providers</legend> <div class="row checkbox left">
<div class="row checkbox"> <input id="use_newznab" type="checkbox" class="bigcheck" name="use_newznab" value="1" ${config['use_newznab']} /><label for="use_newznab"><span class="option">Custom Newznab Providers</span></label>
<input id="use_newznab" type="checkbox" name="use_newznab" onclick="initConfigCheckbox($(this));" value="1" ${config['use_newznab']} /><label>Use Newznab</label>
</div> </div>
<div id="newznab_providers"> <div id="newznab_providers">
<div class="config" id="newznab1"> <div class="config" id="newznab1">
@@ -450,7 +448,7 @@
<input type="text" name="newznab_apikey" value="${config['newznab_apikey']}" size="36"> <input type="text" name="newznab_apikey" value="${config['newznab_apikey']}" size="36">
</div> </div>
<div class="row checkbox"> <div class="row checkbox">
<input id="newznab_enabled" type="checkbox" name="newznab_enabled" onclick="initConfigCheckbox($(this));" value="1" ${config['newznab_enabled']} /><label>Enabled</label> <input id="newznab_enabled" type="checkbox" name="newznab_enabled" value="1" ${config['newznab_enabled']} /><label>Enabled</label>
</div> </div>
</div> </div>
<% <%
@@ -488,9 +486,8 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>NZBs.org</legend> <div class="row checkbox left">
<div class="row checkbox"> <input id="use_nzbsorg" type="checkbox" class="bigcheck" name="use_nzbsorg" value="1" ${config['use_nzbsorg']} /><label for="use_nzbsorg"><span class="option">NZBs.org</span></label>
<input id="use_nzbsorg" type="checkbox" name="use_nzbsorg" onclick="initConfigCheckbox($(this));" value="1" ${config['use_nzbsorg']} /><label>Use NZBs.org</label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
@@ -500,9 +497,8 @@
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>omgwtfnzbs</legend> <div class="row checkbox left">
<div class="row checkbox"> <input id="use_omgwtfnzbs" type="checkbox" class="bigcheck" name="use_omgwtfnzbs" value="1" ${config['use_omgwtfnzbs']} /><label for="use_omgwtfnzbs"><span class="option">omgwtfnzbs</span></label>
<input id="use_omgwtfnzbs" type="checkbox" name="use_omgwtfnzbs" onclick="initConfigCheckbox($(this));" value="1" ${config['use_omgwtfnzbs']} /><label>Use omgwtfnzbs</label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
@@ -521,9 +517,8 @@
<legend>Torrents</legend> <legend>Torrents</legend>
<fieldset> <fieldset>
<legend>The Pirate Bay</legend> <div class="row checkbox left">
<div class="row checkbox"> <input id="use_piratebay" type="checkbox" class="bigcheck" name="use_piratebay" value="1" ${config['use_piratebay']} /><label for="use_piratebay"><span class="option">The Pirate Bay</span></label>
<input id="use_piratebay" type="checkbox" name="use_piratebay" value="1" ${config['use_piratebay']} /><label>Use The Pirate Bay</label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
@@ -539,9 +534,8 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Old Pirate Bay</legend> <div class="row checkbox left">
<div class="row checkbox"> <input id="use_oldpiratebay" type="checkbox" class="bigcheck" name="use_oldpiratebay" value="1" ${config['use_oldpiratebay']} /><label for="use_oldpiratebay"><span class="option">Old Pirate Bay</span></label>
<input id="use_oldpiratebay" type="checkbox" name="use_oldpiratebay" value="1" ${config['use_oldpiratebay']} /><label>Use Old Pirate Bay</label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
@@ -556,9 +550,8 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Kick Ass Torrents</legend> <div class="row checkbox left">
<div class="row checkbox"> <input id="use_kat" type="checkbox" class="bigcheck" name="use_kat" value="1" ${config['use_kat']} /><label for="use_kat"><span class="option">Kick Ass Torrents</span></label>
<input id="use_kat" type="checkbox" name="use_kat" value="1" ${config['use_kat']} /><label>Use Kick Ass Torrents</label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
@@ -574,9 +567,8 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Waffles.fm</legend> <div class="row checkbox left">
<div class="row checkbox"> <input id="use_waffles" type="checkbox" class="bigcheck" name="use_waffles" value="1" ${config['use_waffles']} /><label for="use_waffles"><span class="option">Waffles.fm</span></label>
<input id="use_waffles" type="checkbox" name="use_waffles" onclick="initConfigCheckbox($(this));" value="1" ${config['use_waffles']} /><label>Use Waffles.fm</label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
@@ -595,9 +587,8 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>rutracker.org</legend> <div class="row checkbox left">
<div class="row checkbox"> <input id="use_rutracker" type="checkbox" class="bigcheck" name="use_rutracker" value="1" ${config['use_rutracker']} /><label for="use_rutracker"><span class="option">rutracker.org</span></label>
<input id="use_rutracker" type="checkbox" name="use_rutracker" onclick="initConfigCheckbox($(this));" value="1" ${config['use_rutracker']} /><label>Use rutracker.org</label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
@@ -616,9 +607,8 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>What.cd</legend> <div class="row checkbox left">
<div class="row checkbox"> <input id="use_whatcd" type="checkbox" class="bigcheck" name="use_whatcd" value="1" ${config['use_whatcd']} /><label for="use_whatcd"><span class="option">What.cd</span></label>
<input id="use_whatcd" type="checkbox" name="use_whatcd" onclick="initConfigCheckbox($(this));" value="1" ${config['use_whatcd']} /><label>Use What.cd</label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
@@ -637,9 +627,8 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Mininova</legend> <div class="row checkbox left">
<div class="row checkbox"> <input id="use_mininova" type="checkbox" class="bigcheck" name="use_mininova" value="1" ${config['use_mininova']} /><label for="use_mininova"><span class="option">Mininova</span></label>
<input id="use_mininova" type="checkbox" name="use_mininova" value="1" ${config['use_mininova']} /><label>Use Mininova</label>
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
@@ -659,52 +648,56 @@
<table class="configtable" summary="Quality &amp; Post Processing"> <table class="configtable" summary="Quality &amp; Post Processing">
<tr> <tr>
<td> <td>
<fieldset>
<legend>Quality</legend> <legend>Quality</legend>
<div class="row radio clearfix"> <fieldset>
<div class="row checkbox left clearfix nopad">
<label title="Snatch the highest quality available, excluding lossless."> <label title="Snatch the highest quality available, excluding lossless.">
<input type="radio" name="preferred_quality" id="preferred_quality0" value="0" ${config['pref_qual_0']}> <input type="radio" name="preferred_quality" id="preferred_quality0" value="0" ${config['pref_qual_0']}>
Highest Quality excluding Lossless Highest Quality excluding Lossless
</label> </label>
</div>
<div class="row checkbox left clearfix nopad">
<label title="Snatch the highest quality available, including lossless."> <label title="Snatch the highest quality available, including lossless.">
<input type="radio" name="preferred_quality" id="preferred_quality1" value="1" ${config['pref_qual_1']}> <input type="radio" name="preferred_quality" id="preferred_quality1" value="1" ${config['pref_qual_1']}>
Highest Quality including Lossless Highest Quality including Lossless
</label> </label>
</div>
<div class="row checkbox left clearfix nopad">
<label title="Snatch only lossless quality."> <label title="Snatch only lossless quality.">
<input type="radio" name="preferred_quality" id="lossless_only" value="3" ${config['pref_qual_3']}> <input type="radio" name="preferred_quality" id="lossless_only" value="3" ${config['pref_qual_3']}>
Lossless Only Lossless Only
</label> </label>
<div id="lossless_only_options"> </div>
<div id="lossless_only_options" class="row left clearfix">
<span style="padding-left: 20px"> <span style="padding-left: 20px">
Reject if target size is not in bitrate range: Reject if target size is not in bitrate range:
<input type="text" class="override-float" name="lossless_bitrate_from" value="${config['lossless_bitrate_from']}" size="4" title="e.g. if album length = 40 mins, from = 350 kbps, then min target size = 102.5 mb, anything less will be rejected">to\ <input type="text" class="override-float" name="lossless_bitrate_from" value="${config['lossless_bitrate_from']}" size="4" title="e.g. if album length = 40 mins, from = 350 kbps, then min target size = 102.5 mb, anything less will be rejected">to\
<input type="text" class="override-float" name="lossless_bitrate_to" value="${config['lossless_bitrate_to']}" size="5" title="e.g. if album length = 40 mins, to = 2000 kbps, then max target size = 586 mb, anything greater will be rejected">kbps <input type="text" class="override-float" name="lossless_bitrate_to" value="${config['lossless_bitrate_to']}" size="5" title="e.g. if album length = 40 mins, to = 2000 kbps, then max target size = 586 mb, anything greater will be rejected">kbps
</span> </span>
</div> </div>
<div class="row left clearfix nopad">
<label title="Prefer certain bitrate range, including lossless fallback. Use this option if you have a lot of wrong snatches."> <label title="Prefer certain bitrate range, including lossless fallback. Use this option if you have a lot of wrong snatches.">
<input type="radio" id="preferred_bitrate" name="preferred_quality" value="2" ${config['pref_qual_2']}> <input type="radio" id="preferred_bitrate" name="preferred_quality" value="2" ${config['pref_qual_2']}>
Preferred Bitrate Preferred Bitrate
<label> <label>
<div id="preferred_bitrate_options"> </div>
<div class="row"> <div id="preferred_bitrate_options" class="suboptions">
Target bitrate <div class="row nopad">
Target bitrate:
<input type="text" class="override-float" name="preferred_bitrate" value="${config['preferred_bitrate']}" size="3">kbps<br> <input type="text" class="override-float" name="preferred_bitrate" value="${config['preferred_bitrate']}" size="3">kbps<br>
</div> </div>
<div class="row"> <div class="row nopad">
<span style="padding-left: 20px"> Reject if <strong>less than</strong> <input type="text" class="override-float" name="preferred_bitrate_low_buffer" value="${config['preferred_bitrate_low']}" size="3">% or <strong>more than</strong> <input type="text" class="override-float" name="preferred_bitrate_high_buffer" value="${config['preferred_bitrate_high']}" size="3">% of the target size (leave blank for no limit)
Reject if <strong>less than</strong> <input type="text" class="override-float" name="preferred_bitrate_low_buffer" value="${config['preferred_bitrate_low']}" size="3">% or <strong>more than</strong> <input type="text" class="override-float" name="preferred_bitrate_high_buffer" value="${config['preferred_bitrate_high']}" size="3">% of the target size (leave blank for no limit)<br>
</span>
</div> </div>
<div class="row checkbox left" style="padding-left: 20px"> <div class="row checkbox clearfix left nopad">
<input type="checkbox" name="preferred_bitrate_allow_lossless" value="1" ${config['preferred_bitrate_allow_lossless']}> <input type="checkbox" name="preferred_bitrate_allow_lossless" value="1" ${config['preferred_bitrate_allow_lossless']}>
<label>Allow lossless if no good lossy match found</label> <label>Allow lossless if no good lossy match found</label>
</div> </div>
<div class="row checkbox left" style="padding-left: 20px"> <div class="row checkbox clearfix left nopad">
<input type="checkbox" name="detect_bitrate" value="1" ${config['detect_bitrate']} /> <input type="checkbox" name="detect_bitrate" value="1" ${config['detect_bitrate']} >
<label>Auto-Detect Preferred Bitrate</label> <label>Auto-Detect Preferred Bitrate</label>
</div> </div>
</div> </div>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Search Words</legend> <legend>Search Words</legend>
@@ -724,64 +717,86 @@
<input type="text" name="required_words" value="${config['required_words']}" size="50"> <input type="text" name="required_words" value="${config['required_words']}" size="50">
<small>Results without these words in the title will be filtered out. You can use OR: 'flac OR lossless OR alac, vinyl'</small> <small>Results without these words in the title will be filtered out. You can use OR: 'flac OR lossless OR alac, vinyl'</small>
</div> </div>
<div class="row checkbox left clearfix">
<label title="Filter out releases that contain the words 'clean','edited' or 'censored', as long as those words aren't in the search term">
Ignore clean/censored releases
<input type="checkbox" name="ignore_clean_releases" id="ignore_clean_releases" value="1" ${config['ignore_clean_releases']} />
</label>
</div>
</fieldset> </fieldset>
</td> </td>
<td> <td>
<fieldset> <fieldset>
<legend>Post-Processing</legend> <legend>Post-Processing</legend>
<div class="row checkbox left clearfix"> <div class="row checkbox left clearfix nopad">
<label title="Use associated .cue sheet to split single file albums into multiple tracks. Requires shntool with flac or xld cli (OS X) to be installed.">
Split single file albums into multiple tracks
<input type="checkbox" name="cue_split" id="cue_split" value="1" ${config['cue_split']} />
</label>
</div>
<div class="row checkbox left clearfix">
<label title="Freeze the database, so new artists won't be added automatically. Use this if Headphones adds artists because due to wrong snatches. This check is skipped when the folder name is appended with release group ID.">
Freeze database for adding new artist
<input type="checkbox" name="freeze_db" id="freeze_db" value="1" ${config['freeze_db']} />
</label>
<label> <label>
Move downloads to Destination Folder Move downloads to Destination Folder
<input type="checkbox" name="move_files" id="move_files" value="1" ${config['move_files']} /> <input type="checkbox" name="move_files" id="move_files" value="1" ${config['move_files']} />
</label> </label>
<div id="move_files_options"> </div>
<div class="row indent"> <div id="move_files_options" class="suboptions">
<div class="row checkbox left clearfix nopad">
<label> <label>
Replace existing folders? Replace existing folders?
<input type="checkbox" name="replace_existing_folders" value="1" ${config['replace_existing_folders']}> <input type="checkbox" name="replace_existing_folders" value="1" ${config['replace_existing_folders']}>
</label> </label>
</div> </div>
<div class="row checkbox left clearfix nopad">
<label>
Keep original folder (i.e. copy)?
<input type="checkbox" name="keep_original_folder" value="1" ${config['keep_original_folder']}>
</label>
</div> </div>
</div>
<div class="row checkbox left clearfix nopad">
<label> <label>
Rename files Rename files
<input type="checkbox" name="rename_files" value="1" ${config['rename_files']} /> <input type="checkbox" name="rename_files" value="1" ${config['rename_files']} />
</label> </label>
</div>
<div class="row checkbox left clearfix nopad">
<label> <label>
Correct metadata Correct metadata
<input type="checkbox" name="correct_metadata" value="1" ${config['correct_metadata']} /> <input type="checkbox" name="correct_metadata" value="1" ${config['correct_metadata']} />
</label> </label>
</div>
<div class="row checkbox left clearfix nopad">
<label title="Use associated .cue sheet to split single file albums into multiple tracks. Requires shntool with flac or xld cli (OS X) to be installed.">
Split single file albums into multiple tracks
<input type="checkbox" name="cue_split" id="cue_split" value="1" ${config['cue_split']} />
</label>
</div>
<div class="row checkbox left clearfix nopad">
<label> <label>
Delete leftover files <small>(.m3u, .nfo, .sfv, .nzb, etc.)</small> Delete leftover files <small>(.m3u, .nfo, .sfv, .nzb, etc.)</small>
<input type="checkbox" name="cleanup_files" value="1" ${config['cleanup_files']} /> <input type="checkbox" name="cleanup_files" value="1" ${config['cleanup_files']} />
</label> </label>
</div>
<div class="row checkbox left clearfix nopad">
<label> <label>
Keep original nfo <small>(extension changed to .orig.nfo)</small> Keep original nfo <small>(extension changed to .orig.nfo)</small>
<input type="checkbox" name="keep_nfo" value="1" ${config['keep_nfo']} /> <input type="checkbox" name="keep_nfo" value="1" ${config['keep_nfo']} />
</label> </label>
</div>
<div class="row checkbox left clearfix nopad">
<label> <label>
Add album art jpeg to album folder Add album art jpeg to album folder
<input type="checkbox" name="add_album_art" id="add_album_art" value="1" ${config['add_album_art']}> <input type="checkbox" name="add_album_art" id="add_album_art" value="1" ${config['add_album_art']}>
</label> </label>
</div>
<div id="album_art_options" style="padding-left: 20px"> <div id="album_art_options" style="padding-left: 20px">
<div class="row"> <div class="row">
as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg
</div> </div>
<small>Use $Artist/$artist, $Album/$album, $Year/$year</small> <small>Use $Artist/$artist, $Album/$album, $Year/$year</small>
</div> </div>
<div class="row checkbox left clearfix nopad">
<label> <label>
Embed album art in each file Embed album art in each file
<input type="checkbox" name="embed_album_art" value="1" ${config['embed_album_art']}> <input type="checkbox" name="embed_album_art" value="1" ${config['embed_album_art']}>
</label> </label>
</div>
<div class="row checkbox left clearfix nopad">
<label> <label>
Embed lyrics Embed lyrics
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}> <input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}>
@@ -812,9 +827,57 @@
<tr> <tr>
<td> <td>
<fieldset> <fieldset>
<h3>Growl</h3> <div class="row checkbox left">
<input type="checkbox" class="bigcheck" name="boxcar_enabled" id="boxcar" value="1" ${config['boxcar_enabled']} /><label for="boxcar"><span class="option">Boxcar 2</span></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"> <div class="row checkbox">
<input type="checkbox" name="growl_enabled" id="growl" value="1" ${config['growl_enabled']} /><label>Enable Growl Notifications</label> <input type="checkbox" name="boxcar_onsnatch" value="1" ${config['boxcar_onsnatch']} /><label>Notify on snatch?</label>
</div>
</div>
</fieldset>
<fieldset>
<div class="row checkbox left">
<input type="checkbox" class="bigcheck" name="email_enabled" id="email" value="1" ${config['email_enabled']} /><label for="email"><span class="option">Email</span></label>
</div>
<div id="email_options">
<div class="row">
<label>From</label><input type="text" name="email_from" value="${config['email_from']}" size="254">
</div>
<div class="row">
<label>To</label><input type="text" name="email_to" value="${config['email_to']}" size="254">
</div>
<div class="row">
<label>SMTP Server</label><input type="text" name="email_smtp_server" value="${config['email_smtp_server']}" size="254">
</div>
<div class="row">
<label>SMTP User</label><input type="text" name="email_smtp_user" value="${config['email_smtp_user']}" size="254">
</div>
<div class="row">
<label>SMTP Password</label><input type="password" name="email_smtp_password" value="${config['email_smtp_password']}" size="50">
</div>
<div class="row checkbox">
<input type="text" class="override-float" name="email_smtp_port" value="${config['email_smtp_port']}" size="4"><label>SMTP Port</label>
</div>
<div class="row checkbox">
<input type="checkbox" name="email_ssl" value="1" ${config['email_ssl']} /><label>SSL</label>
</div>
<div class="row checkbox">
<input type="checkbox" name="email_tls" value="1" ${config['email_tls']} /><label>TLS</label>
</div>
<div class="row checkbox">
<input type="checkbox" name="email_onsnatch" value="1" ${config['email_onsnatch']} /><label>Notify on snatch?</label>
</div>
</div>
</fieldset>
<fieldset>
<div class="row checkbox left">
<input type="checkbox" class="bigcheck" name="growl_enabled" id="growl" value="1" ${config['growl_enabled']} /><label for="growl"><span class="option">Growl</span></label>
</div> </div>
<div id="growloptions"> <div id="growloptions">
<div class="row"> <div class="row">
@@ -828,92 +891,61 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<h3>Prowl</h3> <div class="row checkbox left">
<div class="row checkbox"> <input type="checkbox" class="bigcheck" name="xbmc_enabled" id="xbmc" value="1" ${config['xbmc_enabled']} /><label for="xbmc"><span class="option">Kodi / XBMC</span></label>
<input type="checkbox" name="prowl_enabled" id="prowl" value="1" ${config['prowl_enabled']} /><label>Enable Prowl Notifications</label>
</div>
<div id="prowloptions">
<div class="row">
<label>API key</label><input type="text" name="prowl_keys" value="${config['prowl_keys']}" size="50">
</div>
<div class="row checkbox">
<input type="checkbox" name="prowl_onsnatch" value="1" ${config['prowl_onsnatch']} /><label>Notify on snatch?</label>
</div>
<div class="row">
<label>Priority (-2,-1,0,1 or 2)</label>
<input type="text" name="prowl_priority" value="${config['prowl_priority']}" size="2">
</div>
</div>
</fieldset>
<fieldset>
<h3>XBMC</h3>
<div class="checkbox row">
<input type="checkbox" name="xbmc_enabled" id="xbmc" value="1" ${config['xbmc_enabled']} /><label>Enable XBMC Updates</label>
</div> </div>
<div id="xbmcoptions"> <div id="xbmcoptions">
<div class="row"> <div class="row">
<label>XBMC Host:Port</label> <label>Host(s)</label>
<input type="text" name="xbmc_host" value="${config['xbmc_host']}" size="30"> <input type="text" name="xbmc_host" value="${config['xbmc_host']}" size="30">
<small>e.g. http://localhost:8080. Separate hosts with commas</small> <small>e.g. http://localhost:8080. Separate hosts with commas</small>
</div> </div>
<div class="row"> <div class="row">
<label>XBMC Username</label><input type="text" name="xbmc_username" value="${config['xbmc_username']}" size="30"> <label>Username</label><input type="text" name="xbmc_username" value="${config['xbmc_username']}" size="30">
</div> </div>
<div class="row"> <div class="row">
<label>XBMC Password</label><input type="password" name="xbmc_password" value="${config['xbmc_password']}" size="30"> <label>Password</label><input type="password" name="xbmc_password" value="${config['xbmc_password']}" size="30">
</div> </div>
<div class="checkbox row"> <div class="checkbox row">
<input type="checkbox" name="xbmc_update" value="1" ${config['xbmc_update']} /><label>Update XBMC Library</label> <input type="checkbox" name="xbmc_update" value="1" ${config['xbmc_update']} /><label>Update Library</label>
</div> </div>
<div class="checkbox row"> <div class="checkbox row">
<input type="checkbox" name="xbmc_notify" value="1" ${config['xbmc_notify']} /><label>Send Notification to XBMC</label> <input type="checkbox" name="xbmc_notify" value="1" ${config['xbmc_notify']} /><label>Send Notifications</label>
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<h3>Plex Media Server</h3> <div class="row checkbox left">
<div class="checkbox row"> <input type="checkbox" class="bigcheck" name="lms_enabled" id="lms" value="1" ${config['lms_enabled']} /><label for="lms"><span class="option">Logitech Media Server</span></label>
<input type="checkbox" name="plex_enabled" id="plex" value="1" ${config['plex_enabled']} /><label>Enable Plex Updates</label>
</div> </div>
<div id="plexoptions"> <div id="lmsoptions">
<div class="row"> <div class="row">
<label>Plex Server Host:Port</label> <label>LMS Host:Port</label>
<input type="text" name="plex_server_host" value="${config['plex_server_host']}" size="30"> <input type="text" name="lms_host" value="${config['lms_host']}" size="30">
<small>Host running Plex Media Server (eg. http://192.168.1.100:32400)</small> <small>e.g. http://localhost:9000. Seperate hosts with commas</small>
</div>
<div class="row">
<label>Plex Client Host:Port</label>
<input type="text" name="plex_client_host" value="${config['plex_client_host']}" size="30">
<small>Host running Plex Client (eg. http://192.168.1.100:3000)</small>
</div>
<div class="row">
<label>Plex Username</label><input type="text" name="plex_username" value="${config['plex_username']}" size="30">
<small>Username of your Plex client API (blank for none)</small>
</div>
<div class="row">
<label>Plex Password</label><input type="password" name="plex_password" value="${config['plex_password']}" size="30">
<small>Password of your Plex client API (blank for none)</small>
</div>
<div class="checkbox row">
<input type="checkbox" name="plex_update" value="1" ${config['plex_update']} /><label>Update Plex Library</label>
</div>
<div class="checkbox row">
<input type="checkbox" name="plex_notify" value="1" ${config['plex_notify']} /><label>Send Notifications to Plex</label>
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<h3>NotifyMyAndroid</h3> <div class="row checkbox left">
<div class="checkbox row"> <input type="checkbox" class="bigcheck" name="mpc_enabled" id="mpc" value="1" ${config['mpc_enabled']} /><label for="mpc"><span class="option">MPC</span></label>
<input type="checkbox" name="nma_enabled" id="nma" value="1" ${config['nma_enabled']} /><label>Enable NotifyMyAndroid</label> </div>
</fieldset>
<fieldset>
<div class="row checkbox left">
<input type="checkbox" class="bigcheck" name="nma_enabled" id="nma" value="1" ${config['nma_enabled']} /><label for="nma"><span class="option">NotifyMyAndroid</span></label>
</div> </div>
<div id="nmaoptions"> <div id="nmaoptions">
<div class="row checkbox"> <div class="row checkbox">
<input type="checkbox" name="nma_onsnatch" value="1" ${config['nma_onsnatch']} /><label>Notify on snatch?</label> <input type="checkbox" name="nma_onsnatch" value="1" ${config['nma_onsnatch']} /><label>Notify on snatch?</label>
</div> </div>
<div class="row"> <div class="row">
<label>NotifyMyAndroid API Key</label> <label>API Key</label>
<input type="text" name="nma_apikey" value="${config['nma_apikey']}" size="30"> <input type="text" name="nma_apikey" value="${config['nma_apikey']}" size="30">
<small>Separate multiple api keys with commas</small> <small>Separate multiple api keys with commas</small>
</div> </div>
@@ -944,24 +976,87 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<h3>Logitech Media Server</h3> <div class="row checkbox left">
<div class="checkbox row"> <input type="checkbox" class="bigcheck" name="osx_notify_enabled" id="osx_notify" value="1" ${config['osx_notify_enabled']} /><label for="osx_notify"><span class="option">OS X</span></label>
<input type="checkbox" name="lms_enabled" id="lms" value="1" ${config['lms_enabled']} /><label>Enable LMS Updates</label>
</div> </div>
<div id="lmsoptions"> <div id="osx_notify_options">
<div class="row"> <div class="row">
<label>LMS Host:Port</label> <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="text" name="lms_host" value="${config['lms_host']}" size="30"> </div>
<small>e.g. http://localhost:9000. Seperate hosts with commas</small> <div class="row">
<small>Enter the path/application name to be registered with the Notification Center, default is /Applications/Headphones</small>
</div>
<div class="row">
<input type="button" value="Register" id="osxnotifyregister"><label></label>
</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>
</td>
<td>
<fieldset>
<div class="row checkbox left">
<input type="checkbox" class="bigcheck" name="plex_enabled" id="plex" value="1" ${config['plex_enabled']} /><label for="plex"><span class="option">Plex</span></label>
</div>
<div id="plexoptions">
<div class="row">
<label>Plex Server Host:Port</label>
<input type="text" name="plex_server_host" value="${config['plex_server_host']}" size="30">
<small>Host running Plex Media Server (eg. http://192.168.1.100:32400)</small>
</div>
<div class="row">
<label>Plex Client Host:Port</label>
<input type="text" name="plex_client_host" value="${config['plex_client_host']}" size="30">
<small>Host running Plex Client (eg. http://192.168.1.100:3005)</small>
</div>
<div class="row">
<label>Plex Username</label><input type="text" name="plex_username" value="${config['plex_username']}" size="30">
<small>Username of your Plex client API (blank for none)</small>
</div>
<div class="row">
<label>Plex Password</label><input type="password" name="plex_password" value="${config['plex_password']}" size="30">
<small>Password of your Plex client API (blank for none)</small>
</div>
<div class="row">
<label>Plex Token</label><input type="text" name="plex_token" value="${config['plex_token']}" size="30">
<small>Plex Token (for use with Plex Home)</small>
</div>
<div class="checkbox row">
<input type="checkbox" name="plex_update" value="1" ${config['plex_update']} /><label>Update Plex Library</label>
</div>
<div class="checkbox row">
<input type="checkbox" name="plex_notify" value="1" ${config['plex_notify']} /><label>Send Notifications to Plex</label>
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<h3>Pushalot</h3> <div class="row checkbox left">
<div class="checkbox row"> <input type="checkbox" class="bigcheck" name="prowl_enabled" id="prowl" value="1" ${config['prowl_enabled']} /><label for="prowl"><span class="option">Prowl</span></label>
<input type="checkbox" name="pushalot_enabled" id="pushalot" value="1" ${config['pushalot_enabled']} /><label>Enable Pushalot</label> </div>
<div id="prowloptions">
<div class="row">
<label>API key</label><input type="text" name="prowl_keys" value="${config['prowl_keys']}" size="50">
</div>
<div class="row checkbox">
<input type="checkbox" name="prowl_onsnatch" value="1" ${config['prowl_onsnatch']} /><label>Notify on snatch?</label>
</div>
<div class="row">
<label>Priority (-2,-1,0,1 or 2)</label>
<input type="text" name="prowl_priority" value="${config['prowl_priority']}" size="2">
</div>
</div>
</fieldset>
<fieldset>
<div class="row checkbox left">
<input type="checkbox" class="bigcheck" name="pushalot_enabled" id="pushalot" value="1" ${config['pushalot_enabled']} /><label for="pushalot"><span class="option">Pushalot</span></label>
</div> </div>
<div id="pushalotoptions"> <div id="pushalotoptions">
<div class="row checkbox"> <div class="row checkbox">
@@ -976,41 +1071,29 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<h3>Subsonic</h3> <div class="row checkbox left">
<input type="checkbox" class="bigcheck" name="pushbullet_enabled" id="pushbullet" value="1" ${config['pushbullet_enabled']} /><label for="pushbullet"><span class="option">Pushbullet</span></label>
</div>
<div id="pushbulletoptions">
<div class="row">
<label>Access Token</label><input type="text" name="pushbullet_apikey" value="${config['pushbullet_apikey']}" size="50">
</div>
<div class="row">
<label>Device ID</label><input type="text" name="pushbullet_deviceid" value="${config['pushbullet_deviceid']}" size="50"><small>Leave blank to send to all devices</small>
</div>
<div class="row checkbox"> <div class="row checkbox">
<input type="checkbox" name="subsonic_enabled" id="subsonic" value="1" ${config['subsonic_enabled']} /><label>Enable Subsonic Updates</label> <input type="checkbox" name="pushbullet_onsnatch" value="1" ${config['pushbullet_onsnatch']} /><label>Notify on snatch?</label>
</div>
<div id="subsonicoptions">
<div class="row">
<label>Subsonic URL</label><input type="text" name="subsonic_host" value="${config['subsonic_host']}" size="30">
</div>
<div class="row">
<label>Subsonic Username</label><input type="text" name="subsonic_username" value="${config['subsonic_username']}" size="30">
</div>
<div class="row">
<label>Subsonic Password</label><input type="password" name="subsonic_password" value="${config['subsonic_password']}" size="30">
</div> </div>
</div> </div>
</fieldset> </fieldset>
</td>
<td>
<fieldset> <fieldset>
<h3>Synology NAS</h3> <div class="row checkbox left">
<div class="checkbox row"> <input type="checkbox" class="bigcheck" name="pushover_enabled" id="pushover" value="1" ${config['pushover_enabled']} /><label for="pushover"><span class="option">Pushover</span></label>
<input type="checkbox" name="synoindex_enabled" id="synoindex" value="1" ${config['synoindex_enabled']} /><label>Enable Synoindex</label>
</div>
</fieldset>
<fieldset>
<h3>Pushover</h3>
<div class="row checkbox">
<input type="checkbox" name="pushover_enabled" id="pushover" value="1" ${config['pushover_enabled']} /><label>Enable Pushover Notifications</label>
</div> </div>
<div id="pushoveroptions"> <div id="pushoveroptions">
<div class="row"> <div class="row">
<label>User key</label><input type="text" name="pushover_keys" value="${config['pushover_keys']}" size="50"> <label>User Key</label><input type="text" name="pushover_keys" value="${config['pushover_keys']}" size="50">
</div> </div>
<div class="row checkbox"> <div class="row checkbox">
<input type="checkbox" name="pushover_onsnatch" value="1" ${config['pushover_onsnatch']} /><label>Notify on snatch?</label> <input type="checkbox" name="pushover_onsnatch" value="1" ${config['pushover_onsnatch']} /><label>Notify on snatch?</label>
@@ -1026,27 +1109,31 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<h3>Pushbullet</h3> <div class="row checkbox left">
<div class="row checkbox"> <input type="checkbox" class="bigcheck" name="subsonic_enabled" id="subsonic" value="1" ${config['subsonic_enabled']} /><label for="subsonic"><span class="option">Subsonic</span></label>
<input type="checkbox" name="pushbullet_enabled" id="pushbullet" value="1" ${config['pushbullet_enabled']} /><label>Enable PushBullet Notifications</label>
</div> </div>
<div id="pushbulletoptions"> <div id="subsonicoptions">
<div class="row"> <div class="row">
<label>API Key</label><input type="text" name="pushbullet_apikey" value="${config['pushbullet_apikey']}" size="50"> <label>Subsonic URL</label><input type="text" name="subsonic_host" value="${config['subsonic_host']}" size="30">
</div> </div>
<div class="row"> <div class="row">
<label>Device ID</label><input type="text" name="pushbullet_deviceid" value="${config['pushbullet_deviceid']}" size="50"> <label>Subsonic Username</label><input type="text" name="subsonic_username" value="${config['subsonic_username']}" size="30">
</div> </div>
<div class="row checkbox"> <div class="row">
<input type="checkbox" name="pushbullet_onsnatch" value="1" ${config['pushbullet_onsnatch']} /><label>Notify on snatch?</label> <label>Subsonic Password</label><input type="password" name="subsonic_password" value="${config['subsonic_password']}" size="30">
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<h3>Twitter</h3> <div class="row checkbox left">
<div class="row checkbox"> <input type="checkbox" name="synoindex_enabled" id="synoindex" value="1" ${config['synoindex_enabled']} /><label for="synoindex"><span class="option">Synology NAS</span></label>
<input type="checkbox" name="twitter_enabled" id="twitter" value="1" ${config['twitter_enabled']} /><label>Enable Twitter Notifications</label> </div>
</fieldset>
<fieldset>
<div class="row checkbox left">
<input type="checkbox" class="bigcheck" name="twitter_enabled" id="twitter" value="1" ${config['twitter_enabled']} /><label for="twitter"><span class="option">Twitter</span></label>
</div> </div>
<div id="twitteroptions"> <div id="twitteroptions">
<div class="row checkbox"> <div class="row checkbox">
@@ -1065,46 +1152,6 @@
</div> </div>
</fieldset> </fieldset>
<fieldset>
<h3>OS X</h3>
<div class="row checkbox">
<input type="checkbox" name="osx_notify_enabled" id="osx_notify" value="1" ${config['osx_notify_enabled']} /><label>Enable OS X Notifications</label>
</div>
<div id="osx_notify_options">
<div class="row">
<small>Enter the path/application name to be registered with the Notification Center, default is /Applications/Headphones</small>
<input type="text" id="osx_notify_reg" name="osx_notify_app" value="${config['osx_notify_app']}" size="50"><label>Register Notify App</label>
<input type="button" value="Register" id="osxnotifyregister">
</div>
<div class="row checkbox">
<input type="checkbox" name="osx_notify_onsnatch" value="1" ${config['osx_notify_onsnatch']} /><label>Notify on snatch?</label>
</div>
</div>
</fieldset>
<fieldset>
<h3>Boxcar2</h3>
<div class="row checkbox">
<input type="checkbox" name="boxcar_enabled" id="boxcar" value="1" ${config['boxcar_enabled']} /><label>Enable Boxcar2 Notifications</label>
</div>
<div id="boxcar_options">
<div class="row">
<label>Access Token</label><input type="text" name="boxcar_token" value="${config['boxcar_token']}" size="35">
</div>
<div class="row checkbox">
<input type="checkbox" name="boxcar_onsnatch" value="1" ${config['boxcar_onsnatch']} /><label>Notify on snatch?</label>
</div>
</div>
</fieldset>
<fieldset>
<h3>MPC</h3>
<div class="checkbox row">
<input type="checkbox" name="mpc_enabled" id="mpc" value="1" ${config['mpc_enabled']} /><label>Enable MPC Update</label>
</div>
</fieldset>
<fieldset>
</td> </td>
</tr> </tr>
</table> </table>
@@ -1119,7 +1166,7 @@
<div class="row"> <div class="row">
<label>Folder Format</label> <label>Folder Format</label>
<input type="text" name="folder_format" value="${config['folder_format']}" size="43"> <input type="text" name="folder_format" value="${config['folder_format']}" size="43">
<small>Use: $Artist/$artist, $Album/$album, $Year/$year, $Type/$type (release type) and $First/$first (first letter in artist name), $OriginalFolder/$originalfolder (downloaded directory name) <small>Use: $Artist/$artist, $SortArtist/$sortartist, $Album/$album, $Year/$year, $Type/$type (release type) and $First/$first (first letter in artist name), $OriginalFolder/$originalfolder (downloaded directory name)
E.g.: $Type/$First/$artist/$album [$year] = Album/G/girl talk/all day [2010]</small> E.g.: $Type/$First/$artist/$album [$year] = Album/G/girl talk/all day [2010]</small>
</div> </div>
@@ -1213,7 +1260,7 @@
</div> </div>
<div class="row"> <div class="row">
<label>VBR/CBR</label> <label>VBR/CBR</label>
<select name="encodervbrcbr"> <select name="encodervbrcbr" id="encodervbrcbr">
%for x in ['cbr', 'vbr']: %for x in ['cbr', 'vbr']:
<% <%
if config['encodervbrcbr'] == x: if config['encodervbrcbr'] == x:
@@ -1225,6 +1272,7 @@
%endfor %endfor
</select> </select>
</div> </div>
<div id="vbr_options">
<div class="row"> <div class="row">
<label>Quality</label> <label>Quality</label>
<select name="encoderquality"> <select name="encoderquality">
@@ -1239,6 +1287,8 @@
%endfor %endfor
</select> </select>
</div> </div>
</div>
<div id="cbr_options">
<div class="row"> <div class="row">
<label>Bitrate</label> <label>Bitrate</label>
<select name="bitrate"> <select name="bitrate">
@@ -1253,7 +1303,7 @@
%endfor %endfor
</select> </select>
</div> </div>
</div>
<% <%
if config["samplingfrequency"] == 44100: if config["samplingfrequency"] == 44100:
freq44100 = 'selected="selected"' freq44100 = 'selected="selected"'
@@ -1295,7 +1345,7 @@
<td> <td>
<fieldset> <fieldset>
<legend>Miscellaneous</legend> <legend>Miscellaneous</legend>
<div class="checkbox left row"> <div class="row checkbox left clearfix nopad">
<input type="checkbox" name="include_extras" id="include_extras" value="1" ${config['include_extras']} /><label>Automatically include extras when adding an artist <br> <input type="checkbox" name="include_extras" id="include_extras" value="1" ${config['include_extras']} /><label>Automatically include extras when adding an artist <br>
<% <%
which_extras_selected = "" which_extras_selected = ""
@@ -1320,16 +1370,34 @@
%endfor %endfor
</div> </div>
</div> </div>
<div class="row left checkbox"> <div class="row checkbox left clearfix nopad">
<input type="checkbox" name="autowant_upcoming" value="1" ${config['autowant_upcoming']} /><label>Automatically mark upcoming albums as wanted</label> <label><input type="checkbox" name="official_releases_only" value="1" ${config['official_releases_only']} />Only include 'official' extras (i.e. no bootlegs, promos, etc.)</label>
</div> </div>
<div class="row left checkbox"> <div class="row checkbox left clearfix nopad">
<input type="checkbox" name="autowant_all" value="1" ${config['autowant_all']} /><label>Automatically mark all albums as wanted</label> <label><input type="checkbox" name="autowant_upcoming" value="1" ${config['autowant_upcoming']} />Automatically mark upcoming albums as wanted</label>
</div> </div>
<div class="row left checkbox"> <div class="row checkbox left clearfix nopad">
<input type="checkbox" title="Automatically mark manually added albums from search results as wanted" <label><input type="checkbox" name="autowant_all" value="1" ${config['autowant_all']} />Automatically mark all albums as wanted</label>
name="autowant_manually_added" value="1" ${config['autowant_manually_added']} /> </div>
<label>Automatically mark manually added albums as wanted</label> <div class="row checkbox left clearfix nopad">
<label><input type="checkbox" title="Automatically mark manually added albums from search results as wanted"
name="autowant_manually_added" value="1" ${config['autowant_manually_added']} />Automatically mark manually added albums as wanted</label>
</div>
<div class="row checkbox left clearfix nopad">
<label><input type="checkbox" title="Wait until album release date before searching"
name="wait_until_release_date" value="1" ${config['wait_until_release_date']} />Wait until an album's release date before searching</label>
</div>
<div class="row checkbox left clearfix nopad">
<label title="Freeze the database, so new artists won't be added automatically. Use this if Headphones adds artists because due to wrong snatches. This check is skipped when the folder name is appended with release group ID.">
Don't add new artists when post-processing albums
<input type="checkbox" name="freeze_db" id="freeze_db" value="1" ${config['freeze_db']} />
</label>
</div>
<div class="row checkbox left clearfix">
<label title="Use ID3v2.3 instead of ID3v2.4">
Tag using ID3v2.3 instead of ID3v2.4
<input type="checkbox" name="idtag" value="1" ${config['idtag']}>
</label>
</div> </div>
<div class="row"> <div class="row">
<label>Folder Permissions</label> <label>Folder Permissions</label>
@@ -1344,11 +1412,11 @@
<input type="text" name="cache_sizemb" value="${config['cache_sizemb']}" size="7"> <input type="text" name="cache_sizemb" value="${config['cache_sizemb']}" size="7">
</div> </div>
</fieldset> </fieldset>
<fieldset> <!--<fieldset>
<legend>Interface</legend> <legend>Interface</legend>
<div class="row" style="display: none"> <div class="row" style="display: none">
<label>Interface</label> <label>Interface</label>
<select name="interface"><h3> <select name="interface">
%for interface in config['interface_list']: %for interface in config['interface_list']:
<% <%
if interface == headphones.CONFIG.INTERFACE: if interface == headphones.CONFIG.INTERFACE:
@@ -1360,6 +1428,9 @@
%endfor %endfor
</select> </select>
</div> </div>
</fieldset>-->
<fieldset>
<legend>Directories</legend>
<div class="row"> <div class="row">
<label>Log Directory</label> <label>Log Directory</label>
<input type="text" name="log_dir" value="${config['log_dir']}" size="50"> <input type="text" name="log_dir" value="${config['log_dir']}" size="50">
@@ -1373,12 +1444,12 @@
<fieldset> <fieldset>
<legend>Songkick</legend> <legend>Songkick</legend>
<div class="row checkbox"> <div class="row checkbox">
<input type="checkbox" name="songkick_enabled" id="songkick" value="1" ${config['songkick_enabled']} /><label>Display concerts by Songkick</label> <input type="checkbox" name="songkick_enabled" id="songkick" value="1" ${config['songkick_enabled']} /><label>Show concert info</label>
</div> </div>
<div id="songkickoptions"> <div id="songkickoptions">
<div class="row"> <div class="row">
<label>API Key</label> <label>API Key</label>
<input type="text" name="songkick_apikey" value="${config['songkick_apikey']}" size="50"> <input type="text" name="songkick_apikey" value="${config['songkick_apikey']}" size="40">
</div> </div>
<div class="row checkbox"> <div class="row checkbox">
<input type="checkbox" name="songkick_filter_enabled" id="songkick_filter" value="1" ${config['songkick_filter_enabled']} /><label>Filter by Metro Area</label> <input type="checkbox" name="songkick_filter_enabled" id="songkick_filter" value="1" ${config['songkick_filter_enabled']} /><label>Filter by Metro Area</label>
@@ -1416,6 +1487,17 @@
<div class="row"> <div class="row">
<label>Port</label><input type="text" name="customport" value="${config['customport']}" size="8"> <label>Port</label><input type="text" name="customport" value="${config['customport']}" size="8">
</div> </div>
<div class="row checkbox">
<input type="checkbox" name="customauth" id="customauth" value="1" ${config['customauth']} /><label>Requires Authentication</label>
</div>
<div id="customauth_options">
<div class="row">
<label>Username</label><input type="text" class="customuser" name="customuser" value="${config['customuser']}" size="20">
</div>
<div class="row">
<label>Password</label><input type="password" class="custompass" name="custompass" value="${config['custompass']}" size="15"><br>
</div>
</div>
<div class="row"> <div class="row">
<label>Sleep Interval</label><input type="text" name="customsleep" value="${config['customsleep']}" size="4"> <label>Sleep Interval</label><input type="text" name="customsleep" value="${config['customsleep']}" size="4">
</div> </div>
@@ -1426,8 +1508,8 @@
<label>Username</label><input type="text" class="hpuser" name="hpuser" value="${config['hpuser']}" size="20"> <label>Username</label><input type="text" class="hpuser" name="hpuser" value="${config['hpuser']}" size="20">
</div> </div>
<div class="row"> <div class="row">
<label>Password</label><input type="password" class="hppass" name="hppass" value="${config['hppass']}" size="15"><br> <label>Password</label><input type="password" class="hppass" name="hppass" value="${config['hppass']}" size="20"><br>
<a href="http://headphones.codeshy.com/vip" id="vipserver" target="_blank">Get an Account!</a> <a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Get an Account!</a>
</div> </div>
</div> </div>
</fieldset> </fieldset>
@@ -1453,6 +1535,10 @@
$("#lameffmpegproperties").slideUp(); $("#lameffmpegproperties").slideUp();
$("#xldproperties").slideUp(); $("#xldproperties").slideUp();
}; };
hideEncodingOptionDivs = function () {
$("#vbr_options").slideUp();
$("#cbr_options").slideUp();
};
handleNewServerSelection = function () { handleNewServerSelection = function () {
hideServerDivs(); hideServerDivs();
@@ -1486,6 +1572,19 @@
} }
}; };
handleNewEncodingOptionSelection = function () {
hideEncodingOptionDivs();
switch ($(this).val()) {
case 'vbr':
$("#vbr_options").slideDown();
break;
case 'cbr':
$("#cbr_options").slideDown();
break;
}
};
function openExtrasDialog() { function openExtrasDialog() {
$("#dialog").dialog({ close: function(){ $("#dialog").dialog({ close: function(){
var elem = '<input type="button" data-success="Changes saved successfully">'; var elem = '<input type="button" data-success="Changes saved successfully">';
@@ -1525,6 +1624,25 @@
} }
$('#api_key').val(data); $('#api_key').val(data);
}); });
});
if ($("#customauth").is(":checked"))
{
$("#customauth_options").show();
}
else
{
$("#customauth_options").hide();
}
$("#customauth").click(function(){
if ($("#customauth").is(":checked"))
{
$("#customauth_options").slideDown();
}
else
{
$("#customauth_options").slideUp();
}
}); });
if ($("#enable_https").is(":checked")) if ($("#enable_https").is(":checked"))
{ {
@@ -1866,6 +1984,26 @@
} }
}); });
if ($("#email").is(":checked"))
{
$("#email_options").show();
}
else
{
$("#email_options").hide();
}
$("#email").click(function(){
if ($("#email").is(":checked"))
{
$("#email_options").slideDown();
}
else
{
$("#email_options").slideUp();
}
});
if ($("#songkick").is(":checked")) if ($("#songkick").is(":checked"))
{ {
$("#songkickoptions").show(); $("#songkickoptions").show();
@@ -2009,6 +2147,9 @@
$("#encoder").change(handleNewEncoderSelection); $("#encoder").change(handleNewEncoderSelection);
handleNewEncoderSelection.apply($("#encoder")); handleNewEncoderSelection.apply($("#encoder"));
$("#encodervbrcbr").change(handleNewEncodingOptionSelection);
handleNewEncodingOptionSelection.apply($("#encodervbrcbr"));
var deletedNewznabs = 0; var deletedNewznabs = 0;
$(".remove").click(function() { $(".remove").click(function() {
@@ -2074,6 +2215,7 @@
initConfigCheckbox("#use_whatcd"); initConfigCheckbox("#use_whatcd");
initConfigCheckbox("#api_enabled"); initConfigCheckbox("#api_enabled");
initConfigCheckbox("#enable_https"); initConfigCheckbox("#enable_https");
initConfigCheckbox("#customauth");
$('#twitterStep1').click(function () { $('#twitterStep1').click(function () {
+22 -1
View File
@@ -325,7 +325,7 @@ form fieldset small.heading {
margin-top: -15px; margin-top: -15px;
} }
form .row { form .row {
font-family: Helvetica, Arial; font-family: Helvetica, Arial, sans-serif;
margin-bottom: 10px; margin-bottom: 10px;
} }
form .row label { form .row label {
@@ -408,6 +408,18 @@ form .checkbox small {
form .indent input { form .indent input {
margin-left: 15px; margin-left: 15px;
} }
form .suboptions {
margin-left: 15px;
}
.option{
font-size: 15px;
font-weight: 600;
vertical-align: middle;
}
input.bigcheck[type="checkbox"] {
width: 16px;
height: 16px;
}
ul, ul,
ol { ol {
margin-left: 2em; margin-left: 2em;
@@ -1420,6 +1432,13 @@ div#artistheader h2 a {
line-height: 10px; line-height: 10px;
height: 10px; height: 10px;
} }
.nopad {
margin-bottom: 0px !important;
margin-top: 0px !important;
padding-top: 0px !important;
padding-bottom: 0px !important;
padding: 0px 0px !important;
}
#album_table th#albumname, #album_table th#albumname,
#upcoming_table th#artistname, #upcoming_table th#artistname,
#wanted_table th#artistname { #wanted_table th#artistname {
@@ -1445,6 +1464,7 @@ div#artistheader h2 a {
#album_table td#albumname, #album_table td#albumname,
#album_table td#reldate, #album_table td#reldate,
#album_table td#type, #album_table td#type,
#album_table td#score,
#track_table td#duration, #track_table td#duration,
#upcoming_table td#select, #upcoming_table td#select,
#upcoming_table td#status, #upcoming_table td#status,
@@ -1455,6 +1475,7 @@ div#artistheader h2 a {
} }
#album_table td#status, #album_table td#status,
#album_table td#bitrate, #album_table td#bitrate,
#album_table td#score,
#album_table td#albumformat, #album_table td#albumformat,
#album_table td#wantlossless { #album_table td#wantlossless {
font-size: 13px; font-size: 13px;
+6 -1
View File
@@ -52,12 +52,17 @@
fileid = 'torrent' fileid = 'torrent'
if item['URL'].find('rutracker') != -1: if item['URL'].find('rutracker') != -1:
fileid = 'torrent' fileid = 'torrent'
if item['URL'].find('codeshy') != -1:
fileid = 'nzb'
folder = 'Folder: ' + item['FolderName']
%> %>
<tr class="grade${grade}"> <tr class="grade${grade}">
<td id="dateadded">${item['DateAdded']}</td> <td id="dateadded">${item['DateAdded']}</td>
<td id="filename">${cgi.escape(item['Title'], quote=True)} [<a href="${item['URL']}">${fileid}</a>]<a href="albumPage?AlbumID=${item['AlbumID']}">[album page]</a></td> <td id="filename">${cgi.escape(item['Title'], quote=True)} [<a href="${item['URL']}">${fileid}</a>]<a href="albumPage?AlbumID=${item['AlbumID']}">[album page]</a></td>
<td id="size">${helpers.bytes_to_mb(item['Size'])}</td> <td id="size">${helpers.bytes_to_mb(item['Size'])}</td>
<td id="status">${item['Status']}</td> <td title="${folder}" id="status">${item['Status']}</td>
<td id="action">[<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&redirect=history', $(this),'table')" data-success="Retrying download of '${cgi.escape(item['Title'], quote=True)}'">retry</a>][<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&new=True&redirect=history',$(this),'table')" data-success="Looking for a new version of '${cgi.escape(item['Title'], quote=True)}'">new</a>]</td> <td id="action">[<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&redirect=history', $(this),'table')" data-success="Retrying download of '${cgi.escape(item['Title'], quote=True)}'">retry</a>][<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&new=True&redirect=history',$(this),'table')" data-success="Looking for a new version of '${cgi.escape(item['Title'], quote=True)}'">new</a>]</td>
<td id="delete"><a href="#" onclick="doAjaxCall('clearhistory?date_added=${item['DateAdded']}&title=${cgi.escape(item['Title'], quote=True)}',$(this),'table')" data-success="${cgi.escape(item['Title'], quote=True)} cleared from history"><img src="interfaces/default/images/trashcan.png" height="18" width="18" id="trashcan" title="Clear this item from the history"></a> <td id="delete"><a href="#" onclick="doAjaxCall('clearhistory?date_added=${item['DateAdded']}&title=${cgi.escape(item['Title'], quote=True)}',$(this),'table')" data-success="${cgi.escape(item['Title'], quote=True)} cleared from history"><img src="interfaces/default/images/trashcan.png" height="18" width="18" id="trashcan" title="Clear this item from the history"></a>
</tr> </tr>
+1
View File
@@ -7,6 +7,7 @@
<div id="subhead_container"> <div id="subhead_container">
<div id="subhead_menu"> <div id="subhead_menu">
<a class="menu_link_edit" href="clearLogs"><i class="fa fa-trash-o"></i> Clear log</a> <a class="menu_link_edit" href="clearLogs"><i class="fa fa-trash-o"></i> Clear log</a>
<a class="menu_link_edit" href="toggleVerbose"><i class="fa fa-pencil"></i> Toggle Debug Log</a>
</div> </div>
</div> </div>
</%def> </%def>
+59 -12
View File
@@ -137,28 +137,27 @@
No empty artists found. No empty artists found.
%endif %endif
</div> </div>
<a href="#" onclick="doAjaxCall('forcePostProcess',$(this))" data-success="Post-Processor is being loaded" data-error="Error during Post-Processing"><i class="fa fa-wrench fa-fw"></i> Force Post-Process Albums in Download Folder</a> <div id="post_process">
<a href="#" class="btnOpenDialog"><i class="fa fa-wrench fa-fw"></i> Force Post-Process Albums in Download Folder</a>
</div>
</div> </div>
</fieldset> </fieldset>
<form action="forcePostProcess" method="GET">
<fieldset> <fieldset>
<div class="row"> <div class="row" id="post_process_alternate">
<label>Force Post-Process Albums in Alternate Folder</label> <label>Force Post-Process Albums in Alternate Folder</label>
<input type="text" value="" name="dir" id="dir" size="50" /><input type="submit" /> <input type="text" value="" name="dir" id="dir" size="50" />
<input type="button" class="btnOpenDialog" value="Submit" />
</div> </div>
</fieldset> </fieldset>
</form>
<form action="forcePostProcess" method="GET">
<fieldset> <fieldset>
<div class="row"> <div class="row" id="post_process_single">
<label>Post-Process Single Folder</label> <label>Post-Process Single Folder</label>
<input type="text" value="" name="album_dir" id="album_dir" size="50" /><input type="submit" /> <input type="text" value="" name="album_dir" id="album_dir" size="50" />
<input type="button" class="btnOpenDialog" value="Submit" />
</div> </div>
</fieldset> </fieldset>
</form>
</div> </div>
@@ -177,8 +176,7 @@
</fieldset> </fieldset>
</div> </div>
<div id="dialog-confirm"></div>
</div> </div>
</%def> </%def>
@@ -188,6 +186,55 @@
$('#autoadd').append('<input type="hidden" name="scan" value=1 />'); $('#autoadd').append('<input type="hidden" name="scan" value=1 />');
}; };
function fnOpenNormalDialog(id) {
if (id === "post_process"){
var url = "forcePostProcess?"
}
if (id === "post_process_alternate"){
var dir = $('#dir').val();
var url = "forcePostProcess?dir=" + dir + "&"
}
if (id === "post_process_single"){
var dir = $('#album_dir').val();
var url = "forcePostProcess?album_dir=" + dir + "&"
}
var t = $('<a data-success="Post-Processor is being loaded" data-error="Error during Post-Processing">');
$("#dialog-confirm").html("Do you want to keep the original folder(s) after post-processing? If you click no, the folders still might be kept depending on your global settings");
// Define the Dialog and its properties.
$("#dialog-confirm").dialog({
resizable: false,
modal: true,
title: "Keep original folder(s)?",
height: 170,
width: 400,
buttons: {
"Yes": function () {
$(this).dialog('close');
doAjaxCall(url + "keep_original_folder=True", t);
},
"No": function () {
$(this).dialog('close');
doAjaxCall(url + "keep_original_folder=False", t);
}
}
});
}
$('.btnOpenDialog').click(function(e){
e.preventDefault();
var parentId = $(this).closest('div').prop('id');
fnOpenNormalDialog(parentId);
});
function callback(value) {
if (value) {
alert("Confirmed");
} else {
alert("Rejected");
}
}
function initThisPage() { function initThisPage() {
$('#manage_albums').click(function() { $('#manage_albums').click(function() {
$('#dialog').dialog(); $('#dialog').dialog();
+14 -3
View File
@@ -17,8 +17,12 @@
<th id="reldate">Date</th> <th id="reldate">Date</th>
<th id="scoresmall">Score</th> <th id="scoresmall">Score</th>
<th id="mbrelid" style="display:none;"</th> <th id="mbrelid" style="display:none;"</th>
%else: %elif type == 'artist':
<th id="artistname">Artist Name</th> <th id="artistname">Artist Name</th>
<th id="score">Score</th>
%else:
<th id="seriesname">Series Name</th>
<th id="type">Type</th>
<th id="score">Score</th> <th id="score">Score</th>
%endif %endif
<th id="mb"></th> <th id="mb"></th>
@@ -40,8 +44,10 @@
<tr class="grade${grade}"> <tr class="grade${grade}">
%if type == 'album': %if type == 'album':
<td id="albumart" style=" text-align: center; vertical-align: middle;"><div id="artistImg"><img title="${result['albumid']}" class="albumArt" height="50" width="50" onerror="tryCCA(this, '${caa_group_url}')"></div></td> <td id="albumart" style=" text-align: center; vertical-align: middle;"><div id="artistImg"><img title="${result['albumid']}" class="albumArt" height="50" width="50" onerror="tryCCA(this, '${caa_group_url}')"></div></td>
%else: %elif type == 'artist':
<td id="albumart"><div id="artistImg"><img title="${result['id']}" class="albumArt" height="50" width="50"></div></td> <td id="albumart"><div id="artistImg"><img title="${result['id']}" class="albumArt" height="50" width="50"></div></td>
%else:
<td id="albumart"></td>
%endif %endif
%if type == 'album': %if type == 'album':
<td id="albumname"><a href="addReleaseById?rid=${result['albumid']}&rgid=${result['rgid']}" title="${albuminfo}">${result['title']}</a></td> <td id="albumname"><a href="addReleaseById?rid=${result['albumid']}&rgid=${result['rgid']}" title="${albuminfo}">${result['title']}</a></td>
@@ -52,9 +58,14 @@
<td id="score"><a href="${result['albumurl']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td> <td id="score"><a href="${result['albumurl']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
<td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['albumurl']}"><img src="interfaces/default/images/MusicBrainz_Album_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td> <td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['albumurl']}"><img src="interfaces/default/images/MusicBrainz_Album_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td>
<td id="mbrelid" style="display:none;">${result['albumid']}</td> <td id="mbrelid" style="display:none;">${result['albumid']}</td>
%else: %elif type == 'artist':
<td id="artistname"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td> <td id="artistname"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
<td id="score"><a href="${result['url']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td> <td id="score"><a href="${result['url']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
<td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['url']}"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td>
%else:
<td id="seriesname"><a href="addSeries?seriesid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
<td id="type">${result['type']}</td>
<td id="score"><a href="${result['url']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
<td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['url']}"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td> <td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['url']}"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td>
%endif %endif
</tr> </tr>
+24 -3
View File
@@ -176,7 +176,7 @@ def initialize(config_file):
version_lock_file, e) version_lock_file, e)
# Check for new versions # Check for new versions
if CONFIG.CHECK_GITHUB_ON_STARTUP: if CONFIG.CHECK_GITHUB and CONFIG.CHECK_GITHUB_ON_STARTUP:
try: try:
LATEST_VERSION = versioncheck.checkGithub() LATEST_VERSION = versioncheck.checkGithub()
except: except:
@@ -288,6 +288,7 @@ def initialize_scheduler():
schedule_job(updater.dbUpdate, 'MusicBrainz Update', hours=hours, minutes=0) schedule_job(updater.dbUpdate, 'MusicBrainz Update', hours=hours, minutes=0)
#Update check #Update check
if CONFIG.CHECK_GITHUB:
if CONFIG.CHECK_GITHUB_INTERVAL: if CONFIG.CHECK_GITHUB_INTERVAL:
minutes = CONFIG.CHECK_GITHUB_INTERVAL minutes = CONFIG.CHECK_GITHUB_INTERVAL
else: else:
@@ -352,12 +353,12 @@ def dbcheck():
conn = sqlite3.connect(DB_FILE) conn = sqlite3.connect(DB_FILE)
c = conn.cursor() c = conn.cursor()
c.execute( c.execute(
'CREATE TABLE IF NOT EXISTS artists (ArtistID TEXT UNIQUE, ArtistName TEXT, ArtistSortName TEXT, DateAdded TEXT, Status TEXT, IncludeExtras INTEGER, LatestAlbum TEXT, ReleaseDate TEXT, AlbumID TEXT, HaveTracks INTEGER, TotalTracks INTEGER, LastUpdated TEXT, ArtworkURL TEXT, ThumbURL TEXT, Extras TEXT)') 'CREATE TABLE IF NOT EXISTS artists (ArtistID TEXT UNIQUE, ArtistName TEXT, ArtistSortName TEXT, DateAdded TEXT, Status TEXT, IncludeExtras INTEGER, LatestAlbum TEXT, ReleaseDate TEXT, AlbumID TEXT, HaveTracks INTEGER, TotalTracks INTEGER, LastUpdated TEXT, ArtworkURL TEXT, ThumbURL TEXT, Extras TEXT, Type TEXT, MetaCritic TEXT)')
# ReleaseFormat here means CD,Digital,Vinyl, etc. If using the default # ReleaseFormat here means CD,Digital,Vinyl, etc. If using the default
# Headphones hybrid release, ReleaseID will equal AlbumID (AlbumID is # Headphones hybrid release, ReleaseID will equal AlbumID (AlbumID is
# releasegroup id) # releasegroup id)
c.execute( c.execute(
'CREATE TABLE IF NOT EXISTS albums (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, ReleaseDate TEXT, DateAdded TEXT, AlbumID TEXT UNIQUE, Status TEXT, Type TEXT, ArtworkURL TEXT, ThumbURL TEXT, ReleaseID TEXT, ReleaseCountry TEXT, ReleaseFormat TEXT, SearchTerm TEXT)') 'CREATE TABLE IF NOT EXISTS albums (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, ReleaseDate TEXT, DateAdded TEXT, AlbumID TEXT UNIQUE, Status TEXT, Type TEXT, ArtworkURL TEXT, ThumbURL TEXT, ReleaseID TEXT, ReleaseCountry TEXT, ReleaseFormat TEXT, SearchTerm TEXT, CriticScore TEXT, UserScore TEXT)')
# Format here means mp3, flac, etc. # Format here means mp3, flac, etc.
c.execute( c.execute(
'CREATE TABLE IF NOT EXISTS tracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)') 'CREATE TABLE IF NOT EXISTS tracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)')
@@ -583,6 +584,26 @@ def dbcheck():
except sqlite3.OperationalError: except sqlite3.OperationalError:
c.execute('ALTER TABLE albums ADD COLUMN SearchTerm TEXT DEFAULT NULL') c.execute('ALTER TABLE albums ADD COLUMN SearchTerm TEXT DEFAULT NULL')
try:
c.execute('SELECT CriticScore from albums')
except sqlite3.OperationalError:
c.execute('ALTER TABLE albums ADD COLUMN CriticScore TEXT DEFAULT NULL')
try:
c.execute('SELECT UserScore from albums')
except sqlite3.OperationalError:
c.execute('ALTER TABLE albums ADD COLUMN UserScore TEXT DEFAULT NULL')
try:
c.execute('SELECT Type from artists')
except sqlite3.OperationalError:
c.execute('ALTER TABLE artists ADD COLUMN Type TEXT DEFAULT NULL')
try:
c.execute('SELECT MetaCritic from artists')
except sqlite3.OperationalError:
c.execute('ALTER TABLE artists ADD COLUMN MetaCritic TEXT DEFAULT NULL')
conn.commit() conn.commit()
c.close() c.close()
+9 -3
View File
@@ -21,8 +21,8 @@ import json
cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSnatched', 'getSimilar', 'getHistory', 'getLogs', cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSnatched', 'getSimilar', 'getHistory', 'getLogs',
'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist', 'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist',
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'forceActiveArtistsUpdate', 'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'forceActiveArtistsUpdate',
'getVersion', 'checkGithub','shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getVersion', 'checkGithub', 'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt',
'getArtistInfo', 'getAlbumInfo', 'getArtistThumb', 'getAlbumThumb', 'getArtistInfo', 'getAlbumInfo', 'getArtistThumb', 'getAlbumThumb', 'clearLogs',
'choose_specific_download', 'download_specific_release'] 'choose_specific_download', 'download_specific_release']
@@ -176,7 +176,13 @@ class Api(object):
return return
def _getLogs(self, **kwargs): def _getLogs(self, **kwargs):
pass self.data = headphones.LOG_LIST
return
def _clearLogs(self, **kwargs):
headphones.LOG_LIST = []
self.data = 'Cleared log'
return
def _findArtist(self, **kwargs): def _findArtist(self, **kwargs):
if 'name' not in kwargs: if 'name' not in kwargs:
+20 -1
View File
@@ -44,9 +44,12 @@ _CONFIG_DEFINITIONS = {
'CUE_SPLIT': (int, 'General', 1), 'CUE_SPLIT': (int, 'General', 1),
'CUE_SPLIT_FLAC_PATH': (str, 'General', ''), 'CUE_SPLIT_FLAC_PATH': (str, 'General', ''),
'CUE_SPLIT_SHNTOOL_PATH': (str, 'General', ''), 'CUE_SPLIT_SHNTOOL_PATH': (str, 'General', ''),
'CUSTOMAUTH': (int, 'General', 0),
'CUSTOMHOST': (str, 'General', 'localhost'), 'CUSTOMHOST': (str, 'General', 'localhost'),
'CUSTOMPASS': (str, 'General', ''),
'CUSTOMPORT': (int, 'General', 5000), 'CUSTOMPORT': (int, 'General', 5000),
'CUSTOMSLEEP': (int, 'General', 1), 'CUSTOMSLEEP': (int, 'General', 1),
'CUSTOMUSER': (str, 'General', ''),
'DELETE_LOSSLESS_FILES': (int, 'General', 1), 'DELETE_LOSSLESS_FILES': (int, 'General', 1),
'DESTINATION_DIR': (str, 'General', ''), 'DESTINATION_DIR': (str, 'General', ''),
'DETECT_BITRATE': (int, 'General', 0), 'DETECT_BITRATE': (int, 'General', 0),
@@ -54,6 +57,16 @@ _CONFIG_DEFINITIONS = {
'DOWNLOAD_SCAN_INTERVAL': (int, 'General', 5), 'DOWNLOAD_SCAN_INTERVAL': (int, 'General', 5),
'DOWNLOAD_TORRENT_DIR': (str, 'General', ''), 'DOWNLOAD_TORRENT_DIR': (str, 'General', ''),
'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0), 'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0),
'EMAIL_ENABLED': (int, 'Email', 0),
'EMAIL_FROM': (str, 'Email', ''),
'EMAIL_TO': (str, 'Email', ''),
'EMAIL_SMTP_SERVER': (str, 'Email', ''),
'EMAIL_SMTP_USER': (str, 'Email', ''),
'EMAIL_SMTP_PASSWORD': (str, 'Email', ''),
'EMAIL_SMTP_PORT': (int, 'Email', 25),
'EMAIL_SSL': (int, 'Email', 0),
'EMAIL_TLS': (int, 'Email', 0),
'EMAIL_ONSNATCH': (int, 'Email', 0),
'EMBED_ALBUM_ART': (int, 'General', 0), 'EMBED_ALBUM_ART': (int, 'General', 0),
'EMBED_LYRICS': (int, 'General', 0), 'EMBED_LYRICS': (int, 'General', 0),
'ENABLE_HTTPS': (int, 'General', 0), 'ENABLE_HTTPS': (int, 'General', 0),
@@ -86,12 +99,14 @@ _CONFIG_DEFINITIONS = {
'HPUSER': (str, 'General', ''), 'HPUSER': (str, 'General', ''),
'HTTPS_CERT': (str, 'General', ''), 'HTTPS_CERT': (str, 'General', ''),
'HTTPS_KEY': (str, 'General', ''), 'HTTPS_KEY': (str, 'General', ''),
'HTTP_HOST': (str, 'General', '0.0.0.0'), 'HTTP_HOST': (str, 'General', 'localhost'),
'HTTP_PASSWORD': (str, 'General', ''), 'HTTP_PASSWORD': (str, 'General', ''),
'HTTP_PORT': (int, 'General', 8181), 'HTTP_PORT': (int, 'General', 8181),
'HTTP_PROXY': (int, 'General', 0), 'HTTP_PROXY': (int, 'General', 0),
'HTTP_ROOT': (str, 'General', '/'), 'HTTP_ROOT': (str, 'General', '/'),
'HTTP_USERNAME': (str, 'General', ''), 'HTTP_USERNAME': (str, 'General', ''),
'IDTAG': (int, 'Beets', 0),
'IGNORE_CLEAN_RELEASES': (int, 'General', 0),
'IGNORED_WORDS': (str, 'General', ''), 'IGNORED_WORDS': (str, 'General', ''),
'IGNORED_FOLDERS': (list, 'Advanced', []), 'IGNORED_FOLDERS': (list, 'Advanced', []),
'IGNORED_FILES': (list, 'Advanced', []), 'IGNORED_FILES': (list, 'Advanced', []),
@@ -139,6 +154,7 @@ _CONFIG_DEFINITIONS = {
'NZBSORG_HASH': (str, 'NZBsorg', ''), 'NZBSORG_HASH': (str, 'NZBsorg', ''),
'NZBSORG_UID': (str, 'NZBsorg', ''), 'NZBSORG_UID': (str, 'NZBsorg', ''),
'NZB_DOWNLOADER': (int, 'General', 0), 'NZB_DOWNLOADER': (int, 'General', 0),
'OFFICIAL_RELEASES_ONLY': (int, 'General', 0),
'OMGWTFNZBS': (int, 'omgwtfnzbs', 0), 'OMGWTFNZBS': (int, 'omgwtfnzbs', 0),
'OMGWTFNZBS_APIKEY': (str, 'omgwtfnzbs', ''), 'OMGWTFNZBS_APIKEY': (str, 'omgwtfnzbs', ''),
'OMGWTFNZBS_UID': (str, 'omgwtfnzbs', ''), 'OMGWTFNZBS_UID': (str, 'omgwtfnzbs', ''),
@@ -160,6 +176,7 @@ _CONFIG_DEFINITIONS = {
'PLEX_SERVER_HOST': (str, 'Plex', ''), 'PLEX_SERVER_HOST': (str, 'Plex', ''),
'PLEX_UPDATE': (int, 'Plex', 0), 'PLEX_UPDATE': (int, 'Plex', 0),
'PLEX_USERNAME': (str, 'Plex', ''), 'PLEX_USERNAME': (str, 'Plex', ''),
'PLEX_TOKEN': (str, 'Plex', ''),
'PREFERRED_BITRATE': (str, 'General', ''), 'PREFERRED_BITRATE': (str, 'General', ''),
'PREFERRED_BITRATE_ALLOW_LOSSLESS': (int, 'General', 0), 'PREFERRED_BITRATE_ALLOW_LOSSLESS': (int, 'General', 0),
'PREFERRED_BITRATE_HIGH_BUFFER': (int, 'General', 0), 'PREFERRED_BITRATE_HIGH_BUFFER': (int, 'General', 0),
@@ -185,6 +202,7 @@ _CONFIG_DEFINITIONS = {
'PUSHOVER_PRIORITY': (int, 'Pushover', 0), 'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
'RENAME_FILES': (int, 'General', 0), 'RENAME_FILES': (int, 'General', 0),
'REPLACE_EXISTING_FOLDERS': (int, 'General', 0), 'REPLACE_EXISTING_FOLDERS': (int, 'General', 0),
'KEEP_ORIGINAL_FOLDER': (int, 'General', 0),
'REQUIRED_WORDS': (str, 'General', ''), 'REQUIRED_WORDS': (str, 'General', ''),
'RUTRACKER': (int, 'Rutracker', 0), 'RUTRACKER': (int, 'Rutracker', 0),
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''), 'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
@@ -224,6 +242,7 @@ _CONFIG_DEFINITIONS = {
'UTORRENT_PASSWORD': (str, 'uTorrent', ''), 'UTORRENT_PASSWORD': (str, 'uTorrent', ''),
'UTORRENT_USERNAME': (str, 'uTorrent', ''), 'UTORRENT_USERNAME': (str, 'uTorrent', ''),
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1), 'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
'WAIT_UNTIL_RELEASE_DATE' : (int, 'General', 0),
'WAFFLES': (int, 'Waffles', 0), 'WAFFLES': (int, 'Waffles', 0),
'WAFFLES_PASSKEY': (str, 'Waffles', ''), 'WAFFLES_PASSKEY': (str, 'Waffles', ''),
'WAFFLES_RATIO': (str, 'Waffles', ''), 'WAFFLES_RATIO': (str, 'Waffles', ''),
+9 -12
View File
@@ -62,9 +62,7 @@ WAVE_FILE_TYPE_BY_EXTENSION = {
'.flac': 'Free Lossless Audio Codec' '.flac': 'Free Lossless Audio Codec'
} }
# TODO: Only alow flac for now #SHNTOOL_COMPATIBLE = ("Free Lossless Audio Codec", "Waveform Audio", "Monkey's Audio")
#SHNTOOL_COMPATIBLE = ('Waveform Audio', 'WavPack', 'Free Lossless Audio Codec')
SHNTOOL_COMPATIBLE = ('Free Lossless Audio Codec')
# TODO: Make this better! # TODO: Make this better!
# this module-level variable is bad. :( # this module-level variable is bad. :(
@@ -109,10 +107,10 @@ def split_baby(split_file, split_cmd):
env['PATH'] += os.pathsep + headphones.CONFIG.CUE_SPLIT_FLAC_PATH env['PATH'] += os.pathsep + headphones.CONFIG.CUE_SPLIT_FLAC_PATH
process = subprocess.Popen(split_cmd, startupinfo=startupinfo, process = subprocess.Popen(split_cmd, startupinfo=startupinfo,
stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE, stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE,
stderr=subprocess.PIPE, env=env) stderr=subprocess.PIPE, env=env)
stdout, stderr = process.communicate() stdout, stderr = process.communicate()
if process.returncode: if process.returncode:
logger.error('Split failed for %s', split_file.decode(headphones.SYS_ENCODING, 'replace')) logger.error('Split failed for %s', split_file.decode(headphones.SYS_ENCODING, 'replace'))
out = stdout if stdout else stderr out = stdout if stdout else stderr
@@ -592,11 +590,10 @@ def split(albumpath):
splitter = 'shntool' splitter = 'shntool'
if splitter == 'shntool' and not check_splitter(splitter): if splitter == 'shntool' and not check_splitter(splitter):
raise ValueError('Command not found, ensure shntool with FLAC or xld (OS X) installed') raise ValueError('Command not found, ensure shntool or xld installed')
# Determine if file can be split (only flac allowed for shntool) # Determine if file can be split
if 'xld' in splitter and wave.name_ext not in WAVE_FILE_TYPE_BY_EXTENSION.keys() or \ if wave.name_ext not in WAVE_FILE_TYPE_BY_EXTENSION.keys():
wave.type not in SHNTOOL_COMPATIBLE:
raise ValueError('Cannot split, audio file has unsupported extension') raise ValueError('Cannot split, audio file has unsupported extension')
# Split with xld # Split with xld
@@ -640,7 +637,7 @@ def split(albumpath):
cmd.extend(['-f']) cmd.extend(['-f'])
cmd.extend([SPLIT_FILE_NAME]) cmd.extend([SPLIT_FILE_NAME])
cmd.extend(['-o']) cmd.extend(['-o'])
cmd.extend(['flac']) cmd.extend([wave.name_ext.lstrip('.')])
cmd.extend([wave.name]) cmd.extend([wave.name])
split = split_baby(wave.name, cmd) split = split_baby(wave.name, cmd)
os.remove(SPLIT_FILE_NAME) os.remove(SPLIT_FILE_NAME)
@@ -652,9 +649,9 @@ def split(albumpath):
logger.info('Tagging %s...', t.name) logger.info('Tagging %s...', t.name)
t.tag() t.tag()
# rename FLAC files # rename files
if split and CUE_META.count_tracks() == len(base_dir.tracks(ext='.flac', split=True)): if split and CUE_META.count_tracks() == len(base_dir.tracks(ext=wave.name_ext, split=True)):
for t in base_dir.tracks(ext='.flac', split=True): for t in base_dir.tracks(ext=wave.name_ext, split=True):
if t.name != t.filename(): if t.name != t.filename():
logger.info('Renaming %s to %s...', t.name, t.filename()) logger.info('Renaming %s to %s...', t.name, t.filename())
os.rename(t.name, t.filename()) os.rename(t.name, t.filename())
+5 -5
View File
@@ -149,7 +149,7 @@ def get_age(date):
try: try:
days_old = int(split_date[0]) * 365 + int(split_date[1]) * 30 + int(split_date[2]) days_old = int(split_date[0]) * 365 + int(split_date[1]) * 30 + int(split_date[2])
except IndexError: except (IndexError,ValueError):
days_old = False days_old = False
return days_old return days_old
@@ -173,15 +173,15 @@ def piratesize(size):
factor = float(split[0]) factor = float(split[0])
unit = split[1].upper() unit = split[1].upper()
if unit == 'MiB': if unit == 'MIB':
size = factor * 1048576 size = factor * 1048576
elif unit == 'MB': elif unit == 'MB':
size = factor * 1000000 size = factor * 1000000
elif unit == 'GiB': elif unit == 'GIB':
size = factor * 1073741824 size = factor * 1073741824
elif unit == 'GB': elif unit == 'GB':
size = factor * 1000000000 size = factor * 1000000000
elif unit == 'KiB': elif unit == 'KIB':
size = factor * 1024 size = factor * 1024
elif unit == 'KB': elif unit == 'KB':
size = factor * 1000 size = factor * 1000
@@ -519,7 +519,7 @@ def get_downloaded_track_list(albumpath):
return downloaded_track_list return downloaded_track_list
def preserve_torrent_direcory(albumpath): def preserve_torrent_directory(albumpath):
""" """
Copy torrent directory to headphones-modified to keep files for seeding. Copy torrent directory to headphones-modified to keep files for seeding.
""" """
+12 -2
View File
@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from headphones import logger, helpers, db, mb, lastfm from headphones import logger, helpers, db, mb, lastfm, metacritic
from beets.mediafile import MediaFile from beets.mediafile import MediaFile
@@ -111,7 +111,7 @@ def addArtistIDListToDB(artistidlist):
addArtisttoDB(artistid) addArtisttoDB(artistid)
def addArtisttoDB(artistid, extrasonly=False, forcefull=False): def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
# Putting this here to get around the circular import. We're using this to update thumbnails for artist/albums # Putting this here to get around the circular import. We're using this to update thumbnails for artist/albums
from headphones import cache from headphones import cache
@@ -142,11 +142,18 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
"Status": "Loading", "Status": "Loading",
"IncludeExtras": headphones.CONFIG.INCLUDE_EXTRAS, "IncludeExtras": headphones.CONFIG.INCLUDE_EXTRAS,
"Extras": headphones.CONFIG.EXTRAS} "Extras": headphones.CONFIG.EXTRAS}
if type=="series":
newValueDict['Type'] = "series"
else: else:
newValueDict = {"Status": "Loading"} newValueDict = {"Status": "Loading"}
if dbartist["Type"] == "series":
type = "series"
myDB.upsert("artists", newValueDict, controlValueDict) myDB.upsert("artists", newValueDict, controlValueDict)
if type=="series":
artist = mb.getSeries(artistid)
else:
artist = mb.getArtist(artistid, extrasonly) artist = mb.getArtist(artistid, extrasonly)
if artist and artist.get('artist_name') in blacklisted_special_artist_names: if artist and artist.get('artist_name') in blacklisted_special_artist_names:
@@ -488,6 +495,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name']) logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
cache.getThumb(ArtistID=artistid) cache.getThumb(ArtistID=artistid)
logger.info(u"Fetching Metacritic reviews for: %s" % artist['artist_name'])
metacritic.update(artistid, artist['artist_name'], artist['releasegroups'])
if errors: 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'])) 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: else:
+7 -5
View File
@@ -23,7 +23,7 @@ from headphones import db, logger, helpers, importer, lastfm
# You can scan a single directory and append it to the current library by # You can scan a single directory and append it to the current library by
# specifying append=True, ArtistID and ArtistName. # specifying append=True, ArtistID and ArtistName.
def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
cron=False): cron=False, artistScan=False):
if cron and not headphones.CONFIG.LIBRARYSCAN: if cron and not headphones.CONFIG.LIBRARYSCAN:
return return
@@ -36,7 +36,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
# If we're appending a dir, it's coming from the post processor which is # If we're appending a dir, it's coming from the post processor which is
# already bytestring # already bytestring
if not append: if not append or artistScan:
dir = dir.encode(headphones.SYS_ENCODING) dir = dir.encode(headphones.SYS_ENCODING)
if not os.path.isdir(dir): if not os.path.isdir(dir):
@@ -287,7 +287,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
if not append: if not append or artistScan:
logger.info('Updating scanned artist track counts') logger.info('Updating scanned artist track counts')
# Clean up the new artist list # Clean up the new artist list
@@ -334,7 +334,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
for artist in artist_list: for artist in artist_list:
myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist]) myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist])
if headphones.CONFIG.DETECT_BITRATE: if headphones.CONFIG.DETECT_BITRATE and bitrates:
headphones.CONFIG.PREFERRED_BITRATE = sum(bitrates) / len(bitrates) / 1000 headphones.CONFIG.PREFERRED_BITRATE = sum(bitrates) / len(bitrates) / 1000
else: else:
@@ -346,6 +346,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
if not append: if not append:
update_album_status() update_album_status()
if not append and not artistScan:
lastfm.getSimilar() lastfm.getSimilar()
logger.info('Library scan complete') logger.info('Library scan complete')
@@ -374,7 +376,7 @@ def update_album_status(AlbumID=None):
album_completion = 0 album_completion = 0
logger.info('Album %s does not have any tracks in database' % album['AlbumTitle']) logger.info('Album %s does not have any tracks in database' % album['AlbumTitle'])
if album_completion >= headphones.CONFIG.ALBUM_COMPLETION_PCT and album['Status'] == 'Skipped': if album_completion >= headphones.CONFIG.ALBUM_COMPLETION_PCT:
new_album_status = "Downloaded" new_album_status = "Downloaded"
# I don't think we want to change Downloaded->Skipped..... # I don't think we want to change Downloaded->Skipped.....
+82 -10
View File
@@ -47,6 +47,8 @@ def startmb():
elif headphones.CONFIG.MIRROR == "custom": elif headphones.CONFIG.MIRROR == "custom":
mbhost = headphones.CONFIG.CUSTOMHOST mbhost = headphones.CONFIG.CUSTOMHOST
mbport = int(headphones.CONFIG.CUSTOMPORT) mbport = int(headphones.CONFIG.CUSTOMPORT)
mbuser = headphones.CONFIG.CUSTOMUSER
mbpass = headphones.CONFIG.CUSTOMPASS
sleepytime = int(headphones.CONFIG.CUSTOMSLEEP) sleepytime = int(headphones.CONFIG.CUSTOMSLEEP)
elif headphones.CONFIG.MIRROR == "headphones": elif headphones.CONFIG.MIRROR == "headphones":
mbhost = "144.76.94.239" mbhost = "144.76.94.239"
@@ -69,12 +71,16 @@ def startmb():
mb_lock.minimum_delta = sleepytime mb_lock.minimum_delta = sleepytime
# Add headphones credentials # Add headphones credentials
if headphones.CONFIG.MIRROR == "headphones": if headphones.CONFIG.MIRROR == "headphones" or headphones.CONFIG.CUSTOMAUTH:
if not mbuser and mbpass: if not mbuser or not mbpass:
logger.warn("No username or password set for VIP server") logger.warn("No username or password set for MusicBrainz server")
else: else:
musicbrainzngs.hpauth(mbuser, mbpass) musicbrainzngs.hpauth(mbuser, mbpass)
# Let us know if we disable custom authentication
if not headphones.CONFIG.CUSTOMAUTH and headphones.CONFIG.MIRROR == "custom":
musicbrainzngs.disable_hpauth()
logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i', mbhost, mbport, sleepytime) logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i', mbhost, mbport, sleepytime)
return True return True
@@ -129,7 +135,6 @@ def findArtist(name, limit=1):
}) })
return artistlist return artistlist
def findRelease(name, limit=1, artist=None): def findRelease(name, limit=1, artist=None):
releaselist = [] releaselist = []
releaseResults = None releaseResults = None
@@ -207,12 +212,45 @@ def findRelease(name, limit=1, artist=None):
}) })
return releaselist return releaselist
def findSeries(name, limit=1):
serieslist = []
seriesResults = None
chars = set('!?*-')
if any((c in chars) for c in name):
name = '"' + name + '"'
criteria = {'series': name.lower()}
with mb_lock:
try:
seriesResults = musicbrainzngs.search_series(limit=limit, **criteria)['series-list']
except musicbrainzngs.WebServiceError as e:
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
mb_lock.snooze(5)
if not seriesResults:
return False
for result in seriesResults:
if 'disambiguation' in result:
uniquename = unicode(result['name'] + " (" + result['disambiguation'] + ")")
else:
uniquename = unicode(result['name'])
serieslist.append({
'uniquename': uniquename,
'name': unicode(result['name']),
'type': unicode(result['type']),
'id': unicode(result['id']),
'url': unicode("http://musicbrainz.org/series/" + result['id']),#probably needs to be changed
'score': int(result['ext:score'])
})
return serieslist
def getArtist(artistid, extrasonly=False): def getArtist(artistid, extrasonly=False):
artist_dict = {} artist_dict = {}
artist = None artist = None
try: try:
limit = 200 limit = 100
with mb_lock: with mb_lock:
artist = musicbrainzngs.get_artist_by_id(artistid)['artist'] artist = musicbrainzngs.get_artist_by_id(artistid)['artist']
newRgs = None newRgs = None
@@ -282,7 +320,7 @@ def getArtist(artistid, extrasonly=False):
mb_extras_list = [] mb_extras_list = []
try: try:
limit = 200 limit = 100
newRgs = None newRgs = None
while newRgs is None or len(newRgs) >= limit: while newRgs is None or len(newRgs) >= limit:
with mb_lock: with mb_lock:
@@ -310,6 +348,38 @@ def getArtist(artistid, extrasonly=False):
artist_dict['releasegroups'] = releasegroups artist_dict['releasegroups'] = releasegroups
return artist_dict return artist_dict
def getSeries(seriesid):
series_dict = {}
series = None
try:
with mb_lock:
series = musicbrainzngs.get_series_by_id(seriesid,includes=['release-group-rels'])['series']
except musicbrainzngs.WebServiceError as e:
logger.warn('Attempt to retrieve series information from MusicBrainz failed for seriesid: %s (%s)' % (seriesid, str(e)))
mb_lock.snooze(5)
except Exception as e:
pass
if not series:
return False
if 'disambiguation' in series:
series_dict['artist_name'] = unicode(series['name'] + " (" + unicode(series['disambiguation']) + ")")
else:
series_dict['artist_name'] = unicode(series['name'])
releasegroups = []
for rg in series['release_group-relation-list']:
releasegroup = rg['release-group']
releasegroups.append({
'title':releasegroup['title'],
'date':releasegroup['first-release-date'],
'id':releasegroup['id'],
'type':rg['type']
})
series_dict['releasegroups'] = releasegroups
return series_dict
def getReleaseGroup(rgid): def getReleaseGroup(rgid):
""" """
@@ -396,6 +466,11 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
myDB = db.DBConnection() myDB = db.DBConnection()
results = [] results = []
release_status = "official"
if includeExtras and not headphones.CONFIG.OFFICIAL_RELEASES_ONLY:
release_status = []
try: try:
limit = 100 limit = 100
newResults = None newResults = None
@@ -404,6 +479,7 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
newResults = musicbrainzngs.browse_releases( newResults = musicbrainzngs.browse_releases(
release_group=rgid, release_group=rgid,
includes=['artist-credits', 'labels', 'recordings', 'release-groups', 'media'], includes=['artist-credits', 'labels', 'recordings', 'release-groups', 'media'],
release_status = release_status,
limit=limit, limit=limit,
offset=len(results)) offset=len(results))
if 'release-list' not in newResults: if 'release-list' not in newResults:
@@ -443,10 +519,6 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
num_new_releases = 0 num_new_releases = 0
for releasedata in results: for releasedata in results:
#releasedata.get will return None if it doesn't have a status
#all official releases should have the Official status included
if not includeExtras and releasedata.get('status') != 'Official':
continue
release = {} release = {}
rel_id_check = releasedata['id'] rel_id_check = releasedata['id']
+88
View File
@@ -0,0 +1,88 @@
# This file is part of Headphones.
#
# Headphones is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Headphones is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 json
import headphones
from headphones import db, helpers, logger, request
from headphones.common import USER_AGENT
def update(artistid, artist_name,release_groups):
""" Pretty simple and crude function to find the artist page on metacritic,
then parse that page to get critic & user scores for albums"""
# First let's modify the artist name to fit the metacritic convention.
# We could just do a search, then take the top result, but at least this will
# cut down on api calls. If it's ineffective then we'll switch to search
replacements = {" & " : " ", "." : ""}
mc_artist_name = helpers.replace_all(artist_name.lower(),replacements)
mc_artist_name = mc_artist_name.replace(" ","-")
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'}
url = "http://www.metacritic.com/person/" + mc_artist_name + "?filter-options=music&sort_options=date&num_items=100"
res = request.request_soup(url, headers=headers)
rows = None
try:
table = res.find("table", class_="credits person_credits")
rows = table.tbody.find_all('tr')
except:
logger.info("Unable to get metacritic scores for: %s" % artist_name)
myDB = db.DBConnection()
artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [artistid]).fetchone()
score_list = []
# If we couldn't get anything from MetaCritic for whatever reason,
# let's try to load scores from the db
if not rows:
if artist['MetaCritic']:
score_list = json.loads(artist['MetaCritic'])
else:
return
# If we did get scores, let's update the db with them
else:
for row in rows:
title = row.a.string
scores = row.find_all("span")
critic_score = scores[0].string
user_score = scores[1].string
score_dict = {'title':title,'critic_score':critic_score,'user_score':user_score}
score_list.append(score_dict)
# Save scores to the database
controlValueDict = {"ArtistID": artistid}
newValueDict = {'MetaCritic':json.dumps(score_list)}
myDB.upsert("artists", newValueDict, controlValueDict)
for score in score_list:
title = score['title']
# Iterate through the release groups we got passed to see if we can find
# a match
for rg in release_groups:
if rg['title'].lower() == title.lower():
critic_score = score['critic_score']
user_score = score['user_score']
controlValueDict = {"AlbumID": rg['id']}
newValueDict = {'CriticScore':critic_score,'UserScore':user_score}
myDB.upsert("albums", newValueDict, controlValueDict)
+106 -89
View File
@@ -34,6 +34,11 @@ import json
import oauth2 as oauth import oauth2 as oauth
import pythontwitter as twitter import pythontwitter as twitter
from email.mime.text import MIMEText
import smtplib
import email.utils
class GROWL(object): class GROWL(object):
""" """
Growl notifications, for OS X. Growl notifications, for OS X.
@@ -311,34 +316,32 @@ class Plex(object):
self.client_hosts = headphones.CONFIG.PLEX_CLIENT_HOST self.client_hosts = headphones.CONFIG.PLEX_CLIENT_HOST
self.username = headphones.CONFIG.PLEX_USERNAME self.username = headphones.CONFIG.PLEX_USERNAME
self.password = headphones.CONFIG.PLEX_PASSWORD self.password = headphones.CONFIG.PLEX_PASSWORD
self.token = headphones.CONFIG.PLEX_TOKEN
def _sendhttp(self, host, command): def _sendhttp(self, host, command):
username = self.username url = host + '/xbmcCmds/xbmcHttp/?' + command
password = self.password
url_command = urllib.urlencode(command) if self.password:
response = request.request_response(url, auth=(self.username, self.password))
url = host + '/xbmcCmds/xbmcHttp/?' + url_command else:
response = request.request_response(url)
req = urllib2.Request(url)
if password:
base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '')
req.add_header("Authorization", "Basic %s" % base64string)
logger.info('Plex url: %s' % url)
try:
handle = urllib2.urlopen(req)
except Exception as e:
logger.warn('Error opening Plex url: %s' % e)
return
response = handle.read().decode(headphones.SYS_ENCODING)
return response return response
def _sendjson(self, host, method, params={}):
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
headers = {'Content-Type': 'application/json'}
url = host + '/jsonrpc'
if self.password:
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers, auth=(self.username, self.password))
else:
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers)
if response:
return response[0]['result']
def update(self): def update(self):
# From what I read you can't update the music library on a per directory or per path basis # From what I read you can't update the music library on a per directory or per path basis
@@ -349,13 +352,15 @@ class Plex(object):
for host in hosts: for host in hosts:
logger.info('Sending library update command to Plex Media Server@ ' + host) logger.info('Sending library update command to Plex Media Server@ ' + host)
url = "%s/library/sections" % host url = "%s/library/sections" % host
try: if self.token:
xml_sections = minidom.parse(urllib.urlopen(url)) params = {'X-Plex-Token': self.token}
except IOError, e: else:
logger.warn("Error while trying to contact Plex Media Server: %s" % e) params = False
return False
r = request.request_minidom(url, params=params)
sections = r.getElementsByTagName('Directory')
sections = xml_sections.getElementsByTagName('Directory')
if not sections: if not sections:
logger.info(u"Plex Media Server not running on: " + host) logger.info(u"Plex Media Server not running on: " + host)
return False return False
@@ -363,11 +368,7 @@ class Plex(object):
for s in sections: for s in sections:
if s.getAttribute('type') == "artist": if s.getAttribute('type') == "artist":
url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key')) url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key'))
try: request.request_response(url, params=params)
urllib.urlopen(url)
except Exception as e:
logger.warn("Error updating library section for Plex Media Server: %s" % e)
return False
def notify(self, artist, album, albumartpath): def notify(self, artist, album, albumartpath):
@@ -378,17 +379,24 @@ class Plex(object):
time = "3000" # in ms time = "3000" # in ms
for host in hosts: for host in hosts:
logger.info('Sending notification command to Plex Media Server @ ' + host) logger.info('Sending notification command to Plex client @ ' + host)
try: try:
version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version']['major']
if version < 12: #Eden
notification = header + "," + message + "," + time + "," + albumartpath notification = header + "," + message + "," + time + "," + albumartpath
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'} notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'}
request = self._sendhttp(host, notifycommand) request = self._sendhttp(host, notifycommand)
else: #Frodo
params = {'title': header, 'message': message, 'displaytime': int(time), 'image': albumartpath}
request = self._sendjson(host, 'GUI.ShowNotification', params)
if not request: if not request:
raise Exception raise Exception
except: except Exception:
logger.warn('Error sending notification request to Plex Media Server') logger.error('Error sending notification request to Plex client @ ' + host)
class NMA(object): class NMA(object):
@@ -435,53 +443,31 @@ class PUSHBULLET(object):
self.apikey = headphones.CONFIG.PUSHBULLET_APIKEY self.apikey = headphones.CONFIG.PUSHBULLET_APIKEY
self.deviceid = headphones.CONFIG.PUSHBULLET_DEVICEID self.deviceid = headphones.CONFIG.PUSHBULLET_DEVICEID
def conf(self, options): def notify(self, message):
return cherrypy.config['config'].get('PUSHBULLET', options)
def notify(self, message, event):
if not headphones.CONFIG.PUSHBULLET_ENABLED: if not headphones.CONFIG.PUSHBULLET_ENABLED:
return return
http_handler = HTTPSConnection("api.pushbullet.com") url = "https://api.pushbullet.com/v2/pushes"
data = {'type': "note", data = {'type': "note",
'title': "Headphones", 'title': "Headphones",
'body': message.encode("utf-8")} 'body': message}
if self.deviceid:
data['device_iden'] = self.deviceid
http_handler.request("POST",
"/v2/pushes",
headers={'Content-type': "application/json", headers={'Content-type': "application/json",
'Authorization': 'Basic %s' % base64.b64encode(headphones.CONFIG.PUSHBULLET_APIKEY + ":")}, 'Authorization': 'Bearer ' + headphones.CONFIG.PUSHBULLET_APIKEY}
body=json.dumps(data))
response = http_handler.getresponse()
request_status = response.status
logger.debug(u"PushBullet response status: %r" % request_status)
logger.debug(u"PushBullet response headers: %r" % response.getheaders())
logger.debug(u"PushBullet response body: %r" % response.read())
if request_status == 200: response = request.request_json(url, method="post", headers=headers, data=json.dumps(data))
if response:
logger.info(u"PushBullet notifications sent.") logger.info(u"PushBullet notifications sent.")
return True return True
elif request_status >= 400 and request_status < 500:
logger.info(u"PushBullet request failed: %s" % response.reason)
return False
else: else:
logger.info(u"PushBullet notification failed serverside.") logger.info(u"PushBullet notification failed.")
return False return False
def updateLibrary(self):
#For uniformity reasons not removed
return
def test(self, apikey, deviceid):
self.enabled = True
self.apikey = apikey
self.deviceid = deviceid
self.notify('Main Screen Activate', 'Test Message')
class PUSHALOT(object): class PUSHALOT(object):
def notify(self, message, event): def notify(self, message, event):
@@ -578,7 +564,7 @@ class PUSHOVER(object):
if not headphones.CONFIG.PUSHOVER_ENABLED: if not headphones.CONFIG.PUSHOVER_ENABLED:
return return
http_handler = HTTPSConnection("api.pushover.net") url = "https://api.pushover.net/1/messages.json"
data = {'token': self.application_token, data = {'token': self.application_token,
'user': headphones.CONFIG.PUSHOVER_KEYS, 'user': headphones.CONFIG.PUSHOVER_KEYS,
@@ -586,24 +572,15 @@ class PUSHOVER(object):
'message': message.encode("utf-8"), 'message': message.encode("utf-8"),
'priority': headphones.CONFIG.PUSHOVER_PRIORITY} 'priority': headphones.CONFIG.PUSHOVER_PRIORITY}
http_handler.request("POST", headers = {'Content-type': "application/x-www-form-urlencoded"}
"/1/messages.json",
headers={'Content-type': "application/x-www-form-urlencoded"},
body=urlencode(data))
response = http_handler.getresponse()
request_status = response.status
logger.debug(u"Pushover response status: %r" % request_status)
logger.debug(u"Pushover response headers: %r" % response.getheaders())
logger.debug(u"Pushover response body: %r" % response.read())
if request_status == 200: response = request.request_response(url, method="POST", headers=headers, data=data)
if response:
logger.info(u"Pushover notifications sent.") logger.info(u"Pushover notifications sent.")
return True return True
elif request_status >= 400 and request_status < 500:
logger.info(u"Pushover request failed: %s" % response.reason)
return False
else: else:
logger.info(u"Pushover notification failed.") logger.error(u"Pushover notification failed.")
return False return False
def updateLibrary(self): def updateLibrary(self):
@@ -727,23 +704,32 @@ class OSX_NOTIFY(object):
self.objc = __import__("objc") self.objc = __import__("objc")
self.AppKit = __import__("AppKit") self.AppKit = __import__("AppKit")
except: except:
logger.warn('OS X Notification: Cannot import objc or AppKit')
return False return False
def swizzle(self, cls, SEL, func): def swizzle(self, cls, SEL, func):
old_IMP = getattr(cls, SEL, None)
if old_IMP is None:
old_IMP = cls.instanceMethodForSelector_(SEL) old_IMP = cls.instanceMethodForSelector_(SEL)
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
return func(self, old_IMP, *args, **kwargs) return func(self, old_IMP, *args, **kwargs)
new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector,
signature=old_IMP.signature) new_IMP = self.objc.selector(
self.objc.classAddMethod(cls, SEL, new_IMP) wrapper,
selector=old_IMP.selector,
signature=old_IMP.signature
)
self.objc.classAddMethod(cls, SEL.encode(), new_IMP)
def notify(self, title, subtitle=None, text=None, sound=True, image=None): def notify(self, title, subtitle=None, text=None, sound=True, image=None):
try: try:
self.swizzle(self.objc.lookUpClass('NSBundle'), self.swizzle(
b'bundleIdentifier', self.objc.lookUpClass('NSBundle'),
self.swizzled_bundleIdentifier) 'bundleIdentifier',
self.swizzled_bundleIdentifier
)
NSUserNotification = self.objc.lookUpClass('NSUserNotification') NSUserNotification = self.objc.lookUpClass('NSUserNotification')
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter') NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter')
@@ -827,3 +813,34 @@ class SubSonicNotifier(object):
# Invoke request # Invoke request
request.request_response(self.host + "musicFolderSettings.view?scanNow", request.request_response(self.host + "musicFolderSettings.view?scanNow",
auth=(self.username, self.password)) auth=(self.username, self.password))
class Email(object):
def notify(self, subject, message):
message = MIMEText(message, 'plain', "utf-8")
message['Subject'] = subject
message['From'] = email.utils.formataddr(('Headphones', headphones.CONFIG.EMAIL_FROM))
message['To'] = headphones.CONFIG.EMAIL_TO
try:
if (headphones.CONFIG.EMAIL_SSL):
mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER, headphones.CONFIG.EMAIL_SMTP_PORT)
else:
mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER, headphones.CONFIG.EMAIL_SMTP_PORT)
if (headphones.CONFIG.EMAIL_TLS):
mailserver.starttls()
mailserver.ehlo()
if headphones.CONFIG.EMAIL_SMTP_USER:
mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER, headphones.CONFIG.EMAIL_SMTP_PASSWORD)
mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, headphones.CONFIG.EMAIL_TO, message.as_string())
mailserver.quit()
return True
except Exception, e:
logger.warn('Error sending Email: %s' % e)
return False
+7 -7
View File
@@ -32,20 +32,20 @@ from headphones import logger
def sendNZB(nzb): def sendNZB(nzb):
addToTop = False addToTop = False
nzbgetXMLrpc = "%(username)s:%(password)s@%(host)s/xmlrpc" nzbgetXMLrpc = "%(protocol)s://%(username)s:%(password)s@%(host)s/xmlrpc"
if headphones.CONFIG.NZBGET_HOST is None: if not headphones.CONFIG.NZBGET_HOST:
logger.error(u"No NZBget host found in configuration. Please configure it.") logger.error(u"No NZBget host found in configuration. Please configure it.")
return False return False
if headphones.CONFIG.NZBGET_HOST.startswith('https://'): if headphones.CONFIG.NZBGET_HOST.startswith('https://'):
nzbgetXMLrpc = 'https://' + nzbgetXMLrpc protocol = 'https'
headphones.CONFIG.NZBGET_HOST.replace('https://', '', 1) host = headphones.CONFIG.NZBGET_HOST.replace('https://', '', 1)
else: else:
nzbgetXMLrpc = 'http://' + nzbgetXMLrpc protocol = 'http'
headphones.CONFIG.NZBGET_HOST.replace('http://', '', 1) host = headphones.CONFIG.NZBGET_HOST.replace('http://', '', 1)
url = nzbgetXMLrpc % {"host": headphones.CONFIG.NZBGET_HOST, "username": headphones.CONFIG.NZBGET_USERNAME, "password": headphones.CONFIG.NZBGET_PASSWORD} url = nzbgetXMLrpc % {"protocol": protocol, "host": host, "username": headphones.CONFIG.NZBGET_USERNAME, "password": headphones.CONFIG.NZBGET_PASSWORD}
nzbGetRPC = xmlrpclib.ServerProxy(url) nzbGetRPC = xmlrpclib.ServerProxy(url)
try: try:
+40 -29
View File
@@ -23,6 +23,7 @@ import itertools
import headphones import headphones
from beets import autotag from beets import autotag
from beets import config as beetsconfig
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
from beetsplug import lyrics as beetslyrics from beetsplug import lyrics as beetslyrics
@@ -34,7 +35,7 @@ postprocessor_lock = threading.Lock()
def checkFolder(): def checkFolder():
logger.info("Checking download folder for completed downloads (only snatched ones).") logger.debug("Checking download folder for completed downloads (only snatched ones).")
with postprocessor_lock: with postprocessor_lock:
myDB = db.DBConnection() myDB = db.DBConnection()
@@ -48,7 +49,7 @@ def checkFolder():
download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
album_path = os.path.join(download_dir, album['FolderName']).encode(headphones.SYS_ENCODING, 'replace') album_path = os.path.join(download_dir, album['FolderName']).encode(headphones.SYS_ENCODING, 'replace')
logger.info("Checking if %s exists" % album_path) logger.debug("Checking if %s exists" % album_path)
if os.path.exists(album_path): if os.path.exists(album_path):
logger.info('Found "' + album['FolderName'] + '" in ' + album['Kind'] + ' download folder. Verifying....') logger.info('Found "' + album['FolderName'] + '" in ' + album['Kind'] + ' download folder. Verifying....')
@@ -56,9 +57,9 @@ def checkFolder():
else: else:
logger.info("No folder name found for " + album['Title']) logger.info("No folder name found for " + album['Title'])
logger.info("Checking download folder finished.") logger.debug("Checking download folder finished.")
def verify(albumid, albumpath, Kind=None, forced=False): def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=False):
myDB = db.DBConnection() myDB = db.DBConnection()
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone() release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
@@ -187,15 +188,9 @@ def verify(albumid, albumpath, Kind=None, forced=False):
# Split cue # Split cue
if headphones.CONFIG.CUE_SPLIT and downloaded_cuecount and downloaded_cuecount >= len(downloaded_track_list): if headphones.CONFIG.CUE_SPLIT and downloaded_cuecount and downloaded_cuecount >= len(downloaded_track_list):
if headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent": if headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent":
albumpath = helpers.preserve_torrent_direcory(albumpath) albumpath = helpers.preserve_torrent_directory(albumpath)
if albumpath and helpers.cue_split(albumpath): if albumpath and helpers.cue_split(albumpath):
downloaded_track_list = helpers.get_downloaded_track_list(albumpath) downloaded_track_list = helpers.get_downloaded_track_list(albumpath)
else:
myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
if not processed:
renameUnprocessedFolder(albumpath, tag="Unprocessed")
return
# test #1: metadata - usually works # test #1: metadata - usually works
logger.debug('Verifying metadata...') logger.debug('Verifying metadata...')
@@ -221,7 +216,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
logger.debug('Matching metadata album: %s with album name: %s' % (metaalbum, dbalbum)) logger.debug('Matching metadata album: %s with album name: %s' % (metaalbum, dbalbum))
if metaartist == dbartist and metaalbum == dbalbum: if metaartist == dbartist and metaalbum == dbalbum:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind) doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, keep_original_folder)
return return
# test #2: filenames # test #2: filenames
@@ -239,7 +234,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
logger.debug('Checking if track title: %s is in file name: %s' % (dbtrack, filetrack)) logger.debug('Checking if track title: %s is in file name: %s' % (dbtrack, filetrack))
if dbtrack in filetrack: if dbtrack in filetrack:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind) doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, keep_original_folder)
return return
# test #3: number of songs and duration # test #3: number of songs and duration
@@ -271,7 +266,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
logger.debug('Database track duration: %i' % db_track_duration) logger.debug('Database track duration: %i' % db_track_duration)
delta = abs(downloaded_track_duration - db_track_duration) delta = abs(downloaded_track_duration - db_track_duration)
if delta < 240: if delta < 240:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind) doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, keep_original_folder)
return return
logger.warn(u'Could not identify album: %s. It may not be the intended album.' % albumpath.decode(headphones.SYS_ENCODING, 'replace')) logger.warn(u'Could not identify album: %s. It may not be the intended album.' % albumpath.decode(headphones.SYS_ENCODING, 'replace'))
@@ -281,11 +276,11 @@ def verify(albumid, albumpath, Kind=None, forced=False):
renameUnprocessedFolder(albumpath, tag="Unprocessed") renameUnprocessedFolder(albumpath, tag="Unprocessed")
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None): def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None, keep_original_folder=False):
logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle'])) logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle']))
# Check to see if we're preserving the torrent dir # Check to see if we're preserving the torrent dir
if headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent" and 'headphones-modified' not in albumpath: if (headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent" and 'headphones-modified' not in albumpath) or headphones.CONFIG.KEEP_ORIGINAL_FOLDER or keep_original_folder:
new_folder = os.path.join(albumpath, 'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace')) new_folder = os.path.join(albumpath, 'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
logger.info("Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding") logger.info("Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
try: try:
@@ -306,7 +301,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
downloaded_track_list.append(os.path.join(r, files)) downloaded_track_list.append(os.path.join(r, files))
# Check if files are valid media files and are writeable, before the steps # Check if files are valid media files and are writable, before the steps
# below are executed. This simplifies errors and prevents unfinished steps. # below are executed. This simplifies errors and prevents unfinished steps.
for downloaded_track in downloaded_track_list: for downloaded_track in downloaded_track_list:
try: try:
@@ -336,7 +331,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
fp.seek(0) fp.seek(0)
except IOError as e: except IOError as e:
logger.debug("Write check exact error: %s", e) logger.debug("Write check exact error: %s", e)
logger.error("Track file is not writeable. This is required " \ logger.error("Track file is not writable. This is required " \
"for some post processing steps: %s. Not continuing.", "for some post processing steps: %s. Not continuing.",
downloaded_track.decode(headphones.SYS_ENCODING, "replace")) downloaded_track.decode(headphones.SYS_ENCODING, "replace"))
return return
@@ -513,6 +508,12 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
mpc = notifiers.MPC() mpc = notifiers.MPC()
mpc.notify() mpc.notify()
if headphones.CONFIG.EMAIL_ENABLED:
logger.info(u"Sending Email notification")
email = notifiers.Email()
subject = release['ArtistName'] + ' - ' + release['AlbumTitle']
email.notify(subject, "Download and Postprocessing completed")
def embedAlbumArt(artwork, downloaded_track_list): def embedAlbumArt(artwork, downloaded_track_list):
logger.info('Embedding album art') logger.info('Embedding album art')
@@ -858,7 +859,7 @@ def correctMetadata(albumid, release, downloaded_track_list):
except Exception as e: except Exception as e:
logger.error('Error getting recommendation: %s. Not writing metadata', e) logger.error('Error getting recommendation: %s. Not writing metadata', e)
return return
if str(rec) == 'recommendation.none': if str(rec) == 'Recommendation.none':
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle']) logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
return return
@@ -874,6 +875,14 @@ def correctMetadata(albumid, release, downloaded_track_list):
autotag.apply_metadata(info, mapping) autotag.apply_metadata(info, mapping)
# Set ID3 tag version
if headphones.CONFIG.IDTAG:
beetsconfig['id3v23'] = True
logger.debug("Using ID3v2.3")
else:
beetsconfig['id3v23'] = False
logger.debug("Using ID3v2.4")
for item in items: for item in items:
try: try:
item.write() item.write()
@@ -1051,7 +1060,9 @@ def renameUnprocessedFolder(path, tag):
return return
def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None): def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_original_folder=False):
logger.info('Force checking download folder for completed downloads')
ignored = 0 ignored = 0
@@ -1125,7 +1136,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
continue continue
else: else:
logger.info('Found a match in the database: %s. Verifying to make sure it is the correct album', snatched['Title']) logger.info('Found a match in the database: %s. Verifying to make sure it is the correct album', snatched['Title'])
verify(snatched['AlbumID'], folder, snatched['Kind']) verify(snatched['AlbumID'], folder, snatched['Kind'], keep_original_folder=keep_original_folder)
continue continue
# Attempt 2: strip release group id from filename # Attempt 2: strip release group id from filename
@@ -1142,10 +1153,10 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
release = myDB.action('SELECT ArtistName, AlbumTitle, AlbumID from albums WHERE AlbumID=?', [rgid]).fetchone() release = myDB.action('SELECT ArtistName, AlbumTitle, AlbumID from albums WHERE AlbumID=?', [rgid]).fetchone()
if release: if release:
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle']) logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder, forced=True) verify(release['AlbumID'], folder, forced=True, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('Found a (possibly) valid Musicbrainz realse group id in album folder name.') logger.info('Found a (possibly) valid Musicbrainz release group id in album folder name.')
verify(rgid, folder, forced=True) verify(rgid, folder, forced=True)
continue continue
@@ -1161,7 +1172,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', [name, album]).fetchone() release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', [name, album]).fetchone()
if release: if release:
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle']) logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder) verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album) logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
@@ -1172,7 +1183,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
rgid = None rgid = None
if rgid: if rgid:
verify(rgid, folder) verify(rgid, folder, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('No match found on MusicBrainz for: %s - %s', name, album) logger.info('No match found on MusicBrainz for: %s - %s', name, album)
@@ -1196,7 +1207,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', [name, album]).fetchone() release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', [name, album]).fetchone()
if release: if release:
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle']) logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder) verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album) logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
@@ -1207,7 +1218,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
rgid = None rgid = None
if rgid: if rgid:
verify(rgid, folder) verify(rgid, folder, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('No match found on MusicBrainz for: %s - %s', name, album) logger.info('No match found on MusicBrainz for: %s - %s', name, album)
@@ -1220,7 +1231,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?', [folder_basename]).fetchone() release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?', [folder_basename]).fetchone()
if release: if release:
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle']) logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
verify(release['AlbumID'], folder) verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('Querying MusicBrainz for the release group id for: %s', folder_basename) logger.info('Querying MusicBrainz for the release group id for: %s', folder_basename)
@@ -1231,7 +1242,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
rgid = None rgid = None
if rgid: if rgid:
verify(rgid, folder) verify(rgid, folder, keep_original_folder=keep_original_folder)
continue continue
else: else:
logger.info('No match found on MusicBrainz for: %s - %s', name, album) logger.info('No match found on MusicBrainz for: %s - %s', name, album)
+2 -2
View File
@@ -95,7 +95,7 @@ def request_response(url, method="get", auto_raise=True,
"host is up and running.") "host is up and running.")
except requests.Timeout: except requests.Timeout:
logger.error( logger.error(
"Request timed out. The remote host did not respond timely.") "Request timed out. The remote host did not respond in a timely manner.")
except requests.HTTPError as e: except requests.HTTPError as e:
if e.response is not None: if e.response is not None:
if e.response.status_code >= 500: if e.response.status_code >= 500:
@@ -206,7 +206,7 @@ def server_message(response):
message = None message = None
# First attempt is to 'read' the response as HTML # First attempt is to 'read' the response as HTML
if "text/html" in response.headers.get("content-type"): if response.headers.get("content-type") and "text/html" in response.headers.get("content-type"):
try: try:
soup = BeautifulSoup(response.content, "html5lib") soup = BeautifulSoup(response.content, "html5lib")
except Exception: except Exception:
+39 -83
View File
@@ -20,19 +20,21 @@
import MultipartPostHandler import MultipartPostHandler
import headphones import headphones
import cookielib import cookielib
import urllib2
import httplib import httplib
import urllib
import ast
from headphones.common import USER_AGENT from headphones.common import USER_AGENT
from headphones import logger from headphones import logger, helpers, request
from headphones import helpers
def sendNZB(nzb): def sab_api_call(request_type=None, params={}, **kwargs):
params = {} if not headphones.CONFIG.SAB_HOST.startswith('http'):
headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST
if headphones.CONFIG.SAB_HOST.endswith('/'):
headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[0:len(headphones.CONFIG.SAB_HOST) - 1]
url = headphones.CONFIG.SAB_HOST + "/" + "api?"
if headphones.CONFIG.SAB_USERNAME: if headphones.CONFIG.SAB_USERNAME:
params['ma_username'] = headphones.CONFIG.SAB_USERNAME params['ma_username'] = headphones.CONFIG.SAB_USERNAME
@@ -40,9 +42,24 @@ def sendNZB(nzb):
params['ma_password'] = headphones.CONFIG.SAB_PASSWORD params['ma_password'] = headphones.CONFIG.SAB_PASSWORD
if headphones.CONFIG.SAB_APIKEY: if headphones.CONFIG.SAB_APIKEY:
params['apikey'] = headphones.CONFIG.SAB_APIKEY params['apikey'] = headphones.CONFIG.SAB_APIKEY
if headphones.CONFIG.SAB_CATEGORY:
if request_type=='send_nzb' and headphones.CONFIG.SAB_CATEGORY:
params['cat'] = headphones.CONFIG.SAB_CATEGORY params['cat'] = headphones.CONFIG.SAB_CATEGORY
params['output']='json'
response = request.request_json(url, params=params, **kwargs)
if not response:
logger.error("Error connecting to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST)
return False
else:
logger.debug("Successfully connected to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST)
return response
def sendNZB(nzb):
params = {}
# if it's a normal result we just pass SAB the URL # if it's a normal result we just pass SAB the URL
if nzb.resultType == "nzb": if nzb.resultType == "nzb":
# for newzbin results send the ID to sab specifically # for newzbin results send the ID to sab specifically
@@ -62,102 +79,41 @@ def sendNZB(nzb):
# Sanitize the file a bit, since we can only use ascii chars with MultiPartPostHandler # Sanitize the file a bit, since we can only use ascii chars with MultiPartPostHandler
nzbdata = helpers.latinToAscii(nzb.extraInfo[0]) nzbdata = helpers.latinToAscii(nzb.extraInfo[0])
params['mode'] = 'addfile' params['mode'] = 'addfile'
multiPartParams = {"nzbfile": (helpers.latinToAscii(nzb.name) + ".nzb", nzbdata)} files = {"nzbfile": (helpers.latinToAscii(nzb.name) + ".nzb", nzbdata)}
headers = {'User-Agent': USER_AGENT}
if not headphones.CONFIG.SAB_HOST.startswith('http'):
headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST
if headphones.CONFIG.SAB_HOST.endswith('/'):
headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[0:len(headphones.CONFIG.SAB_HOST) - 1]
url = headphones.CONFIG.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
try:
logger.info("Attempting to connect to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST)
if nzb.resultType == "nzb": if nzb.resultType == "nzb":
f = urllib.urlopen(url) response = sab_api_call('send_nzb', params=params)
elif nzb.resultType == "nzbdata": elif nzb.resultType == "nzbdata":
cookies = cookielib.CookieJar() cookies = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), response = sab_api_call('send_nzb', params=params, method="post", files=files, cookies=cookies, headers=headers)
MultipartPostHandler.MultipartPostHandler)
req = urllib2.Request(url, if not response:
multiPartParams,
headers={'User-Agent': USER_AGENT})
f = opener.open(req)
except (EOFError, IOError) as e:
logger.error(u"Unable to connect to SAB with URL: %s" % url)
return False
except httplib.InvalidURL as e:
logger.error(u"Invalid SAB host, check your config. Current host: %s" % headphones.CONFIG.SAB_HOST)
return False
except Exception as e:
logger.error(u"Error: " + str(e))
return False
if f is None:
logger.info(u"No data returned from SABnzbd, NZB not sent") logger.info(u"No data returned from SABnzbd, NZB not sent")
return False return False
try: if response['status']:
result = f.readlines() logger.info(u"NZB sent to SABnzbd successfully")
except Exception as e:
logger.info(u"Error trying to get result from SAB, NZB not sent: ")
return False
if len(result) == 0:
logger.info(u"No data returned from SABnzbd, NZB not sent")
return False
sabText = result[0].strip()
logger.info(u"Result text from SAB: " + sabText)
if sabText == "ok":
logger.info(u"NZB sent to SAB successfully")
return True return True
elif sabText == "Missing authentication":
logger.info(u"Incorrect username/password sent to SAB, NZB not sent")
return False
else: else:
logger.info(u"Unknown failure sending NZB to sab. Return text is: " + sabText) logger.error(u"Error sending NZB to SABnzbd: %s" % response['error'])
return False return False
def checkConfig(): def checkConfig():
params = {'mode': 'get_config', params = {'mode': 'get_config',
'section': 'misc' 'section': 'misc',
} }
if headphones.CONFIG.SAB_USERNAME: config_options = sab_api_call(params=params)
params['ma_username'] = headphones.CONFIG.SAB_USERNAME
if headphones.CONFIG.SAB_PASSWORD:
params['ma_password'] = headphones.CONFIG.SAB_PASSWORD
if headphones.CONFIG.SAB_APIKEY:
params['apikey'] = headphones.CONFIG.SAB_APIKEY
if not headphones.CONFIG.SAB_HOST.startswith('http'): if not config_options:
headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST
if headphones.CONFIG.SAB_HOST.endswith('/'):
headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[0:len(headphones.CONFIG.SAB_HOST) - 1]
url = headphones.CONFIG.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
try:
f = urllib.urlopen(url).read()
except Exception:
logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)") logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)")
return (0, 0) return (0, 0)
config_options = ast.literal_eval(f) replace_spaces = config_options['config']['misc']['replace_spaces']
replace_dots = config_options['config']['misc']['replace_dots']
replace_spaces = config_options['misc']['replace_spaces']
replace_dots = config_options['misc']['replace_dots']
return (replace_spaces, replace_dots) return (replace_spaces, replace_dots)
+72 -38
View File
@@ -29,6 +29,7 @@ import string
import shutil import shutil
import random import random
import urllib import urllib
import datetime
import headphones import headphones
import subprocess import subprocess
import unicodedata import unicodedata
@@ -189,10 +190,22 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
results = myDB.select('SELECT * from albums WHERE Status="Wanted" OR Status="Wanted Lossless"') results = myDB.select('SELECT * from albums WHERE Status="Wanted" OR Status="Wanted Lossless"')
for album in results: for album in results:
if not album['AlbumTitle'] or not album['ArtistName']: if not album['AlbumTitle'] or not album['ArtistName']:
logger.warn('Skipping release %s. No title available', album['AlbumID']) logger.warn('Skipping release %s. No title available', album['AlbumID'])
continue continue
if headphones.CONFIG.WAIT_UNTIL_RELEASE_DATE and album['ReleaseDate']:
try:
release_date = datetime.datetime.strptime(album['ReleaseDate'], "%Y-%m-%d")
except:
logger.warn("No valid date for: %s. Skipping automatic search" % album['AlbumTitle'])
continue
if release_date > datetime.datetime.today():
logger.info("Skipping: %s. Waiting for release date of: %s" % (album['AlbumTitle'], album['ReleaseDate']))
continue
new = True new = True
if album['Status'] == "Wanted Lossless": if album['Status'] == "Wanted Lossless":
@@ -225,7 +238,7 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
myDB = db.DBConnection() myDB = db.DBConnection()
albumlength = myDB.select('SELECT sum(TrackDuration) from tracks WHERE AlbumID=?', [album['AlbumID']])[0][0] albumlength = myDB.select('SELECT sum(TrackDuration) from tracks WHERE AlbumID=?', [album['AlbumID']])[0][0]
if headphones.CONFIG.PREFER_TORRENTS == 0: if headphones.CONFIG.PREFER_TORRENTS == 0 and not choose_specific_download:
if NZB_PROVIDERS and NZB_DOWNLOADERS: if NZB_PROVIDERS and NZB_DOWNLOADERS:
results = searchNZB(album, new, losslessOnly, albumlength) results = searchNZB(album, new, losslessOnly, albumlength)
@@ -233,7 +246,7 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
if not results and TORRENT_PROVIDERS: if not results and TORRENT_PROVIDERS:
results = searchTorrent(album, new, losslessOnly, albumlength) results = searchTorrent(album, new, losslessOnly, albumlength)
elif headphones.CONFIG.PREFER_TORRENTS == 1: elif headphones.CONFIG.PREFER_TORRENTS == 1 and not choose_specific_download:
if TORRENT_PROVIDERS: if TORRENT_PROVIDERS:
results = searchTorrent(album, new, losslessOnly, albumlength) results = searchTorrent(album, new, losslessOnly, albumlength)
@@ -247,10 +260,10 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
torrent_results = None torrent_results = None
if NZB_PROVIDERS and NZB_DOWNLOADERS: if NZB_PROVIDERS and NZB_DOWNLOADERS:
nzb_results = searchNZB(album, new, losslessOnly, albumlength) nzb_results = searchNZB(album, new, losslessOnly, albumlength, choose_specific_download)
if TORRENT_PROVIDERS: if TORRENT_PROVIDERS:
torrent_results = searchTorrent(album, new, losslessOnly, albumlength) torrent_results = searchTorrent(album, new, losslessOnly, albumlength, choose_specific_download)
if not nzb_results: if not nzb_results:
nzb_results = [] nzb_results = []
@@ -306,9 +319,9 @@ def more_filtering(results, album, albumlength, new):
targetsize = albumlength / 1000 * int(headphones.CONFIG.PREFERRED_BITRATE) * 128 targetsize = albumlength / 1000 * int(headphones.CONFIG.PREFERRED_BITRATE) * 128
logger.info('Target size: %s' % helpers.bytes_to_mb(targetsize)) logger.info('Target size: %s' % helpers.bytes_to_mb(targetsize))
if headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER: if headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER:
low_size_limit = targetsize - (targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER) / 100) low_size_limit = targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER) / 100
if headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER: if headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER:
high_size_limit = targetsize + (targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER) / 100) high_size_limit = targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER) / 100
if headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS: if headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS:
allow_lossless = True allow_lossless = True
@@ -321,8 +334,10 @@ def more_filtering(results, album, albumlength, new):
normalizedResultTitle = removeDisallowedFilenameChars(result[0]) normalizedResultTitle = removeDisallowedFilenameChars(result[0])
artistTitleCount = normalizedResultTitle.count(normalizedAlbumArtist) artistTitleCount = normalizedResultTitle.count(normalizedAlbumArtist)
if normalizedAlbumArtist in normalizedAlbumTitle and artistTitleCount < 2: # WHAT DOES THIS DO?
continue #if normalizedAlbumArtist in normalizedAlbumTitle and artistTitleCount < 2:
# logger.info("Removing %s from %s" % (result[0], result[3]))
# continue
if low_size_limit and (int(result[1]) < low_size_limit): if low_size_limit and (int(result[1]) < low_size_limit):
logger.info("%s from %s is too small for this album - not considering it. (Size: %s, Minsize: %s)", result[0], result[3], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(low_size_limit)) logger.info("%s from %s is too small for this album - not considering it. (Size: %s, Minsize: %s)", result[0], result[3], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(low_size_limit))
@@ -400,7 +415,7 @@ def sort_search_results(resultlist, album, new, albumlength):
finallist = sorted(newlist, key=lambda title: (-title[5], title[6])) finallist = sorted(newlist, key=lambda title: (-title[5], title[6]))
if not len(finallist) and len(flac_list) and headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS: if not len(finallist) and len(flac_list) and headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS:
logger.info("Since there were no appropriate lossy matches (and at least one lossless match, going to use lossless instead") logger.info("Since there were no appropriate lossy matches (and at least one lossless match), going to use lossless instead")
finallist = sorted(flac_list, key=lambda title: (title[5], int(title[1])), reverse=True) finallist = sorted(flac_list, key=lambda title: (title[5], int(title[1])), reverse=True)
except Exception: except Exception:
logger.exception('Unhandled exception') logger.exception('Unhandled exception')
@@ -429,7 +444,7 @@ def get_year_from_release_date(release_date):
return year return year
def searchNZB(album, new=False, losslessOnly=False, albumlength=None): def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_specific_download=False):
reldate = album['ReleaseDate'] reldate = album['ReleaseDate']
year = get_year_from_release_date(reldate) year = get_year_from_release_date(reldate)
@@ -441,6 +456,8 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None):
# Use the provided search term if available, otherwise build a search term # Use the provided search term if available, otherwise build a search term
if album['SearchTerm']: if album['SearchTerm']:
term = album['SearchTerm'] term = album['SearchTerm']
elif album['Type'] == 'part of':
term = cleanalbum + " " + year
else: else:
# FLAC usually doesn't have a year for some reason so leave it out. # FLAC usually doesn't have a year for some reason so leave it out.
# Various Artist albums might be listed as VA, so I'll leave that out too # Various Artist albums might be listed as VA, so I'll leave that out too
@@ -481,7 +498,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None):
categories = "3030" categories = "3030"
# Request results # Request results
logger.info('Parsing results from Headphones Indexer') logger.info('Searching Headphones Indexer with search term: %s' % term)
headers = {'User-Agent': USER_AGENT} headers = {'User-Agent': USER_AGENT}
params = { params = {
@@ -493,7 +510,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None):
} }
data = request.request_feed( data = request.request_feed(
url="http://indexer.codeshy.com/api", url="https://indexer.codeshy.com/api",
params=params, headers=headers, params=params, headers=headers,
auth=(headphones.CONFIG.HPUSER, headphones.CONFIG.HPPASS) auth=(headphones.CONFIG.HPUSER, headphones.CONFIG.HPPASS)
) )
@@ -552,7 +569,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None):
categories = categories + ",4050" categories = categories + ",4050"
# Request results # Request results
logger.info('Parsing results from %s', newznab_host[0]) logger.info('Parsing results from %s using search term: %s' % (newznab_host[0],term))
headers = {'User-Agent': USER_AGENT} headers = {'User-Agent': USER_AGENT}
params = { params = {
@@ -600,9 +617,6 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None):
categories = "3030" categories = "3030"
logger.info("Album type is audiobook/spokenword. Using audiobook category") logger.info("Album type is audiobook/spokenword. Using audiobook category")
# Request results
logger.info('Requesting from nzbs.org')
headers = {'User-Agent': USER_AGENT} headers = {'User-Agent': USER_AGENT}
params = { params = {
"t": "search", "t": "search",
@@ -618,7 +632,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None):
timeout=5 timeout=5
) )
logger.info('Parsing results from nzbs.org') logger.info('Parsing results from nzbs.org using search term: %s' % term)
# Process feed # Process feed
if data: if data:
if not len(data.entries): if not len(data.entries):
@@ -650,7 +664,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None):
logger.info("Album type is audiobook/spokenword. Searching all music categories") logger.info("Album type is audiobook/spokenword. Searching all music categories")
# Request results # Request results
logger.info('Parsing results from omgwtfnzbs') logger.info('Parsing results from omgwtfnzbs using search term: %s' % term)
headers = {'User-Agent': USER_AGENT} headers = {'User-Agent': USER_AGENT}
params = { params = {
@@ -691,7 +705,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None):
results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)] results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)]
# Additional filtering for size etc # Additional filtering for size etc
if results: if results and not choose_specific_download:
results = more_filtering(results, album, albumlength, new) results = more_filtering(results, album, albumlength, new)
return results return results
@@ -946,7 +960,11 @@ def send_to_downloader(data, bestqual, album):
b2msg = 'From ' + provider + '<br></br>' + name b2msg = 'From ' + provider + '<br></br>' + name
boxcar = notifiers.BOXCAR() boxcar = notifiers.BOXCAR()
boxcar.notify('Headphones snatched: ' + title, b2msg, rgid) boxcar.notify('Headphones snatched: ' + title, b2msg, rgid)
if headphones.CONFIG.EMAIL_ENABLED and headphones.CONFIG.EMAIL_ONSNATCH:
logger.info(u"Sending Email notification")
email = notifiers.Email()
message = 'Snatched from ' + provider + '. ' + name
email.notify(title, message)
def verifyresult(title, artistterm, term, lossless): def verifyresult(title, artistterm, term, lossless):
@@ -995,6 +1013,13 @@ def verifyresult(title, artistterm, term, lossless):
logger.info("Removed '%s' from results because it doesn't contain required word: '%s'", title, each_word) logger.info("Removed '%s' from results because it doesn't contain required word: '%s'", title, each_word)
return False return False
if headphones.CONFIG.IGNORE_CLEAN_RELEASES:
for each_word in ['clean','edited','censored']:
logger.debug("Checking if '%s' is in search result: '%s'", each_word, title)
if each_word.lower() in title.lower() and each_word.lower() not in term.lower():
logger.info("Removed '%s' from results because it contains clean album word: '%s'", title, each_word)
return False
tokens = re.split('\W', term, re.IGNORECASE | re.UNICODE) tokens = re.split('\W', term, re.IGNORECASE | re.UNICODE)
for token in tokens: for token in tokens:
@@ -1014,7 +1039,7 @@ def verifyresult(title, artistterm, term, lossless):
return True return True
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None): def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose_specific_download=False):
global gazelle # persistent what.cd api object to reduce number of login attempts global gazelle # persistent what.cd api object to reduce number of login attempts
# rutracker login # rutracker login
@@ -1040,7 +1065,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None):
# of these torrent providers are just using cleanartist/cleanalbum terms # of these torrent providers are just using cleanartist/cleanalbum terms
if album['SearchTerm']: if album['SearchTerm']:
term = album['SearchTerm'] term = album['SearchTerm']
elif album['Type'] == 'part of':
term = cleanalbum + " " + year
else: else:
# FLAC usually doesn't have a year for some reason so I'll leave it out # FLAC usually doesn't have a year for some reason so I'll leave it out
# Various Artist albums might be listed as VA, so I'll leave that out too # Various Artist albums might be listed as VA, so I'll leave that out too
@@ -1092,10 +1118,10 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None):
if headphones.CONFIG.KAT_PROXY_URL: if headphones.CONFIG.KAT_PROXY_URL:
providerurl = fix_url(set_proxy(headphones.CONFIG.KAT_PROXY_URL)) providerurl = fix_url(set_proxy(headphones.CONFIG.KAT_PROXY_URL))
else: else:
providerurl = fix_url("https://kickass.so") providerurl = fix_url("https://kickass.to")
# Build URL # Build URL
providerurl = providerurl + "/usearch/" + ka_term providerurl = providerurl + "/json.php?"
# Pick category for torrents # Pick category for torrents
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly: if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
@@ -1109,28 +1135,27 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None):
maxsize = 300000000 maxsize = 300000000
# Requesting content # Requesting content
logger.info("Searching KAT using term: %s", ka_term) logger.info("Searching %s using term: %s" % (provider,ka_term))
params = { params = {
"categories[0]": "music", "q": ka_term + "+category:music",
"field": "seeders", "field": "seeders",
"sorder": "desc", "sorder": "desc"
"rss": "1"
} }
data = request.request_feed(url=providerurl, params=params) data = request.request_json(url=providerurl, params=params)
# Process feed # Process feed
if data: if data:
if not len(data.entries): if not data['list']:
logger.info("No results found") logger.info("No results found on %s using search term: %s" % (provider, ka_term))
else: else:
for item in data.entries: for item in data['list']:
try: try:
rightformat = True rightformat = True
title = item['title'] title = item['title']
seeders = item['torrent_seeds'] seeders = item['seeds']
url = item['links'][1]['href'] url = item['torrentLink']
size = int(item['links'][1]['length']) size = int(item['size'])
if format == "2": if format == "2":
torrent = request.request_content(url) torrent = request.request_content(url)
@@ -1179,7 +1204,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None):
query_items.append('bitrate:"%s"' % bitrate) query_items.append('bitrate:"%s"' % bitrate)
# Requesting content # Requesting content
logger.info('Parsing results from Waffles') logger.info('Parsing results from Waffles.fm')
params = { params = {
"uid": headphones.CONFIG.WAFFLES_UID, "uid": headphones.CONFIG.WAFFLES_UID,
@@ -1268,6 +1293,12 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None):
search_formats = [None] # should return all search_formats = [None] # should return all
bitrate = headphones.CONFIG.PREFERRED_BITRATE bitrate = headphones.CONFIG.PREFERRED_BITRATE
if bitrate: if bitrate:
if 225 <= int(bitrate) < 256:
bitrate = 'V0'
elif 200 <= int(bitrate) < 225:
bitrate = 'V1'
elif 175 <= int(bitrate) < 200:
bitrate = 'V2'
for encoding_string in gazelleencoding.ALL_ENCODINGS: for encoding_string in gazelleencoding.ALL_ENCODINGS:
if re.search(bitrate, encoding_string, flags=re.I): if re.search(bitrate, encoding_string, flags=re.I):
bitrate_string = encoding_string bitrate_string = encoding_string
@@ -1294,6 +1325,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None):
logger.info(u"Searching %s..." % provider) logger.info(u"Searching %s..." % provider)
all_torrents = [] all_torrents = []
for search_format in search_formats: for search_format in search_formats:
if usersearchterm:
all_torrents.extend(gazelle.search_torrents(searchstr=usersearchterm, format=search_format, encoding=bitrate_string)['results'])
else:
all_torrents.extend(gazelle.search_torrents(artistname=semi_clean_artist_term, all_torrents.extend(gazelle.search_torrents(artistname=semi_clean_artist_term,
groupname=semi_clean_album_term, groupname=semi_clean_album_term,
format=search_format, encoding=bitrate_string)['results']) format=search_format, encoding=bitrate_string)['results'])
@@ -1367,7 +1401,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None):
rows = data.select('table tbody tr') rows = data.select('table tbody tr')
if not rows: if not rows:
logger.info("No results found") logger.info("No results found from The Pirate Bay using term: %s" % tpb_term)
else: else:
for item in rows: for item in rows:
try: try:
@@ -1521,7 +1555,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None):
results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)] results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)]
# Additional filtering for size etc # Additional filtering for size etc
if results: if results and not choose_specific_download:
results = more_filtering(results, album, albumlength, new) results = more_filtering(results, album, albumlength, new)
return results return results
+5
View File
@@ -73,6 +73,7 @@ class utorrentclient(object):
except urllib2.HTTPError as err: except urllib2.HTTPError as err:
logger.debug('URL: ' + str(url)) logger.debug('URL: ' + str(url))
logger.debug('Error getting Token. uTorrent responded with error: ' + str(err)) logger.debug('Error getting Token. uTorrent responded with error: ' + str(err))
return
match = re.search(utorrentclient.TOKEN_REGEX, response.read()) match = re.search(utorrentclient.TOKEN_REGEX, response.read())
return match.group(1) return match.group(1)
@@ -147,6 +148,10 @@ class utorrentclient(object):
return self._action(params) return self._action(params)
def _action(self, params, body=None, content_type=None): def _action(self, params, body=None, content_type=None):
if not self.token:
return
url = self.base_url + '/gui/' + '?token=' + self.token + '&' + urllib.urlencode(params) url = self.base_url + '/gui/' + '?token=' + self.token + '&' + urllib.urlencode(params)
request = urllib2.Request(url) request = urllib2.Request(url)
+128 -16
View File
@@ -32,8 +32,10 @@ import random
import urllib import urllib
import json import json
import time import time
import cgi
import sys import sys
import os import os
import re
try: try:
# pylint:disable=E0611 # pylint:disable=E0611
@@ -145,9 +147,11 @@ class WebInterface(object):
raise cherrypy.HTTPRedirect("home") raise cherrypy.HTTPRedirect("home")
if type == 'artist': if type == 'artist':
searchresults = mb.findArtist(name, limit=100) searchresults = mb.findArtist(name, limit=100)
else: elif type == 'album':
searchresults = mb.findRelease(name, limit=100) searchresults = mb.findRelease(name, limit=100)
return serve_template(templatename="searchresults.html", title='Search Results for: "' + name + '"', searchresults=searchresults, name=name, type=type) else:
searchresults = mb.findSeries(name, limit=100)
return serve_template(templatename="searchresults.html", title='Search Results for: "' + cgi.escape(name) + '"', searchresults=searchresults, name=cgi.escape(name), type=type)
@cherrypy.expose @cherrypy.expose
def addArtist(self, artistid): def addArtist(self, artistid):
@@ -156,6 +160,13 @@ class WebInterface(object):
thread.join(1) thread.join(1)
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % artistid) raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % artistid)
@cherrypy.expose
def addSeries(self, seriesid):
thread = threading.Thread(target=importer.addArtisttoDB, args=[seriesid, False, False, "series"])
thread.start()
thread.join(1)
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % seriesid)
@cherrypy.expose @cherrypy.expose
def getExtras(self, ArtistID, newstyle=False, **kwargs): def getExtras(self, ArtistID, newstyle=False, **kwargs):
# if calling this function without the newstyle, they're using the old format # if calling this function without the newstyle, they're using the old format
@@ -256,14 +267,72 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
def scanArtist(self, ArtistID): def scanArtist(self, ArtistID):
logger.info(u"Scanning artist: %s", ArtistID)
myDB = db.DBConnection() myDB = db.DBConnection()
artistname = myDB.select('SELECT DISTINCT ArtistName FROM artists WHERE ArtistID=?', [ArtistID]) artist_name = myDB.select('SELECT DISTINCT ArtistName FROM artists WHERE ArtistID=?', [ArtistID])[0][0]
artistfolder = os.path.join(headphones.CONFIG.DESTINATION_DIR, artistname[0][0])
try: logger.info(u"Scanning artist: %s", artist_name)
threading.Thread(target=librarysync.libraryScan(dir=artistfolder)).start()
except Exception as e: full_folder_format = headphones.CONFIG.FOLDER_FORMAT
logger.error('Unable to complete the scan: %s', e) folder_format = re.findall(r'(.*[Aa]rtist?)\.*', full_folder_format)[0]
acceptable_formats = ["$artist","$sortartist","$first/$artist","$first/$sortartist"]
if not folder_format.lower() in acceptable_formats:
logger.info("Can't determine the artist folder from the configured folder_format. Not scanning")
return
# Format the folder to match the settings
artist = artist_name.replace('/', '_')
if headphones.CONFIG.FILE_UNDERSCORES:
artist = artist.replace(' ', '_')
if artist.startswith('The '):
sortname = artist[4:] + ", The"
else:
sortname = artist
if sortname[0].isdigit():
firstchar = u'0-9'
else:
firstchar = sortname[0]
values = {'$Artist': artist,
'$SortArtist': sortname,
'$First': firstchar.upper(),
'$artist': artist.lower(),
'$sortartist': sortname.lower(),
'$first': firstchar.lower(),
}
folder = helpers.replace_all(folder_format.strip(), values, normalize=True)
folder = helpers.replace_illegal_chars(folder, type="folder")
folder = folder.replace('./', '_/').replace('/.', '/_')
if folder.endswith('.'):
folder = folder[:-1] + '_'
if folder.startswith('.'):
folder = '_' + folder[1:]
dirs = []
if headphones.CONFIG.MUSIC_DIR:
dirs.append(headphones.CONFIG.MUSIC_DIR)
if headphones.CONFIG.DESTINATION_DIR:
dirs.append(headphones.CONFIG.DESTINATION_DIR)
if headphones.CONFIG.LOSSLESS_DESTINATION_DIR:
dirs.append(headphones.CONFIG.LOSSLESS_DESTINATION_DIR)
dirs = set(dirs)
for dir in dirs:
artistfolder = os.path.join(dir, folder)
if not os.path.isdir(artistfolder):
logger.debug("Cannot find directory: " + artistfolder)
continue
threading.Thread(target=librarysync.libraryScan, kwargs={"dir":artistfolder, "artistScan":True, "ArtistID":ArtistID, "ArtistName":artist_name}).start()
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID) raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
@cherrypy.expose @cherrypy.expose
@@ -374,6 +443,9 @@ class WebInterface(object):
myDB = db.DBConnection() myDB = db.DBConnection()
album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone() album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone()
searcher.send_to_downloader(data, bestqual, album) searcher.send_to_downloader(data, bestqual, album)
return json.dumps({'result':'success'})
else:
return json.dumps({'result':'failure'})
@cherrypy.expose @cherrypy.expose
def unqueueAlbum(self, AlbumID, ArtistID): def unqueueAlbum(self, AlbumID, ArtistID):
@@ -728,9 +800,9 @@ class WebInterface(object):
raise cherrypy.HTTPRedirect("home") raise cherrypy.HTTPRedirect("home")
@cherrypy.expose @cherrypy.expose
def forcePostProcess(self, dir=None, album_dir=None): def forcePostProcess(self, dir=None, album_dir=None, keep_original_folder=False):
from headphones import postprocessor from headphones import postprocessor
threading.Thread(target=postprocessor.forcePostProcess, kwargs={'dir': dir, 'album_dir': album_dir}).start() threading.Thread(target=postprocessor.forcePostProcess, kwargs={'dir': dir, 'album_dir': album_dir, 'keep_original_folder':keep_original_folder}).start()
raise cherrypy.HTTPRedirect("home") raise cherrypy.HTTPRedirect("home")
@cherrypy.expose @cherrypy.expose
@@ -877,6 +949,7 @@ class WebInterface(object):
cherrypy.response.headers['Content-type'] = 'application/json' cherrypy.response.headers['Content-type'] = 'application/json'
return json_albums return json_albums
@cherrypy.expose
def getArtistjson(self, ArtistID, **kwargs): def getArtistjson(self, ArtistID, **kwargs):
myDB = db.DBConnection() myDB = db.DBConnection()
artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [ArtistID]).fetchone() artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [ArtistID]).fetchone()
@@ -1001,6 +1074,7 @@ class WebInterface(object):
"preferred_words": headphones.CONFIG.PREFERRED_WORDS, "preferred_words": headphones.CONFIG.PREFERRED_WORDS,
"ignored_words": headphones.CONFIG.IGNORED_WORDS, "ignored_words": headphones.CONFIG.IGNORED_WORDS,
"required_words": headphones.CONFIG.REQUIRED_WORDS, "required_words": headphones.CONFIG.REQUIRED_WORDS,
"ignore_clean_releases": checked(headphones.CONFIG.IGNORE_CLEAN_RELEASES),
"torrentblackhole_dir": headphones.CONFIG.TORRENTBLACKHOLE_DIR, "torrentblackhole_dir": headphones.CONFIG.TORRENTBLACKHOLE_DIR,
"download_torrent_dir": headphones.CONFIG.DOWNLOAD_TORRENT_DIR, "download_torrent_dir": headphones.CONFIG.DOWNLOAD_TORRENT_DIR,
"numberofseeders": headphones.CONFIG.NUMBEROFSEEDERS, "numberofseeders": headphones.CONFIG.NUMBEROFSEEDERS,
@@ -1052,12 +1126,15 @@ class WebInterface(object):
"embed_album_art": checked(headphones.CONFIG.EMBED_ALBUM_ART), "embed_album_art": checked(headphones.CONFIG.EMBED_ALBUM_ART),
"embed_lyrics": checked(headphones.CONFIG.EMBED_LYRICS), "embed_lyrics": checked(headphones.CONFIG.EMBED_LYRICS),
"replace_existing_folders": checked(headphones.CONFIG.REPLACE_EXISTING_FOLDERS), "replace_existing_folders": checked(headphones.CONFIG.REPLACE_EXISTING_FOLDERS),
"keep_original_folder" : checked(headphones.CONFIG.KEEP_ORIGINAL_FOLDER),
"destination_dir": headphones.CONFIG.DESTINATION_DIR, "destination_dir": headphones.CONFIG.DESTINATION_DIR,
"lossless_destination_dir": headphones.CONFIG.LOSSLESS_DESTINATION_DIR, "lossless_destination_dir": headphones.CONFIG.LOSSLESS_DESTINATION_DIR,
"folder_format": headphones.CONFIG.FOLDER_FORMAT, "folder_format": headphones.CONFIG.FOLDER_FORMAT,
"file_format": headphones.CONFIG.FILE_FORMAT, "file_format": headphones.CONFIG.FILE_FORMAT,
"file_underscores": checked(headphones.CONFIG.FILE_UNDERSCORES), "file_underscores": checked(headphones.CONFIG.FILE_UNDERSCORES),
"include_extras": checked(headphones.CONFIG.INCLUDE_EXTRAS), "include_extras": checked(headphones.CONFIG.INCLUDE_EXTRAS),
"official_releases_only": checked(headphones.CONFIG.OFFICIAL_RELEASES_ONLY),
"wait_until_release_date": checked(headphones.CONFIG.WAIT_UNTIL_RELEASE_DATE),
"autowant_upcoming": checked(headphones.CONFIG.AUTOWANT_UPCOMING), "autowant_upcoming": checked(headphones.CONFIG.AUTOWANT_UPCOMING),
"autowant_all": checked(headphones.CONFIG.AUTOWANT_ALL), "autowant_all": checked(headphones.CONFIG.AUTOWANT_ALL),
"autowant_manually_added": checked(headphones.CONFIG.AUTOWANT_MANUALLY_ADDED), "autowant_manually_added": checked(headphones.CONFIG.AUTOWANT_MANUALLY_ADDED),
@@ -1106,6 +1183,7 @@ class WebInterface(object):
"plex_client_host": headphones.CONFIG.PLEX_CLIENT_HOST, "plex_client_host": headphones.CONFIG.PLEX_CLIENT_HOST,
"plex_username": headphones.CONFIG.PLEX_USERNAME, "plex_username": headphones.CONFIG.PLEX_USERNAME,
"plex_password": headphones.CONFIG.PLEX_PASSWORD, "plex_password": headphones.CONFIG.PLEX_PASSWORD,
"plex_token": headphones.CONFIG.PLEX_TOKEN,
"plex_update": checked(headphones.CONFIG.PLEX_UPDATE), "plex_update": checked(headphones.CONFIG.PLEX_UPDATE),
"plex_notify": checked(headphones.CONFIG.PLEX_NOTIFY), "plex_notify": checked(headphones.CONFIG.PLEX_NOTIFY),
"nma_enabled": checked(headphones.CONFIG.NMA_ENABLED), "nma_enabled": checked(headphones.CONFIG.NMA_ENABLED),
@@ -1142,6 +1220,9 @@ class WebInterface(object):
"customhost": headphones.CONFIG.CUSTOMHOST, "customhost": headphones.CONFIG.CUSTOMHOST,
"customport": headphones.CONFIG.CUSTOMPORT, "customport": headphones.CONFIG.CUSTOMPORT,
"customsleep": headphones.CONFIG.CUSTOMSLEEP, "customsleep": headphones.CONFIG.CUSTOMSLEEP,
"customauth": checked(headphones.CONFIG.CUSTOMAUTH),
"customuser": headphones.CONFIG.CUSTOMUSER,
"custompass": headphones.CONFIG.CUSTOMPASS,
"hpuser": headphones.CONFIG.HPUSER, "hpuser": headphones.CONFIG.HPUSER,
"hppass": headphones.CONFIG.HPPASS, "hppass": headphones.CONFIG.HPPASS,
"songkick_enabled": checked(headphones.CONFIG.SONGKICK_ENABLED), "songkick_enabled": checked(headphones.CONFIG.SONGKICK_ENABLED),
@@ -1151,7 +1232,18 @@ class WebInterface(object):
"cache_sizemb": headphones.CONFIG.CACHE_SIZEMB, "cache_sizemb": headphones.CONFIG.CACHE_SIZEMB,
"file_permissions": headphones.CONFIG.FILE_PERMISSIONS, "file_permissions": headphones.CONFIG.FILE_PERMISSIONS,
"folder_permissions": headphones.CONFIG.FOLDER_PERMISSIONS, "folder_permissions": headphones.CONFIG.FOLDER_PERMISSIONS,
"mpc_enabled": checked(headphones.CONFIG.MPC_ENABLED) "mpc_enabled": checked(headphones.CONFIG.MPC_ENABLED),
"email_enabled": checked(headphones.CONFIG.EMAIL_ENABLED),
"email_from": headphones.CONFIG.EMAIL_FROM,
"email_to": headphones.CONFIG.EMAIL_TO,
"email_smtp_server": headphones.CONFIG.EMAIL_SMTP_SERVER,
"email_smtp_user": headphones.CONFIG.EMAIL_SMTP_USER,
"email_smtp_password": headphones.CONFIG.EMAIL_SMTP_PASSWORD,
"email_smtp_port": int(headphones.CONFIG.EMAIL_SMTP_PORT),
"email_ssl": checked(headphones.CONFIG.EMAIL_SSL),
"email_tls": checked(headphones.CONFIG.EMAIL_TLS),
"email_onsnatch": checked(headphones.CONFIG.EMAIL_ONSNATCH),
"idtag": checked(headphones.CONFIG.IDTAG)
} }
# Need to convert EXTRAS to a dictionary we can pass to the config: # Need to convert EXTRAS to a dictionary we can pass to the config:
@@ -1188,15 +1280,16 @@ class WebInterface(object):
checked_configs = [ checked_configs = [
"launch_browser", "enable_https", "api_enabled", "use_blackhole", "headphones_indexer", "use_newznab", "newznab_enabled", "launch_browser", "enable_https", "api_enabled", "use_blackhole", "headphones_indexer", "use_newznab", "newznab_enabled",
"use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay", "use_mininova", "use_waffles", "use_rutracker", "use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay", "use_mininova", "use_waffles", "use_rutracker",
"use_whatcd", "preferred_bitrate_allow_lossless", "detect_bitrate", "freeze_db", "cue_split", "move_files", "rename_files", "use_whatcd", "preferred_bitrate_allow_lossless", "detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files",
"correct_metadata", "cleanup_files", "keep_nfo", "add_album_art", "embed_album_art", "embed_lyrics", "replace_existing_folders", "rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art", "embed_album_art", "embed_lyrics",
"file_underscores", "include_extras", "autowant_upcoming", "autowant_all", "autowant_manually_added", "keep_torrent_files", "music_encoder", "replace_existing_folders", "keep_original_folder", "file_underscores", "include_extras", "official_releases_only",
"wait_until_release_date", "autowant_upcoming", "autowant_all", "autowant_manually_added", "keep_torrent_files", "music_encoder",
"encoderlossless", "encoder_multicore", "delete_lossless_files", "growl_enabled", "growl_onsnatch", "prowl_enabled", "encoderlossless", "encoder_multicore", "delete_lossless_files", "growl_enabled", "growl_onsnatch", "prowl_enabled",
"prowl_onsnatch", "xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled", "plex_enabled", "plex_update", "plex_notify", "prowl_onsnatch", "xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled", "plex_enabled", "plex_update", "plex_notify",
"nma_enabled", "nma_onsnatch", "pushalot_enabled", "pushalot_onsnatch", "synoindex_enabled", "pushover_enabled", "nma_enabled", "nma_onsnatch", "pushalot_enabled", "pushalot_onsnatch", "synoindex_enabled", "pushover_enabled",
"pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled", "twitter_enabled", "twitter_onsnatch", "pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled", "twitter_enabled", "twitter_onsnatch",
"osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch", "songkick_enabled", "songkick_filter_enabled", "osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch", "songkick_enabled", "songkick_filter_enabled",
"mpc_enabled" "mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch", "customauth", "idtag"
] ]
for checked_config in checked_configs: for checked_config in checked_configs:
if checked_config not in kwargs: if checked_config not in kwargs:
@@ -1396,6 +1489,25 @@ class WebInterface(object):
logger.warn(msg) logger.warn(msg)
return msg return msg
@cherrypy.expose
def testPushover(self):
logger.info(u"Sending Pushover notification")
pushover = notifiers.PUSHOVER()
result = pushover.notify("hooray!", "This is a test")
return str(result)
@cherrypy.expose
def testPlex(self):
logger.info(u"Testing plex notifications")
plex = notifiers.Plex()
plex.notify("hellooooo", "test album!", "")
@cherrypy.expose
def testPushbullet(self):
logger.info("Testing Pushbullet notifications")
pushbullet = notifiers.PUSHBULLET()
pushbullet.notify("it works!")
class Artwork(object): class Artwork(object):
@cherrypy.expose @cherrypy.expose
def index(self): def index(self):
+5 -5
View File
@@ -3,16 +3,16 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>Label</key> <key>Label</key>
<string>com.headphones.headphones</string> <string>headphones</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<!-- Modify these two lines if you need to to reflect your python location and Headphones install location -->
<string>/usr/bin/python</string> <string>/usr/bin/python</string>
<string>/Applications/Headphones/headphones.py</string> <string>/Applications/Headphones/Headphones.py</string>
<string>--quiet</string>
<string>--daemon</string>
<string>--nolaunch</string>
</array> </array>
<key>RunAtLoad</key> <key>RunAtLoad</key>
<true/> <true/>
<key>KeepAlive</key>
<true/>
</dict> </dict>
</plist> </plist>
+1
View File
@@ -1 +1,2 @@
from musicbrainzngs.musicbrainz import * from musicbrainzngs.musicbrainz import *
from musicbrainzngs.caa import *
+177
View File
@@ -0,0 +1,177 @@
# This file is part of the musicbrainzngs library
# Copyright (C) Alastair Porter, Wieland Hoffmann, and others
# This file is distributed under a BSD-2-Clause type license.
# See the COPYING file for more information.
__all__ = [
'set_caa_hostname', 'get_image_list', 'get_release_group_image_list',
'get_release_group_image_front', 'get_image_front', 'get_image_back',
'get_image'
]
import json
from musicbrainzngs import compat
from musicbrainzngs import musicbrainz
hostname = "coverartarchive.org"
def set_caa_hostname(new_hostname):
"""Set the base hostname for Cover Art Archive requests.
Defaults to 'coverartarchive.org'."""
global hostname
hostname = new_hostname
def _caa_request(mbid, imageid=None, size=None, entitytype="release"):
""" Make a CAA request.
:param imageid: ``front``, ``back`` or a number from the listing obtained
with :meth:`get_image_list`.
:type imageid: str
:param size: 250, 500
:type size: str or None
:param entitytype: ``release`` or ``release-group``
:type entitytype: str
"""
# Construct the full URL for the request, including hostname and
# query string.
path = [entitytype, mbid]
if imageid and size:
path.append("%s-%s" % (imageid, size))
elif imageid:
path.append(imageid)
url = compat.urlunparse((
'http',
hostname,
'/%s' % '/'.join(path),
'',
'',
''
))
musicbrainz._log.debug("GET request for %s" % (url, ))
# Set up HTTP request handler and URL opener.
httpHandler = compat.HTTPHandler(debuglevel=0)
handlers = [httpHandler]
opener = compat.build_opener(*handlers)
# Make request.
req = musicbrainz._MusicbrainzHttpRequest("GET", url, None)
# Useragent isn't needed for CAA, but we'll add it if it exists
if musicbrainz._useragent != "":
req.add_header('User-Agent', musicbrainz._useragent)
musicbrainz._log.debug("requesting with UA %s" % musicbrainz._useragent)
resp = musicbrainz._safe_read(opener, req, None)
# TODO: The content type declared by the CAA for JSON files is
# 'applicaiton/octet-stream'. This is not useful to detect whether the
# content is JSON, so default to decoding JSON if no imageid was supplied.
# http://tickets.musicbrainz.org/browse/CAA-75
if imageid:
# If we asked for an image, return the image
return resp
else:
# Otherwise it's json
return json.loads(resp)
def get_image_list(releaseid):
"""Get the list of cover art associated with a release.
The return value is the deserialized response of the `JSON listing
<http://musicbrainz.org/doc/Cover_Art_Archive/API#.2Frelease.2F.7Bmbid.7D.2F>`_
returned by the Cover Art Archive API.
If an error occurs then a musicbrainz.ResponseError will
be raised with one of the following HTTP codes:
* 400: `Releaseid` is not a valid UUID
* 404: No release exists with an MBID of `releaseid`
* 503: Ratelimit exceeded
"""
return _caa_request(releaseid)
def get_release_group_image_list(releasegroupid):
"""Get the list of cover art associated with a release group.
The return value is the deserialized response of the `JSON listing
<http://musicbrainz.org/doc/Cover_Art_Archive/API#.2Frelease-group.2F.7Bmbid.7D.2F>`_
returned by the Cover Art Archive API.
If an error occurs then a musicbrainz.ResponseError will
be raised with one of the following HTTP codes:
* 400: `Releaseid` is not a valid UUID
* 404: No release exists with an MBID of `releaseid`
* 503: Ratelimit exceeded
"""
return _caa_request(releasegroupid, entitytype="release-group")
def get_release_group_image_front(releasegroupid, size=None):
"""Download the front cover art for a release group.
The `size` argument and the possible error conditions are the same as for
:meth:`get_image`.
"""
return get_image(releasegroupid, "front", size=size,
entitytype="release-group")
def get_image_front(releaseid, size=None):
"""Download the front cover art for a release.
The `size` argument and the possible error conditions are the same as for
:meth:`get_image`.
"""
return get_image(releaseid, "front", size=size)
def get_image_back(releaseid, size=None):
"""Download the back cover art for a release.
The `size` argument and the possible error conditions are the same as for
:meth:`get_image`.
"""
return get_image(releaseid, "back", size=size)
def get_image(mbid, coverid, size=None, entitytype="release"):
"""Download cover art for a release. The coverart file to download
is specified by the `coverid` argument.
If `size` is not specified, download the largest copy present, which can be
very large.
If an error occurs then a musicbrainz.ResponseError will be raised with one
of the following HTTP codes:
* 400: `Releaseid` is not a valid UUID or `coverid` is invalid
* 404: No release exists with an MBID of `releaseid`
* 503: Ratelimit exceeded
:param coverid: ``front``, ``back`` or a number from the listing obtained with
:meth:`get_image_list`
:type coverid: int or str
:param size: 250, 500 or None. If it is None, the largest available picture
will be downloaded. If the image originally uploaded to the
Cover Art Archive was smaller than the requested size, only
the original image will be returned.
:type size: str or None
:param entitytype: The type of entity for which to download the cover art.
This is either ``release`` or ``release-group``.
:type entitytype: str
:return: The binary image data
:type: str
"""
if isinstance(coverid, int):
coverid = "%d" % (coverid, )
if isinstance(size, int):
size = "%d" % (size, )
return _caa_request(mbid, coverid, size=size, entitytype=entitytype)
+76 -29
View File
@@ -36,6 +36,22 @@ NS_MAP = {"http://musicbrainz.org/ns/mmd-2.0#": "ws2",
"http://musicbrainz.org/ns/ext#-2.0": "ext"} "http://musicbrainz.org/ns/ext#-2.0": "ext"}
_log = logging.getLogger("musicbrainzngs") _log = logging.getLogger("musicbrainzngs")
def get_error_message(error):
""" Given an error XML message from the webservice containing
<error><text>x</text><text>y</text></error>, return a list
of [x, y]"""
try:
tree = util.bytes_to_elementtree(error)
root = tree.getroot()
errors = []
if root.tag == "error":
for ch in root:
if ch.tag == "text":
errors.append(ch.text)
return errors
except ET.ParseError:
return None
def make_artist_credit(artists): def make_artist_credit(artists):
names = [] names = []
for artist in artists: for artist in artists:
@@ -123,6 +139,7 @@ def parse_message(message):
"place": parse_place, "place": parse_place,
"release": parse_release, "release": parse_release,
"release-group": parse_release_group, "release-group": parse_release_group,
"series": parse_series,
"recording": parse_recording, "recording": parse_recording,
"work": parse_work, "work": parse_work,
"url": parse_url, "url": parse_url,
@@ -138,6 +155,7 @@ def parse_message(message):
"place-list": parse_place_list, "place-list": parse_place_list,
"release-list": parse_release_list, "release-list": parse_release_list,
"release-group-list": parse_release_group_list, "release-group-list": parse_release_group_list,
"series-list": parse_series_list,
"recording-list": parse_recording_list, "recording-list": parse_recording_list,
"work-list": parse_work_list, "work-list": parse_work_list,
"url-list": parse_url_list, "url-list": parse_url_list,
@@ -297,7 +315,7 @@ def parse_relation_list(rl):
def parse_relation(relation): def parse_relation(relation):
result = {} result = {}
attribs = ["type", "type-id"] attribs = ["type", "type-id"]
elements = ["target", "direction", "begin", "end", "ended"] elements = ["target", "direction", "begin", "end", "ended", "ordering-key"]
inner_els = {"area": parse_area, inner_els = {"area": parse_area,
"artist": parse_artist, "artist": parse_artist,
"label": parse_label, "label": parse_label,
@@ -305,6 +323,7 @@ def parse_relation(relation):
"recording": parse_recording, "recording": parse_recording,
"release": parse_release, "release": parse_release,
"release-group": parse_release_group, "release-group": parse_release_group,
"series": parse_series,
"attribute-list": parse_element_list, "attribute-list": parse_element_list,
"work": parse_work, "work": parse_work,
"target": parse_relation_target "target": parse_relation_target
@@ -324,6 +343,8 @@ def parse_release(release):
"label-info-list": parse_label_info_list, "label-info-list": parse_label_info_list,
"medium-list": parse_medium_list, "medium-list": parse_medium_list,
"release-group": parse_release_group, "release-group": parse_release_group,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"relation-list": parse_relation_list, "relation-list": parse_relation_list,
"annotation": parse_annotation, "annotation": parse_annotation,
"cover-art-archive": parse_caa, "cover-art-archive": parse_caa,
@@ -408,6 +429,22 @@ def parse_recording(recording):
return result return result
def parse_series_list(sl):
return [parse_series(s) for s in sl]
def parse_series(series):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "disambiguation"]
inner_els = {"alias-list": parse_alias_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, series))
result.update(parse_elements(elements, inner_els, series))
return result
def parse_external_id_list(pl): def parse_external_id_list(pl):
return [parse_attributes(["id"], p)["id"] for p in pl] return [parse_attributes(["id"], p)["id"] for p in pl]
@@ -427,13 +464,28 @@ def parse_work(work):
"alias-list": parse_alias_list, "alias-list": parse_alias_list,
"iswc-list": parse_element_list, "iswc-list": parse_element_list,
"relation-list": parse_relation_list, "relation-list": parse_relation_list,
"annotation": parse_response_message} "annotation": parse_response_message,
"attribute-list": parse_work_attribute_list
}
result.update(parse_attributes(attribs, work)) result.update(parse_attributes(attribs, work))
result.update(parse_elements(elements, inner_els, work)) result.update(parse_elements(elements, inner_els, work))
return result return result
def parse_work_attribute_list(wal):
return [parse_work_attribute(wa) for wa in wal]
def parse_work_attribute(wa):
result = {}
attribs = ["type"]
result.update(parse_attributes(attribs, wa))
result["attribute"] = wa.text
return result
def parse_url_list(ul): def parse_url_list(ul):
return [parse_url(u) for u in ul] return [parse_url(u) for u in ul]
@@ -617,45 +669,40 @@ def make_barcode_request(release2barcode):
return ET.tostring(root, "utf-8") return ET.tostring(root, "utf-8")
def make_tag_request(artist2tags, recording2tags): def make_tag_request(**kwargs):
NS = "http://musicbrainz.org/ns/mmd-2.0#" NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS) root = ET.Element("{%s}metadata" % NS)
rec_list = ET.SubElement(root, "{%s}recording-list" % NS) for entity_type in ['artist', 'label', 'place', 'recording', 'release', 'release_group', 'work']:
for rec, tags in recording2tags.items(): entity_tags = kwargs.pop(entity_type + '_tags', None)
rec_xml = ET.SubElement(rec_list, "{%s}recording" % NS) if entity_tags is not None:
rec_xml.set("{%s}id" % NS, rec) e_list = ET.SubElement(root, "{%s}%s-list" % (NS, entity_type.replace('_', '-')))
taglist = ET.SubElement(rec_xml, "{%s}user-tag-list" % NS) for e, tags in entity_tags.items():
for tag in tags: e_xml = ET.SubElement(e_list, "{%s}%s" % (NS, entity_type.replace('_', '-')))
usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS) e_xml.set("{%s}id" % NS, e)
name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS) taglist = ET.SubElement(e_xml, "{%s}user-tag-list" % NS)
name_xml.text = tag
art_list = ET.SubElement(root, "{%s}artist-list" % NS)
for art, tags in artist2tags.items():
art_xml = ET.SubElement(art_list, "{%s}artist" % NS)
art_xml.set("{%s}id" % NS, art)
taglist = ET.SubElement(art_xml, "{%s}user-tag-list" % NS)
for tag in tags: for tag in tags:
usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS) usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS)
name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS) name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS)
name_xml.text = tag name_xml.text = tag
if kwargs.keys():
raise TypeError("make_tag_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0])
return ET.tostring(root, "utf-8") return ET.tostring(root, "utf-8")
def make_rating_request(artist2rating, recording2rating): def make_rating_request(**kwargs):
NS = "http://musicbrainz.org/ns/mmd-2.0#" NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS) root = ET.Element("{%s}metadata" % NS)
rec_list = ET.SubElement(root, "{%s}recording-list" % NS) for entity_type in ['artist', 'label', 'recording', 'release_group', 'work']:
for rec, rating in recording2rating.items(): entity_ratings = kwargs.pop(entity_type + '_ratings', None)
rec_xml = ET.SubElement(rec_list, "{%s}recording" % NS) if entity_ratings is not None:
rec_xml.set("{%s}id" % NS, rec) e_list = ET.SubElement(root, "{%s}%s-list" % (NS, entity_type.replace('_', '-')))
rating_xml = ET.SubElement(rec_xml, "{%s}user-rating" % NS) for e, rating in entity_ratings.items():
rating_xml.text = str(rating) e_xml = ET.SubElement(e_list, "{%s}%s" % (NS, entity_type.replace('_', '-')))
art_list = ET.SubElement(root, "{%s}artist-list" % NS) e_xml.set("{%s}id" % NS, e)
for art, rating in artist2rating.items(): rating_xml = ET.SubElement(e_xml, "{%s}user-rating" % NS)
art_xml = ET.SubElement(art_list, "{%s}artist" % NS)
art_xml.set("{%s}id" % NS, art)
rating_xml = ET.SubElement(art_xml, "{%s}user-rating" % NS)
rating_xml.text = str(rating) rating_xml.text = str(rating)
if kwargs.keys():
raise TypeError("make_rating_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0])
return ET.tostring(root, "utf-8") return ET.tostring(root, "utf-8")
+141 -49
View File
@@ -11,25 +11,25 @@ import socket
import hashlib import hashlib
import locale import locale
import sys import sys
import base64 import json
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from xml.parsers import expat from xml.parsers import expat
from warnings import warn, simplefilter from warnings import warn
from musicbrainzngs import mbxml from musicbrainzngs import mbxml
from musicbrainzngs import util from musicbrainzngs import util
from musicbrainzngs import compat from musicbrainzngs import compat
import base64
_version = "0.6devMODIFIED" _version = "0.6devMODIFIED"
_log = logging.getLogger("musicbrainzngs") _log = logging.getLogger("musicbrainzngs")
# turn on DeprecationWarnings below LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])'
simplefilter(action="once", category=DeprecationWarning)
# Constants for validation. # Constants for validation.
RELATABLE_TYPES = ['area', 'artist', 'label', 'place', 'recording', 'release', 'release-group', 'url', 'work'] RELATABLE_TYPES = ['area', 'artist', 'label', 'place', 'recording', 'release', 'release-group', 'series', 'url', 'work']
RELATION_INCLUDES = [entity + '-rels' for entity in RELATABLE_TYPES] RELATION_INCLUDES = [entity + '-rels' for entity in RELATABLE_TYPES]
TAG_INCLUDES = ["tags", "user-tags"] TAG_INCLUDES = ["tags", "user-tags"]
RATING_INCLUDES = ["ratings", "user-ratings"] RATING_INCLUDES = ["ratings", "user-ratings"]
@@ -43,6 +43,9 @@ VALID_INCLUDES = {
] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES, ] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES,
'annotation': [ 'annotation': [
],
'instrument': [
], ],
'label': [ 'label': [
"releases", # Subqueries "releases", # Subqueries
@@ -59,20 +62,23 @@ VALID_INCLUDES = {
"artists", "labels", "recordings", "release-groups", "media", "artists", "labels", "recordings", "release-groups", "media",
"artist-credits", "discids", "puids", "isrcs", "artist-credits", "discids", "puids", "isrcs",
"recording-level-rels", "work-level-rels", "annotation", "aliases" "recording-level-rels", "work-level-rels", "annotation", "aliases"
] + RELATION_INCLUDES, ] + TAG_INCLUDES + RELATION_INCLUDES,
'release-group': [ 'release-group': [
"artists", "releases", "discids", "media", "artists", "releases", "discids", "media",
"artist-credits", "annotation", "aliases" "artist-credits", "annotation", "aliases"
] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'series': [
"annotation", "aliases"
] + RELATION_INCLUDES,
'work': [ 'work': [
"artists", # Subqueries "artists", # Subqueries
"aliases", "annotation" "aliases", "annotation"
] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'url': RELATION_INCLUDES, 'url': RELATION_INCLUDES,
'discid': [ 'discid': [ # Discid should be the same as release
"artists", "labels", "recordings", "release-groups", "media", "artists", "labels", "recordings", "release-groups", "media",
"artist-credits", "discids", "puids", "isrcs", "artist-credits", "discids", "puids", "isrcs",
"recording-level-rels", "work-level-rels" "recording-level-rels", "work-level-rels", "annotation", "aliases"
] + RELATION_INCLUDES, ] + RELATION_INCLUDES,
'isrc': ["artists", "releases", "puids", "isrcs"], 'isrc': ["artists", "releases", "puids", "isrcs"],
'iswc': ["artists"], 'iswc': ["artists"],
@@ -93,7 +99,8 @@ VALID_RELEASE_TYPES = [
"nat", "nat",
"album", "single", "ep", "broadcast", "other", # primary types "album", "single", "ep", "broadcast", "other", # primary types
"compilation", "soundtrack", "spokenword", "interview", "audiobook", "compilation", "soundtrack", "spokenword", "interview", "audiobook",
"live", "remix", "dj-mix", "mixtape/street", "demo", # secondary types "live", "remix", "dj-mix", "mixtape/street", # secondary types
"demo", # headphones
] ]
#: These can be used to filter whenever releases or release-groups are involved #: These can be used to filter whenever releases or release-groups are involved
VALID_RELEASE_STATUSES = ["official", "promotion", "bootleg", "pseudo-release"] VALID_RELEASE_STATUSES = ["official", "promotion", "bootleg", "pseudo-release"]
@@ -101,6 +108,10 @@ VALID_SEARCH_FIELDS = {
'annotation': [ 'annotation': [
'entity', 'name', 'text', 'type' 'entity', 'name', 'text', 'type'
], ],
'area': [
'aid', 'area', 'alias', 'begin', 'comment', 'end', 'ended',
'iso', 'iso1', 'iso2', 'iso3', 'type'
],
'artist': [ 'artist': [
'arid', 'artist', 'artistaccent', 'alias', 'begin', 'comment', 'arid', 'artist', 'artistaccent', 'alias', 'begin', 'comment',
'country', 'end', 'ended', 'gender', 'ipi', 'sortname', 'tag', 'type', 'country', 'end', 'ended', 'gender', 'ipi', 'sortname', 'tag', 'type',
@@ -133,12 +144,20 @@ VALID_SEARCH_FIELDS = {
'rgid', 'script', 'secondarytype', 'status', 'tag', 'tracks', 'rgid', 'script', 'secondarytype', 'status', 'tag', 'tracks',
'tracksmedium', 'type' 'tracksmedium', 'type'
], ],
'series': [
'alias', 'comment', 'sid', 'series', 'type'
],
'work': [ 'work': [
'alias', 'arid', 'artist', 'comment', 'iswc', 'lang', 'tag', 'alias', 'arid', 'artist', 'comment', 'iswc', 'lang', 'tag',
'type', 'wid', 'work', 'workaccent' 'type', 'wid', 'work', 'workaccent'
], ],
} }
# Constants
class AUTH_YES: pass
class AUTH_NO: pass
class AUTH_IFSET: pass
# Exceptions. # Exceptions.
@@ -271,6 +290,7 @@ user = password = ""
hostname = "musicbrainz.org" hostname = "musicbrainz.org"
_client = "" _client = ""
_useragent = "" _useragent = ""
mb_auth = False
def auth(u, p): def auth(u, p):
"""Set the username and password to be used in subsequent queries to """Set the username and password to be used in subsequent queries to
@@ -284,9 +304,16 @@ def hpauth(u, p):
"""Set the username and password to be used in subsequent queries to """Set the username and password to be used in subsequent queries to
the MusicBrainz XML API that require authentication. the MusicBrainz XML API that require authentication.
""" """
global hpuser, hppassword global hpuser, hppassword, mb_auth
hpuser = u hpuser = u
hppassword = p hppassword = p
mb_auth = True
def disable_hpauth():
"""Disable the authentication for MusicBrainz XML API
"""
global mb_auth
mb_auth = False
def set_useragent(app, version, contact=None): def set_useragent(app, version, contact=None):
"""Set the User-Agent to be used for requests to the MusicBrainz webservice. """Set the User-Agent to be used for requests to the MusicBrainz webservice.
@@ -551,28 +578,33 @@ def set_format(fmt="xml"):
"""Sets the format that should be returned by the Web Service. """Sets the format that should be returned by the Web Service.
The server currently supports `xml` and `json`. The server currently supports `xml` and `json`.
When you set the format to anything different from the default, This method will set a default parser for the specified format,
you need to provide your own parser with :func:`set_parser`. but you can modify it with :func:`set_parser`.
.. warning:: The json format used by the server is different from .. warning:: The json format used by the server is different from
the json format returned by the `musicbrainzngs` internal parser the json format returned by the `musicbrainzngs` internal parser
when using the `xml` format! when using the `xml` format! This format may change at any time.
""" """
global ws_format global ws_format
if fmt not in ["xml", "json"]: if fmt == "xml":
raise ValueError("invalid format: %s" % fmt)
else:
ws_format = fmt ws_format = fmt
set_parser() # set to default
elif fmt == "json":
ws_format = fmt
warn("The json format is non-official and may change at any time")
set_parser(json.loads)
else:
raise ValueError("invalid format: %s" % fmt)
@_rate_limit @_rate_limit
def _mb_request(path, method='GET', auth_required=False, client_required=False, def _mb_request(path, method='GET', auth_required=AUTH_NO,
args=None, data=None, body=None): client_required=False, args=None, data=None, body=None):
"""Makes a request for the specified `path` (endpoint) on /ws/2 on """Makes a request for the specified `path` (endpoint) on /ws/2 on
the globally-specified hostname. Parses the responses and returns the globally-specified hostname. Parses the responses and returns
the resulting object. `auth_required` and `client_required` control the resulting object. `auth_required` and `client_required` control
whether exceptions should be raised if the client and whether exceptions should be raised if the username/password and
username/password are left unspecified, respectively. client are left unspecified, respectively.
""" """
global parser_fun global parser_fun
@@ -618,11 +650,19 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
handlers = [httpHandler] handlers = [httpHandler]
# Add credentials if required. # Add credentials if required.
if auth_required: add_auth = False
if auth_required == AUTH_YES:
_log.debug("Auth required for %s" % url) _log.debug("Auth required for %s" % url)
if not user: if not user:
raise UsageError("authorization required; " raise UsageError("authorization required; "
"use auth(user, pass) first") "use auth(user, pass) first")
add_auth = True
if auth_required == AUTH_IFSET and user:
_log.debug("Using auth for %s because user and pass is set" % url)
add_auth = True
if add_auth:
passwordMgr = _RedirectPasswordMgr() passwordMgr = _RedirectPasswordMgr()
authHandler = _DigestAuthHandler(passwordMgr) authHandler = _DigestAuthHandler(passwordMgr)
authHandler.add_password("musicbrainz.org", (), user, password) authHandler.add_password("musicbrainz.org", (), user, password)
@@ -634,8 +674,9 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
req = _MusicbrainzHttpRequest(method, url, data) req = _MusicbrainzHttpRequest(method, url, data)
req.add_header('User-Agent', _useragent) req.add_header('User-Agent', _useragent)
# Add headphones credentials # Add headphones credentials
if hostname == '144.76.94.239:8181': if mb_auth:
base64string = base64.encodestring('%s:%s' % (hpuser, hppassword)).replace('\n', '') base64string = base64.encodestring('%s:%s' % (hpuser, hppassword)).replace('\n', '')
req.add_header("Authorization", "Basic %s" % base64string) req.add_header("Authorization", "Basic %s" % base64string)
@@ -650,16 +691,19 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
return parser_fun(resp) return parser_fun(resp)
def _is_auth_required(entity, includes): def _get_auth_type(entity, id, includes):
""" Some calls require authentication. This returns """ Some calls require authentication. This returns
True if a call does, False otherwise True if a call does, False otherwise
""" """
if "user-tags" in includes or "user-ratings" in includes: if "user-tags" in includes or "user-ratings" in includes:
return True return AUTH_YES
elif entity.startswith("collection"): elif entity.startswith("collection"):
return True if not id:
return AUTH_YES
else: else:
return False return AUTH_IFSET
else:
return AUTH_NO
def _do_mb_query(entity, id, includes=[], params={}): def _do_mb_query(entity, id, includes=[], params={}):
"""Make a single GET call to the MusicBrainz XML API. `entity` is a """Make a single GET call to the MusicBrainz XML API. `entity` is a
@@ -673,7 +717,7 @@ def _do_mb_query(entity, id, includes=[], params={}):
if not isinstance(includes, list): if not isinstance(includes, list):
includes = [includes] includes = [includes]
_check_includes(entity, includes) _check_includes(entity, includes)
auth_required = _is_auth_required(entity, includes) auth_required = _get_auth_type(entity, id, includes)
args = dict(params) args = dict(params)
if len(includes) > 0: if len(includes) > 0:
inc = " ".join(includes) inc = " ".join(includes)
@@ -696,8 +740,8 @@ def _do_mb_search(entity, query='', fields={},
if query: if query:
clean_query = util._unicode(query) clean_query = util._unicode(query)
if fields: if fields:
clean_query = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\])', clean_query = re.sub(LUCENE_SPECIAL, r'\\\1',
r'\\\1', clean_query) clean_query)
if strict: if strict:
query_parts.append('"%s"' % clean_query) query_parts.append('"%s"' % clean_query)
else: else:
@@ -713,11 +757,11 @@ def _do_mb_search(entity, query='', fields={},
elif key == "puid": elif key == "puid":
warn("PUID support was removed from server\n" warn("PUID support was removed from server\n"
"the 'puid' field is ignored", "the 'puid' field is ignored",
DeprecationWarning, stacklevel=2) Warning, stacklevel=2)
# Escape Lucene's special characters. # Escape Lucene's special characters.
value = util._unicode(value) value = util._unicode(value)
value = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\\/])', r'\\\1', value) value = re.sub(LUCENE_SPECIAL, r'\\\1', value)
if value: if value:
if strict: if strict:
query_parts.append('%s:"%s"' % (key, value)) query_parts.append('%s:"%s"' % (key, value))
@@ -744,18 +788,18 @@ def _do_mb_search(entity, query='', fields={},
def _do_mb_delete(path): def _do_mb_delete(path):
"""Send a DELETE request for the specified object. """Send a DELETE request for the specified object.
""" """
return _mb_request(path, 'DELETE', True, True) return _mb_request(path, 'DELETE', AUTH_YES, True)
def _do_mb_put(path): def _do_mb_put(path):
"""Send a PUT request for the specified object. """Send a PUT request for the specified object.
""" """
return _mb_request(path, 'PUT', True, True) return _mb_request(path, 'PUT', AUTH_YES, True)
def _do_mb_post(path, body): def _do_mb_post(path, body):
"""Perform a single POST call for an endpoint with a specified """Perform a single POST call for an endpoint with a specified
request body. request body.
""" """
return _mb_request(path, 'POST', True, True, body=body) return _mb_request(path, 'POST', AUTH_YES, True, body=body)
# The main interface! # The main interface!
@@ -780,6 +824,15 @@ def get_artist_by_id(id, includes=[], release_status=[], release_type=[]):
release_status, release_type) release_status, release_type)
return _do_mb_query("artist", id, includes, params) return _do_mb_query("artist", id, includes, params)
@_docstring('instrument')
def get_instrument_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the instrument with the MusicBrainz `id` as a dict with an 'artist' key.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("instrument", includes,
release_status, release_type)
return _do_mb_query("instrument", id, includes, params)
@_docstring('label') @_docstring('label')
def get_label_by_id(id, includes=[], release_status=[], release_type=[]): def get_label_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the label with the MusicBrainz `id` as a dict with a 'label' key. """Get the label with the MusicBrainz `id` as a dict with a 'label' key.
@@ -828,6 +881,13 @@ def get_release_group_by_id(id, includes=[],
release_status, release_type) release_status, release_type)
return _do_mb_query("release-group", id, includes, params) return _do_mb_query("release-group", id, includes, params)
@_docstring('series')
def get_series_by_id(id, includes=[]):
"""Get the series with the MusicBrainz `id` as a dict with a 'series' key.
*Available includes*: {includes}"""
return _do_mb_query("series", id, includes)
@_docstring('work') @_docstring('work')
def get_work_by_id(id, includes=[]): def get_work_by_id(id, includes=[]):
"""Get the work with the MusicBrainz `id` as a dict with a 'work' key. """Get the work with the MusicBrainz `id` as a dict with a 'work' key.
@@ -852,6 +912,13 @@ def search_annotations(query='', limit=None, offset=None, strict=False, **fields
*Available search fields*: {fields}""" *Available search fields*: {fields}"""
return _do_mb_search('annotation', query, fields, limit, offset, strict) return _do_mb_search('annotation', query, fields, limit, offset, strict)
@_docstring('area')
def search_areas(query='', limit=None, offset=None, strict=False, **fields):
"""Search for areas and return a dict with an 'area-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('area', query, fields, limit, offset, strict)
@_docstring('artist') @_docstring('artist')
def search_artists(query='', limit=None, offset=None, strict=False, **fields): def search_artists(query='', limit=None, offset=None, strict=False, **fields):
"""Search for artists and return a dict with an 'artist-list' key. """Search for artists and return a dict with an 'artist-list' key.
@@ -890,6 +957,13 @@ def search_release_groups(query='', limit=None, offset=None,
*Available search fields*: {fields}""" *Available search fields*: {fields}"""
return _do_mb_search('release-group', query, fields, limit, offset, strict) return _do_mb_search('release-group', query, fields, limit, offset, strict)
@_docstring('series')
def search_series(query='', limit=None, offset=None, strict=False, **fields):
"""Search for series and return a dict with a 'series-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('series', query, fields, limit, offset, strict)
@_docstring('work') @_docstring('work')
def search_works(query='', limit=None, offset=None, strict=False, **fields): def search_works(query='', limit=None, offset=None, strict=False, **fields):
"""Search for works and return a dict with a 'work-list' key. """Search for works and return a dict with a 'work-list' key.
@@ -899,18 +973,25 @@ def search_works(query='', limit=None, offset=None, strict=False, **fields):
# Lists of entities # Lists of entities
@_docstring('release') @_docstring('discid')
def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True): def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True, media_format=None):
"""Search for releases with a :musicbrainz:`Disc ID`. """Search for releases with a :musicbrainz:`Disc ID` or table of contents.
When a `toc` is provided and no release with the disc ID is found, When a `toc` is provided and no release with the disc ID is found,
a fuzzy search by the toc is done. a fuzzy search by the toc is done.
The `toc` should have to same format as :attr:`discid.Disc.toc_string`. The `toc` should have to same format as :attr:`discid.Disc.toc_string`.
When a `toc` is provided, the format of the discid itself is not
checked server-side, so any value may be passed if searching by only
`toc` is desired.
If no toc matches in musicbrainz but a :musicbrainz:`CD Stub` does, If no toc matches in musicbrainz but a :musicbrainz:`CD Stub` does,
the CD Stub will be returned. Prevent this from happening by the CD Stub will be returned. Prevent this from happening by
passing `cdstubs=False`. passing `cdstubs=False`.
By default only results that match a format that allows discids
(e.g. CD) are included. To include all media formats, pass
`media_format='all'`.
The result is a dict with either a 'disc' , a 'cdstub' key The result is a dict with either a 'disc' , a 'cdstub' key
or a 'release-list' (fuzzy match with TOC). or a 'release-list' (fuzzy match with TOC).
A 'disc' has a 'release-list' and a 'cdstub' key has direct 'artist' A 'disc' has a 'release-list' and a 'cdstub' key has direct 'artist'
@@ -923,6 +1004,8 @@ def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True):
params["toc"] = toc params["toc"] = toc
if not cdstubs: if not cdstubs:
params["cdstubs"] = "no" params["cdstubs"] = "no"
if media_format:
params["media-format"] = media_format
return _do_mb_query("discid", id, includes, params) return _do_mb_query("discid", id, includes, params)
@_docstring('recording') @_docstring('recording')
@@ -932,7 +1015,7 @@ def get_recordings_by_echoprint(echoprint, includes=[], release_status=[],
(not available on server)""" (not available on server)"""
warn("Echoprints were never introduced\n" warn("Echoprints were never introduced\n"
"and will not be found (404)", "and will not be found (404)",
DeprecationWarning, stacklevel=2) Warning, stacklevel=2)
raise ResponseError(cause=compat.HTTPError( raise ResponseError(cause=compat.HTTPError(
None, 404, "Not Found", None, None)) None, 404, "Not Found", None, None))
@@ -943,7 +1026,7 @@ def get_recordings_by_puid(puid, includes=[], release_status=[],
(not available on server)""" (not available on server)"""
warn("PUID support was removed from the server\n" warn("PUID support was removed from the server\n"
"and no PUIDs will be found (404)", "and no PUIDs will be found (404)",
DeprecationWarning, stacklevel=2) Warning, stacklevel=2)
raise ResponseError(cause=compat.HTTPError( raise ResponseError(cause=compat.HTTPError(
None, 404, "Not Found", None, None)) None, 404, "Not Found", None, None))
@@ -1108,7 +1191,7 @@ def submit_puids(recording_puids):
""" """
warn("PUID support was dropped at the server\n" warn("PUID support was dropped at the server\n"
"nothing will be submitted", "nothing will be submitted",
DeprecationWarning, stacklevel=2) Warning, stacklevel=2)
return {'message': {'text': 'OK'}} return {'message': {'text': 'OK'}}
def submit_echoprints(recording_echoprints): def submit_echoprints(recording_echoprints):
@@ -1117,7 +1200,7 @@ def submit_echoprints(recording_echoprints):
""" """
warn("Echoprints were never introduced\n" warn("Echoprints were never introduced\n"
"nothing will be submitted", "nothing will be submitted",
DeprecationWarning, stacklevel=2) Warning, stacklevel=2)
return {'message': {'text': 'OK'}} return {'message': {'text': 'OK'}}
def submit_isrcs(recording_isrcs): def submit_isrcs(recording_isrcs):
@@ -1131,20 +1214,29 @@ def submit_isrcs(recording_isrcs):
query = mbxml.make_isrc_request(rec2isrcs) query = mbxml.make_isrc_request(rec2isrcs)
return _do_mb_post("recording", query) return _do_mb_post("recording", query)
def submit_tags(artist_tags={}, recording_tags={}): def submit_tags(**kwargs):
"""Submit user tags. """Submit user tags.
Artist or recording parameters are of the form: Takes parameters named e.g. 'artist_tags', 'recording_tags', etc.,
and of the form:
{entity_id1: [tag1, ...], ...} {entity_id1: [tag1, ...], ...}
The user's tags for each entity will be set to that list, adding or
removing tags as necessary. Submitting an empty list for an entity
will remove all tags for that entity by the user.
""" """
query = mbxml.make_tag_request(artist_tags, recording_tags) query = mbxml.make_tag_request(**kwargs)
return _do_mb_post("tag", query) return _do_mb_post("tag", query)
def submit_ratings(artist_ratings={}, recording_ratings={}): def submit_ratings(**kwargs):
""" Submit user ratings. """Submit user ratings.
Artist or recording parameters are of the form: Takes parameters named e.g. 'artist_ratings', 'recording_ratings', etc.,
and of the form:
{entity_id1: rating, ...} {entity_id1: rating, ...}
Ratings are numbers from 0-100, at intervals of 20 (20 per 'star').
Submitting a rating of 0 will remove the user's rating.
""" """
query = mbxml.make_rating_request(artist_ratings, recording_ratings) query = mbxml.make_rating_request(**kwargs)
return _do_mb_post("rating", query) return _do_mb_post("rating", query)
def add_releases_to_collection(collection, releases=[]): def add_releases_to_collection(collection, releases=[]):
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python
from pynma import PyNMA from .pynma import PyNMA
Regular → Executable
+29 -18
View File
@@ -1,12 +1,20 @@
#!/usr/bin/python #!/usr/bin/python
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
from httplib import HTTPSConnection
from urllib import urlencode
__version__ = "0.1" try:
from http.client import HTTPSConnection
except ImportError:
from httplib import HTTPSConnection
API_SERVER = 'nma.usk.bz' try:
from urllib.parse import urlencode
except ImportError:
from urllib import urlencode
__version__ = "1.0"
API_SERVER = 'www.notifymyandroid.com'
ADD_PATH = '/publicapi/notify' ADD_PATH = '/publicapi/notify'
USER_AGENT="PyNMA/v%s"%__version__ USER_AGENT="PyNMA/v%s"%__version__
@@ -18,14 +26,14 @@ def uniq_preserve(seq): # Dave Kirby
def uniq(seq): def uniq(seq):
# Not order preserving # Not order preserving
return {}.fromkeys(seq).keys() return list({}.fromkeys(seq).keys())
class PyNMA(object): class PyNMA(object):
"""PyNMA(apikey=[], developerkey=None) """PyNMA(apikey=[], developerkey=None)
takes 2 optional arguments: takes 2 optional arguments:
- (opt) apykey: might me a string containing 1 key or an array of keys - (opt) apykey: might me a string containing 1 key or an array of keys
- (opt) developerkey: where you can store your developer key - (opt) developerkey: where you can store your developer key
""" """
def __init__(self, apikey=[], developerkey=None): def __init__(self, apikey=[], developerkey=None):
self._developerkey = None self._developerkey = None
@@ -60,19 +68,20 @@ class PyNMA(object):
if type(developerkey) == str and len(developerkey) == 48: if type(developerkey) == str and len(developerkey) == 48:
self._developerkey = developerkey self._developerkey = developerkey
def push(self, application="", event="", description="", url="", priority=0, batch_mode=False): def push(self, application="", event="", description="", url="", contenttype=None, priority=0, batch_mode=False, html=False):
"""Pushes a message on the registered API keys. """Pushes a message on the registered API keys.
takes 5 arguments: takes 5 arguments:
- (req) application: application name [256] - (req) application: application name [256]
- (req) event: event name [1000] - (req) event: event name [1000]
- (req) description: description [10000] - (req) description: description [10000]
- (opt) url: url [512] - (opt) url: url [512]
- (opt) contenttype: Content Type (act: None (plain text) or text/html)
- (opt) priority: from -2 (lowest) to 2 (highest) (def:0) - (opt) priority: from -2 (lowest) to 2 (highest) (def:0)
- (opt) batch_mode: call API 5 by 5 (def:False) - (opt) batch_mode: push to all keys at once (def:False)
- (opt) html: shortcut for contenttype=text/html
Warning: using batch_mode will return error only if all API keys are bad Warning: using batch_mode will return error only if all API keys are bad
cf: http://nma.usk.bz/api.php cf: http://nma.usk.bz/api.php
""" """
datas = { datas = {
'application': application[:256].encode('utf8'), 'application': application[:256].encode('utf8'),
'event': event[:1024].encode('utf8'), 'event': event[:1024].encode('utf8'),
@@ -83,6 +92,9 @@ class PyNMA(object):
if url: if url:
datas['url'] = url[:512] datas['url'] = url[:512]
if contenttype == "text/html" or html == True: # Currently only accepted content type
datas['content-type'] = "text/html"
if self._developerkey: if self._developerkey:
datas['developerkey'] = self._developerkey datas['developerkey'] = self._developerkey
@@ -94,8 +106,7 @@ class PyNMA(object):
res = self.callapi('POST', ADD_PATH, datas) res = self.callapi('POST', ADD_PATH, datas)
results[key] = res results[key] = res
else: else:
for i in range(0, len(self._apikey), 5): datas['apikey'] = ",".join(self._apikey)
datas['apikey'] = ",".join(self._apikey[i:i+5])
res = self.callapi('POST', ADD_PATH, datas) res = self.callapi('POST', ADD_PATH, datas)
results[datas['apikey']] = res results[datas['apikey']] = res
return results return results
@@ -110,7 +121,7 @@ class PyNMA(object):
try: try:
res = self._parse_reponse(resp.read()) res = self._parse_reponse(resp.read())
except Exception, e: except Exception as e:
res = {'type': "pynmaerror", res = {'type': "pynmaerror",
'code': 600, 'code': 600,
'message': str(e) 'message': str(e)
@@ -124,12 +135,12 @@ class PyNMA(object):
for elem in root.childNodes: for elem in root.childNodes:
if elem.nodeType == elem.TEXT_NODE: continue if elem.nodeType == elem.TEXT_NODE: continue
if elem.tagName == 'success': if elem.tagName == 'success':
res = dict(elem.attributes.items()) res = dict(list(elem.attributes.items()))
res['message'] = "" res['message'] = ""
res['type'] = elem.tagName res['type'] = elem.tagName
return res return res
if elem.tagName == 'error': if elem.tagName == 'error':
res = dict(elem.attributes.items()) res = dict(list(elem.attributes.items()))
res['message'] = elem.firstChild.nodeValue res['message'] = elem.firstChild.nodeValue
res['type'] = elem.tagName res['type'] = elem.tagName
return res return res