Compare commits

...
122 Commits
Author SHA1 Message Date
rembo10 88af84579c Merge branch 'develop' 2014-05-15 21:10:41 -07:00
rembo10 917dc7695f Fix for #1627: adding albums didn't add a ReleaseID to the database 2014-05-15 21:07:04 -07:00
rembo10 65e3e4faa2 Merge remote-tracking branch 'BenMagyar/master' into develop 2014-05-15 19:39:34 -07:00
rembo10 9e7ec3a65c Merge remote-tracking branch 'basilfx/bugfixes5' into develop 2014-05-15 19:36:40 -07:00
rembo10 7ac7b127c6 Merge remote-tracking branch 'AdeHub/ade' into develop 2014-05-15 19:35:26 -07:00
rembo10 3945753c5c Merge remote-tracking branch 'texke/master' into develop 2014-05-15 19:30:28 -07:00
rembo10 599585d57a Merge remote-tracking branch 'delphiactual/develop' into develop 2014-05-15 19:29:18 -07:00
BenM 10cf36401f Changed Add Artist and View MB 2014-05-13 21:11:31 -04:00
Bas Stottelaar fe2834f4a6 Be quiet when running daemon 2014-05-12 12:12:40 +02:00
Bas Stottelaar 674617c0f8 Ensure correct types 2014-05-12 12:09:26 +02:00
Bas Stottelaar f92f8928b0 Remove the validator, since it returns a dict on error and a list on results. Validator always expected a dict 2014-05-12 11:48:57 +02:00
Bas Stottelaar 45868bc523 Toggle VERBOSE at runtime. Just surf to /toggleVerbose. Comes in handy for the ones who don't want to change startup scripts 2014-05-12 11:45:28 +02:00
Bas Stottelaar 9b2594355f Let the --quiet option don't silence --verbose if we want verbose 2014-05-12 11:16:58 +02:00
Bas Stottelaar e30cb26200 Clarified meaning of HTTP error codes. Will show if it's local or remote error 2014-05-12 11:11:57 +02:00
Bas Stottelaar 63d3ab1cc1 Advanced option to turn off remote SSL certificate validation 2014-05-12 11:06:02 +02:00
Bas Stottelaar 188d907e10 GET, POST, HEAD etc are in lower case in requests.py 2014-05-12 11:02:25 +02:00
delphiactual e972a86233 Easier to follow logic for uTorrent 2014-05-11 21:26:58 -06:00
Ade 52a76e87ad Allow search provider names in preferred words
Adds to the priority if the word matches the search provider. The
provider priority is weighted based on position in list.
Preferred words in title take precedence over preferred providers.
Would be great if we could do something like sb, so until then…
2014-05-11 19:35:41 +12:00
Ade 8ab19f8121 remove rutracker track count check
should be able to filter out unwanted entries with Quality and Search
Word settings
2014-05-10 19:06:09 +12:00
Ade 6f3c8c68d4 Lossless min/max filter
filter results if calculated target size outside bitrate range
2014-05-10 18:53:58 +12:00
Ade 98385ff4c8 Transmission change for rutracker
use metainfo for local .torrent files (will only be rutracker)
2014-05-10 09:32:05 +12:00
delphiactual 62a6be26af PyNMA lib 2014-05-07 08:19:08 -06:00
delphiactual 5c1169ba9d Removed leftover DownloadStation [beta] changes 2014-05-06 21:38:28 -06:00
rembo10 0b47a5ca3a Merge remote-tracking branch 'delphiactual/develop' into develop 2014-05-06 19:51:20 -07:00
delphiactual 9da75a777b NMA fix 2014-05-06 20:22:13 -06:00
delphiactual 2fc2190b59 Merge pull request #2 from rembo10/develop
Develop
2014-05-06 19:48:09 -06:00
rembo10 e108935430 Fix for spinner not showing up anymore when marking albums as wanted 2014-05-06 17:01:30 -07:00
Jens Rogier bbde74db8d Fix: "Ignored" albums not ignored during scheduled updates. 2014-05-06 20:41:18 +02:00
rembo10 9fce4edf00 Fix for #1598, everything bring updated, even albums older than pause_delta 2014-05-06 11:21:37 -07:00
rembo10 ea3c6c3278 Changed VERBOSE back to 1 2014-05-06 11:06:31 -07:00
rembo10 6a0bd397cd Merge remote-tracking branch 'delphiactual/develop' into develop
Conflicts:
	headphones/utorrent.py
