mirror of
https://github.com/rembo10/headphones.git
synced 2026-09-10 16:51:42 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84a6c19677 | ||
|
|
624a1d5509 | ||
|
|
8b6d35cfe2 | ||
|
|
867e502448 | ||
|
|
74275b969e | ||
|
|
46b470f30b | ||
|
|
fbfec95a8a | ||
|
|
1e75a18f55 | ||
|
|
1b998e9d01 | ||
|
|
ebd6d3f3f9 | ||
|
|
1204afadbb | ||
|
|
cd08b72df7 | ||
|
|
c5ad7c98e2 | ||
|
|
4b8222d792 | ||
|
|
a3524ef8aa | ||
|
|
49ab8aa67d | ||
|
|
fa85639efb | ||
|
|
84bca81a9a | ||
|
|
206ea72a27 | ||
|
|
cc2aceb3d8 | ||
|
|
00b95aa6c3 | ||
|
|
a7632cc1e5 | ||
|
|
39f07025c6 | ||
|
|
658cb0403f | ||
|
|
db1519bcea | ||
|
|
885d1cc77c | ||
|
|
4005becd0a | ||
|
|
4c16b53c2a | ||
|
|
93dbf13d88 | ||
|
|
c5dd1a9002 | ||
|
|
18af7d6392 | ||
|
|
dbeb31d4fe | ||
|
|
0fb9234ebc | ||
|
|
6e4cec829a | ||
|
|
5a433c42e7 | ||
|
|
f808dd131b | ||
|
|
1bbf9a2318 | ||
|
|
ccff70d9c4 | ||
|
|
0347ca1bcc | ||
|
|
23564c14f9 | ||
|
|
93fca13510 | ||
|
|
1a39066cfc | ||
|
|
70b1574aa6 | ||
|
|
898f814dce | ||
|
|
9d425d33ab | ||
|
|
c6b0f58074 | ||
|
|
78b3802f8f | ||
|
|
833b90ccff | ||
|
|
c27a381286 | ||
|
|
ad29b43836 | ||
|
|
5cd9c51410 | ||
|
|
6476d42371 | ||
|
|
a48d9006ad | ||
|
|
c5c2057859 | ||
|
|
1889ab4825 | ||
|
|
0c81560f2c | ||
|
|
26005d6584 | ||
|
|
b0b0bcbdb7 | ||
|
|
546099fa2d | ||
|
|
c53fde6dca | ||
|
|
c9aeefc67a | ||
|
|
37594d3384 | ||
|
|
00dc5d8c38 | ||
|
|
ff8f6b1d00 | ||
|
|
aa60ba1fb1 | ||
|
|
3b8341f249 | ||
|
|
a68bcb6ee1 | ||
|
|
24a52a294c | ||
|
|
03ea67b20b | ||
|
|
4f20003f9a | ||
|
|
a92d13652d |
+55
-1
@@ -1,5 +1,59 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
@@ -155,4 +209,4 @@ The full list of commits can be found [here](https://github.com/rembo10/headphon
|
|||||||
## v0.1
|
## v0.1
|
||||||
Released 05 August 2013
|
Released 05 August 2013
|
||||||
|
|
||||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/2156e1341405d07c5bcfbe994f6b354b32d94cda...v0.1).
|
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/2156e1341405d07c5bcfbe994f6b354b32d94cda...v0.1).
|
||||||
|
|||||||
@@ -337,6 +337,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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -429,7 +429,7 @@
|
|||||||
<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>
|
||||||
@@ -724,6 +724,12 @@
|
|||||||
<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>
|
||||||
@@ -736,10 +742,6 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="row checkbox left clearfix">
|
<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']} />
|
||||||
@@ -751,6 +753,12 @@
|
|||||||
<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 indent">
|
||||||
|
<label>
|
||||||
|
Keep original folder?
|
||||||
|
<input type="checkbox" name="keep_original_folder" value="1" ${config['keep_original_folder']}>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
Rename files
|
Rename files
|
||||||
@@ -786,6 +794,10 @@
|
|||||||
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']}>
|
||||||
</label>
|
</label>
|
||||||
|
<label title="Use ID3v2.3 instead of ID3v2.4">
|
||||||
|
Tag using ID3v2.3
|
||||||
|
<input type="checkbox" name="idtag" value="1" ${config['idtag']}>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>
|
<label>
|
||||||
@@ -895,6 +907,10 @@
|
|||||||
<label>Plex Password</label><input type="password" name="plex_password" value="${config['plex_password']}" size="30">
|
<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>
|
<small>Password of your Plex client API (blank for none)</small>
|
||||||
</div>
|
</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">
|
<div class="checkbox row">
|
||||||
<input type="checkbox" name="plex_update" value="1" ${config['plex_update']} /><label>Update Plex Library</label>
|
<input type="checkbox" name="plex_update" value="1" ${config['plex_update']} /><label>Update Plex Library</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -1104,6 +1120,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<h3>Email</h3>
|
||||||
|
<div class="row checkbox">
|
||||||
|
<input type="checkbox" name="email_enabled" id="email" value="1" ${config['email_enabled']} /><label>Enable Email Notifications</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>
|
<fieldset>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1331,6 +1383,12 @@
|
|||||||
name="autowant_manually_added" value="1" ${config['autowant_manually_added']} />
|
name="autowant_manually_added" value="1" ${config['autowant_manually_added']} />
|
||||||
<label>Automatically mark manually added albums as wanted</label>
|
<label>Automatically mark manually added albums as wanted</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row left checkbox">
|
||||||
|
<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">
|
<div class="row">
|
||||||
<label>Folder Permissions</label>
|
<label>Folder Permissions</label>
|
||||||
<input type="text" name="folder_permissions" value="${config['folder_permissions']}" size="7">
|
<input type="text" name="folder_permissions" value="${config['folder_permissions']}" size="7">
|
||||||
@@ -1416,6 +1474,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>
|
||||||
@@ -1427,7 +1496,7 @@
|
|||||||
</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="15"><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>
|
||||||
@@ -1526,6 +1595,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"))
|
||||||
{
|
{
|
||||||
$("#https_options").show();
|
$("#https_options").show();
|
||||||
@@ -1866,6 +1954,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();
|
||||||
@@ -2074,6 +2182,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 () {
|
||||||
|
|||||||
@@ -1445,6 +1445,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 +1456,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;
|
||||||
|
|||||||
@@ -32,32 +32,35 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
%for item in history:
|
%for item in history:
|
||||||
<%
|
<%
|
||||||
if item['Status'] == 'Processed':
|
if item['Status'] == 'Processed':
|
||||||
grade = 'A'
|
grade = 'A'
|
||||||
elif item['Status'] == 'Snatched':
|
elif item['Status'] == 'Snatched':
|
||||||
grade = 'C'
|
grade = 'C'
|
||||||
elif item['Status'] == 'Unprocessed':
|
elif item['Status'] == 'Unprocessed':
|
||||||
grade = 'X'
|
grade = 'X'
|
||||||
elif item['Status'] == 'Frozen':
|
elif item['Status'] == 'Frozen':
|
||||||
grade = 'X'
|
grade = 'X'
|
||||||
else:
|
else:
|
||||||
grade = 'U'
|
grade = 'U'
|
||||||
|
|
||||||
fileid = 'unknown'
|
fileid = 'unknown'
|
||||||
if item['URL'].find('nzb') != -1:
|
if item['URL'].find('nzb') != -1:
|
||||||
fileid = 'nzb'
|
fileid = 'nzb'
|
||||||
if item['URL'].find('torrent') != -1:
|
if item['URL'].find('torrent') != -1:
|
||||||
fileid = 'torrent'
|
fileid = 'torrent'
|
||||||
if item['URL'].find('rutracker') != -1:
|
if item['URL'].find('rutracker') != -1:
|
||||||
fileid = 'torrent'
|
fileid = 'torrent'
|
||||||
%>
|
|
||||||
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" id="post_process_alternate">
|
||||||
<div class="row">
|
<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="text" value="" name="dir" id="dir" size="50" /><input type="submit" />
|
<input type="button" class="btnOpenDialog" value="Submit" />
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
</form>
|
<fieldset>
|
||||||
<form action="forcePostProcess" method="GET">
|
<div class="row" id="post_process_single">
|
||||||
<fieldset>
|
<label>Post-Process Single Folder</label>
|
||||||
<div class="row">
|
<input type="text" value="" name="album_dir" id="album_dir" size="50" />
|
||||||
<label>Post-Process Single Folder</label>
|
<input type="button" class="btnOpenDialog" value="Submit" />
|
||||||
<input type="text" value="" name="album_dir" id="album_dir" size="50" /><input type="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>
|
||||||
@@ -187,6 +185,55 @@
|
|||||||
function addScanAction() {
|
function addScanAction() {
|
||||||
$('#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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+29
-8
@@ -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,11 +288,12 @@ 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_INTERVAL:
|
if CONFIG.CHECK_GITHUB:
|
||||||
minutes = CONFIG.CHECK_GITHUB_INTERVAL
|
if CONFIG.CHECK_GITHUB_INTERVAL:
|
||||||
else:
|
minutes = CONFIG.CHECK_GITHUB_INTERVAL
|
||||||
minutes = 0
|
else:
|
||||||
schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0, minutes=minutes)
|
minutes = 0
|
||||||
|
schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0, minutes=minutes)
|
||||||
|
|
||||||
# Remove Torrent + data if Post Processed and finished Seeding
|
# Remove Torrent + data if Post Processed and finished Seeding
|
||||||
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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),
|
||||||
@@ -92,6 +105,8 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'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', []),
|
||||||
@@ -160,6 +175,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 +201,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', ''),
|
||||||
|
|||||||
+11
-14
@@ -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())
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+13
-3
@@ -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,12 +142,19 @@ 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)
|
||||||
|
|
||||||
artist = mb.getArtist(artistid, extrasonly)
|
if type=="series":
|
||||||
|
artist = mb.getSeries(artistid)
|
||||||
|
else:
|
||||||
|
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:
|
||||||
logger.warn('Cannot import blocked special purpose artist: %s' % artist.get('artist_name'))
|
logger.warn('Cannot import blocked special purpose artist: %s' % artist.get('artist_name'))
|
||||||
@@ -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:
|
||||||
|
|||||||
+76
-6
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# 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, parser='html.parser')
|
||||||
|
|
||||||
|
rows = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
rows = res.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)
|
||||||
+71
-36
@@ -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,6 +316,7 @@ 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):
|
||||||
|
|
||||||
@@ -349,13 +355,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 +371,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):
|
||||||
|
|
||||||
@@ -578,7 +582,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,25 +590,16 @@ 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)
|
||||||
logger.info(u"Pushover notifications sent.")
|
|
||||||
return True
|
if response:
|
||||||
elif request_status >= 400 and request_status < 500:
|
logger.info(u"Pushover notifications sent.")
|
||||||
logger.info(u"Pushover request failed: %s" % response.reason)
|
return True
|
||||||
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):
|
||||||
#For uniformity reasons not removed
|
#For uniformity reasons not removed
|
||||||
@@ -727,23 +722,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 = cls.instanceMethodForSelector_(SEL)
|
old_IMP = getattr(cls, SEL, None)
|
||||||
|
if old_IMP is None:
|
||||||
|
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 +831,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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
+39
-28
@@ -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
|
||||||
|
|
||||||
@@ -873,6 +874,14 @@ def correctMetadata(albumid, release, downloaded_track_list):
|
|||||||
# TODO: Handle extra_items & extra_tracks
|
# TODO: Handle extra_items & extra_tracks
|
||||||
|
|
||||||
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:
|
||||||
@@ -1051,8 +1060,10 @@ 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
|
||||||
|
|
||||||
if album_dir:
|
if album_dir:
|
||||||
@@ -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,7 +1153,7 @@ 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 realse group id in album folder name.')
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
+43
-87
@@ -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'):
|
logger.info("Attempting to connect to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST)
|
||||||
headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST
|
if nzb.resultType == "nzb":
|
||||||
|
response = sab_api_call('send_nzb', params=params)
|
||||||
|
elif nzb.resultType == "nzbdata":
|
||||||
|
cookies = cookielib.CookieJar()
|
||||||
|
response = sab_api_call('send_nzb', params=params, method="post", files=files, cookies=cookies, headers=headers)
|
||||||
|
|
||||||
if headphones.CONFIG.SAB_HOST.endswith('/'):
|
if not response:
|
||||||
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:
|
|
||||||
|
|
||||||
if nzb.resultType == "nzb":
|
|
||||||
f = urllib.urlopen(url)
|
|
||||||
elif nzb.resultType == "nzbdata":
|
|
||||||
cookies = cookielib.CookieJar()
|
|
||||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies),
|
|
||||||
MultipartPostHandler.MultipartPostHandler)
|
|
||||||
|
|
||||||
req = urllib2.Request(url,
|
|
||||||
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:
|
if not config_options:
|
||||||
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'):
|
|
||||||
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)
|
||||||
|
|||||||
+56
-38
@@ -225,7 +225,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 +233,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 +247,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 +306,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 +321,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 +402,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 +431,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 +443,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 +485,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 +497,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 +556,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 +604,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 +619,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 +651,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 +692,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 +947,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 +1000,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 +1026,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 +1052,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 +1105,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 +1122,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 +1191,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 +1280,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
|
||||||
@@ -1367,7 +1385,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 +1539,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
|
||||||
|
|||||||
+50
-7
@@ -145,8 +145,10 @@ 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)
|
||||||
|
else:
|
||||||
|
searchresults = mb.findSeries(name, limit=100)
|
||||||
return serve_template(templatename="searchresults.html", title='Search Results for: "' + name + '"', searchresults=searchresults, name=name, type=type)
|
return serve_template(templatename="searchresults.html", title='Search Results for: "' + name + '"', searchresults=searchresults, name=name, type=type)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@@ -156,6 +158,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
|
||||||
@@ -374,6 +383,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 +740,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 +889,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 +1014,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,6 +1066,7 @@ 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,
|
||||||
@@ -1106,6 +1121,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 +1158,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 +1170,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 +1218,15 @@ 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", "rename_files",
|
||||||
"correct_metadata", "cleanup_files", "keep_nfo", "add_album_art", "embed_album_art", "embed_lyrics", "replace_existing_folders",
|
"correct_metadata", "cleanup_files", "keep_nfo", "add_album_art", "embed_album_art", "embed_lyrics", "replace_existing_folders", "keep_original_folder",
|
||||||
"file_underscores", "include_extras", "autowant_upcoming", "autowant_all", "autowant_manually_added", "keep_torrent_files", "music_encoder",
|
"file_underscores", "include_extras", "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 +1426,19 @@ 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 updates")
|
||||||
|
plex = notifiers.Plex()
|
||||||
|
plex.update()
|
||||||
|
|
||||||
class Artwork(object):
|
class Artwork(object):
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def index(self):
|
def index(self):
|
||||||
|
|||||||
@@ -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 +1,2 @@
|
|||||||
from musicbrainzngs.musicbrainz import *
|
from musicbrainzngs.musicbrainz import *
|
||||||
|
from musicbrainzngs.caa import *
|
||||||
|
|||||||
@@ -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)
|
||||||
+81
-34
@@ -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
|
for tag in tags:
|
||||||
art_list = ET.SubElement(root, "{%s}artist-list" % NS)
|
usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS)
|
||||||
for art, tags in artist2tags.items():
|
name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS)
|
||||||
art_xml = ET.SubElement(art_list, "{%s}artist" % NS)
|
name_xml.text = tag
|
||||||
art_xml.set("{%s}id" % NS, art)
|
if kwargs.keys():
|
||||||
taglist = ET.SubElement(art_xml, "{%s}user-tag-list" % NS)
|
raise TypeError("make_tag_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0])
|
||||||
for tag in tags:
|
|
||||||
usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS)
|
|
||||||
name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS)
|
|
||||||
name_xml.text = tag
|
|
||||||
|
|
||||||
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)
|
rating_xml.text = str(rating)
|
||||||
art_xml.set("{%s}id" % NS, art)
|
if kwargs.keys():
|
||||||
rating_xml = ET.SubElement(art_xml, "{%s}user-rating" % NS)
|
raise TypeError("make_rating_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0])
|
||||||
rating_xml.text = str(rating)
|
|
||||||
|
|
||||||
return ET.tostring(root, "utf-8")
|
return ET.tostring(root, "utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -279,14 +299,21 @@ def auth(u, p):
|
|||||||
global user, password
|
global user, password
|
||||||
user = u
|
user = u
|
||||||
password = p
|
password = p
|
||||||
|
|
||||||
def hpauth(u, p):
|
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)
|
||||||
@@ -633,9 +673,10 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
|
|||||||
# Make request.
|
# Make request.
|
||||||
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:
|
||||||
else:
|
return AUTH_YES
|
||||||
return False
|
else:
|
||||||
|
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=[]):
|
||||||
|
|||||||
Reference in New Issue
Block a user