2014-05-06 11:05:54 -07:00
delphiactual 7bbb137fd9 uTorrent changes... 2014-05-06 02:12:17 -06:00
rembo10 e67d32445f Merge branch 'develop' 2014-05-05 23:49:47 -07:00
rembo10 83280f3f99 Fix for #1603, typo in utorrent 2014-05-05 23:43:32 -07:00
delphiactual 9c81fbaa2b Merge pull request #1 from rembo10/develop
Develop
2014-05-05 23:23:49 -06:00
rembo10 1d7415b91d Catch errors if we can't grab release info in post processor (#1602) 2014-05-05 18:56:47 -07:00
rembo10 bc5a042dcc Merge branch 'master' into develop 2014-05-05 17:46:25 -07:00
rembo10 e5af0bdd52 Merge branch 'develop' 2014-05-05 17:44:26 -07:00
rembo10 dc2939a4f9 Merge branch 'artist-page-refresh' into develop 2014-05-05 17:38:30 -07:00
rembo10 f52ec56a0f Couple fixes to get everything looking nice 2014-05-05 17:38:18 -07:00
rembo10 75b38df3d6 Typo 2014-05-05 16:58:31 -07:00
rembo10 0f5aeea345 some fixes 2014-05-05 16:58:31 -07:00
rembo10 d80fc36b29 Move to json so we can get the artist name as well 2014-05-05 16:58:31 -07:00
rembo10 4ca42c9c11 Initial changes to stop artist page from refreshing when artist is loading 2014-05-05 16:58:31 -07:00
rembo10 6c760902d6 Merge branch 'develop' 2014-05-05 16:49:58 -07:00
rembo10 2a0fcc7566 Fix for importer failing on adding new release groups 2014-05-05 16:49:43 -07:00
rembo10 b763ccdd08 Don't update all artist statuses when appending a single dir from the postprocessor. Also, don't change any other status but Skipped->Downloaded 2014-05-05 16:41:55 -07:00
rembo10 e72a0d1141 Merge pull request #1599 from delphiactual/master
Fix version check
2014-05-05 16:05:28 -07:00
rembo10 295e5a1ccf Merge branch 'develop': update Headphones Indexer url, fix for #1598, artist updates not working 2014-05-05 16:01:36 -07:00
rembo10 497fafb555 Don't need to query the db twice 2014-05-05 15:09:58 -07:00
rembo10 585f556006 Fix for #1598: Active artist update no longer updates any existing release groups 2014-05-05 14:26:44 -07:00
delphiactual 7b115126c9 Fix version check 2014-05-03 23:10:57 -06:00
rembo10 1a9165a084 Merge remote-tracking branch 'delphiactual/develop' into develop 2014-05-03 18:41:14 -07:00
rembo10 4a74d54cdc Merge remote-tracking branch 'tocsick/master' into develop 2014-05-03 18:40:11 -07:00
rembo10 a0755d4b1c Headphones indexer changes 2014-05-03 18:38:16 -07:00
delphiactual 084181a9a5 password field 2014-05-02 00:46:20 -06:00
delphiactual 5c381b9410 Added Labeling based off of hash for utorrent and fixed a typo in postprocessor.py 2014-05-01 21:58:05 -06:00
rembo10 0be9500518 Merge branch 'utorrent' into develop 2014-05-01 01:10:46 -07:00
rembo10 9c2da72dcc Bug fixes to get utorrent working 2014-05-01 01:09:26 -07:00
rembo10 3f6145cd07 Took out debug logging, put the torrent checking in the right place 2014-05-01 00:28:50 -07:00
rembo10 e155f19d0d Debug logging 2014-05-01 00:14:10 -07:00
rembo10 0c7ea8a5a0 typo 2014-05-01 00:09:45 -07:00
rembo10 1f3474a154 bug fix 2014-05-01 00:06:25 -07:00
rembo10 c0c1f9cc61 Some changes. Getting close 2014-05-01 00:02:29 -07:00
rembo10 e4382c23a3 fixin some bugs 2014-04-30 22:38:20 -07:00
rembo10 29b5305ab4 Slowly making progress 2014-04-30 21:58:46 -07:00
rembo10 758154e87e Some initial changes for utorrent 2014-04-30 21:00:30 -07:00
Kane Wallmann ea8b1815f6 Fixed tab indent to match file 2014-04-30 20:18:48 +10:00
Kane Wallmann ebc0b7205f Added MPC notifier, calls 'mpc update' on local machine 2014-04-30 19:53:59 +10:00
rembo10 06ee770184 Bug fix for postprocessor fix 2014-04-28 20:08:22 -07:00
rembo10 900325186b Url fix for pirate bay (credit to dsm1212) 2014-04-28 20:03:02 -07:00
rembo10 3b9816f3b3 Took out logging messages. Took out nzb possibilities from checkFolder since we're doing it in searcher.py anyways 2014-04-28 19:52:25 -07:00
rembo10 3e5779e5ce more log messages for testing #1575 issue 2014-04-28 19:22:28 -07:00
rembo10 66d0cb4a1b Added some more logging to see why postprocessor is not automatically running 2014-04-28 15:56:52 -07:00
rembo10 54c85c7951 Added some logging to see why postprocessor is not automatically running 2014-04-28 15:55:42 -07:00
rembo10 941da4be1c Merge remote-tracking branch 'sildur/master' into develop 2014-04-28 15:26:13 -07:00
rembo10 69c94baf18 Merge remote-tracking branch 'texke/master' into develop 2014-04-28 15:25:40 -07:00
Jens Rogier 90016f6eda Cleaned up query some more 2014-04-20 19:23:10 +02:00
Roberto Romero 06b647e30d Fix error: AttributeError: 'module' object has no attribute 'POST' 2014-04-19 01:41:23 -03:00
rembo10 84ebcb84f9 Changed 'newznab' -> 'custom newznab providers' to hopefully get rid of some confusion 2014-04-18 11:04:52 -07:00
Jens Rogier b11610face Fixed major mistake where tracklisting on the index would count the total tracks of the whole database when updating albums of the particular artist. 2014-04-18 10:40:15 +02:00
rembo10 6e90419014 Don't need to get commits behind if current version == latest version (versioncheck.py) 2014-04-17 20:44:28 -07:00
rembo10 0457c3c30e Merge branch 'develop': OSX & Boxcar notifications, bug fixes 2014-04-17 20:37:45 -07:00
rembo10 945cfbf21b Took out some leftover boxcar 1 stuff 2014-04-17 17:34:42 -07:00
rembo10 5769cb0dee Merge remote-tracking branch 'NovaXeros/patch-1' into develop
Conflicts:
	headphones/notifiers.py
2014-04-17 17:17:25 -07:00
rembo10 efd043e309 Merge remote-tracking branch 'texke/master' into develop 2014-04-17 17:15:35 -07:00
rembo10 fd19563160 Merge remote-tracking branch 'AdeHub/notify' into develop. Replaced boxcar notifications from daktak
Conflicts:
	headphones/__init__.py
	headphones/notifiers.py
	headphones/sab.py
	headphones/webserve.py
2014-04-17 17:14:37 -07:00
rembo10 1a2ed24566 Merge remote-tracking branch 'basilfx/bugfixes4' into develop 2014-04-17 16:57:19 -07:00
rembo10 d2fecc47ec Possible fix for #1560, what.cd choking when preferred bitrate is not one of the what.cd categories 2014-04-17 13:50:27 -07:00
Jens Rogier f9b0a5efb9 Fixed little bug where the trackcount on the index page was screwing up when choosing different release. 2014-04-17 19:41:40 +02:00
Jens Rogier e304e34ac7 Added ability to ignore albums.
Trackcount on homepage now is now excluding ignored albums.
Added "Manage ignored albums" to the Manage function.
2014-04-17 19:23:39 +02:00
Ade ff3826c0c8 More Boxcar2
Added MusicBrainz link to the message
2014-04-17 23:05:30 +12:00
NovaXeros c96b043a19 Update notifiers.py 2014-04-16 21:01:14 +01:00
NovaXeros a1d0f14eb8 Change LMS title in config options.
Squeezebox is an old unused name, and LMS isn't very self-explanitory.
2014-04-16 20:13:12 +01:00
NovaXeros 0234f4dab9 Update notifiers.py 2014-04-16 20:06:19 +01:00
NovaXeros e00411f7ad Cleaned LMS error reporting.
Even upon successful rescan, the code would throw a keyerror back. Not certain why, but LMS sends some really weird unicode JSON back as its result, unlike XBMC, so having response[0]['result'] will always throw back a keyerror.

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

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