Merge branch 'develop' into master
@@ -11,7 +11,6 @@ cache:
|
||||
- lib
|
||||
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
|
||||
install:
|
||||
|
||||
23
CHANGELOG.md
@@ -1,5 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## v0.5.18
|
||||
Released 01 December 2016
|
||||
|
||||
Highlights:
|
||||
* Added: PassTheHeadphones support
|
||||
* Fixed: Special characters in password fields breaking on config page
|
||||
* Improved: Updated t411 url
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.17...v0.5.18).
|
||||
|
||||
|
||||
## v0.5.17
|
||||
Released 10 November 2016
|
||||
|
||||
Highlights:
|
||||
* Added: t411 support
|
||||
* Fixed: Rutracker login
|
||||
* Fixed: Deluge empty password
|
||||
* Fixed: FreeBSD init script
|
||||
* Improved: Musicbrainz searching
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.16...v0.5.17).
|
||||
|
||||
## v0.5.16
|
||||
Released 10 June 2016
|
||||
|
||||
|
||||
@@ -54,7 +54,10 @@ def main():
|
||||
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
headphones.SYS_ENCODING = locale.getpreferredencoding()
|
||||
if headphones.SYS_PLATFORM == 'win32':
|
||||
headphones.SYS_ENCODING = sys.getdefaultencoding().upper()
|
||||
else:
|
||||
headphones.SYS_ENCODING = locale.getpreferredencoding()
|
||||
except (locale.Error, IOError):
|
||||
pass
|
||||
|
||||
@@ -81,6 +84,8 @@ def main():
|
||||
help='Prevent browser from launching on startup')
|
||||
parser.add_argument(
|
||||
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
|
||||
parser.add_argument(
|
||||
'--host', help='Specify a host (default - localhost)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -170,6 +175,13 @@ def main():
|
||||
else:
|
||||
http_port = int(headphones.CONFIG.HTTP_PORT)
|
||||
|
||||
# Force the http host if neccessary
|
||||
if args.host:
|
||||
http_host = args.host
|
||||
logger.info('Using forced web server host: %s', http_host)
|
||||
else:
|
||||
http_host = headphones.CONFIG.HTTP_HOST
|
||||
|
||||
# Check if pyOpenSSL is installed. It is required for certificate generation
|
||||
# and for CherryPy.
|
||||
if headphones.CONFIG.ENABLE_HTTPS:
|
||||
@@ -183,7 +195,7 @@ def main():
|
||||
# Try to start the server. Will exit here is address is already in use.
|
||||
web_config = {
|
||||
'http_port': http_port,
|
||||
'http_host': headphones.CONFIG.HTTP_HOST,
|
||||
'http_host': http_host,
|
||||
'http_root': headphones.CONFIG.HTTP_ROOT,
|
||||
'http_proxy': headphones.CONFIG.HTTP_PROXY,
|
||||
'enable_https': headphones.CONFIG.ENABLE_HTTPS,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## Headphones
|
||||
##  Headphones
|
||||
|
||||
[](https://travis-ci.org/rembo10/headphones)
|
||||
[](https://travis-ci.org/rembo10/headphones)
|
||||
|
||||
|
Before Width: | Height: | Size: 612 B After Width: | Height: | Size: 576 B |
|
Before Width: | Height: | Size: 807 B After Width: | Height: | Size: 772 B |
|
Before Width: | Height: | Size: 635 B After Width: | Height: | Size: 601 B |
|
Before Width: | Height: | Size: 852 B After Width: | Height: | Size: 817 B |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 212 B After Width: | Height: | Size: 79 B |
|
Before Width: | Height: | Size: 220 B After Width: | Height: | Size: 86 B |
|
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 86 B |
|
Before Width: | Height: | Size: 220 B After Width: | Height: | Size: 86 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 125 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 121 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 121 B |
|
Before Width: | Height: | Size: 253 B After Width: | Height: | Size: 114 B |
|
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 120 B |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -56,7 +56,7 @@
|
||||
<label title="Password for web server authentication. Leave empty to disable.">
|
||||
HTTP Password
|
||||
</label>
|
||||
<input type="password" name="http_password" value="${config['http_password']}" size="30">
|
||||
<input type="password" name="http_password" value="${config['http_password'] | h}" size="30">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="launch_browser" value="1" ${config['launch_browser']} />
|
||||
@@ -179,7 +179,7 @@
|
||||
<label title="SABnzbd password. Leave empty if not applicable.">
|
||||
SABnzbd Password
|
||||
</label>
|
||||
<input type="password" name="sab_password" value="${config['sab_password']}" size="20">
|
||||
<input type="password" name="sab_password" value="${config['sab_password'] | h}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label title="SABnzbd API key. Can be found in SABnzbd settings.">
|
||||
@@ -213,7 +213,7 @@
|
||||
<label title="NZBGet password. Leave empty if not applicable">
|
||||
NZBget Password
|
||||
</label>
|
||||
<input type="password" name="nzbget_password" value="${config['nzbget_password']}" size="20">
|
||||
<input type="password" name="nzbget_password" value="${config['nzbget_password'] | h}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label title="Name of NZBget category to add downloads to.">
|
||||
@@ -318,6 +318,7 @@
|
||||
<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)
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="3" ${config['torrent_downloader_deluge']}> Deluge (Beta)
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_qbittorrent" value="4" ${config['torrent_downloader_qbittorrent']}> QBitTorrent
|
||||
</fieldset>
|
||||
<fieldset id="torrent_blackhole_options">
|
||||
<div class="row">
|
||||
@@ -365,7 +366,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Transmission Password</label>
|
||||
<input type="password" name="transmission_password" value="${config['transmission_password']}" size="30">
|
||||
<input type="password" name="transmission_password" value="${config['transmission_password'] | h}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<small>Note: With Transmission, you can specify a different download directory for downloads sent from Headphones.
|
||||
@@ -385,13 +386,33 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>uTorrent Password</label>
|
||||
<input type="password" name="utorrent_password" value="${config['utorrent_password']}" size="30">
|
||||
<input type="password" name="utorrent_password" value="${config['utorrent_password'] | h}" 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="qbittorrent_options">
|
||||
<small class="heading"><i class="fa fa-info-circle"></i> Note: Works with WebAPI Rev 11 and later (QBitTorrent 3.4.0 and later) </small>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Host</label>
|
||||
<input type="text" name="qbittorrent_host" value="${config['qbittorrent_host']}" size="30">
|
||||
<small>usually http://localhost:8081</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Username</label>
|
||||
<input type="text" name="qbittorrent_username" value="${config['qbittorrent_username']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Password</label>
|
||||
<input type="password" name="qbittorrent_password" value="${config['qbittorrent_password']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Label</label>
|
||||
<input type="text" name="qbittorrent_label" value="${config['qbittorrent_label']}" size="30">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="deluge_options">
|
||||
<div class="row">
|
||||
<label>Deluge WebUI Host and Port</label>
|
||||
@@ -406,7 +427,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Deluge Password</label>
|
||||
<input type="password" name="deluge_password" value="${config['deluge_password']}" size="30">
|
||||
<input type="password" name="deluge_password" value="${config['deluge_password'] | h}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<small>Note: With Deluge, you can specify a different download directory for downloads sent from Headphones.
|
||||
@@ -472,7 +493,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label>
|
||||
<input class="hppass" type="password" value="${config['hppass']}" size="20">
|
||||
<input class="hppass" type="password" value="${config['hppass'] | h}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Don't have an account? Sign up!</a>
|
||||
@@ -628,31 +649,59 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label>
|
||||
<input type="password" name="rutracker_password" value="${config['rutracker_password']}" size="36">
|
||||
<input type="password" name="rutracker_password" value="${config['rutracker_password'] | h}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Seed Ratio</label>
|
||||
<input type="text" class="override-float" name="rutracker_ratio" value="${config['rutracker_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Session Cookie (Optional - Advanced)</label>
|
||||
<input type="text" class="override-float" name="rutracker_cookie" value="${config['rutracker_cookie']}" size="10" title="bb_session cookie (Advanced)">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input id="use_whatcd" type="checkbox" class="bigcheck" name="use_whatcd" value="1" ${config['use_whatcd']} /><label for="use_whatcd"><span class="option">What.cd</span></label>
|
||||
<input id="use_apollo" type="checkbox" class="bigcheck" name="use_apollo" value="1" ${config['use_apollo']} /><label for="use_apollo"><span class="option">Apollo.rip</span></label>
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="row">
|
||||
<label>Username</label>
|
||||
<input type="text" name="whatcd_username" value="${config['whatcd_username']}" size="36">
|
||||
<input type="text" name="apollo_username" value="${config['apollo_username']}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label>
|
||||
<input type="password" name="whatcd_password" value="${config['whatcd_password']}" size="36">
|
||||
<input type="password" name="apollo_password" value="${config['apollo_password'] | h}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>URL</label>
|
||||
<input type="text" name="apollo_url" value="${config['apollo_url']}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Seed Ratio</label>
|
||||
<input type="text" class="override-float" name="whatcd_ratio" value="${config['whatcd_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
|
||||
<input type="text" class="override-float" name="apollo_ratio" value="${config['apollo_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input id="use_redacted" type="checkbox" class="bigcheck" name="use_redacted" value="1" ${config['use_redacted']} /><label for="use_redacted"><span class="option">Redacted</span></label>
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="row">
|
||||
<label>Username</label>
|
||||
<input type="text" name="redacted_username" value="${config['redacted_username']}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label>
|
||||
<input type="password" name="redacted_password" value="${config['redacted_password'] | h}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Seed Ratio</label>
|
||||
<input type="text" class="override-float" name="redacted_ratio" value="${config['redacted_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -677,8 +726,8 @@
|
||||
<div class="config" id="torznab1">
|
||||
<div class="row">
|
||||
<label>Torznab Host</label>
|
||||
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="30">
|
||||
<small>e.g. http://localhost:9117/torznab/iptorrents</small>
|
||||
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="36">
|
||||
<small>e.g. http://localhost:9117/api/v2.0/indexers/demonoid/results/torznab/</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab API</label>
|
||||
@@ -701,7 +750,7 @@
|
||||
<div class="config" id="torznab${torznab_number}">
|
||||
<div class="row">
|
||||
<label>Torznab Host</label>
|
||||
<input type="text" name="torznab_host${torznab_number}" value="${torznab[0]}" size="30">
|
||||
<input type="text" name="torznab_host${torznab_number}" value="${torznab[0]}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab API</label>
|
||||
@@ -749,6 +798,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input id="use_tquattrecentonze" type="checkbox" class="bigcheck" name="use_tquattrecentonze" value="1" ${config['use_tquattrecentonze']} /><label for="use_tquattrecentonze"><span class="option">t411</span></label>
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="row">
|
||||
<label>Username</label>
|
||||
<input type="text" name="tquattrecentonze_user" value="${config['tquattrecentonze_user']}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label>
|
||||
<input type="password" name="tquattrecentonze_password" value="${config['tquattrecentonze_password'] | h}" size="36">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</fieldset>
|
||||
</td>
|
||||
@@ -890,30 +955,42 @@
|
||||
<input type="checkbox" name="keep_nfo" value="1" ${config['keep_nfo']} />
|
||||
</label>
|
||||
</div>
|
||||
<div class="row checkbox left clearfix nopad">
|
||||
<label>
|
||||
Embed lyrics
|
||||
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row checkbox left clearfix nopad">
|
||||
<label>
|
||||
Embed album art in each file
|
||||
<input type="checkbox" name="embed_album_art" id="embed_album_art" value="1" ${config['embed_album_art']}>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row checkbox left clearfix nopad">
|
||||
<label>
|
||||
Add album art jpeg to album folder
|
||||
<input type="checkbox" name="add_album_art" id="add_album_art" value="1" ${config['add_album_art']}>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="album_art_options" style="padding-left: 20px">
|
||||
<div class="row">
|
||||
as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg
|
||||
</div>
|
||||
<small>Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').</small>
|
||||
</div>
|
||||
<div class="row checkbox left clearfix nopad">
|
||||
<label>
|
||||
Embed album art in each file
|
||||
<input type="checkbox" name="embed_album_art" value="1" ${config['embed_album_art']}>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row checkbox left clearfix nopad">
|
||||
<label>
|
||||
Embed lyrics
|
||||
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}>
|
||||
</label>
|
||||
|
||||
<div id="album_art_size_options" style="padding-left: 20px">
|
||||
<div class="row">
|
||||
Album art min width <input type="text" class="override-float" name="album_art_min_width" value="${config['album_art_min_width']}" size="4" title="Only images with a pixel width greater or equal are considered as valid album art candidates. Default: 0.">\
|
||||
Album art max width <input type="text" class="override-float" name="album_art_max_width" value="${config['album_art_max_width']}" size="4" title="A maximum image width to downscale fetched images if they are too big.">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>
|
||||
Destination Directory:
|
||||
@@ -970,7 +1047,7 @@
|
||||
<label>SMTP User</label><input type="text" name="email_smtp_user" value="${config['email_smtp_user']}" size="254">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>SMTP Password</label><input type="password" name="email_smtp_password" value="${config['email_smtp_password']}" size="50">
|
||||
<label>SMTP Password</label><input type="password" name="email_smtp_password" value="${config['email_smtp_password'] | h}" size="50">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="text" class="override-float" name="email_smtp_port" value="${config['email_smtp_port']}" size="4"><label>SMTP Port</label>
|
||||
@@ -996,7 +1073,7 @@
|
||||
<label>Growl Host:Port</label><input type="text" name="growl_host" value="${config['growl_host']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Growl Password</label><input type="password" name="growl_password" value="${config['growl_password']}" size="30">
|
||||
<label>Growl Password</label><input type="password" name="growl_password" value="${config['growl_password'] | h}" size="30">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="growl_onsnatch" value="1" ${config['growl_onsnatch']} /><label>Notify on snatch?</label>
|
||||
@@ -1018,7 +1095,7 @@
|
||||
<label>Username</label><input type="text" name="xbmc_username" value="${config['xbmc_username']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label><input type="password" name="xbmc_password" value="${config['xbmc_password']}" size="30">
|
||||
<label>Password</label><input type="password" name="xbmc_password" value="${config['xbmc_password'] | h}" size="30">
|
||||
</div>
|
||||
<div class="checkbox row">
|
||||
<input type="checkbox" name="xbmc_update" value="1" ${config['xbmc_update']} /><label>Update Library</label>
|
||||
@@ -1132,7 +1209,7 @@
|
||||
<small>Username of your Plex client API (blank for none)</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Plex Password</label><input type="password" name="plex_password" value="${config['plex_password']}" size="30">
|
||||
<label>Plex Password</label><input type="password" name="plex_password" value="${config['plex_password'] | h}" size="30">
|
||||
<small>Password of your Plex client API (blank for none)</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -1232,7 +1309,7 @@
|
||||
<label>Subsonic Username</label><input type="text" name="subsonic_username" value="${config['subsonic_username']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Subsonic Password</label><input type="password" name="subsonic_password" value="${config['subsonic_password']}" size="30">
|
||||
<label>Subsonic Password</label><input type="password" name="subsonic_password" value="${config['subsonic_password'] | h}" size="30">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -1264,6 +1341,26 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input type="checkbox" class="bigcheck" name="slack_enabled" id="slack" value="1" ${config['slack_enabled']} /><label for="slack"><span class="option">Slack</span></label>
|
||||
</div>
|
||||
<div id="slackoptions">
|
||||
<div class="row">
|
||||
<label>Slack Webhook</label><input type="text" name="slack_url" value="${config['slack_url']}" size="50">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Channel</label><input type="text" name="slack_channel" value="${config['slack_channel']}" size="50">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<label>Emoji</label><input type="text" name="slack_emoji" value="${config['slack_emoji']}" size="50">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="slack_onsnatch" value="1" ${config['slack_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input type="checkbox" class="bigcheck" name="telegram_enabled" id="telegram" value="1" ${config['telegram_enabled']} /><label for="telegram"><span class="option">Telegram</span></label>
|
||||
@@ -1281,6 +1378,23 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input type="checkbox" class="bigcheck" name="join_enabled" id="join" value="1" ${config['join_enabled']} /><label for="join"><span class="option">Join</span></label>
|
||||
</div>
|
||||
<div id="joinoptions">
|
||||
<div class="row">
|
||||
<label>Join API Key</label><input type="text" name="join_apikey" value="${config['join_apikey']}" size="50">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Device ID(s)</label><input type="text" name="join_deviceid" value="${config['join_deviceid']}" size="50"><small>Comma separated list. Leave blank to send to all devices</small>
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="join_onsnatch" value="1" ${config['join_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -1576,6 +1690,10 @@
|
||||
<label>Cache Directory</label>
|
||||
<input type="text" name="cache_dir" value="${config['cache_dir']}" size="50">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Post Processing Temporary Directory</label>
|
||||
<input type="text" name="keep_torrent_files_dir" value="${config['keep_torrent_files_dir']}" size="50" title="Enter the directory to preserve files for seeding, default is system temp directory">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
@@ -1632,7 +1750,7 @@
|
||||
<label>Username</label><input type="text" class="customuser" name="customuser" value="${config['customuser']}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label><input type="password" class="custompass" name="custompass" value="${config['custompass']}" size="15"><br>
|
||||
<label>Password</label><input type="password" class="custompass" name="custompass" value="${config['custompass'] | h}" size="15"><br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -1645,7 +1763,7 @@
|
||||
<label>Username</label><input type="text" class="hpuser" name="hpuser" value="${config['hpuser']}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label><input type="password" class="hppass" name="hppass" value="${config['hppass']}" size="20"><br>
|
||||
<label>Password</label><input type="password" class="hppass" name="hppass" value="${config['hppass'] | h}" size="20"><br>
|
||||
<a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Get an Account!</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1820,9 +1938,36 @@
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#embed_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_size_options").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!$("#add_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_size_options").hide();
|
||||
}
|
||||
}
|
||||
|
||||
$("#embed_album_art").click(function(){
|
||||
if ($("#embed_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_size_options").slideDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!$("#add_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_size_options").slideUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#add_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_options").show();
|
||||
$("#album_art_size_options").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1833,10 +1978,15 @@
|
||||
if ($("#add_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_options").slideDown();
|
||||
$("#album_art_size_options").slideDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#album_art_options").slideUp();
|
||||
if (!$("#embed_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_size_options").slideUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2041,6 +2191,27 @@
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#join").is(":checked"))
|
||||
{
|
||||
$("#joinoptions").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#joinoptions").hide();
|
||||
}
|
||||
|
||||
|
||||
$("#join").click(function(){
|
||||
if ($("#join").is(":checked"))
|
||||
{
|
||||
$("#joinoptions").slideDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#joinoptions").slideUp();
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#twitter").is(":checked"))
|
||||
{
|
||||
$("#twitteroptions").show();
|
||||
@@ -2061,6 +2232,27 @@
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#slack").is(":checked"))
|
||||
{
|
||||
$("#slackoptions").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#slackoptions").hide();
|
||||
}
|
||||
|
||||
$("#slack").click(function(){
|
||||
if ($("#slack").is(":checked"))
|
||||
{
|
||||
$("#slackoptions").slideDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#slackoptions").slideUp();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if ($("#telegram").is(":checked"))
|
||||
{
|
||||
$("#telegramoptions").show();
|
||||
@@ -2237,26 +2429,30 @@
|
||||
|
||||
if ($("#torrent_downloader_blackhole").is(":checked"))
|
||||
{
|
||||
$("#transmission_options,#utorrent_options,#deluge_options").hide();
|
||||
$("#transmission_options,#utorrent_options,#deluge_options,#qbittorrent_options").hide();
|
||||
$("#torrent_blackhole_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_transmission").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").hide();
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options,#qbittorrent_options").hide();
|
||||
$("#transmission_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_utorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options").hide();
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options,#qbittorrent_options").hide();
|
||||
$("#utorrent_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_qbittorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#deluge_options").hide();
|
||||
$("#qbittorrent_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_deluge").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options").hide();
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#qbittorrent_options").hide();
|
||||
$("#deluge_options").show();
|
||||
}
|
||||
|
||||
|
||||
$('input[type=radio]').change(function(){
|
||||
if ($("#preferred_bitrate").is(":checked"))
|
||||
{
|
||||
@@ -2292,19 +2488,23 @@
|
||||
}
|
||||
if ($("#torrent_downloader_blackhole").is(":checked"))
|
||||
{
|
||||
$("#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
|
||||
$("#transmission_options,#utorrent_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_transmission").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_utorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_qbittorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#qbittorrent_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_deluge").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#utorrent_options,#transmission_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
|
||||
$("#torrent_blackhole_options,#utorrent_options,#transmission_options,#qbittorrent_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2401,13 +2601,14 @@
|
||||
initConfigCheckbox("#use_mininova");
|
||||
initConfigCheckbox("#use_waffles");
|
||||
initConfigCheckbox("#use_rutracker");
|
||||
initConfigCheckbox("#use_whatcd");
|
||||
initConfigCheckbox("#use_apollo");
|
||||
initConfigCheckbox("#use_redacted");
|
||||
initConfigCheckbox("#use_strike");
|
||||
initConfigCheckbox("#api_enabled");
|
||||
initConfigCheckbox("#enable_https");
|
||||
initConfigCheckbox("#customauth");
|
||||
initConfigCheckbox("#mb_ignore_age_missing");
|
||||
|
||||
initConfigCheckbox("#use_tquattrecentonze");
|
||||
|
||||
$('#twitterStep1').click(function () {
|
||||
$.get("/twitterStep1", function (data) {window.open(data); })
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 860 B |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 95 B After Width: | Height: | Size: 89 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 93 B |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 299 B |
|
Before Width: | Height: | Size: 757 B After Width: | Height: | Size: 749 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 860 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 176 B |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 251 B |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 264 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 99 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 485 B |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 345 B After Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 386 B |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 688 B |
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 688 B |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 87 B |
|
Before Width: | Height: | Size: 135 B After Width: | Height: | Size: 101 B |
|
Before Width: | Height: | Size: 975 B After Width: | Height: | Size: 83 B |
|
Before Width: | Height: | Size: 838 B After Width: | Height: | Size: 56 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 116 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1003 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1010 B |
|
Before Width: | Height: | Size: 107 B After Width: | Height: | Size: 106 B |
|
Before Width: | Height: | Size: 106 B After Width: | Height: | Size: 98 B |
|
Before Width: | Height: | Size: 111 B After Width: | Height: | Size: 107 B |
|
Before Width: | Height: | Size: 352 B After Width: | Height: | Size: 326 B |
|
Before Width: | Height: | Size: 103 B After Width: | Height: | Size: 98 B |
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 203 B After Width: | Height: | Size: 158 B |
|
Before Width: | Height: | Size: 176 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
@@ -312,6 +312,7 @@ function doAjaxCall(url,elem,reload,form) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
data: dataString,
|
||||
type: 'POST',
|
||||
beforeSend: function(jqXHR, settings) {
|
||||
// Start loader etc.
|
||||
feedback.prepend(loader);
|
||||
|
||||
@@ -86,8 +86,8 @@ CURRENT_VERSION = None
|
||||
LATEST_VERSION = None
|
||||
COMMITS_BEHIND = None
|
||||
|
||||
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma"]
|
||||
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff"]
|
||||
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma", "opus"]
|
||||
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff", "aif"]
|
||||
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
|
||||
|
||||
MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
|
||||
@@ -306,9 +306,10 @@ def initialize_scheduler():
|
||||
minutes=minutes)
|
||||
|
||||
# Remove Torrent + data if Post Processed and finished Seeding
|
||||
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
||||
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
||||
minutes=minutes)
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER != 0:
|
||||
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
||||
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
||||
minutes=minutes)
|
||||
|
||||
# Start scheduler
|
||||
if start_jobs and len(SCHED.get_jobs()):
|
||||
@@ -376,7 +377,7 @@ def dbcheck():
|
||||
c.execute(
|
||||
'CREATE TABLE IF NOT EXISTS alltracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)')
|
||||
c.execute(
|
||||
'CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT, Kind TEXT)')
|
||||
'CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT, Kind TEXT, TorrentHash TEXT)')
|
||||
# Matched is a temporary value used to see if there was a match found in
|
||||
# alltracks
|
||||
c.execute(
|
||||
@@ -613,6 +614,20 @@ def dbcheck():
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE artists ADD COLUMN MetaCritic TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT TorrentHash from snatched')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE snatched ADD COLUMN TorrentHash TEXT')
|
||||
c.execute('UPDATE snatched SET TorrentHash = FolderName WHERE Status LIKE "Seed_%"')
|
||||
|
||||
# One off script to set CleanName to lower case
|
||||
clean_name_mixed = c.execute('SELECT CleanName FROM have ORDER BY Date Desc').fetchone()
|
||||
if clean_name_mixed and clean_name_mixed[0] != clean_name_mixed[0].lower():
|
||||
logger.info("Updating track clean name, this could take some time...")
|
||||
c.execute('UPDATE tracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||
c.execute('UPDATE alltracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||
c.execute('UPDATE have SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||
|
||||
conn.commit()
|
||||
c.close()
|
||||
|
||||
|
||||
@@ -13,16 +13,164 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from headphones import request, db, logger
|
||||
import struct
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from io import BytesIO
|
||||
|
||||
import headphones
|
||||
from headphones import db, request, logger
|
||||
|
||||
|
||||
def getAlbumArt(albumid):
|
||||
myDB = db.DBConnection()
|
||||
asin = myDB.action(
|
||||
'SELECT AlbumASIN from albums WHERE AlbumID=?', [albumid]).fetchone()[0]
|
||||
|
||||
if asin:
|
||||
return 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % asin
|
||||
artwork_path = None
|
||||
artwork = None
|
||||
|
||||
# CAA
|
||||
logger.info("Searching for artwork at CAA")
|
||||
artwork_path = 'http://coverartarchive.org/release-group/%s/front' % albumid
|
||||
artwork = getartwork(artwork_path)
|
||||
if artwork:
|
||||
logger.info("Artwork found at CAA")
|
||||
return artwork_path, artwork
|
||||
|
||||
# Amazon
|
||||
logger.info("Searching for artwork at Amazon")
|
||||
myDB = db.DBConnection()
|
||||
dbalbum = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, ReleaseID, AlbumASIN FROM albums WHERE AlbumID=?',
|
||||
[albumid]).fetchone()
|
||||
if dbalbum['AlbumASIN']:
|
||||
artwork_path = 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % dbalbum['AlbumASIN']
|
||||
artwork = getartwork(artwork_path)
|
||||
if artwork:
|
||||
logger.info("Artwork found at Amazon")
|
||||
return artwork_path, artwork
|
||||
|
||||
# last.fm
|
||||
from headphones import lastfm
|
||||
logger.info("Searching for artwork at last.fm")
|
||||
if dbalbum['ReleaseID'] != albumid:
|
||||
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'])
|
||||
if not data:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'])
|
||||
else:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'])
|
||||
|
||||
if data:
|
||||
try:
|
||||
images = data['album']['image']
|
||||
for image in images:
|
||||
if image['size'] == 'extralarge':
|
||||
artwork_path = image['#text']
|
||||
elif image['size'] == 'mega':
|
||||
artwork_path = image['#text']
|
||||
break
|
||||
except KeyError:
|
||||
artwork_path = None
|
||||
|
||||
if artwork_path:
|
||||
artwork = getartwork(artwork_path)
|
||||
if artwork:
|
||||
logger.info("Artwork found at last.fm")
|
||||
return artwork_path, artwork
|
||||
|
||||
logger.info("No suitable album art found.")
|
||||
return None, None
|
||||
|
||||
|
||||
def jpeg(bites):
|
||||
fhandle = BytesIO(bites)
|
||||
try:
|
||||
fhandle.seek(0)
|
||||
size = 2
|
||||
ftype = 0
|
||||
while not 0xc0 <= ftype <= 0xcf:
|
||||
fhandle.seek(size, 1)
|
||||
byte = fhandle.read(1)
|
||||
while ord(byte) == 0xff:
|
||||
byte = fhandle.read(1)
|
||||
ftype = ord(byte)
|
||||
size = struct.unpack('>H', fhandle.read(2))[0] - 2
|
||||
fhandle.seek(1, 1)
|
||||
height, width = struct.unpack('>HH', fhandle.read(4))
|
||||
return width, height
|
||||
except struct.error:
|
||||
return None, None
|
||||
except TypeError:
|
||||
return None, None
|
||||
|
||||
|
||||
def png(bites):
|
||||
try:
|
||||
check = struct.unpack('>i', bites[4:8])[0]
|
||||
if check != 0x0d0a1a0a:
|
||||
return None, None
|
||||
return struct.unpack('>ii', bites[16:24])
|
||||
except struct.error:
|
||||
return None, None
|
||||
|
||||
|
||||
def get_image_data(bites):
|
||||
type = None
|
||||
width = None
|
||||
height = None
|
||||
if len(bites) < 24:
|
||||
return None, None, None
|
||||
|
||||
peek = bites[0:2]
|
||||
if peek == b'\xff\xd8':
|
||||
width, height = jpeg(bites)
|
||||
type = 'jpg'
|
||||
elif peek == b'\x89P':
|
||||
width, height = png(bites)
|
||||
type = 'png'
|
||||
return type, width, height
|
||||
|
||||
|
||||
def getartwork(artwork_path):
|
||||
artwork = bytes()
|
||||
minwidth = 0
|
||||
maxwidth = 0
|
||||
if headphones.CONFIG.ALBUM_ART_MIN_WIDTH:
|
||||
minwidth = int(headphones.CONFIG.ALBUM_ART_MIN_WIDTH)
|
||||
if headphones.CONFIG.ALBUM_ART_MAX_WIDTH:
|
||||
maxwidth = int(headphones.CONFIG.ALBUM_ART_MAX_WIDTH)
|
||||
|
||||
resp = request.request_response(artwork_path, timeout=20, stream=True, whitelist_status_code=404)
|
||||
|
||||
if resp:
|
||||
img_width = None
|
||||
for chunk in resp.iter_content(chunk_size=1024):
|
||||
artwork += chunk
|
||||
if not img_width and (minwidth or maxwidth):
|
||||
img_type, img_width, img_height = get_image_data(artwork)
|
||||
# Check min/max
|
||||
if img_width and (minwidth or maxwidth):
|
||||
if minwidth and img_width < minwidth:
|
||||
logger.info("Artwork is too small. Type: %s. Width: %s. Height: %s",
|
||||
img_type, img_width, img_height)
|
||||
artwork = None
|
||||
break
|
||||
elif maxwidth and img_width > maxwidth:
|
||||
# Downsize using proxy service to max width
|
||||
artwork_path = '{0}?{1}'.format('http://images.weserv.nl/', urlencode({
|
||||
'url': artwork_path.replace('http://', ''),
|
||||
'w': maxwidth,
|
||||
}))
|
||||
artwork = bytes()
|
||||
r = request.request_response(artwork_path, timeout=20, stream=True, whitelist_status_code=404)
|
||||
if r:
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
artwork += chunk
|
||||
r.close()
|
||||
logger.info("Artwork is greater than the maximum width, downsized using proxy service")
|
||||
break
|
||||
resp.close()
|
||||
|
||||
return artwork
|
||||
|
||||
|
||||
def getCachedArt(albumid):
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import os
|
||||
|
||||
import headphones
|
||||
from headphones import db, helpers, logger, lastfm, request
|
||||
from headphones import db, helpers, logger, lastfm, request, mb
|
||||
|
||||
LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
|
||||
|
||||
@@ -290,6 +290,14 @@ class Cache(object):
|
||||
|
||||
data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=LASTFM_API_KEY)
|
||||
|
||||
# Try with name if not found
|
||||
if not data:
|
||||
dbartist = myDB.action('SELECT ArtistName, Type FROM artists WHERE ArtistID=?', [self.id]).fetchone()
|
||||
if dbartist:
|
||||
data = lastfm.request_lastfm("artist.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(dbartist['ArtistName']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
@@ -315,18 +323,31 @@ class Cache(object):
|
||||
|
||||
else:
|
||||
dbalbum = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?',
|
||||
'SELECT ArtistName, AlbumTitle, ReleaseID, Type FROM albums WHERE AlbumID=?',
|
||||
[self.id]).fetchone()
|
||||
if dbalbum['ReleaseID'] != self.id:
|
||||
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'],
|
||||
api_key=LASTFM_API_KEY)
|
||||
if not data:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'],
|
||||
data = lastfm.request_lastfm("album.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
|
||||
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
else:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
|
||||
if dbalbum['Type'] != "part of":
|
||||
data = lastfm.request_lastfm("album.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
|
||||
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
else:
|
||||
|
||||
# Series, use actual artist for the release-group
|
||||
artist = mb.getArtistForReleaseGroup(self.id)
|
||||
if artist:
|
||||
data = lastfm.request_lastfm("album.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(artist),
|
||||
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
@@ -34,11 +34,18 @@ _CONFIG_DEFINITIONS = {
|
||||
'ADD_ALBUM_ART': (int, 'General', 0),
|
||||
'ADVANCEDENCODER': (str, 'General', ''),
|
||||
'ALBUM_ART_FORMAT': (str, 'General', 'folder'),
|
||||
'ALBUM_ART_MIN_WIDTH': (str, 'General', ''),
|
||||
'ALBUM_ART_MAX_WIDTH': (str, 'General', ''),
|
||||
# This is used in importer.py to determine how complete an album needs to
|
||||
# be - to be considered "downloaded". Percentage from 0-100
|
||||
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
|
||||
'API_ENABLED': (int, 'General', 0),
|
||||
'API_KEY': (str, 'General', ''),
|
||||
'APOLLO': (int, 'Apollo.rip', 0),
|
||||
'APOLLO_PASSWORD': (str, 'Apollo.rip', ''),
|
||||
'APOLLO_RATIO': (str, 'Apollo.rip', ''),
|
||||
'APOLLO_USERNAME': (str, 'Apollo.rip', ''),
|
||||
'APOLLO_URL': (str, 'Apollo.rip', 'https://apollo.rip'),
|
||||
'AUTOWANT_ALL': (int, 'General', 0),
|
||||
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
|
||||
'AUTOWANT_UPCOMING': (int, 'General', 1),
|
||||
@@ -138,12 +145,17 @@ _CONFIG_DEFINITIONS = {
|
||||
'IGNORED_FILES': (list, 'Advanced', []), # path
|
||||
'INCLUDE_EXTRAS': (int, 'General', 0),
|
||||
'INTERFACE': (str, 'General', 'default'),
|
||||
'JOIN_APIKEY': (str, 'Join', ''),
|
||||
'JOIN_DEVICEID': (str, 'Join', ''),
|
||||
'JOIN_ENABLED': (int, 'Join', 0),
|
||||
'JOIN_ONSNATCH': (int, 'Join', 0),
|
||||
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
||||
'KAT': (int, 'Kat', 0),
|
||||
'KAT_PROXY_URL': (str, 'Kat', ''),
|
||||
'KAT_RATIO': (str, 'Kat', ''),
|
||||
'KEEP_NFO': (int, 'General', 0),
|
||||
'KEEP_TORRENT_FILES': (int, 'General', 0),
|
||||
'KEEP_TORRENT_FILES_DIR': (path, 'General', ''),
|
||||
'LASTFM_USERNAME': (str, 'General', ''),
|
||||
'LAUNCH_BROWSER': (int, 'General', 1),
|
||||
'LIBRARYSCAN': (int, 'General', 1),
|
||||
@@ -227,6 +239,10 @@ _CONFIG_DEFINITIONS = {
|
||||
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
||||
'PUSHOVER_ONSNATCH': (int, 'Pushover', 0),
|
||||
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
||||
'QBITTORRENT_HOST': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_LABEL': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_PASSWORD': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_USERNAME': (str, 'QBitTorrent', ''),
|
||||
'RENAME_FILES': (int, 'General', 0),
|
||||
'RENAME_UNPROCESSED': (bool_int, 'General', 1),
|
||||
'RENAME_FROZEN': (bool_int, 'General', 1),
|
||||
@@ -237,6 +253,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
|
||||
'RUTRACKER_RATIO': (str, 'Rutracker', ''),
|
||||
'RUTRACKER_USER': (str, 'Rutracker', ''),
|
||||
'RUTRACKER_COOKIE': (str, 'Rutracker', ''),
|
||||
'SAB_APIKEY': (str, 'SABnzbd', ''),
|
||||
'SAB_CATEGORY': (str, 'SABnzbd', ''),
|
||||
'SAB_HOST': (str, 'SABnzbd', ''),
|
||||
@@ -244,6 +261,11 @@ _CONFIG_DEFINITIONS = {
|
||||
'SAB_USERNAME': (str, 'SABnzbd', ''),
|
||||
'SAMPLINGFREQUENCY': (int, 'General', 44100),
|
||||
'SEARCH_INTERVAL': (int, 'General', 1440),
|
||||
'SLACK_ENABLED': (int, 'Slack', 0),
|
||||
'SLACK_URL': (str, 'Slack', ''),
|
||||
'SLACK_CHANNEL': (str, 'Slack', ''),
|
||||
'SLACK_EMOJI': (str, 'Slack', ''),
|
||||
'SLACK_ONSNATCH': (int, 'Slack', 0),
|
||||
'SOFT_CHROOT': (path, 'General', ''),
|
||||
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
|
||||
'SONGKICK_ENABLED': (int, 'Songkick', 1),
|
||||
@@ -275,6 +297,9 @@ _CONFIG_DEFINITIONS = {
|
||||
'TWITTER_PASSWORD': (str, 'Twitter', ''),
|
||||
'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'),
|
||||
'TWITTER_USERNAME': (str, 'Twitter', ''),
|
||||
'TQUATTRECENTONZE': (int, 'tquattrecentonze', 0),
|
||||
'TQUATTRECENTONZE_PASSWORD': (str, 'tquattrecentonze', ''),
|
||||
'TQUATTRECENTONZE_USER': (str, 'tquattrecentonze', ''),
|
||||
'UPDATE_DB_INTERVAL': (int, 'General', 24),
|
||||
'USENET_RETENTION': (int, 'General', '1500'),
|
||||
'UTORRENT_HOST': (str, 'uTorrent', ''),
|
||||
@@ -287,10 +312,10 @@ _CONFIG_DEFINITIONS = {
|
||||
'WAFFLES_PASSKEY': (str, 'Waffles', ''),
|
||||
'WAFFLES_RATIO': (str, 'Waffles', ''),
|
||||
'WAFFLES_UID': (str, 'Waffles', ''),
|
||||
'WHATCD': (int, 'What.cd', 0),
|
||||
'WHATCD_PASSWORD': (str, 'What.cd', ''),
|
||||
'WHATCD_RATIO': (str, 'What.cd', ''),
|
||||
'WHATCD_USERNAME': (str, 'What.cd', ''),
|
||||
'REDACTED': (int, 'Redacted', 0),
|
||||
'REDACTED_USERNAME': (str, 'Redacted', ''),
|
||||
'REDACTED_PASSWORD': (str, 'Redacted', ''),
|
||||
'REDACTED_RATIO': (str, 'Redacted', ''),
|
||||
'XBMC_ENABLED': (int, 'XBMC', 0),
|
||||
'XBMC_HOST': (str, 'XBMC', ''),
|
||||
'XBMC_NOTIFY': (int, 'XBMC', 0),
|
||||
|
||||
36
headphones/crier.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import pprint
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from headphones import logger
|
||||
|
||||
|
||||
def cry():
|
||||
"""
|
||||
Logs thread traces.
|
||||
"""
|
||||
tmap = {}
|
||||
main_thread = None
|
||||
# get a map of threads by their ID so we can print their names
|
||||
# during the traceback dump
|
||||
for t in threading.enumerate():
|
||||
if t.ident:
|
||||
tmap[t.ident] = t
|
||||
else:
|
||||
main_thread = t
|
||||
|
||||
# Loop over each thread's current frame, writing info about it
|
||||
for tid, frame in sys._current_frames().iteritems():
|
||||
thread = tmap.get(tid, main_thread)
|
||||
|
||||
lines = []
|
||||
lines.append('%s\n' % thread.getName())
|
||||
lines.append('========================================\n')
|
||||
lines += traceback.format_stack(frame)
|
||||
lines.append('========================================\n')
|
||||
lines.append('LOCAL VARIABLES:\n')
|
||||
lines.append('========================================\n')
|
||||
lines.append(pprint.pformat(frame.f_locals))
|
||||
lines.append('\n\n')
|
||||
logger.info("".join(lines))
|
||||
@@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of Headphones.
|
||||
#
|
||||
# Headphones is free software: you can redistribute it and/or modify
|
||||
@@ -49,6 +51,7 @@ delugeweb_auth = {}
|
||||
delugeweb_url = ''
|
||||
deluge_verify_cert = False
|
||||
scrub_logs = True
|
||||
headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
def _scrubber(text):
|
||||
@@ -80,7 +83,7 @@ def addTorrent(link, data=None, name=None):
|
||||
|
||||
result = {}
|
||||
retid = False
|
||||
url_what = ['https://what.cd/', 'http://what.cd/']
|
||||
url_apollo = ['https://apollo.rip/', 'http://apollo.rip/']
|
||||
url_waffles = ['https://waffles.ch/', 'http://waffles.ch/']
|
||||
|
||||
if link.lower().startswith('magnet:'):
|
||||
@@ -94,7 +97,7 @@ def addTorrent(link, data=None, name=None):
|
||||
if link.lower().startswith(tuple(url_waffles)):
|
||||
if 'rss=' not in link:
|
||||
link = link + '&rss=1'
|
||||
if link.lower().startswith(tuple(url_what)):
|
||||
if link.lower().startswith(tuple(url_apollo)):
|
||||
logger.debug('Deluge: Using different User-Agent for this site')
|
||||
user_agent = 'Headphones'
|
||||
# This method will make Deluge download the file
|
||||
@@ -104,11 +107,11 @@ def addTorrent(link, data=None, name=None):
|
||||
# return addTorrent(local_torrent_path)
|
||||
else:
|
||||
user_agent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
|
||||
headers = {'User-Agent': user_agent}
|
||||
get_headers = {'User-Agent': user_agent}
|
||||
torrentfile = ''
|
||||
logger.debug('Deluge: Trying to download (GET)')
|
||||
try:
|
||||
r = requests.get(link, headers=headers)
|
||||
r = requests.get(link, headers=get_headers)
|
||||
if r.status_code == 200:
|
||||
logger.debug('Deluge: 200 OK')
|
||||
# .text will ruin the encoding for some torrents
|
||||
@@ -135,7 +138,10 @@ def addTorrent(link, data=None, name=None):
|
||||
# remove '.torrent' suffix
|
||||
if name[-len('.torrent'):] == '.torrent':
|
||||
name = name[:-len('.torrent')]
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||
try:
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||
except:
|
||||
logger.debug('Deluge: Sending Deluge torrent with problematic name and some content')
|
||||
result = {'type': 'torrent',
|
||||
'name': name,
|
||||
'content': torrentfile}
|
||||
@@ -164,7 +170,10 @@ def addTorrent(link, data=None, name=None):
|
||||
# remove '.torrent' suffix
|
||||
if name[-len('.torrent'):] == '.torrent':
|
||||
name = name[:-len('.torrent')]
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||
try:
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||
except UnicodeDecodeError:
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name.decode('utf-8'), str(torrentfile)[:40]))
|
||||
result = {'type': 'torrent',
|
||||
'name': name,
|
||||
'content': torrentfile}
|
||||
@@ -199,7 +208,7 @@ def getTorrentFolder(result):
|
||||
],
|
||||
"id": 21})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['total_done'] = json.loads(response.text)['result']['total_done']
|
||||
|
||||
tries = 0
|
||||
@@ -207,7 +216,7 @@ def getTorrentFolder(result):
|
||||
tries += 1
|
||||
time.sleep(5)
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['total_done'] = json.loads(response.text)['result']['total_done']
|
||||
|
||||
post_data = json.dumps({"method": "web.get_torrent_status",
|
||||
@@ -226,7 +235,7 @@ def getTorrentFolder(result):
|
||||
"id": 23})
|
||||
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
result['save_path'] = json.loads(response.text)['result']['save_path']
|
||||
result['name'] = json.loads(response.text)['result']['name']
|
||||
@@ -242,18 +251,45 @@ def removeTorrent(torrentid, remove_data=False):
|
||||
_get_auth()
|
||||
|
||||
try:
|
||||
result = False
|
||||
post_data = json.dumps({"method": "core.remove_torrent",
|
||||
logger.debug('Deluge: Checking if torrent %s finished seeding' % str(torrentid))
|
||||
post_data = json.dumps({"method": "web.get_torrent_status",
|
||||
"params": [
|
||||
torrentid,
|
||||
remove_data
|
||||
],
|
||||
"id": 25})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
result = json.loads(response.text)['result']
|
||||
[
|
||||
"name",
|
||||
"ratio",
|
||||
"state"
|
||||
]
|
||||
],
|
||||
"id": 26})
|
||||
|
||||
return result
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
try:
|
||||
state = json.loads(response.text)['result']['state']
|
||||
except KeyError as e:
|
||||
logger.debug('Deluge: "state" KeyError when trying to remove torrent %s' % str(torrentid))
|
||||
return False
|
||||
|
||||
not_finished = ["queued", "seeding", "downloading", "checking", "error"]
|
||||
result = False
|
||||
if state.lower() in not_finished:
|
||||
logger.debug('Deluge: Torrent %s is either queued or seeding, not removing yet' % str(torrentid))
|
||||
return False
|
||||
else:
|
||||
logger.debug('Deluge: Removing torrent %s' % str(torrentid))
|
||||
post_data = json.dumps({"method": "core.remove_torrent",
|
||||
"params": [
|
||||
torrentid,
|
||||
remove_data
|
||||
],
|
||||
"id": 25})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result = json.loads(response.text)['result']
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Removing torrent failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
@@ -269,7 +305,8 @@ def _get_auth():
|
||||
delugeweb_host = headphones.CONFIG.DELUGE_HOST
|
||||
delugeweb_cert = headphones.CONFIG.DELUGE_CERT
|
||||
delugeweb_password = headphones.CONFIG.DELUGE_PASSWORD
|
||||
logger.debug('Deluge: Using password %s******%s' % (delugeweb_password[0], delugeweb_password[-1]))
|
||||
if len(delugeweb_password) > 0:
|
||||
logger.debug('Deluge: Using password %s******%s' % (delugeweb_password[0], delugeweb_password[-1]))
|
||||
|
||||
if not delugeweb_host.startswith('http'):
|
||||
delugeweb_host = 'http://%s' % delugeweb_host
|
||||
@@ -292,12 +329,12 @@ def _get_auth():
|
||||
"id": 1})
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
except requests.ConnectionError:
|
||||
try:
|
||||
logger.debug('Deluge: Connection failed, let\'s try HTTPS just in case')
|
||||
response = requests.post(delugeweb_url.replace('http:', 'https:'), data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
# If the previous line didn't fail, change delugeweb_url for the rest of this session
|
||||
logger.error('Deluge: Switching to HTTPS, but certificate won\'t be verified because NO CERTIFICATE WAS CONFIGURED!')
|
||||
delugeweb_url = delugeweb_url.replace('http:', 'https:')
|
||||
@@ -322,7 +359,7 @@ def _get_auth():
|
||||
"id": 10})
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
@@ -339,7 +376,7 @@ def _get_auth():
|
||||
"id": 11})
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
@@ -347,7 +384,8 @@ def _get_auth():
|
||||
return None
|
||||
|
||||
delugeweb_hosts = json.loads(response.text)['result']
|
||||
if len(delugeweb_hosts) == 0:
|
||||
# Check if delugeweb_hosts is None before checking its length
|
||||
if not delugeweb_hosts or len(delugeweb_hosts) == 0:
|
||||
logger.error('Deluge: WebUI does not contain daemons')
|
||||
return None
|
||||
|
||||
@@ -357,7 +395,7 @@ def _get_auth():
|
||||
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
@@ -370,7 +408,7 @@ def _get_auth():
|
||||
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
@@ -395,7 +433,7 @@ def _add_torrent_magnet(result):
|
||||
"params": [result['url'], {}],
|
||||
"id": 2})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['hash'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
@@ -415,7 +453,7 @@ def _add_torrent_url(result):
|
||||
"params": [result['url'], {}],
|
||||
"id": 32})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['location'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
@@ -433,10 +471,11 @@ def _add_torrent_file(result):
|
||||
try:
|
||||
# content is torrent file contents that needs to be encoded to base64
|
||||
post_data = json.dumps({"method": "core.add_torrent_file",
|
||||
"params": [result['name'] + '.torrent', b64encode(result['content'].encode('utf8')), {}],
|
||||
"params": [result['name'] + '.torrent',
|
||||
b64encode(result['content'].encode('utf8')), {}],
|
||||
"id": 2})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['hash'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
@@ -444,11 +483,13 @@ def _add_torrent_file(result):
|
||||
try:
|
||||
# content is torrent file contents that needs to be encoded to base64
|
||||
# this time let's try leaving the encoding as is
|
||||
logger.debug('Deluge: There was a decoding issue, let\'s try again')
|
||||
post_data = json.dumps({"method": "core.add_torrent_file",
|
||||
"params": [result['name'] + '.torrent', b64encode(result['content']), {}],
|
||||
"params": [result['name'].decode('utf8') + '.torrent',
|
||||
b64encode(result['content']), {}],
|
||||
"id": 22})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['hash'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
@@ -480,7 +521,7 @@ def setTorrentLabel(result):
|
||||
"params": [],
|
||||
"id": 3})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
labels = json.loads(response.text)['result']
|
||||
|
||||
if labels is not None:
|
||||
@@ -491,7 +532,7 @@ def setTorrentLabel(result):
|
||||
"params": [label],
|
||||
"id": 4})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
logger.debug('Deluge: %s label added to Deluge' % label)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Setting label failed: %s' % str(e))
|
||||
@@ -503,7 +544,7 @@ def setTorrentLabel(result):
|
||||
"params": [result['hash'], label],
|
||||
"id": 5})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
logger.debug('Deluge: %s label added to torrent' % label)
|
||||
else:
|
||||
logger.debug('Deluge: Label plugin not detected')
|
||||
@@ -527,12 +568,12 @@ def setSeedRatio(result):
|
||||
"params": [result['hash'], True],
|
||||
"id": 5})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
post_data = json.dumps({"method": "core.set_torrent_stop_ratio",
|
||||
"params": [result['hash'], float(ratio)],
|
||||
"id": 6})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
return not json.loads(response.text)['error']
|
||||
|
||||
@@ -555,7 +596,7 @@ def setTorrentPath(result):
|
||||
"params": [result['hash'], True],
|
||||
"id": 7})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
|
||||
move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
|
||||
@@ -569,7 +610,7 @@ def setTorrentPath(result):
|
||||
"params": [result['hash'], move_to],
|
||||
"id": 8})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
return not json.loads(response.text)['error']
|
||||
|
||||
@@ -592,7 +633,7 @@ def setTorrentPause(result):
|
||||
"params": [[result['hash']]],
|
||||
"id": 9})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
return not json.loads(response.text)['error']
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ import datetime
|
||||
import shutil
|
||||
import time
|
||||
import sys
|
||||
import tempfile
|
||||
import glob
|
||||
|
||||
from beets import logging as beetslogging
|
||||
import six
|
||||
from contextlib import contextmanager
|
||||
|
||||
import fnmatch
|
||||
import re
|
||||
@@ -217,8 +223,7 @@ def replace_illegal_chars(string, type="file"):
|
||||
if type == "file":
|
||||
string = re.sub('[\?"*:|<>/]', '_', string)
|
||||
if type == "folder":
|
||||
string = re.sub('[:\?<>"|]', '_', string)
|
||||
|
||||
string = re.sub('[:\?<>"|*]', '_', string)
|
||||
return string
|
||||
|
||||
|
||||
@@ -251,9 +256,17 @@ _XLATE_SPECIAL = {
|
||||
# Translation table.
|
||||
# Cover additional special characters processing normalization.
|
||||
u"'": '', # replace apostrophe with nothing
|
||||
u"’": '', # replace musicbrainz style apostrophe with nothing
|
||||
u'&': ' and ', # expand & to ' and '
|
||||
}
|
||||
|
||||
_XLATE_MUSICBRAINZ = {
|
||||
# Translation table for Musicbrainz.
|
||||
u"…": '...', # HORIZONTAL ELLIPSIS (U+2026)
|
||||
u"’": "'", # APOSTROPHE (U+0027)
|
||||
u"‐": "-", # EN DASH (U+2013)
|
||||
}
|
||||
|
||||
|
||||
def _translate(s, dictionary):
|
||||
# type: (basestring,Mapping[basestring,basestring])->basestring
|
||||
@@ -319,9 +332,27 @@ def clean_name(s):
|
||||
# 6. trim
|
||||
u = u.strip()
|
||||
# 7. lowercase
|
||||
u = u.lower()
|
||||
return u
|
||||
|
||||
|
||||
def clean_musicbrainz_name(s, return_as_string=True):
|
||||
# type: (basestring)->unicode
|
||||
"""Substitute special Musicbrainz characters.
|
||||
:param s: string to clean up, probably unicode.
|
||||
:return: cleaned-up version of input string.
|
||||
"""
|
||||
if not isinstance(s, unicode):
|
||||
u = unicode(s, 'ascii', 'replace')
|
||||
else:
|
||||
u = s
|
||||
u = _translate(u, _XLATE_MUSICBRAINZ)
|
||||
if return_as_string:
|
||||
return u.encode('utf-8')
|
||||
else:
|
||||
return u
|
||||
|
||||
|
||||
def cleanTitle(title):
|
||||
title = re.sub('[\.\-\/\_]', ' ', title).lower()
|
||||
|
||||
@@ -620,26 +651,68 @@ def get_downloaded_track_list(albumpath):
|
||||
return downloaded_track_list
|
||||
|
||||
|
||||
def preserve_torrent_directory(albumpath):
|
||||
def preserve_torrent_directory(albumpath, forced=False, single=False):
|
||||
"""
|
||||
Copy torrent directory to headphones-modified to keep files for seeding.
|
||||
Copy torrent directory to temp headphones_ directory to keep files for seeding.
|
||||
"""
|
||||
from headphones import logger
|
||||
new_folder = os.path.join(albumpath,
|
||||
'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info(
|
||||
"Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
|
||||
|
||||
# Create temp dir
|
||||
if headphones.CONFIG.KEEP_TORRENT_FILES_DIR:
|
||||
tempdir = headphones.CONFIG.KEEP_TORRENT_FILES_DIR
|
||||
else:
|
||||
tempdir = tempfile.gettempdir()
|
||||
|
||||
logger.info("Preparing to copy to a temporary directory for post processing: " + albumpath.decode(
|
||||
headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
try:
|
||||
shutil.copytree(albumpath, new_folder)
|
||||
return new_folder
|
||||
file_name = os.path.basename(os.path.normpath(albumpath))
|
||||
if not single:
|
||||
prefix = "headphones_" + file_name + "_"
|
||||
else:
|
||||
prefix = "headphones_" + os.path.splitext(file_name)[0] + "_"
|
||||
new_folder = tempfile.mkdtemp(prefix=prefix, dir=tempdir)
|
||||
except Exception as e:
|
||||
logger.warn("Cannot copy/move files to temp folder: " +
|
||||
new_folder.decode(headphones.SYS_ENCODING, 'replace') +
|
||||
". Not continuing. Error: " + str(e))
|
||||
logger.error("Cannot create temp directory: " + tempdir.decode(
|
||||
headphones.SYS_ENCODING, 'replace') + ". Error: " + str(e))
|
||||
return None
|
||||
|
||||
# Attempt to stop multiple temp dirs being created for the same albumpath
|
||||
if not forced:
|
||||
try:
|
||||
workdir = os.path.join(tempdir, prefix)
|
||||
workdir = re.sub(r'\[', '[[]', workdir)
|
||||
workdir = re.sub(r'(?<!\[)\]', '[]]', workdir)
|
||||
if len(glob.glob(workdir + '*/')) >= 3:
|
||||
logger.error(
|
||||
"Looks like a temp directory has previously been created for this albumpath, not continuing " + workdir.decode(
|
||||
headphones.SYS_ENCODING, 'replace'))
|
||||
shutil.rmtree(new_folder)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warn("Cannot determine if already copied/processed, will copy anyway: Warning: " + str(e))
|
||||
|
||||
# Copy to temp dir
|
||||
try:
|
||||
subdir = os.path.join(new_folder, "headphones")
|
||||
logger.info("Copying files to " + subdir.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
if not single:
|
||||
shutil.copytree(albumpath, subdir)
|
||||
else:
|
||||
os.makedirs(subdir)
|
||||
shutil.copy(albumpath, subdir)
|
||||
# Update the album path with the new location
|
||||
return subdir
|
||||
except Exception as e:
|
||||
logger.warn("Cannot copy/move files to temp directory: " + new_folder.decode(headphones.SYS_ENCODING,
|
||||
'replace') + ". Not continuing. Error: " + str(
|
||||
e))
|
||||
shutil.rmtree(new_folder)
|
||||
return None
|
||||
|
||||
|
||||
def cue_split(albumpath):
|
||||
def cue_split(albumpath, keep_original_folder=False):
|
||||
"""
|
||||
Attempts to check and split audio files by a cue for the given directory.
|
||||
"""
|
||||
@@ -661,6 +734,15 @@ def cue_split(albumpath):
|
||||
# Split cue
|
||||
if cue_count and cue_count >= count and cue_dirs:
|
||||
|
||||
# Copy to temp directory
|
||||
if keep_original_folder:
|
||||
temppath = preserve_torrent_directory(albumpath)
|
||||
if temppath:
|
||||
cue_dirs = [cue_dir.replace(albumpath, temppath) for cue_dir in cue_dirs]
|
||||
albumpath = temppath
|
||||
else:
|
||||
return None
|
||||
|
||||
from headphones import logger, cuesplit
|
||||
logger.info("Attempting to split audio files by cue")
|
||||
|
||||
@@ -671,12 +753,12 @@ def cue_split(albumpath):
|
||||
except Exception as e:
|
||||
os.chdir(cwd)
|
||||
logger.warn("Cue not split: " + str(e))
|
||||
return False
|
||||
return None
|
||||
|
||||
os.chdir(cwd)
|
||||
return True
|
||||
return albumpath
|
||||
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def extract_logline(s):
|
||||
@@ -897,3 +979,24 @@ def create_https_certificates(ssl_cert, ssl_key):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BeetsLogCapture(beetslogging.Handler):
|
||||
|
||||
def __init__(self):
|
||||
beetslogging.Handler.__init__(self)
|
||||
self.messages = []
|
||||
|
||||
def emit(self, record):
|
||||
self.messages.append(six.text_type(record.msg))
|
||||
|
||||
|
||||
@contextmanager
|
||||
def capture_beets_log(logger='beets'):
|
||||
capture = BeetsLogCapture()
|
||||
log = beetslogging.getLogger(logger)
|
||||
log.addHandler(capture)
|
||||
try:
|
||||
yield capture.messages
|
||||
finally:
|
||||
log.removeHandler(capture)
|
||||
|
||||
@@ -55,7 +55,7 @@ def request_lastfm(method, **kwargs):
|
||||
return
|
||||
|
||||
if "error" in data:
|
||||
logger.error("Last.FM returned an error: %s", data["message"])
|
||||
logger.debug("Last.FM returned an error: %s", data["message"])
|
||||
return
|
||||
|
||||
return data
|
||||
@@ -149,7 +149,10 @@ def getTagTopArtists(tag, limit=50):
|
||||
logger.debug("Fetched %d artists from Last.FM", len(artists))
|
||||
|
||||
for artist in artists:
|
||||
artist_mbid = artist["mbid"]
|
||||
try:
|
||||
artist_mbid = artist["mbid"]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
if not any(artist_mbid in x for x in results):
|
||||
artistlist.append(artist_mbid)
|
||||
|
||||
@@ -91,10 +91,6 @@ def findArtist(name, limit=1):
|
||||
artistlist = []
|
||||
artistResults = None
|
||||
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
|
||||
criteria = {'artist': name.lower()}
|
||||
|
||||
with mb_lock:
|
||||
@@ -156,16 +152,13 @@ def findRelease(name, limit=1, artist=None):
|
||||
if not artist and ':' in name:
|
||||
name, artist = name.rsplit(":", 1)
|
||||
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
if artist and any((c in chars) for c in artist):
|
||||
artist = '"' + artist + '"'
|
||||
criteria = {'release': name.lower()}
|
||||
if artist:
|
||||
criteria['artist'] = artist.lower()
|
||||
|
||||
with mb_lock:
|
||||
try:
|
||||
releaseResults = musicbrainzngs.search_releases(query=name, limit=limit, artist=artist)[
|
||||
'release-list']
|
||||
releaseResults = musicbrainzngs.search_releases(limit=limit, **criteria)['release-list']
|
||||
except musicbrainzngs.WebServiceError as e: # need to update exceptions
|
||||
logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
@@ -234,10 +227,6 @@ def findSeries(name, limit=1):
|
||||
serieslist = []
|
||||
seriesResults = None
|
||||
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
|
||||
criteria = {'series': name.lower()}
|
||||
|
||||
with mb_lock:
|
||||
@@ -759,19 +748,12 @@ def findArtistbyAlbum(name):
|
||||
|
||||
def findAlbumID(artist=None, album=None):
|
||||
results = None
|
||||
chars = set('!?*-')
|
||||
|
||||
try:
|
||||
if album and artist:
|
||||
if any((c in chars) for c in album):
|
||||
album = '"' + album + '"'
|
||||
if any((c in chars) for c in artist):
|
||||
artist = '"' + artist + '"'
|
||||
criteria = {'release': album.lower()}
|
||||
criteria['artist'] = artist.lower()
|
||||
else:
|
||||
if any((c in chars) for c in album):
|
||||
album = '"' + album + '"'
|
||||
criteria = {'release': album.lower()}
|
||||
with mb_lock:
|
||||
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get(
|
||||
@@ -788,3 +770,26 @@ def findAlbumID(artist=None, album=None):
|
||||
return False
|
||||
rgid = unicode(results[0]['id'])
|
||||
return rgid
|
||||
|
||||
|
||||
def getArtistForReleaseGroup(rgid):
|
||||
"""
|
||||
Returns artist name for a release group
|
||||
Used for series where we store the series instead of the artist
|
||||
"""
|
||||
releaseGroup = None
|
||||
try:
|
||||
with mb_lock:
|
||||
releaseGroup = musicbrainzngs.get_release_group_by_id(
|
||||
rgid, ["artists"])
|
||||
releaseGroup = releaseGroup['release-group']
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn(
|
||||
'Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (
|
||||
rgid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not releaseGroup:
|
||||
return False
|
||||
else:
|
||||
return releaseGroup['artist-credit'][0]['artist']['name']
|
||||
|
||||
@@ -27,8 +27,9 @@ def update(artistid, artist_name, release_groups):
|
||||
# cut down on api calls. If it's ineffective then we'll switch to search
|
||||
|
||||
replacements = {" & ": " ", ".": ""}
|
||||
mc_artist_name = helpers.clean_musicbrainz_name(artist_name, return_as_string=False)
|
||||
mc_artist_name = mc_artist_name.replace("'", " ")
|
||||
mc_artist_name = helpers.replace_all(artist_name.lower(), replacements)
|
||||
|
||||
mc_artist_name = mc_artist_name.replace(" ", "-")
|
||||
|
||||
headers = {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from urllib import urlencode
|
||||
from urllib import urlencode, quote_plus
|
||||
import urllib
|
||||
import subprocess
|
||||
import json
|
||||
@@ -148,7 +148,9 @@ class PROWL(object):
|
||||
|
||||
http_handler.request("POST",
|
||||
"/publicapi/add",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
headers={
|
||||
'Content-type':
|
||||
"application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
@@ -203,28 +205,34 @@ class XBMC(object):
|
||||
url = host + '/xbmcCmds/xbmcHttp/?' + url_command
|
||||
|
||||
if self.password:
|
||||
return request.request_content(url, auth=(self.username, self.password))
|
||||
return request.request_content(url,
|
||||
auth=(self.username, self.password))
|
||||
else:
|
||||
return request.request_content(url)
|
||||
|
||||
def _sendjson(self, host, method, params={}):
|
||||
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
data = [
|
||||
{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
url = host + '/jsonrpc'
|
||||
|
||||
if self.password:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers, auth=(self.username, self.password))
|
||||
response = request.request_json(
|
||||
url, method="post",
|
||||
data=json.dumps(data),
|
||||
headers=headers, auth=(
|
||||
self.username, self.password))
|
||||
else:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
response = request.request_json(url, method="post",
|
||||
data=json.dumps(data),
|
||||
headers=headers)
|
||||
|
||||
if response:
|
||||
return response[0]['result']
|
||||
|
||||
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
|
||||
# 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
|
||||
|
||||
hosts = [x.strip() for x in self.hosts.split(',')]
|
||||
|
||||
@@ -247,18 +255,23 @@ class XBMC(object):
|
||||
logger.info('Sending notification command to XMBC @ ' + host)
|
||||
try:
|
||||
version = self._sendjson(host, 'Application.GetProperties',
|
||||
{'properties': ['version']})['version']['major']
|
||||
{'properties': ['version']})[
|
||||
'version']['major']
|
||||
|
||||
if version < 12: # Eden
|
||||
notification = header + "," + message + "," + time + "," + albumartpath
|
||||
notification = header + "," + message + "," + time + \
|
||||
"," + albumartpath
|
||||
notifycommand = {'command': 'ExecBuiltIn',
|
||||
'parameter': 'Notification(' + notification + ')'}
|
||||
'parameter': 'Notification(' +
|
||||
notification + ')'}
|
||||
request = self._sendhttp(host, notifycommand)
|
||||
|
||||
else: # Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time),
|
||||
params = {'title': header, 'message': message,
|
||||
'displaytime': int(time),
|
||||
'image': albumartpath}
|
||||
request = self._sendjson(host, 'GUI.ShowNotification', params)
|
||||
request = self._sendjson(host, 'GUI.ShowNotification',
|
||||
params)
|
||||
|
||||
if not request:
|
||||
raise Exception
|
||||
@@ -323,22 +336,28 @@ class Plex(object):
|
||||
url = host + '/xbmcCmds/xbmcHttp/?' + command
|
||||
|
||||
if self.password:
|
||||
response = request.request_response(url, auth=(self.username, self.password))
|
||||
response = request.request_response(url, auth=(
|
||||
self.username, self.password))
|
||||
else:
|
||||
response = request.request_response(url)
|
||||
|
||||
return response
|
||||
|
||||
def _sendjson(self, host, method, params={}):
|
||||
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
data = [
|
||||
{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
url = host + '/jsonrpc'
|
||||
|
||||
if self.password:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers, auth=(self.username, self.password))
|
||||
response = request.request_json(
|
||||
url, method="post",
|
||||
data=json.dumps(data),
|
||||
headers=headers, auth=(
|
||||
self.username, self.password))
|
||||
else:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
response = request.request_json(url, method="post",
|
||||
data=json.dumps(data),
|
||||
headers=headers)
|
||||
|
||||
if response:
|
||||
@@ -346,13 +365,14 @@ class Plex(object):
|
||||
|
||||
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
|
||||
# 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
|
||||
|
||||
hosts = [x.strip() for x in self.server_hosts.split(',')]
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending library update command to Plex Media Server@ ' + host)
|
||||
logger.info(
|
||||
'Sending library update command to Plex Media Server@ ' + host)
|
||||
url = "%s/library/sections" % host
|
||||
if self.token:
|
||||
params = {'X-Plex-Token': self.token}
|
||||
@@ -369,7 +389,8 @@ class Plex(object):
|
||||
|
||||
for s in sections:
|
||||
if s.getAttribute('type') == "artist":
|
||||
url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key'))
|
||||
url = "%s/library/sections/%s/refresh" % (
|
||||
host, s.getAttribute('key'))
|
||||
request.request_response(url, params=params)
|
||||
|
||||
def notify(self, artist, album, albumartpath):
|
||||
@@ -381,27 +402,35 @@ class Plex(object):
|
||||
time = "3000" # in ms
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending notification command to Plex client @ ' + host)
|
||||
logger.info(
|
||||
'Sending notification command to Plex client @ ' + host)
|
||||
try:
|
||||
version = self._sendjson(host, 'Application.GetProperties',
|
||||
{'properties': ['version']})['version']['major']
|
||||
{'properties': ['version']})[
|
||||
'version']['major']
|
||||
|
||||
if version < 12: # Eden
|
||||
notification = header + "," + message + "," + time + "," + albumartpath
|
||||
notification = header + "," + message + "," + time + \
|
||||
"," + albumartpath
|
||||
notifycommand = {'command': 'ExecBuiltIn',
|
||||
'parameter': 'Notification(' + notification + ')'}
|
||||
'parameter': 'Notification(' +
|
||||
notification + ')'}
|
||||
request = self._sendhttp(host, notifycommand)
|
||||
|
||||
else: # Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time),
|
||||
params = {'title': header, 'message': message,
|
||||
'displaytime': int(time),
|
||||
'image': albumartpath}
|
||||
request = self._sendjson(host, 'GUI.ShowNotification', params)
|
||||
request = self._sendjson(host, 'GUI.ShowNotification',
|
||||
params)
|
||||
|
||||
if not request:
|
||||
raise Exception
|
||||
|
||||
except Exception:
|
||||
logger.error('Error sending notification request to Plex client @ ' + host)
|
||||
logger.error(
|
||||
'Error sending notification request to Plex client @ ' +
|
||||
host)
|
||||
|
||||
|
||||
class NMA(object):
|
||||
@@ -419,7 +448,8 @@ class NMA(object):
|
||||
message = "Headphones has snatched: " + snatched
|
||||
else:
|
||||
event = artist + ' - ' + album + ' complete!'
|
||||
message = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']'
|
||||
message = "Headphones has downloaded and postprocessed: " + \
|
||||
artist + ' [' + album + ']'
|
||||
|
||||
logger.debug(u"NMA event: " + event)
|
||||
logger.debug(u"NMA message: " + message)
|
||||
@@ -433,7 +463,8 @@ class NMA(object):
|
||||
if len(keys) > 1:
|
||||
batch = True
|
||||
|
||||
response = p.push(title, event, message, priority=nma_priority, batch_mode=batch)
|
||||
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')
|
||||
@@ -461,9 +492,11 @@ class PUSHBULLET(object):
|
||||
data['device_iden'] = self.deviceid
|
||||
|
||||
headers = {'Content-type': "application/json",
|
||||
'Authorization': 'Bearer ' + headphones.CONFIG.PUSHBULLET_APIKEY}
|
||||
'Authorization': 'Bearer ' +
|
||||
headphones.CONFIG.PUSHBULLET_APIKEY}
|
||||
|
||||
response = request.request_json(url, method="post", headers=headers, data=json.dumps(data))
|
||||
response = request.request_json(url, method="post", headers=headers,
|
||||
data=json.dumps(data))
|
||||
|
||||
if response:
|
||||
logger.info(u"PushBullet notifications sent.")
|
||||
@@ -492,7 +525,9 @@ class PUSHALOT(object):
|
||||
|
||||
http_handler.request("POST",
|
||||
"/api/sendmessage",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
headers={
|
||||
'Content-type':
|
||||
"application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
@@ -512,6 +547,49 @@ class PUSHALOT(object):
|
||||
return False
|
||||
|
||||
|
||||
class JOIN(object):
|
||||
def __init__(self):
|
||||
|
||||
self.enabled = headphones.CONFIG.JOIN_ENABLED
|
||||
self.apikey = headphones.CONFIG.JOIN_APIKEY
|
||||
self.deviceid = headphones.CONFIG.JOIN_DEVICEID
|
||||
self.url = 'https://joinjoaomgcd.appspot.com/_ah/' \
|
||||
'api/messaging/v1/sendPush?apikey={apikey}' \
|
||||
'&title={title}&text={text}' \
|
||||
'&icon={icon}'
|
||||
|
||||
def notify(self, message, event):
|
||||
if not headphones.CONFIG.JOIN_ENABLED or \
|
||||
not headphones.CONFIG.JOIN_APIKEY:
|
||||
return
|
||||
|
||||
icon = "https://cdn.rawgit.com/Headphones/" \
|
||||
"headphones/develop/data/images/headphoneslogo.png"
|
||||
|
||||
if not self.deviceid:
|
||||
self.deviceid = "group.all"
|
||||
l = [x.strip() for x in self.deviceid.split(',')]
|
||||
if len(l) > 1:
|
||||
self.url += '&deviceIds={deviceid}'
|
||||
else:
|
||||
self.url += '&deviceId={deviceid}'
|
||||
|
||||
response = urllib2.urlopen(self.url.format(apikey=self.apikey,
|
||||
title=quote_plus(event),
|
||||
text=quote_plus(
|
||||
message.encode(
|
||||
"utf-8")),
|
||||
icon=icon,
|
||||
deviceid=self.deviceid))
|
||||
|
||||
if response:
|
||||
logger.info(u"Join notifications sent.")
|
||||
return True
|
||||
else:
|
||||
logger.error(u"Join notification failed.")
|
||||
return False
|
||||
|
||||
|
||||
class Synoindex(object):
|
||||
def __init__(self, util_loc='/usr/syno/bin/synoindex'):
|
||||
self.util_loc = util_loc
|
||||
@@ -524,7 +602,8 @@ class Synoindex(object):
|
||||
|
||||
if not self.util_exists():
|
||||
logger.warn(
|
||||
"Error sending notification: synoindex utility not found at %s" % self.util_loc)
|
||||
"Error sending notification: synoindex utility "
|
||||
"not found at %s" % self.util_loc)
|
||||
return
|
||||
|
||||
if os.path.isfile(path):
|
||||
@@ -533,16 +612,19 @@ class Synoindex(object):
|
||||
cmd_arg = '-A'
|
||||
else:
|
||||
logger.warn(
|
||||
"Error sending notification: Path passed to synoindex was not a file or folder.")
|
||||
"Error sending notification: Path passed to synoindex "
|
||||
"was not a file or folder.")
|
||||
return
|
||||
|
||||
cmd = [self.util_loc, cmd_arg, path]
|
||||
logger.info("Calling synoindex command: %s" % str(cmd))
|
||||
try:
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=headphones.PROG_DIR)
|
||||
out, error = p.communicate()
|
||||
# synoindex never returns any codes other than '0', highly irritating
|
||||
# synoindex never returns any codes other than '0',
|
||||
# highly irritating
|
||||
except OSError, e:
|
||||
logger.warn("Error sending notification: %s" % str(e))
|
||||
|
||||
@@ -580,7 +662,8 @@ class PUSHOVER(object):
|
||||
|
||||
headers = {'Content-type': "application/x-www-form-urlencoded"}
|
||||
|
||||
response = request.request_response(url, method="POST", headers=headers, data=data)
|
||||
response = request.request_response(url, method="POST",
|
||||
headers=headers, data=data)
|
||||
|
||||
if response:
|
||||
logger.info(u"Pushover notifications sent.")
|
||||
@@ -614,20 +697,25 @@ class TwitterNotifier(object):
|
||||
def notify_snatch(self, title):
|
||||
if headphones.CONFIG.TWITTER_ONSNATCH:
|
||||
self._notifyTwitter(
|
||||
common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now())
|
||||
common.notifyStrings[
|
||||
common.NOTIFY_SNATCH] + ': ' + title + ' at ' +
|
||||
helpers.now())
|
||||
|
||||
def notify_download(self, title):
|
||||
if headphones.CONFIG.TWITTER_ENABLED:
|
||||
self._notifyTwitter(common.notifyStrings[
|
||||
common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now())
|
||||
common.NOTIFY_DOWNLOAD] + ': ' +
|
||||
title + ' at ' + helpers.now())
|
||||
|
||||
def test_notify(self):
|
||||
return self._notifyTwitter(
|
||||
"This is a test notification from Headphones at " + helpers.now(), force=True)
|
||||
"This is a test notification from Headphones at " + helpers.now(),
|
||||
force=True)
|
||||
|
||||
def _get_authorization(self):
|
||||
|
||||
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
|
||||
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')
|
||||
@@ -635,32 +723,42 @@ class TwitterNotifier(object):
|
||||
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'])
|
||||
logger.info(
|
||||
'Invalid respond from Twitter requesting temp token: %s' %
|
||||
resp['status'])
|
||||
else:
|
||||
request_token = dict(parse_qsl(content))
|
||||
|
||||
headphones.CONFIG.TWITTER_USERNAME = request_token['oauth_token']
|
||||
headphones.CONFIG.TWITTER_PASSWORD = request_token['oauth_token_secret']
|
||||
headphones.CONFIG.TWITTER_PASSWORD = request_token[
|
||||
'oauth_token_secret']
|
||||
|
||||
return self.AUTHORIZATION_URL + "?oauth_token=" + request_token['oauth_token']
|
||||
return self.AUTHORIZATION_URL + "?oauth_token=" + request_token[
|
||||
'oauth_token']
|
||||
|
||||
def _get_credentials(self, key):
|
||||
request_token = {}
|
||||
|
||||
request_token['oauth_token'] = headphones.CONFIG.TWITTER_USERNAME
|
||||
request_token['oauth_token_secret'] = headphones.CONFIG.TWITTER_PASSWORD
|
||||
request_token[
|
||||
'oauth_token_secret'] = headphones.CONFIG.TWITTER_PASSWORD
|
||||
request_token['oauth_callback_confirmed'] = 'true'
|
||||
|
||||
token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret'])
|
||||
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)
|
||||
logger.info(
|
||||
'Generating and signing request for an access token using key ' +
|
||||
key)
|
||||
|
||||
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
|
||||
oauth_consumer = oauth.Consumer(key=self.consumer_key,
|
||||
secret=self.consumer_secret)
|
||||
logger.info('oauth_consumer: ' + str(oauth_consumer))
|
||||
oauth_client = oauth.Client(oauth_consumer, token)
|
||||
logger.info('oauth_client: ' + str(oauth_client))
|
||||
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST',
|
||||
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL,
|
||||
method='POST',
|
||||
body='oauth_verifier=%s' % key)
|
||||
logger.info('resp, content: ' + str(resp) + ',' + str(content))
|
||||
|
||||
@@ -669,14 +767,18 @@ class TwitterNotifier(object):
|
||||
|
||||
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.info('The request for a token with did not succeed: ' + str(
|
||||
resp['status']),
|
||||
logger.ERROR)
|
||||
return False
|
||||
else:
|
||||
logger.info('Your Twitter Access Token key: %s' % access_token['oauth_token'])
|
||||
logger.info('Access Token secret: %s' % access_token['oauth_token_secret'])
|
||||
logger.info('Your Twitter Access Token key: %s' % access_token[
|
||||
'oauth_token'])
|
||||
logger.info(
|
||||
'Access Token secret: %s' % access_token['oauth_token_secret'])
|
||||
headphones.CONFIG.TWITTER_USERNAME = access_token['oauth_token']
|
||||
headphones.CONFIG.TWITTER_PASSWORD = access_token['oauth_token_secret']
|
||||
headphones.CONFIG.TWITTER_PASSWORD = access_token[
|
||||
'oauth_token_secret']
|
||||
return True
|
||||
|
||||
def _send_tweet(self, message=None):
|
||||
@@ -688,7 +790,8 @@ class TwitterNotifier(object):
|
||||
|
||||
logger.info(u"Sending tweet: " + message)
|
||||
|
||||
api = twitter.Api(username, password, access_token_key, access_token_secret)
|
||||
api = twitter.Api(username, password, access_token_key,
|
||||
access_token_secret)
|
||||
|
||||
try:
|
||||
api.PostUpdate(message)
|
||||
@@ -741,7 +844,8 @@ class OSX_NOTIFY(object):
|
||||
)
|
||||
|
||||
NSUserNotification = self.objc.lookUpClass('NSUserNotification')
|
||||
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter')
|
||||
NSUserNotificationCenter = self.objc.lookUpClass(
|
||||
'NSUserNotificationCenter')
|
||||
NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')
|
||||
|
||||
if not NSUserNotification or not NSUserNotificationCenter:
|
||||
@@ -756,14 +860,17 @@ class OSX_NOTIFY(object):
|
||||
if text:
|
||||
notification.setInformativeText_(text)
|
||||
if sound:
|
||||
notification.setSoundName_("NSUserNotificationDefaultSoundName")
|
||||
notification.setSoundName_(
|
||||
"NSUserNotificationDefaultSoundName")
|
||||
if image:
|
||||
source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image)
|
||||
source_img = self.AppKit.NSImage.alloc().\
|
||||
initByReferencingFile_(image)
|
||||
notification.setContentImage_(source_img)
|
||||
# notification.set_identityImage_(source_img)
|
||||
notification.setHasActionButton_(False)
|
||||
|
||||
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
||||
notification_center = NSUserNotificationCenter.\
|
||||
defaultUserNotificationCenter()
|
||||
notification_center.deliverNotification_(notification)
|
||||
|
||||
del pool
|
||||
@@ -784,13 +891,16 @@ class BOXCAR(object):
|
||||
def notify(self, title, message, rgid=None):
|
||||
try:
|
||||
if rgid:
|
||||
message += '<br></br><a href="http://musicbrainz.org/release-group/%s">MusicBrainz</a>' % rgid
|
||||
message += '<br></br><a href="http://musicbrainz.org/' \
|
||||
'release-group/%s">MusicBrainz</a>' % rgid
|
||||
|
||||
data = urllib.urlencode({
|
||||
'user_credentials': headphones.CONFIG.BOXCAR_TOKEN,
|
||||
'notification[title]': title.encode('utf-8'),
|
||||
'notification[long_message]': message.encode('utf-8'),
|
||||
'notification[sound]': "done"
|
||||
'notification[sound]': "done",
|
||||
'notification[icon_url]': "https://raw.githubusercontent.com/rembo10/headphones/master/data/images"
|
||||
"/headphoneslogo.png"
|
||||
})
|
||||
|
||||
req = urllib2.Request(self.url)
|
||||
@@ -818,8 +928,9 @@ class SubSonicNotifier(object):
|
||||
self.host = self.host + "/"
|
||||
|
||||
# Invoke request
|
||||
request.request_response(self.host + "musicFolderSettings.view?scanNow",
|
||||
auth=(self.username, self.password))
|
||||
request.request_response(
|
||||
self.host + "musicFolderSettings.view?scanNow",
|
||||
auth=(self.username, self.password))
|
||||
|
||||
|
||||
class Email(object):
|
||||
@@ -827,13 +938,15 @@ class Email(object):
|
||||
|
||||
message = MIMEText(message, 'plain', "utf-8")
|
||||
message['Subject'] = subject
|
||||
message['From'] = email.utils.formataddr(('Headphones', headphones.CONFIG.EMAIL_FROM))
|
||||
message['From'] = email.utils.formataddr(
|
||||
('Headphones', headphones.CONFIG.EMAIL_FROM))
|
||||
message['To'] = headphones.CONFIG.EMAIL_TO
|
||||
|
||||
try:
|
||||
if headphones.CONFIG.EMAIL_SSL:
|
||||
mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
mailserver = smtplib.SMTP_SSL(
|
||||
headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
else:
|
||||
mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
@@ -847,7 +960,8 @@ class Email(object):
|
||||
mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PASSWORD)
|
||||
|
||||
mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, headphones.CONFIG.EMAIL_TO,
|
||||
mailserver.sendmail(headphones.CONFIG.EMAIL_FROM,
|
||||
headphones.CONFIG.EMAIL_TO,
|
||||
message.as_string())
|
||||
mailserver.quit()
|
||||
return True
|
||||
@@ -858,7 +972,6 @@ class Email(object):
|
||||
|
||||
|
||||
class TELEGRAM(object):
|
||||
|
||||
def notify(self, message, status):
|
||||
if not headphones.CONFIG.TELEGRAM_ENABLED:
|
||||
return
|
||||
@@ -876,15 +989,49 @@ class TELEGRAM(object):
|
||||
|
||||
# Send message to user using Telegram's Bot API
|
||||
try:
|
||||
response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload)
|
||||
response = requests.post(TELEGRAM_API % (token, "sendMessage"),
|
||||
data=payload)
|
||||
except Exception, e:
|
||||
logger.info(u'Telegram notify failed: ' + str(e))
|
||||
|
||||
# Error logging
|
||||
sent_successfuly = True
|
||||
if not response.status_code == 200:
|
||||
logger.info(u'Could not send notification to TelegramBot (token=%s). Response: [%s]', (token, response.text))
|
||||
logger.info(
|
||||
u'Could not send notification to TelegramBot '
|
||||
u'(token=%s). Response: [%s]',
|
||||
(token, response.text))
|
||||
sent_successfuly = False
|
||||
|
||||
logger.info(u"Telegram notifications sent.")
|
||||
return sent_successfuly
|
||||
|
||||
|
||||
class SLACK(object):
|
||||
def notify(self, message, status):
|
||||
if not headphones.CONFIG.SLACK_ENABLED:
|
||||
return
|
||||
|
||||
import requests
|
||||
|
||||
SLACK_URL = headphones.CONFIG.SLACK_URL
|
||||
channel = headphones.CONFIG.SLACK_CHANNEL
|
||||
emoji = headphones.CONFIG.SLACK_EMOJI
|
||||
|
||||
payload = {'channel': channel, 'text': status + ': ' + message,
|
||||
'icon_emoji': emoji}
|
||||
|
||||
try:
|
||||
response = requests.post(SLACK_URL, json=payload)
|
||||
except Exception, e:
|
||||
logger.info(u'Slack notify failed: ' + str(e))
|
||||
|
||||
sent_successfuly = True
|
||||
if not response.status_code == 200:
|
||||
logger.info(
|
||||
u'Could not send notification to Slack. Response: [%s]',
|
||||
(response.text))
|
||||
sent_successfuly = False
|
||||
|
||||
logger.info(u"Slack notifications sent.")
|
||||
return sent_successfuly
|
||||
|
||||
@@ -17,7 +17,6 @@ import shutil
|
||||
import uuid
|
||||
import threading
|
||||
import itertools
|
||||
import tempfile
|
||||
|
||||
import os
|
||||
import re
|
||||
@@ -25,11 +24,12 @@ import beets
|
||||
import headphones
|
||||
from beets import autotag
|
||||
from beets import config as beetsconfig
|
||||
from beets import logging as beetslogging
|
||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||
from beetsplug import lyrics as beetslyrics
|
||||
from headphones import notifiers, utorrent, transmission, deluge
|
||||
from headphones import notifiers, utorrent, transmission, deluge, qbittorrent
|
||||
from headphones import db, albumart, librarysync
|
||||
from headphones import logger, helpers, request, mb, music_encoder
|
||||
from headphones import logger, helpers, mb, music_encoder
|
||||
from headphones import metadata
|
||||
|
||||
postprocessor_lock = threading.Lock()
|
||||
@@ -44,6 +44,8 @@ def checkFolder():
|
||||
|
||||
for album in snatched:
|
||||
if album['FolderName']:
|
||||
folder_name = album['FolderName']
|
||||
single = False
|
||||
if album['Kind'] == 'nzb':
|
||||
download_dir = headphones.CONFIG.DOWNLOAD_DIR
|
||||
else:
|
||||
@@ -52,21 +54,32 @@ def checkFolder():
|
||||
else:
|
||||
download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
|
||||
|
||||
album_path = os.path.join(download_dir, album['FolderName']).encode(
|
||||
headphones.SYS_ENCODING, 'replace')
|
||||
logger.debug("Checking if %s exists" % album_path)
|
||||
# Get folder from torrent hash
|
||||
if album['TorrentHash'] and headphones.CONFIG.TORRENT_DOWNLOADER:
|
||||
torrent_folder_name = None
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
|
||||
torrent_folder_name, single = transmission.getFolder(album['TorrentHash'])
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 4:
|
||||
torrent_folder_name, single = qbittorrent.getFolder(album['TorrentHash'])
|
||||
if torrent_folder_name:
|
||||
folder_name = torrent_folder_name
|
||||
|
||||
if os.path.exists(album_path):
|
||||
logger.info('Found "' + album['FolderName'] + '" in ' + album[
|
||||
'Kind'] + ' download folder. Verifying....')
|
||||
verify(album['AlbumID'], album_path, album['Kind'])
|
||||
if folder_name:
|
||||
album_path = os.path.join(download_dir, folder_name).encode(
|
||||
headphones.SYS_ENCODING, 'replace')
|
||||
logger.debug("Checking if %s exists" % album_path)
|
||||
|
||||
if os.path.exists(album_path):
|
||||
logger.info('Found "' + folder_name + '" in ' + album[
|
||||
'Kind'] + ' download folder. Verifying....')
|
||||
verify(album['AlbumID'], album_path, album['Kind'], single=single)
|
||||
else:
|
||||
logger.info("No folder name found for " + album['Title'])
|
||||
|
||||
logger.debug("Checking download folder finished.")
|
||||
|
||||
|
||||
def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=False):
|
||||
def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=False, single=False):
|
||||
myDB = db.DBConnection()
|
||||
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
|
||||
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
|
||||
@@ -207,13 +220,38 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
'replace') + " isn't complete yet. Will try again on the next run")
|
||||
return
|
||||
|
||||
# Split cue
|
||||
# Force single file through
|
||||
if single and not downloaded_track_list:
|
||||
downloaded_track_list.append(albumpath)
|
||||
|
||||
# Check to see if we're preserving the torrent dir
|
||||
if (headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent") or headphones.CONFIG.KEEP_ORIGINAL_FOLDER:
|
||||
keep_original_folder = True
|
||||
|
||||
# Split cue before metadata check
|
||||
if headphones.CONFIG.CUE_SPLIT and downloaded_cuecount and downloaded_cuecount >= len(
|
||||
downloaded_track_list):
|
||||
if headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent":
|
||||
albumpath = helpers.preserve_torrent_directory(albumpath)
|
||||
if albumpath and helpers.cue_split(albumpath):
|
||||
new_folder = None
|
||||
new_albumpath = albumpath
|
||||
if keep_original_folder:
|
||||
temp_path = helpers.preserve_torrent_directory(new_albumpath, forced)
|
||||
if not temp_path:
|
||||
markAsUnprocessed(albumid, new_albumpath, keep_original_folder)
|
||||
return
|
||||
else:
|
||||
new_albumpath = temp_path
|
||||
new_folder = os.path.split(new_albumpath)[0]
|
||||
Kind = "cue_split"
|
||||
cuepath = helpers.cue_split(new_albumpath)
|
||||
if not cuepath:
|
||||
if new_folder:
|
||||
shutil.rmtree(new_folder)
|
||||
markAsUnprocessed(albumid, albumpath, keep_original_folder)
|
||||
return
|
||||
else:
|
||||
albumpath = cuepath
|
||||
downloaded_track_list = helpers.get_downloaded_track_list(albumpath)
|
||||
keep_original_folder = False
|
||||
|
||||
# test #1: metadata - usually works
|
||||
logger.debug('Verifying metadata...')
|
||||
@@ -243,7 +281,7 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
|
||||
if metaartist == dbartist and metaalbum == dbalbum:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||
keep_original_folder)
|
||||
keep_original_folder, forced, single)
|
||||
return
|
||||
|
||||
# test #2: filenames
|
||||
@@ -262,7 +300,7 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
|
||||
if dbtrack in filetrack:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||
keep_original_folder)
|
||||
keep_original_folder, forced, single)
|
||||
return
|
||||
|
||||
# test #3: number of songs and duration
|
||||
@@ -295,46 +333,49 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
delta = abs(downloaded_track_duration - db_track_duration)
|
||||
if delta < 240:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||
keep_original_folder)
|
||||
keep_original_folder, forced, single)
|
||||
return
|
||||
|
||||
logger.warn(u'Could not identify album: %s. It may not be the intended album.',
|
||||
albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
markAsUnprocessed(albumid, albumpath, keep_original_folder)
|
||||
|
||||
|
||||
def markAsUnprocessed(albumid, albumpath, keep_original_folder=False):
|
||||
myDB = db.DBConnection()
|
||||
myDB.action(
|
||||
'UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?',
|
||||
[albumid])
|
||||
'UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
|
||||
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
|
||||
if not processed:
|
||||
if headphones.CONFIG.RENAME_UNPROCESSED:
|
||||
if headphones.CONFIG.RENAME_UNPROCESSED and not keep_original_folder:
|
||||
renameUnprocessedFolder(albumpath, tag="Unprocessed")
|
||||
else:
|
||||
logger.warn(u"Won't rename %s to mark as 'Unprocessed', because it is disabled.",
|
||||
logger.warn(u"Won't rename %s to mark as 'Unprocessed', because it is disabled or folder is being kept.",
|
||||
albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
|
||||
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None,
|
||||
keep_original_folder=False):
|
||||
keep_original_folder=False, forced=False, single=False):
|
||||
logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle']))
|
||||
new_folder = None
|
||||
# Check to see if we're preserving the torrent dir
|
||||
if (headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent" and 'headphones-modified' not in albumpath) or headphones.CONFIG.KEEP_ORIGINAL_FOLDER or keep_original_folder:
|
||||
new_folder = tempfile.mkdtemp(prefix="headphones_")
|
||||
subdir = os.path.join(new_folder, "headphones")
|
||||
logger.info("Copying files to " + subdir.decode(headphones.SYS_ENCODING, 'replace') + " subfolder to preserve downloaded files for seeding")
|
||||
try:
|
||||
shutil.copytree(albumpath, subdir)
|
||||
# Update the album path with the new location
|
||||
albumpath = subdir
|
||||
except Exception as e:
|
||||
logger.warn("Cannot copy/move files to temp folder: " + new_folder.decode(headphones.SYS_ENCODING, 'replace') + ". Not continuing. Error: " + str(e))
|
||||
shutil.rmtree(new_folder)
|
||||
|
||||
# Preserve the torrent dir
|
||||
if keep_original_folder or single:
|
||||
temp_path = helpers.preserve_torrent_directory(albumpath, forced, single)
|
||||
if not temp_path:
|
||||
markAsUnprocessed(albumid, albumpath, keep_original_folder)
|
||||
return
|
||||
else:
|
||||
albumpath = temp_path
|
||||
new_folder = os.path.split(albumpath)[0]
|
||||
elif Kind == "cue_split":
|
||||
new_folder = os.path.split(albumpath)[0]
|
||||
|
||||
# Need to update the downloaded track list with the new location.
|
||||
# Could probably just throw in the "headphones-modified" folder,
|
||||
# but this is good to make sure we're not counting files that may have failed to move
|
||||
# Need to update the downloaded track list with the new location.
|
||||
# Could probably just throw in the "headphones-modified" folder,
|
||||
# but this is good to make sure we're not counting files that may have failed to move
|
||||
if new_folder:
|
||||
downloaded_track_list = []
|
||||
|
||||
for r, d, f in os.walk(albumpath):
|
||||
for files in f:
|
||||
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
|
||||
@@ -352,7 +393,8 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, "replace"))
|
||||
return
|
||||
except IOError:
|
||||
logger.error("Unable to find media file: %s. Not continuing.")
|
||||
logger.error("Unable to find media file: %s. Not continuing.", downloaded_track.decode(
|
||||
headphones.SYS_ENCODING, "replace"))
|
||||
if new_folder:
|
||||
shutil.rmtree(new_folder)
|
||||
return
|
||||
@@ -387,21 +429,13 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
shutil.rmtree(new_folder)
|
||||
return
|
||||
|
||||
# get artwork and path
|
||||
album_art_path = None
|
||||
artwork = None
|
||||
album_art_path = albumart.getAlbumArt(albumid)
|
||||
if headphones.CONFIG.EMBED_ALBUM_ART or headphones.CONFIG.ADD_ALBUM_ART:
|
||||
|
||||
if album_art_path:
|
||||
artwork = request.request_content(album_art_path)
|
||||
else:
|
||||
artwork = None
|
||||
|
||||
if not album_art_path or not artwork or len(artwork) < 100:
|
||||
logger.info("No suitable album art found from Amazon. Checking Last.FM....")
|
||||
artwork = albumart.getCachedArt(albumid)
|
||||
if not artwork or len(artwork) < 100:
|
||||
artwork = False
|
||||
logger.info("No suitable album art found from Last.FM. Not adding album art")
|
||||
if headphones.CONFIG.EMBED_ALBUM_ART or headphones.CONFIG.ADD_ALBUM_ART or \
|
||||
(headphones.CONFIG.PLEX_ENABLED and headphones.CONFIG.PLEX_NOTIFY) or \
|
||||
(headphones.CONFIG.XBMC_ENABLED and headphones.CONFIG.XBMC_NOTIFY):
|
||||
album_art_path, artwork = albumart.getAlbumArt(albumid)
|
||||
|
||||
if headphones.CONFIG.EMBED_ALBUM_ART and artwork:
|
||||
embedAlbumArt(artwork, downloaded_track_list)
|
||||
@@ -446,12 +480,12 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
[albumid])
|
||||
|
||||
# Check if torrent has finished seeding
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1 or headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER != 0:
|
||||
seed_snatched = myDB.action(
|
||||
'SELECT * from snatched WHERE Status="Seed_Snatched" and AlbumID=?',
|
||||
[albumid]).fetchone()
|
||||
if seed_snatched:
|
||||
hash = seed_snatched['FolderName']
|
||||
hash = seed_snatched['TorrentHash']
|
||||
torrent_removed = False
|
||||
logger.info(u'%s - %s. Checking if torrent has finished seeding and can be removed' % (
|
||||
release['ArtistName'], release['AlbumTitle']))
|
||||
@@ -459,8 +493,10 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
torrent_removed = transmission.removeTorrent(hash, True)
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
|
||||
torrent_removed = deluge.removeTorrent(hash, True)
|
||||
else:
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
torrent_removed = utorrent.removeTorrent(hash, True)
|
||||
else:
|
||||
torrent_removed = qbittorrent.removeTorrent(hash, True)
|
||||
|
||||
# Torrent removed, delete the snatched record, else update Status for scheduled job to check
|
||||
if torrent_removed:
|
||||
@@ -538,6 +574,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
pushbullet = notifiers.PUSHBULLET()
|
||||
pushbullet.notify(pushmessage, statusmessage)
|
||||
|
||||
if headphones.CONFIG.JOIN_ENABLED:
|
||||
logger.info(u"Join request")
|
||||
join = notifiers.JOIN()
|
||||
join.notify(pushmessage, statusmessage)
|
||||
|
||||
if headphones.CONFIG.TELEGRAM_ENABLED:
|
||||
logger.info(u"Telegram request")
|
||||
telegram = notifiers.TELEGRAM()
|
||||
@@ -918,12 +959,29 @@ def correctMetadata(albumid, release, downloaded_track_list):
|
||||
if not items:
|
||||
continue
|
||||
|
||||
search_ids = []
|
||||
logger.debug('Getting recommendation from beets. Artist: %s. Album: %s. Tracks: %s', release['ArtistName'],
|
||||
release['AlbumTitle'], len(items))
|
||||
|
||||
# Try with specific release, e.g. alternate release selected from albumPage
|
||||
if release['ReleaseID'] != release['AlbumID']:
|
||||
logger.debug('trying beets with specific Release ID: %s', release['ReleaseID'])
|
||||
search_ids = [release['ReleaseID']]
|
||||
|
||||
try:
|
||||
cur_artist, cur_album, candidates, rec = autotag.tag_album(items,
|
||||
search_artist=helpers.latinToAscii(
|
||||
release['ArtistName']),
|
||||
search_album=helpers.latinToAscii(
|
||||
release['AlbumTitle']))
|
||||
beetslog = beetslogging.getLogger('beets')
|
||||
beetslog.set_global_level(beetslogging.DEBUG) if headphones.VERBOSE else beetslog.set_global_level(
|
||||
beetslogging.CRITICAL)
|
||||
with helpers.capture_beets_log() as logs:
|
||||
cur_artist, cur_album, prop = autotag.tag_album(items,
|
||||
search_artist=release['ArtistName'],
|
||||
search_album=release['AlbumTitle'],
|
||||
search_ids=search_ids)
|
||||
candidates = prop.candidates
|
||||
rec = prop.recommendation
|
||||
for log in logs:
|
||||
logger.debug('Beets: %s', log)
|
||||
beetslog.set_global_level(beetslogging.NOTSET)
|
||||
except Exception as e:
|
||||
logger.error('Error getting recommendation: %s. Not writing metadata', e)
|
||||
return False
|
||||
@@ -1186,7 +1244,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
'Found a match in the database: %s. Verifying to make sure it is the correct album',
|
||||
snatched['Title'])
|
||||
verify(snatched['AlbumID'], folder, snatched['Kind'],
|
||||
keep_original_folder=keep_original_folder)
|
||||
forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
|
||||
# Attempt 2: strip release group id from filename
|
||||
@@ -1213,7 +1271,8 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
else:
|
||||
logger.info(
|
||||
'Found a (possibly) valid Musicbrainz release group id in album folder name.')
|
||||
verify(rgid, folder, forced=True)
|
||||
verify(rgid, folder, forced=True,
|
||||
keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
|
||||
# Attempt 3a: parse the folder name into a valid format
|
||||
@@ -1232,7 +1291,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
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, keep_original_folder=keep_original_folder)
|
||||
verify(release['AlbumID'], folder, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name,
|
||||
@@ -1244,7 +1303,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
rgid = None
|
||||
|
||||
if rgid:
|
||||
verify(rgid, folder, keep_original_folder=keep_original_folder)
|
||||
verify(rgid, folder, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
||||
@@ -1257,12 +1316,23 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
except Exception:
|
||||
name = album = None
|
||||
|
||||
# Check if there's a cue to split
|
||||
if headphones.CONFIG.CUE_SPLIT and not name and not album and helpers.cue_split(folder):
|
||||
try:
|
||||
name, album, year = helpers.extract_metadata(folder)
|
||||
except Exception:
|
||||
name = album = None
|
||||
# Not found from meta data, check if there's a cue to split and try meta data again
|
||||
kind = None
|
||||
if headphones.CONFIG.CUE_SPLIT and not name and not album:
|
||||
cue_folder = helpers.cue_split(folder, keep_original_folder=keep_original_folder)
|
||||
if cue_folder:
|
||||
try:
|
||||
name, album, year = helpers.extract_metadata(cue_folder)
|
||||
except Exception:
|
||||
name = album = None
|
||||
if name:
|
||||
folder = cue_folder
|
||||
if keep_original_folder:
|
||||
keep_original_folder = False
|
||||
kind = "cue_split"
|
||||
elif folder != cue_folder:
|
||||
cue_folder = os.path.split(cue_folder)[0]
|
||||
shutil.rmtree(cue_folder)
|
||||
|
||||
if name and album:
|
||||
release = myDB.action(
|
||||
@@ -1272,7 +1342,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
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, keep_original_folder=keep_original_folder)
|
||||
verify(release['AlbumID'], folder, Kind=kind, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name,
|
||||
@@ -1284,7 +1354,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
rgid = None
|
||||
|
||||
if rgid:
|
||||
verify(rgid, folder, keep_original_folder=keep_original_folder)
|
||||
verify(rgid, folder, Kind=kind, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
||||
@@ -1301,7 +1371,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
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, keep_original_folder=keep_original_folder)
|
||||
verify(release['AlbumID'], folder, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s',
|
||||
@@ -1313,7 +1383,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
rgid = None
|
||||
|
||||
if rgid:
|
||||
verify(rgid, folder, keep_original_folder=keep_original_folder)
|
||||
verify(rgid, folder, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
||||
|
||||
294
headphones/qbittorrent.py
Normal file
@@ -0,0 +1,294 @@
|
||||
# This file is part of Headphones.
|
||||
#
|
||||
# Headphones is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Headphones is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import urllib
|
||||
import urllib2
|
||||
import cookielib
|
||||
import json
|
||||
import time
|
||||
import mimetypes
|
||||
import random
|
||||
import string
|
||||
import os
|
||||
|
||||
import headphones
|
||||
|
||||
from headphones import logger
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class qbittorrentclient(object):
|
||||
|
||||
TOKEN_REGEX = "<div id='token' style='display:none;'>([^<>]+)</div>"
|
||||
UTSetting = namedtuple("UTSetting", ["name", "int", "str", "access"])
|
||||
|
||||
def __init__(self, base_url=None, username=None, password=None,):
|
||||
|
||||
host = headphones.CONFIG.QBITTORRENT_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.CONFIG.QBITTORRENT_USERNAME
|
||||
self.password = headphones.CONFIG.QBITTORRENT_PASSWORD
|
||||
self.cookiejar = cookielib.CookieJar()
|
||||
self.opener = self._make_opener()
|
||||
self._get_sid(self.base_url, self.username, self.password)
|
||||
|
||||
def _make_opener(self):
|
||||
# create opener with cookie handler to carry QBitTorrent SID cookie
|
||||
cookie_handler = urllib2.HTTPCookieProcessor(self.cookiejar)
|
||||
handlers = [cookie_handler]
|
||||
return urllib2.build_opener(*handlers)
|
||||
|
||||
def _get_sid(self, base_url, username, password):
|
||||
# login so we can capture SID cookie
|
||||
login_data = urllib.urlencode({'username': username, 'password': password})
|
||||
try:
|
||||
self.opener.open(base_url + '/login', login_data)
|
||||
except urllib2.URLError as err:
|
||||
logger.debug('Error getting SID. qBittorrent responded with error: ' + str(err.reason))
|
||||
return
|
||||
for cookie in self.cookiejar:
|
||||
logger.debug('login cookie: ' + cookie.name + ', value: ' + cookie.value)
|
||||
return
|
||||
|
||||
def _command(self, command, args=None, content_type=None, files=None):
|
||||
logger.debug('QBittorrent WebAPI Command: %s' % command)
|
||||
|
||||
url = self.base_url + '/' + command
|
||||
|
||||
data = None
|
||||
headers = dict()
|
||||
if content_type == 'multipart/form-data':
|
||||
data, headers = encode_multipart(args, files)
|
||||
else:
|
||||
if args:
|
||||
data = urllib.urlencode(args)
|
||||
if content_type:
|
||||
headers['Content-Type'] = content_type
|
||||
|
||||
logger.debug('%s' % json.dumps(headers, indent=4))
|
||||
logger.debug('%s' % data)
|
||||
|
||||
request = urllib2.Request(url, data, headers)
|
||||
try:
|
||||
response = self.opener.open(request)
|
||||
info = response.info()
|
||||
if info:
|
||||
if info.getheader('content-type'):
|
||||
if info.getheader('content-type') == 'application/json':
|
||||
resp = ''
|
||||
for line in response:
|
||||
resp = resp + line
|
||||
logger.debug('response code: %s' % str(response.code))
|
||||
logger.debug('response: %s' % resp)
|
||||
return response.code, json.loads(resp)
|
||||
logger.debug('response code: %s' % str(response.code))
|
||||
return response.code, None
|
||||
except urllib2.URLError as err:
|
||||
logger.debug('Failed URL: %s' % url)
|
||||
logger.debug('QBitTorrent webUI raised the following error: %s' % str(err))
|
||||
return None, None
|
||||
|
||||
def _get_list(self, **args):
|
||||
return self._command('query/torrents', args)
|
||||
|
||||
def _get_settings(self):
|
||||
status, value = self._command('query/preferences')
|
||||
logger.debug('get_settings() returned %d items' % len(value))
|
||||
return value
|
||||
|
||||
def get_savepath(self, hash):
|
||||
logger.debug('qb.get_savepath(%s)' % hash)
|
||||
status, torrentList = self._get_list()
|
||||
for torrent in torrentList:
|
||||
if torrent['hash']:
|
||||
if torrent['hash'].upper() == hash.upper():
|
||||
return torrent['save_path']
|
||||
return None
|
||||
|
||||
def start(self, hash):
|
||||
logger.debug('qb.start(%s)' % hash)
|
||||
args = {'hash': hash}
|
||||
return self._command('command/resume', args, 'application/x-www-form-urlencoded')
|
||||
|
||||
def pause(self, hash):
|
||||
logger.debug('qb.pause(%s)' % hash)
|
||||
args = {'hash': hash}
|
||||
return self._command('command/pause', args, 'application/x-www-form-urlencoded')
|
||||
|
||||
def getfiles(self, hash):
|
||||
logger.debug('qb.getfiles(%s)' % hash)
|
||||
return self._command('query/propertiesFiles/' + hash)
|
||||
|
||||
def getprops(self, hash):
|
||||
logger.debug('qb.getprops(%s)' % hash)
|
||||
return self._command('query/propertiesGeneral/' + hash)
|
||||
|
||||
def setprio(self, hash, priority):
|
||||
logger.debug('qb.setprio(%s,%d)' % (hash, priority))
|
||||
args = {'hash': hash, 'priority': priority}
|
||||
return self._command('command/setFilePrio', args, 'application/x-www-form-urlencoded')
|
||||
|
||||
def remove(self, hash, remove_data=False):
|
||||
logger.debug('qb.remove(%s,%s)' % (hash, remove_data))
|
||||
|
||||
args = {'hashes': hash}
|
||||
if remove_data:
|
||||
command = 'command/deletePerm'
|
||||
else:
|
||||
command = 'command/delete'
|
||||
return self._command(command, args, 'application/x-www-form-urlencoded')
|
||||
|
||||
|
||||
def removeTorrent(hash, remove_data=False):
|
||||
|
||||
logger.debug('removeTorrent(%s,%s)' % (hash, remove_data))
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
status, torrentList = qbclient._get_list()
|
||||
for torrent in torrentList:
|
||||
if torrent['hash'].upper() == hash.upper():
|
||||
if torrent['state'] == 'uploading' or torrent['state'] == 'stalledUP':
|
||||
logger.info('%s has finished seeding, removing torrent and data' % torrent['name'])
|
||||
qbclient.remove(hash, remove_data)
|
||||
return True
|
||||
else:
|
||||
logger.info('%s has not finished seeding yet, torrent will not be removed, will try again on next run' % torrent['name'])
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def addTorrent(link):
|
||||
logger.debug('addTorrent(%s)' % link)
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
args = {'urls': link, 'savepath': headphones.CONFIG.DOWNLOAD_TORRENT_DIR}
|
||||
if headphones.CONFIG.QBITTORRENT_LABEL:
|
||||
args['category'] = headphones.CONFIG.QBITTORRENT_LABEL
|
||||
|
||||
return qbclient._command('command/download', args, 'multipart/form-data')
|
||||
|
||||
|
||||
def addFile(data):
|
||||
logger.debug('addFile(data)')
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
files = {'torrents': {'filename': '', 'content': data}}
|
||||
|
||||
return qbclient._command('command/upload', filelist=files)
|
||||
|
||||
|
||||
def getName(hash):
|
||||
logger.debug('getName(%s)' % hash)
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
|
||||
tries = 1
|
||||
while tries <= 6:
|
||||
time.sleep(10)
|
||||
status, torrentlist = qbclient._get_list()
|
||||
for torrent in torrentlist:
|
||||
if torrent['hash'].lower() == hash.lower():
|
||||
return torrent['name']
|
||||
tries += 1
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def getFolder(hash):
|
||||
logger.debug('getFolder(%s)' % hash)
|
||||
|
||||
torrent_folder = None
|
||||
single_file = False
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
|
||||
try:
|
||||
status, torrent_files = qbclient.getfiles(hash.lower())
|
||||
if torrent_files:
|
||||
if len(torrent_files) == 1:
|
||||
torrent_folder = torrent_files[0]['name']
|
||||
single_file = True
|
||||
else:
|
||||
torrent_folder = os.path.split(torrent_files[0]['name'])[0]
|
||||
single_file = False
|
||||
except:
|
||||
torrent_folder = None
|
||||
single_file = False
|
||||
|
||||
return torrent_folder, single_file
|
||||
|
||||
|
||||
_BOUNDARY_CHARS = string.digits + string.ascii_letters
|
||||
|
||||
|
||||
# Taken from http://code.activestate.com/recipes/578668-encode-multipart-form-data-for-uploading-files-via/
|
||||
# "MIT License" which is compatible with GPL
|
||||
def encode_multipart(args, files, boundary=None):
|
||||
logger.debug('encode_multipart()')
|
||||
|
||||
def escape_quote(s):
|
||||
return s.replace('"', '\\"')
|
||||
|
||||
if boundary is None:
|
||||
boundary = ''.join(random.choice(_BOUNDARY_CHARS) for i in range(30))
|
||||
lines = []
|
||||
|
||||
if args:
|
||||
for name, value in args.items():
|
||||
lines.extend((
|
||||
'--{0}'.format(boundary),
|
||||
'Content-Disposition: form-data; name="{0}"'.format(escape_quote(name)),
|
||||
'',
|
||||
str(value),
|
||||
))
|
||||
logger.debug(''.join(lines))
|
||||
|
||||
if files:
|
||||
for name, value in files.items():
|
||||
filename = value['filename']
|
||||
if 'mimetype' in value:
|
||||
mimetype = value['mimetype']
|
||||
else:
|
||||
mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||
lines.extend((
|
||||
'--{0}'.format(boundary),
|
||||
'Content-Disposition: form-data; name="{0}"; filename="{1}"'.format(
|
||||
escape_quote(name), escape_quote(filename)),
|
||||
'Content-Type: {0}'.format(mimetype),
|
||||
'',
|
||||
value['content'],
|
||||
))
|
||||
|
||||
lines.extend((
|
||||
'--{0}--'.format(boundary),
|
||||
'',
|
||||
))
|
||||
body = '\r\n'.join(lines)
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'multipart/form-data; boundary={0}'.format(boundary),
|
||||
'Content-Length': str(len(body)),
|
||||
}
|
||||
|
||||
return (body, headers)
|
||||
@@ -6,6 +6,7 @@ from urlparse import urlparse
|
||||
import re
|
||||
|
||||
import requests as requests
|
||||
# from requests.auth import HTTPDigestAuth
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
import headphones
|
||||
@@ -24,7 +25,7 @@ class Rutracker(object):
|
||||
return self.loggedin
|
||||
|
||||
def still_logged_in(self, html):
|
||||
if not html or "action=\"http://login.rutracker.org/forum/login.php\">" in html:
|
||||
if not html or "action=\"http://rutracker.org/forum/login.php\">" in html:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@@ -34,7 +35,7 @@ class Rutracker(object):
|
||||
Logs in user
|
||||
"""
|
||||
|
||||
loginpage = 'http://login.rutracker.org/forum/login.php'
|
||||
loginpage = 'http://rutracker.org/forum/login.php'
|
||||
post_params = {
|
||||
'login_username': headphones.CONFIG.RUTRACKER_USER,
|
||||
'login_password': headphones.CONFIG.RUTRACKER_PASSWORD,
|
||||
@@ -44,28 +45,34 @@ class Rutracker(object):
|
||||
logger.info("Attempting to log in to rutracker...")
|
||||
|
||||
try:
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout)
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
|
||||
# try again
|
||||
if 'bb_data' not in r.cookies.keys():
|
||||
if not self.has_bb_session_cookie(r):
|
||||
time.sleep(10)
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout)
|
||||
if r.status_code != 200:
|
||||
logger.error("rutracker login returned status code %s" % r.status_code)
|
||||
self.loggedin = False
|
||||
else:
|
||||
if 'bb_data' in r.cookies.keys():
|
||||
self.loggedin = True
|
||||
logger.info("Successfully logged in to rutracker")
|
||||
if headphones.CONFIG.RUTRACKER_COOKIE:
|
||||
logger.info("Attempting to log in using predefined cookie...")
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False, cookies={'bb_session': headphones.CONFIG.RUTRACKER_COOKIE})
|
||||
else:
|
||||
logger.error(
|
||||
"Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later")
|
||||
self.loggedin = False
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
|
||||
if self.has_bb_session_cookie(r):
|
||||
self.loggedin = True
|
||||
logger.info("Successfully logged in to rutracker")
|
||||
else:
|
||||
logger.error(
|
||||
"Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later")
|
||||
self.loggedin = False
|
||||
return self.loggedin
|
||||
except Exception as e:
|
||||
logger.error("Unknown error logging in to rutracker: %s" % e)
|
||||
self.loggedin = False
|
||||
return self.loggedin
|
||||
|
||||
def has_bb_session_cookie(self, response):
|
||||
if 'bb_session' in response.cookies.keys():
|
||||
return True
|
||||
# Rutracker randomly send a 302 redirect code, cookie may be present in response history
|
||||
return next(('bb_session' in r.cookies.keys() for r in response.history), False)
|
||||
|
||||
def searchurl(self, artist, album, year, format):
|
||||
"""
|
||||
Return the search url
|
||||
@@ -91,7 +98,11 @@ class Rutracker(object):
|
||||
|
||||
# sort by size, descending.
|
||||
sort = '&o=7&s=2'
|
||||
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
|
||||
try:
|
||||
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
|
||||
except:
|
||||
searchterm = searchterm.encode('utf-8')
|
||||
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
|
||||
logger.info("Searching rutracker using term: %s", searchterm)
|
||||
return searchurl
|
||||
|
||||
@@ -167,7 +178,7 @@ class Rutracker(object):
|
||||
return the .torrent data
|
||||
"""
|
||||
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
|
||||
downloadurl = 'http://dl.rutracker.org/forum/dl.php?t=' + torrent_id
|
||||
downloadurl = 'http://rutracker.org/forum/dl.php?t=' + torrent_id
|
||||
cookie = {'bb_dl': torrent_id}
|
||||
try:
|
||||
headers = {'Referer': url}
|
||||
@@ -214,3 +225,37 @@ class Rutracker(object):
|
||||
self.session.post(url, params={'action': 'add-file'}, files=files)
|
||||
except Exception as e:
|
||||
logger.exception('Error adding file to utorrent %s', e)
|
||||
|
||||
# TODO get this working in qbittorrent.py
|
||||
def qbittorrent_add_file(self, data):
|
||||
host = headphones.CONFIG.QBITTORRENT_HOST
|
||||
if not host.startswith('http'):
|
||||
host = 'http://' + host
|
||||
if host.endswith('/'):
|
||||
host = host[:-1]
|
||||
if host.endswith('/gui'):
|
||||
host = host[:-4]
|
||||
base_url = host
|
||||
|
||||
# self.session.auth = HTTPDigestAuth(headphones.CONFIG.QBITTORRENT_USERNAME, headphones.CONFIG.QBITTORRENT_PASSWORD)
|
||||
|
||||
url = base_url + '/login'
|
||||
try:
|
||||
self.session.post(url, data={'username': headphones.CONFIG.QBITTORRENT_USERNAME,
|
||||
'password': headphones.CONFIG.QBITTORRENT_PASSWORD})
|
||||
except Exception as e:
|
||||
logger.exception('Error adding file to qbittorrent %s', e)
|
||||
return
|
||||
|
||||
url = base_url + '/command/upload'
|
||||
|
||||
args = {'savepath': headphones.CONFIG.DOWNLOAD_TORRENT_DIR}
|
||||
if headphones.CONFIG.QBITTORRENT_LABEL:
|
||||
args['category'] = headphones.CONFIG.QBITTORRENT_LABEL
|
||||
|
||||
torrent_files = {'torrents': data}
|
||||
|
||||
try:
|
||||
self.session.post(url, data=args, files=torrent_files)
|
||||
except Exception as e:
|
||||
logger.exception('Error adding file to qbittorrent %s', e)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
|
||||
# t411 support added by a1ex, @likeitneverwentaway on github for maintenance
|
||||
|
||||
from base64 import b16encode, b32decode
|
||||
from hashlib import sha1
|
||||
@@ -24,29 +25,33 @@ import datetime
|
||||
import subprocess
|
||||
import unicodedata
|
||||
import urlparse
|
||||
from json import loads
|
||||
|
||||
import os
|
||||
import re
|
||||
from pygazelle import api as gazelleapi
|
||||
from pygazelle import encoding as gazelleencoding
|
||||
from pygazelle import format as gazelleformat
|
||||
from pygazelle import release_type as gazellerelease_type
|
||||
import headphones
|
||||
from headphones.common import USER_AGENT
|
||||
from headphones import logger, db, helpers, classes, sab, nzbget, request
|
||||
from headphones import utorrent, transmission, notifiers, rutracker, deluge
|
||||
from headphones import utorrent, transmission, notifiers, rutracker, deluge, qbittorrent
|
||||
from bencode import bencode, bdecode
|
||||
|
||||
|
||||
# Magnet to torrent services, for Black hole. Stolen from CouchPotato.
|
||||
TORRENT_TO_MAGNET_SERVICES = [
|
||||
# 'https://zoink.it/torrent/%s.torrent',
|
||||
# 'http://torrage.com/torrent/%s.torrent',
|
||||
'https://torcache.net/torrent/%s.torrent',
|
||||
# 'https://torcache.net/torrent/%s.torrent',
|
||||
'http://itorrents.org/torrent/%s.torrent',
|
||||
]
|
||||
|
||||
# Persistent What.cd API object
|
||||
gazelle = None
|
||||
# Persistent Apollo.rip API object
|
||||
apolloobj = None
|
||||
ruobj = None
|
||||
# Persistent RED API object
|
||||
redobj = None
|
||||
|
||||
|
||||
def fix_url(s, charset="utf-8"):
|
||||
@@ -159,8 +164,10 @@ def get_seed_ratio(provider):
|
||||
seed_ratio = headphones.CONFIG.RUTRACKER_RATIO
|
||||
elif provider == 'Kick Ass Torrents':
|
||||
seed_ratio = headphones.CONFIG.KAT_RATIO
|
||||
elif provider == 'What.cd':
|
||||
seed_ratio = headphones.CONFIG.WHATCD_RATIO
|
||||
elif provider == 'Apollo.rip':
|
||||
seed_ratio = headphones.CONFIG.APOLLO_RATIO
|
||||
elif provider == 'Redacted':
|
||||
seed_ratio = headphones.CONFIG.REDACTED_RATIO
|
||||
elif provider == 'The Pirate Bay':
|
||||
seed_ratio = headphones.CONFIG.PIRATEBAY_RATIO
|
||||
elif provider == 'Old Pirate Bay':
|
||||
@@ -199,14 +206,13 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
|
||||
continue
|
||||
|
||||
if headphones.CONFIG.WAIT_UNTIL_RELEASE_DATE and album['ReleaseDate']:
|
||||
try:
|
||||
release_date = datetime.datetime.strptime(album['ReleaseDate'], "%Y-%m-%d")
|
||||
except:
|
||||
logger.warn(
|
||||
"No valid date for: %s. Skipping automatic search" % album['AlbumTitle'])
|
||||
release_date = strptime_musicbrainz(album['ReleaseDate'])
|
||||
if not release_date:
|
||||
logger.warn("No valid date for: %s. Skipping automatic search" %
|
||||
album['AlbumTitle'])
|
||||
continue
|
||||
|
||||
if release_date > datetime.datetime.today():
|
||||
elif release_date > datetime.datetime.today():
|
||||
logger.info("Skipping: %s. Waiting for release date of: %s" % (
|
||||
album['AlbumTitle'], album['ReleaseDate']))
|
||||
continue
|
||||
@@ -235,6 +241,26 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
|
||||
logger.info('Search for wanted albums complete')
|
||||
|
||||
|
||||
def strptime_musicbrainz(date_str):
|
||||
"""
|
||||
Release date as returned by Musicbrainz may contain the full date (Year-Month-Day)
|
||||
but it may as well be just Year-Month or even just the year.
|
||||
|
||||
Args:
|
||||
date_str: the date as a string (ex: "2003-05-01", "2003-03", "2003")
|
||||
|
||||
Returns:
|
||||
The more accurate datetime object we can create or None if parse failed
|
||||
"""
|
||||
acceptable_formats = ('%Y-%m-%d', '%Y-%m', '%Y')
|
||||
for date_format in acceptable_formats:
|
||||
try:
|
||||
return datetime.datetime.strptime(date_str, date_format)
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
|
||||
NZB_PROVIDERS = (headphones.CONFIG.HEADPHONES_INDEXER or
|
||||
headphones.CONFIG.NEWZNAB or
|
||||
@@ -252,8 +278,10 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
|
||||
headphones.CONFIG.MININOVA or
|
||||
headphones.CONFIG.WAFFLES or
|
||||
headphones.CONFIG.RUTRACKER or
|
||||
headphones.CONFIG.WHATCD or
|
||||
headphones.CONFIG.STRIKE)
|
||||
headphones.CONFIG.APOLLO or
|
||||
headphones.CONFIG.REDACTED or
|
||||
headphones.CONFIG.STRIKE or
|
||||
headphones.CONFIG.TQUATTRECENTONZE)
|
||||
|
||||
results = []
|
||||
myDB = db.DBConnection()
|
||||
@@ -714,7 +742,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None,
|
||||
}
|
||||
|
||||
data = request.request_json(
|
||||
url='http://api.omgwtfnzbs.org/json/',
|
||||
url='http://api.omgwtfnzbs.me/json/',
|
||||
params=params, headers=headers
|
||||
)
|
||||
|
||||
@@ -813,6 +841,19 @@ def send_to_downloader(data, bestqual, album):
|
||||
torrent_name = helpers.replace_illegal_chars(folder_name) + '.torrent'
|
||||
download_path = os.path.join(headphones.CONFIG.TORRENTBLACKHOLE_DIR, torrent_name)
|
||||
|
||||
# Blackhole for t411
|
||||
if bestqual[2].lower().startswith("http://api.t411"):
|
||||
if headphones.CONFIG.MAGNET_LINKS == 2:
|
||||
try:
|
||||
url = bestqual[2].split('TOKEN')[0]
|
||||
token = bestqual[2].split('TOKEN')[1]
|
||||
data = request.request_content(url, headers={'Authorization': token})
|
||||
torrent_to_file(download_path, data)
|
||||
logger.info('Successfully converted magnet to torrent file')
|
||||
except Exception as e:
|
||||
logger.error("Error converting magnet link: %s" % str(e))
|
||||
return
|
||||
|
||||
if bestqual[2].lower().startswith("magnet:"):
|
||||
if headphones.CONFIG.MAGNET_LINKS == 1:
|
||||
try:
|
||||
@@ -838,12 +879,11 @@ def send_to_downloader(data, bestqual, album):
|
||||
services = TORRENT_TO_MAGNET_SERVICES[:]
|
||||
random.shuffle(services)
|
||||
headers = {'User-Agent': USER_AGENT}
|
||||
headers['Referer'] = 'https://torcache.net/'
|
||||
|
||||
for service in services:
|
||||
|
||||
data = request.request_content(service % torrent_hash, headers=headers)
|
||||
if data and "torcache" in data:
|
||||
if data:
|
||||
if not torrent_to_file(download_path, data):
|
||||
return
|
||||
# Extract folder name from torrent
|
||||
@@ -889,11 +929,11 @@ def send_to_downloader(data, bestqual, album):
|
||||
logger.error("Error sending torrent to Transmission. Are you sure it's running?")
|
||||
return
|
||||
|
||||
folder_name = transmission.getTorrentFolder(torrentid)
|
||||
folder_name = transmission.getName(torrentid)
|
||||
if folder_name:
|
||||
logger.info('Torrent folder name: %s' % folder_name)
|
||||
logger.info('Torrent name: %s' % folder_name)
|
||||
else:
|
||||
logger.error('Torrent folder name could not be determined')
|
||||
logger.error('Torrent name could not be determined')
|
||||
return
|
||||
|
||||
# Set Seed Ratio
|
||||
@@ -943,7 +983,7 @@ def send_to_downloader(data, bestqual, album):
|
||||
except Exception as e:
|
||||
logger.error('Error sending torrent to Deluge: %s' % str(e))
|
||||
|
||||
else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
logger.info("Sending torrent to uTorrent")
|
||||
|
||||
# Add torrent
|
||||
@@ -974,19 +1014,42 @@ def send_to_downloader(data, bestqual, album):
|
||||
seed_ratio = get_seed_ratio(bestqual[3])
|
||||
if seed_ratio is not None:
|
||||
utorrent.setSeedRatio(torrentid, seed_ratio)
|
||||
else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 4:
|
||||
logger.info("Sending torrent to QBiTorrent")
|
||||
|
||||
# Add torrent
|
||||
if bestqual[3] == 'rutracker.org':
|
||||
ruobj.qbittorrent_add_file(data)
|
||||
else:
|
||||
qbittorrent.addTorrent(bestqual[2])
|
||||
|
||||
# Get hash
|
||||
torrentid = calculate_torrent_hash(bestqual[2], data)
|
||||
torrentid = torrentid.lower()
|
||||
if not torrentid:
|
||||
logger.error('Torrent id could not be determined')
|
||||
return
|
||||
|
||||
# Get name
|
||||
folder_name = qbittorrent.getName(torrentid)
|
||||
if folder_name:
|
||||
logger.info('Torrent name: %s' % folder_name)
|
||||
else:
|
||||
logger.error('Torrent name could not be determined')
|
||||
return
|
||||
|
||||
myDB = db.DBConnection()
|
||||
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']])
|
||||
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)',
|
||||
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)',
|
||||
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name,
|
||||
kind])
|
||||
kind, torrentid])
|
||||
|
||||
# Store the torrent id so we can check later if it's finished seeding and can be removed
|
||||
if seed_ratio is not None and seed_ratio != 0 and torrentid:
|
||||
myDB.action(
|
||||
'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)',
|
||||
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", torrentid,
|
||||
kind])
|
||||
'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)',
|
||||
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", folder_name,
|
||||
kind, torrentid])
|
||||
|
||||
# notify
|
||||
artist = album[1]
|
||||
@@ -1014,6 +1077,14 @@ def send_to_downloader(data, bestqual, album):
|
||||
logger.info(u"Sending PushBullet notification")
|
||||
pushbullet = notifiers.PUSHBULLET()
|
||||
pushbullet.notify(name, "Download started")
|
||||
if headphones.CONFIG.JOIN_ENABLED and headphones.CONFIG.JOIN_ONSNATCH:
|
||||
logger.info(u"Sending Join notification")
|
||||
join = notifiers.JOIN()
|
||||
join.notify(name, "Download started")
|
||||
if headphones.CONFIG.SLACK_ENABLED and headphones.CONFIG.SLACK_ONSNATCH:
|
||||
logger.info(u"Sending Slack notification")
|
||||
slack = notifiers.SLACK()
|
||||
slack.notify(name, "Download started")
|
||||
if headphones.CONFIG.TELEGRAM_ENABLED and headphones.CONFIG.TELEGRAM_ONSNATCH:
|
||||
logger.info(u"Sending Telegram notification")
|
||||
telegram = notifiers.TELEGRAM()
|
||||
@@ -1140,7 +1211,8 @@ def verifyresult(title, artistterm, term, lossless):
|
||||
|
||||
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
choose_specific_download=False):
|
||||
global gazelle # persistent what.cd api object to reduce number of login attempts
|
||||
global apolloobj # persistent apollo.rip api object to reduce number of login attempts
|
||||
global redobj # persistent redacted api object to reduce number of login attempts
|
||||
global ruobj # and rutracker
|
||||
|
||||
reldate = album['ReleaseDate']
|
||||
@@ -1221,9 +1293,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||
categories = "3040"
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
|
||||
categories = "3040,3010"
|
||||
categories = "3040,3010,3050"
|
||||
else:
|
||||
categories = "3010"
|
||||
categories = "3010,3050"
|
||||
|
||||
if album['Type'] == 'Other':
|
||||
categories = "3030"
|
||||
@@ -1233,8 +1305,12 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
|
||||
provider = torznab_host[0]
|
||||
|
||||
# Format Jackett provider
|
||||
if "api/v2.0/indexers" in torznab_host[0]:
|
||||
provider = "Jackett_" + provider.split("/indexers/", 1)[1].split('/', 1)[0]
|
||||
|
||||
# Request results
|
||||
logger.info('Parsing results from %s using search term: %s' % (torznab_host[0], term))
|
||||
logger.info('Parsing results from %s using search term: %s' % (provider, term))
|
||||
|
||||
headers = {'User-Agent': USER_AGENT}
|
||||
params = {
|
||||
@@ -1246,20 +1322,35 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
}
|
||||
|
||||
data = request.request_feed(
|
||||
url=torznab_host[0] + '/api?',
|
||||
url=torznab_host[0],
|
||||
params=params, headers=headers
|
||||
)
|
||||
|
||||
# Process feed
|
||||
if data:
|
||||
if not len(data.entries):
|
||||
logger.info(u"No results found from %s for %s", torznab_host[0], term)
|
||||
logger.info(u"No results found from %s for %s", provider, term)
|
||||
else:
|
||||
for item in data.entries:
|
||||
try:
|
||||
url = item.link
|
||||
title = item.title
|
||||
size = int(item.links[1]['length'])
|
||||
|
||||
# Torrentech hack - size currently not returned, make it up
|
||||
if 'torrentech' in torznab_host[0]:
|
||||
if albumlength:
|
||||
if 'Lossless' in title:
|
||||
size = albumlength / 1000 * 800 * 128
|
||||
elif 'MP3' in title:
|
||||
size = albumlength / 1000 * 320 * 128
|
||||
else:
|
||||
size = albumlength / 1000 * 256 * 128
|
||||
else:
|
||||
logger.info('Skipping %s, could not determine size' % title)
|
||||
continue
|
||||
else:
|
||||
size = int(item.links[1]['length'])
|
||||
|
||||
if all(word.lower() in title.lower() for word in term.split()):
|
||||
logger.info(
|
||||
'Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
@@ -1279,7 +1370,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
if headphones.CONFIG.KAT_PROXY_URL:
|
||||
providerurl = fix_url(set_proxy(headphones.CONFIG.KAT_PROXY_URL))
|
||||
else:
|
||||
providerurl = fix_url("https://kat.cr")
|
||||
providerurl = fix_url("https://katcr.co/new/")
|
||||
|
||||
# Build URL
|
||||
providerurl = providerurl + "/usearch/" + ka_term
|
||||
@@ -1355,8 +1446,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
query_items = [usersearchterm]
|
||||
|
||||
query_items.extend(['format:(%s)' % format,
|
||||
'size:[0 TO %d]' % maxsize,
|
||||
'-seeders:0']) # cut out dead torrents
|
||||
'size:[0 TO %d]' % maxsize])
|
||||
# (25/03/2017 Waffles back up after 5 months, all torrents currently have no seeders, remove for now)
|
||||
# '-seeders:0']) cut out dead torrents
|
||||
|
||||
if bitrate:
|
||||
query_items.append('bitrate:"%s"' % bitrate)
|
||||
@@ -1432,9 +1524,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
if rulist:
|
||||
resultlist.extend(rulist)
|
||||
|
||||
if headphones.CONFIG.WHATCD:
|
||||
provider = "What.cd"
|
||||
providerurl = "http://what.cd/"
|
||||
if headphones.CONFIG.APOLLO:
|
||||
provider = "Apollo.rip"
|
||||
providerurl = "http://apollo.rip/"
|
||||
|
||||
bitrate = None
|
||||
bitrate_string = bitrate
|
||||
@@ -1457,7 +1549,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
bitrate_string = encoding_string
|
||||
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
||||
logger.info(
|
||||
u"Your preferred bitrate is not one of the available What.cd filters, so not using it as a search parameter.")
|
||||
u"Your preferred bitrate is not one of the available Apollo.rip filters, so not using it as a search parameter.")
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
|
||||
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
||||
@@ -1466,27 +1558,157 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
search_formats = [gazelleformat.MP3]
|
||||
maxsize = 300000000
|
||||
|
||||
if not gazelle or not gazelle.logged_in():
|
||||
if not apolloobj or not apolloobj.logged_in():
|
||||
try:
|
||||
logger.info(u"Attempting to log in to What.cd...")
|
||||
gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.WHATCD_USERNAME,
|
||||
headphones.CONFIG.WHATCD_PASSWORD)
|
||||
gazelle._login()
|
||||
logger.info(u"Attempting to log in to Apollo.rip...")
|
||||
apolloobj = gazelleapi.GazelleAPI(headphones.CONFIG.APOLLO_USERNAME,
|
||||
headphones.CONFIG.APOLLO_PASSWORD,
|
||||
headphones.CONFIG.APOLLO_URL)
|
||||
apolloobj._login()
|
||||
except Exception as e:
|
||||
gazelle = None
|
||||
logger.error(u"What.cd credentials incorrect or site is down. Error: %s %s" % (
|
||||
apolloobj = None
|
||||
logger.error(u"Apollo.rip credentials incorrect or site is down. Error: %s %s" % (
|
||||
e.__class__.__name__, str(e)))
|
||||
|
||||
if gazelle and gazelle.logged_in():
|
||||
if apolloobj and apolloobj.logged_in():
|
||||
logger.info(u"Searching %s..." % provider)
|
||||
all_torrents = []
|
||||
|
||||
# Specify release types to filter by
|
||||
if album['Type'] == 'Album':
|
||||
album_type = [gazellerelease_type.ALBUM]
|
||||
if album['Type'] == 'Soundtrack':
|
||||
album_type = [gazellerelease_type.SOUNDTRACK]
|
||||
if album['Type'] == 'EP':
|
||||
album_type = [gazellerelease_type.EP]
|
||||
# No musicbrainz match for this type
|
||||
# if album['Type'] == 'Anthology':
|
||||
# album_type = [gazellerelease_type.ANTHOLOGY]
|
||||
if album['Type'] == 'Compilation':
|
||||
album_type = [gazellerelease_type.COMPILATION]
|
||||
if album['Type'] == 'DJ-mix':
|
||||
album_type = [gazellerelease_type.DJ_MIX]
|
||||
if album['Type'] == 'Single':
|
||||
album_type = [gazellerelease_type.SINGLE]
|
||||
if album['Type'] == 'Live':
|
||||
album_type = [gazellerelease_type.LIVE_ALBUM]
|
||||
if album['Type'] == 'Remix':
|
||||
album_type = [gazellerelease_type.REMIX]
|
||||
if album['Type'] == 'Bootleg':
|
||||
album_type = [gazellerelease_type.BOOTLEG]
|
||||
if album['Type'] == 'Interview':
|
||||
album_type = [gazellerelease_type.INTERVIEW]
|
||||
if album['Type'] == 'Mixtape/Street':
|
||||
album_type = [gazellerelease_type.MIXTAPE]
|
||||
|
||||
for search_format in search_formats:
|
||||
if usersearchterm:
|
||||
all_torrents.extend(
|
||||
apolloobj.search_torrents(searchstr=usersearchterm, format=search_format,
|
||||
encoding=bitrate_string, releasetype=album_type)['results'])
|
||||
else:
|
||||
all_torrents.extend(apolloobj.search_torrents(artistname=semi_clean_artist_term,
|
||||
groupname=semi_clean_album_term,
|
||||
format=search_format,
|
||||
encoding=bitrate_string,
|
||||
releasetype=album_type)['results'])
|
||||
|
||||
# filter on format, size, and num seeders
|
||||
logger.info(u"Filtering torrents by format, maximum size, and minimum seeders...")
|
||||
match_torrents = [t for t in all_torrents if
|
||||
t.size <= maxsize and t.seeders >= minimumseeders]
|
||||
|
||||
logger.info(
|
||||
u"Remaining torrents: %s" % ", ".join(repr(torrent) for torrent in match_torrents))
|
||||
|
||||
# sort by times d/l'd
|
||||
if not len(match_torrents):
|
||||
logger.info(u"No results found from %s for %s after filtering" % (provider, term))
|
||||
elif len(match_torrents) > 1:
|
||||
logger.info(u"Found %d matching releases from %s for %s - %s after filtering" %
|
||||
(len(match_torrents), provider, artistterm, albumterm))
|
||||
logger.info(
|
||||
"Sorting torrents by times snatched and preferred bitrate %s..." % bitrate_string)
|
||||
match_torrents.sort(key=lambda x: int(x.snatched), reverse=True)
|
||||
if gazelleformat.MP3 in search_formats:
|
||||
# sort by size after rounding to nearest 10MB...hacky, but will favor highest quality
|
||||
match_torrents.sort(key=lambda x: int(10 * round(x.size / 1024. / 1024. / 10.)),
|
||||
reverse=True)
|
||||
if search_formats and None not in search_formats:
|
||||
match_torrents.sort(
|
||||
key=lambda x: int(search_formats.index(x.format))) # prefer lossless
|
||||
# if bitrate:
|
||||
# match_torrents.sort(key=lambda x: re.match("mp3", x.getTorrentDetails(), flags=re.I), reverse=True)
|
||||
# match_torrents.sort(key=lambda x: str(bitrate) in x.getTorrentFolderName(), reverse=True)
|
||||
logger.info(
|
||||
u"New order: %s" % ", ".join(repr(torrent) for torrent in match_torrents))
|
||||
|
||||
for torrent in match_torrents:
|
||||
if not torrent.file_path:
|
||||
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
||||
resultlist.append((torrent.file_path,
|
||||
torrent.size,
|
||||
apolloobj.generate_torrent_link(torrent.id),
|
||||
provider,
|
||||
'torrent', True))
|
||||
|
||||
# Redacted - Using same logic as What.CD as it's also Gazelle, so should really make this into something reusable
|
||||
if headphones.CONFIG.REDACTED:
|
||||
provider = "Redacted"
|
||||
providerurl = "https://redacted.ch"
|
||||
|
||||
bitrate = None
|
||||
bitrate_string = bitrate
|
||||
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly: # Lossless Only mode
|
||||
search_formats = [gazelleformat.FLAC]
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 2: # Preferred quality mode
|
||||
search_formats = [None] # should return all
|
||||
bitrate = headphones.CONFIG.PREFERRED_BITRATE
|
||||
if bitrate:
|
||||
if 225 <= int(bitrate) < 256:
|
||||
bitrate = 'V0'
|
||||
elif 200 <= int(bitrate) < 225:
|
||||
bitrate = 'V1'
|
||||
elif 175 <= int(bitrate) < 200:
|
||||
bitrate = 'V2'
|
||||
for encoding_string in gazelleencoding.ALL_ENCODINGS:
|
||||
if re.search(bitrate, encoding_string, flags=re.I):
|
||||
bitrate_string = encoding_string
|
||||
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
||||
logger.info(
|
||||
u"Your preferred bitrate is not one of the available RED filters, so not using it as a search parameter.")
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
|
||||
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
||||
maxsize = 10000000000
|
||||
else: # Highest quality excluding lossless
|
||||
search_formats = [gazelleformat.MP3]
|
||||
maxsize = 300000000
|
||||
|
||||
if not redobj or not redobj.logged_in():
|
||||
try:
|
||||
logger.info(u"Attempting to log in to Redacted...")
|
||||
redobj = gazelleapi.GazelleAPI(headphones.CONFIG.REDACTED_USERNAME,
|
||||
headphones.CONFIG.REDACTED_PASSWORD,
|
||||
providerurl)
|
||||
redobj._login()
|
||||
except Exception as e:
|
||||
redobj = None
|
||||
logger.error(u"Redacted credentials incorrect or site is down. Error: %s %s" % (
|
||||
e.__class__.__name__, str(e)))
|
||||
|
||||
if redobj and redobj.logged_in():
|
||||
logger.info(u"Searching %s..." % provider)
|
||||
all_torrents = []
|
||||
for search_format in search_formats:
|
||||
if usersearchterm:
|
||||
all_torrents.extend(
|
||||
gazelle.search_torrents(searchstr=usersearchterm, format=search_format,
|
||||
redobj.search_torrents(searchstr=usersearchterm, format=search_format,
|
||||
encoding=bitrate_string)['results'])
|
||||
else:
|
||||
all_torrents.extend(gazelle.search_torrents(artistname=semi_clean_artist_term,
|
||||
all_torrents.extend(redobj.search_torrents(artistname=semi_clean_artist_term,
|
||||
groupname=semi_clean_album_term,
|
||||
format=search_format,
|
||||
encoding=bitrate_string)['results'])
|
||||
@@ -1526,20 +1748,20 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
||||
resultlist.append((torrent.file_path,
|
||||
torrent.size,
|
||||
gazelle.generate_torrent_link(torrent.id),
|
||||
redobj.generate_torrent_link(torrent.id),
|
||||
provider,
|
||||
'torrent', True))
|
||||
|
||||
# Pirate Bay
|
||||
if headphones.CONFIG.PIRATEBAY:
|
||||
provider = "The Pirate Bay"
|
||||
tpb_term = term.replace("!", "")
|
||||
tpb_term = term.replace("!", "").replace("'", " ")
|
||||
|
||||
# Use proxy if specified
|
||||
if headphones.CONFIG.PIRATEBAY_PROXY_URL:
|
||||
providerurl = fix_url(set_proxy(headphones.CONFIG.PIRATEBAY_PROXY_URL))
|
||||
else:
|
||||
providerurl = fix_url("https://thepiratebay.se")
|
||||
providerurl = fix_url("https://thepiratebay.org")
|
||||
|
||||
# Build URL
|
||||
providerurl = providerurl + "/search/" + tpb_term + "/0/7/" # 7 is sort by seeders
|
||||
@@ -1763,6 +1985,77 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
resultlist.append((title, size, url, provider, 'torrent', match))
|
||||
except Exception as e:
|
||||
logger.exception("Unhandled exception in Mininova Parser")
|
||||
# t411
|
||||
if headphones.CONFIG.TQUATTRECENTONZE:
|
||||
username = headphones.CONFIG.TQUATTRECENTONZE_USER
|
||||
password = headphones.CONFIG.TQUATTRECENTONZE_PASSWORD
|
||||
API_URL = "http://api.t411.ai"
|
||||
AUTH_URL = API_URL + '/auth'
|
||||
DL_URL = API_URL + '/torrents/download/'
|
||||
provider = "t411"
|
||||
t411_term = term.replace(" ", "%20")
|
||||
SEARCH_URL = API_URL + '/torrents/search/' + t411_term + "?limit=15&cid=395&subcat=623"
|
||||
headers_login = {'username': username, 'password': password}
|
||||
|
||||
# Requesting content
|
||||
logger.info('Parsing results from t411 using search term: %s' % term)
|
||||
req = request.request_content(AUTH_URL, method='post', data=headers_login)
|
||||
|
||||
if len(req.split('"')) == 9:
|
||||
token = req.split('"')[7]
|
||||
headers_auth = {'Authorization': token}
|
||||
logger.info('t411 - User %s logged in' % username)
|
||||
else:
|
||||
logger.info('t411 - Login error : %s' % req.split('"')[3])
|
||||
|
||||
# Quality
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||
providerurl = fix_url(SEARCH_URL + "&term[16][]=529&term[16][]=1184")
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
|
||||
providerurl = fix_url(SEARCH_URL + "&term[16][]=685&term[16][]=527&term[16][]=1070&term[16][]=528&term[16][]=1167&term[16][]=1166&term[16][]=530&term[16][]=529&term[16][]=1184&term[16][]=532&term[16][]=533&term[16][]=1085&term[16][]=534&term[16][]=535&term[16][]=1069&term[16][]=537&term[16][]=538")
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 0:
|
||||
providerurl = fix_url(SEARCH_URL + "&term[16][]=685&term[16][]=527&term[16][]=1070&term[16][]=528&term[16][]=1167&term[16][]=1166&term[16][]=530&term[16][]=532&term[16][]=533&term[16][]=1085&term[16][]=534&term[16][]=535&term[16][]=1069&term[16][]=537&term[16][]=538")
|
||||
else:
|
||||
providerurl = fix_url(SEARCH_URL)
|
||||
|
||||
# Tracker search
|
||||
req = request.request_content(providerurl, headers=headers_auth)
|
||||
req = loads(req)
|
||||
total = req['total']
|
||||
|
||||
# Process feed
|
||||
if total == '0':
|
||||
logger.info("No results found from t411 for %s" % term)
|
||||
else:
|
||||
logger.info('Found %s results from t411' % total)
|
||||
torrents = req['torrents']
|
||||
for torrent in torrents:
|
||||
try:
|
||||
title = torrent['name']
|
||||
if torrent['seeders'] < minimumseeders:
|
||||
logger.info('Skipping torrent %s : seeders below minimum set' % title)
|
||||
continue
|
||||
id = torrent['id']
|
||||
size = int(torrent['size'])
|
||||
data = request.request_content(DL_URL + id, headers=headers_auth)
|
||||
|
||||
# Blackhole
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 0 and headphones.CONFIG.MAGNET_LINKS == 2:
|
||||
url = DL_URL + id + 'TOKEN' + token
|
||||
resultlist.append((title, size, url, provider, 'torrent', True))
|
||||
|
||||
# Build magnet
|
||||
else:
|
||||
metadata = bdecode(data)
|
||||
hashcontents = bencode(metadata['info'])
|
||||
digest = sha1(hashcontents).hexdigest()
|
||||
trackers = [metadata["announce"]][0]
|
||||
url = 'magnet:?xt=urn:btih:%s&tr=%s' % (digest, trackers)
|
||||
resultlist.append((title, size, url, provider, 'torrent', True))
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error converting magnet link: %s" % str(e))
|
||||
return
|
||||
|
||||
# attempt to verify that this isn't a substring result
|
||||
# when looking for "Foo - Foo" we don't want "Foobar"
|
||||
@@ -1800,7 +2093,9 @@ def preprocess(resultlist):
|
||||
if result[3] == 'Kick Ass Torrents':
|
||||
headers['Referer'] = 'https://torcache.net/'
|
||||
headers['User-Agent'] = USER_AGENT
|
||||
elif result[3] == 'What.cd':
|
||||
elif result[3] == 'Apollo.rip':
|
||||
headers['User-Agent'] = 'Headphones'
|
||||
elif result[3] == 'Redacted':
|
||||
headers['User-Agent'] = 'Headphones'
|
||||
elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay":
|
||||
headers[
|
||||
|
||||
@@ -13,13 +13,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import threading
|
||||
|
||||
from headphones import db, utorrent, transmission, logger
|
||||
from headphones import db, utorrent, transmission, deluge, qbittorrent, logger
|
||||
import headphones
|
||||
|
||||
postprocessor_lock = threading.Lock()
|
||||
|
||||
|
||||
def checkTorrentFinished():
|
||||
"""
|
||||
@@ -28,21 +25,25 @@ def checkTorrentFinished():
|
||||
|
||||
logger.info("Checking if any torrents have finished seeding and can be removed")
|
||||
|
||||
with postprocessor_lock:
|
||||
myDB = db.DBConnection()
|
||||
results = myDB.select('SELECT * from snatched WHERE Status="Seed_Processed"')
|
||||
myDB = db.DBConnection()
|
||||
results = myDB.select('SELECT * from snatched WHERE Status="Seed_Processed"')
|
||||
|
||||
for album in results:
|
||||
hash = album['FolderName']
|
||||
albumid = album['AlbumID']
|
||||
torrent_removed = False
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
|
||||
torrent_removed = transmission.removeTorrent(hash, True)
|
||||
else:
|
||||
torrent_removed = utorrent.removeTorrent(hash, True)
|
||||
for album in results:
|
||||
hash = album['TorrentHash']
|
||||
albumid = album['AlbumID']
|
||||
torrent_removed = False
|
||||
|
||||
if torrent_removed:
|
||||
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?',
|
||||
[albumid])
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
|
||||
torrent_removed = transmission.removeTorrent(hash, True)
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
torrent_removed = utorrent.removeTorrent(hash, True)
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3:
|
||||
torrent_removed = deluge.removeTorrent(hash, True)
|
||||
else:
|
||||
torrent_removed = qbittorrent.removeTorrent(hash, True)
|
||||
|
||||
if torrent_removed:
|
||||
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?',
|
||||
[albumid])
|
||||
|
||||
logger.info("Checking finished torrents completed")
|
||||
|
||||
@@ -17,6 +17,7 @@ import time
|
||||
import json
|
||||
import base64
|
||||
import urlparse
|
||||
import os
|
||||
|
||||
from headphones import logger, request
|
||||
import headphones
|
||||
@@ -33,7 +34,7 @@ _session_id = None
|
||||
def addTorrent(link, data=None):
|
||||
method = 'torrent-add'
|
||||
|
||||
if link.endswith('.torrent') and not link.startswith('http') or data:
|
||||
if link.endswith('.torrent') and not link.startswith(('http', 'magnet')) or data:
|
||||
if data:
|
||||
metainfo = str(base64.b64encode(data))
|
||||
else:
|
||||
@@ -64,7 +65,31 @@ def addTorrent(link, data=None):
|
||||
return False
|
||||
|
||||
|
||||
def getTorrentFolder(torrentid):
|
||||
def getFolder(torrentid):
|
||||
torrent_folder = None
|
||||
single_file = False
|
||||
method = 'torrent-get'
|
||||
arguments = {'ids': torrentid, 'fields': ['files']}
|
||||
|
||||
response = torrentAction(method, arguments)
|
||||
|
||||
try:
|
||||
torrent_files = response['arguments']['torrents'][0]['files']
|
||||
if torrent_files:
|
||||
if len(torrent_files) == 1:
|
||||
torrent_folder = torrent_files[0]['name']
|
||||
single_file = True
|
||||
else:
|
||||
torrent_folder = os.path.split(torrent_files[0]['name'])[0]
|
||||
single_file = False
|
||||
except:
|
||||
torrent_folder = None
|
||||
single_file = False
|
||||
|
||||
return torrent_folder, single_file
|
||||
|
||||
|
||||
def getName(torrentid):
|
||||
method = 'torrent-get'
|
||||
arguments = {'ids': torrentid, 'fields': ['name', 'percentDone']}
|
||||
|
||||
|
||||
@@ -39,14 +39,15 @@ def runGit(args):
|
||||
|
||||
try:
|
||||
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + headphones.PROG_DIR)
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True,
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
shell=True,
|
||||
cwd=headphones.PROG_DIR)
|
||||
output, err = p.communicate()
|
||||
output = output.strip()
|
||||
|
||||
logger.debug('Git output: ' + output)
|
||||
except OSError:
|
||||
logger.debug('Command failed: %s', cmd)
|
||||
except OSError as e:
|
||||
logger.debug('Command failed: %s. Error: %s' % (cmd, e))
|
||||
continue
|
||||
|
||||
if 'not found' in output or "not recognized as an internal or external command" in output:
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
|
||||
# t411 support added by a1ex, @likeitneverwentaway on github for maintenance
|
||||
|
||||
from operator import itemgetter
|
||||
import threading
|
||||
@@ -28,7 +29,7 @@ import urllib2
|
||||
|
||||
import os
|
||||
import re
|
||||
from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers
|
||||
from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers, crier
|
||||
from headphones.helpers import checked, radio, today, clean_name
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako import exceptions
|
||||
@@ -69,6 +70,11 @@ class WebInterface(object):
|
||||
artists = myDB.select('SELECT * from artists order by ArtistSortName COLLATE NOCASE')
|
||||
return serve_template(templatename="index.html", title="Home", artists=artists)
|
||||
|
||||
@cherrypy.expose
|
||||
def threads(self):
|
||||
crier.cry()
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
|
||||
@cherrypy.expose
|
||||
def artistPage(self, ArtistID):
|
||||
myDB = db.DBConnection()
|
||||
@@ -246,7 +252,10 @@ class WebInterface(object):
|
||||
namecheck = myDB.select('SELECT ArtistName from artists where ArtistID=?', [ArtistID])
|
||||
for name in namecheck:
|
||||
artistname = name['ArtistName']
|
||||
logger.info(u"Deleting all traces of artist: " + artistname)
|
||||
try:
|
||||
logger.info(u"Deleting all traces of artist: " + artistname)
|
||||
except TypeError:
|
||||
logger.info(u"Deleting all traces of artist: null")
|
||||
myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID])
|
||||
|
||||
from headphones import cache
|
||||
@@ -339,14 +348,18 @@ class WebInterface(object):
|
||||
|
||||
dirs = set(dirs)
|
||||
|
||||
for dir in dirs:
|
||||
artistfolder = os.path.join(dir, folder)
|
||||
if not os.path.isdir(artistfolder.encode(headphones.SYS_ENCODING)):
|
||||
logger.debug("Cannot find directory: " + artistfolder)
|
||||
continue
|
||||
threading.Thread(target=librarysync.libraryScan,
|
||||
kwargs={"dir": artistfolder, "artistScan": True, "ArtistID": ArtistID,
|
||||
"ArtistName": artist_name}).start()
|
||||
try:
|
||||
for dir in dirs:
|
||||
artistfolder = os.path.join(dir, folder)
|
||||
if not os.path.isdir(artistfolder.encode(headphones.SYS_ENCODING)):
|
||||
logger.debug("Cannot find directory: " + artistfolder)
|
||||
continue
|
||||
threading.Thread(target=librarysync.libraryScan,
|
||||
kwargs={"dir": artistfolder, "artistScan": True, "ArtistID": ArtistID,
|
||||
"ArtistName": artist_name}).start()
|
||||
except Exception as e:
|
||||
logger.error('Unable to complete the scan: %s' % e)
|
||||
|
||||
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
|
||||
|
||||
@cherrypy.expose
|
||||
@@ -528,7 +541,7 @@ class WebInterface(object):
|
||||
myDB = db.DBConnection()
|
||||
upcoming = myDB.select(
|
||||
"SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate ASC")
|
||||
wanted = myDB.select("SELECT * from albums WHERE Status='Wanted'")
|
||||
wanted = myDB.select("SELECT * from albums WHERE Status='Wanted' order by ReleaseDate ASC")
|
||||
return serve_template(templatename="upcoming.html", title="Upcoming", upcoming=upcoming,
|
||||
wanted=wanted)
|
||||
|
||||
@@ -865,11 +878,19 @@ class WebInterface(object):
|
||||
def musicScan(self, path, scan=0, redirect=None, autoadd=0, libraryscan=0):
|
||||
headphones.CONFIG.LIBRARYSCAN = libraryscan
|
||||
headphones.CONFIG.AUTO_ADD_ARTISTS = autoadd
|
||||
headphones.CONFIG.MUSIC_DIR = path
|
||||
headphones.CONFIG.write()
|
||||
|
||||
try:
|
||||
params = {}
|
||||
headphones.CONFIG.MUSIC_DIR = path
|
||||
headphones.CONFIG.write()
|
||||
except Exception as e:
|
||||
logger.warn("Cannot save scan directory to config: %s", e)
|
||||
if scan:
|
||||
params = {"dir": path}
|
||||
|
||||
if scan:
|
||||
try:
|
||||
threading.Thread(target=librarysync.libraryScan).start()
|
||||
threading.Thread(target=librarysync.libraryScan, kwargs=params).start()
|
||||
except Exception as e:
|
||||
logger.error('Unable to complete the scan: %s' % e)
|
||||
if redirect:
|
||||
@@ -1152,6 +1173,10 @@ class WebInterface(object):
|
||||
"nzbget_password": headphones.CONFIG.NZBGET_PASSWORD,
|
||||
"nzbget_category": headphones.CONFIG.NZBGET_CATEGORY,
|
||||
"nzbget_priority": headphones.CONFIG.NZBGET_PRIORITY,
|
||||
"qbittorrent_host": headphones.CONFIG.QBITTORRENT_HOST,
|
||||
"qbittorrent_username": headphones.CONFIG.QBITTORRENT_USERNAME,
|
||||
"qbittorrent_password": headphones.CONFIG.QBITTORRENT_PASSWORD,
|
||||
"qbittorrent_label": headphones.CONFIG.QBITTORRENT_LABEL,
|
||||
"transmission_host": headphones.CONFIG.TRANSMISSION_HOST,
|
||||
"transmission_username": headphones.CONFIG.TRANSMISSION_USERNAME,
|
||||
"transmission_password": headphones.CONFIG.TRANSMISSION_PASSWORD,
|
||||
@@ -1172,6 +1197,7 @@ class WebInterface(object):
|
||||
"torrent_downloader_transmission": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 1),
|
||||
"torrent_downloader_utorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 2),
|
||||
"torrent_downloader_deluge": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 3),
|
||||
"torrent_downloader_qbittorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 4),
|
||||
"download_dir": headphones.CONFIG.DOWNLOAD_DIR,
|
||||
"use_blackhole": checked(headphones.CONFIG.BLACKHOLE),
|
||||
"blackhole_dir": headphones.CONFIG.BLACKHOLE_DIR,
|
||||
@@ -1219,12 +1245,21 @@ class WebInterface(object):
|
||||
"rutracker_user": headphones.CONFIG.RUTRACKER_USER,
|
||||
"rutracker_password": headphones.CONFIG.RUTRACKER_PASSWORD,
|
||||
"rutracker_ratio": headphones.CONFIG.RUTRACKER_RATIO,
|
||||
"use_whatcd": checked(headphones.CONFIG.WHATCD),
|
||||
"whatcd_username": headphones.CONFIG.WHATCD_USERNAME,
|
||||
"whatcd_password": headphones.CONFIG.WHATCD_PASSWORD,
|
||||
"whatcd_ratio": headphones.CONFIG.WHATCD_RATIO,
|
||||
"rutracker_cookie": headphones.CONFIG.RUTRACKER_COOKIE,
|
||||
"use_apollo": checked(headphones.CONFIG.APOLLO),
|
||||
"apollo_username": headphones.CONFIG.APOLLO_USERNAME,
|
||||
"apollo_password": headphones.CONFIG.APOLLO_PASSWORD,
|
||||
"apollo_ratio": headphones.CONFIG.APOLLO_RATIO,
|
||||
"apollo_url": headphones.CONFIG.APOLLO_URL,
|
||||
"use_redacted": checked(headphones.CONFIG.REDACTED),
|
||||
"redacted_username": headphones.CONFIG.REDACTED_USERNAME,
|
||||
"redacted_password": headphones.CONFIG.REDACTED_PASSWORD,
|
||||
"redacted_ratio": headphones.CONFIG.REDACTED_RATIO,
|
||||
"use_strike": checked(headphones.CONFIG.STRIKE),
|
||||
"strike_ratio": headphones.CONFIG.STRIKE_RATIO,
|
||||
"use_tquattrecentonze": checked(headphones.CONFIG.TQUATTRECENTONZE),
|
||||
"tquattrecentonze_user": headphones.CONFIG.TQUATTRECENTONZE_USER,
|
||||
"tquattrecentonze_password": headphones.CONFIG.TQUATTRECENTONZE_PASSWORD,
|
||||
"pref_qual_0": radio(headphones.CONFIG.PREFERRED_QUALITY, 0),
|
||||
"pref_qual_1": radio(headphones.CONFIG.PREFERRED_QUALITY, 1),
|
||||
"pref_qual_2": radio(headphones.CONFIG.PREFERRED_QUALITY, 2),
|
||||
@@ -1248,6 +1283,8 @@ class WebInterface(object):
|
||||
"keep_nfo": checked(headphones.CONFIG.KEEP_NFO),
|
||||
"add_album_art": checked(headphones.CONFIG.ADD_ALBUM_ART),
|
||||
"album_art_format": headphones.CONFIG.ALBUM_ART_FORMAT,
|
||||
"album_art_min_width": headphones.CONFIG.ALBUM_ART_MIN_WIDTH,
|
||||
"album_art_max_width": headphones.CONFIG.ALBUM_ART_MAX_WIDTH,
|
||||
"embed_album_art": checked(headphones.CONFIG.EMBED_ALBUM_ART),
|
||||
"embed_lyrics": checked(headphones.CONFIG.EMBED_LYRICS),
|
||||
"replace_existing_folders": checked(headphones.CONFIG.REPLACE_EXISTING_FOLDERS),
|
||||
@@ -1274,6 +1311,7 @@ class WebInterface(object):
|
||||
"magnet_links_3": radio(headphones.CONFIG.MAGNET_LINKS, 3),
|
||||
"log_dir": headphones.CONFIG.LOG_DIR,
|
||||
"cache_dir": headphones.CONFIG.CACHE_DIR,
|
||||
"keep_torrent_files_dir": headphones.CONFIG.KEEP_TORRENT_FILES_DIR,
|
||||
"interface_list": interface_list,
|
||||
"music_encoder": checked(headphones.CONFIG.MUSIC_ENCODER),
|
||||
"encoder": headphones.CONFIG.ENCODER,
|
||||
@@ -1374,7 +1412,16 @@ class WebInterface(object):
|
||||
"email_ssl": checked(headphones.CONFIG.EMAIL_SSL),
|
||||
"email_tls": checked(headphones.CONFIG.EMAIL_TLS),
|
||||
"email_onsnatch": checked(headphones.CONFIG.EMAIL_ONSNATCH),
|
||||
"idtag": checked(headphones.CONFIG.IDTAG)
|
||||
"idtag": checked(headphones.CONFIG.IDTAG),
|
||||
"slack_enabled": checked(headphones.CONFIG.SLACK_ENABLED),
|
||||
"slack_url": headphones.CONFIG.SLACK_URL,
|
||||
"slack_channel": headphones.CONFIG.SLACK_CHANNEL,
|
||||
"slack_emoji": headphones.CONFIG.SLACK_EMOJI,
|
||||
"slack_onsnatch": checked(headphones.CONFIG.SLACK_ONSNATCH),
|
||||
"join_enabled": checked(headphones.CONFIG.JOIN_ENABLED),
|
||||
"join_onsnatch": checked(headphones.CONFIG.JOIN_ONSNATCH),
|
||||
"join_apikey": headphones.CONFIG.JOIN_APIKEY,
|
||||
"join_deviceid": headphones.CONFIG.JOIN_DEVICEID
|
||||
}
|
||||
|
||||
for k, v in config.iteritems():
|
||||
@@ -1421,8 +1468,8 @@ class WebInterface(object):
|
||||
"use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled",
|
||||
"use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay",
|
||||
"use_mininova", "use_waffles", "use_rutracker",
|
||||
"use_whatcd", "use_strike", "preferred_bitrate_allow_lossless", "detect_bitrate",
|
||||
"ignore_clean_releases", "freeze_db", "cue_split", "move_files",
|
||||
"use_apollo", "use_redacted", "use_strike", "use_tquattrecentonze", "preferred_bitrate_allow_lossless",
|
||||
"detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files",
|
||||
"rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art",
|
||||
"embed_album_art", "embed_lyrics",
|
||||
"replace_existing_folders", "keep_original_folder", "file_underscores",
|
||||
@@ -1442,7 +1489,8 @@ class WebInterface(object):
|
||||
"osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch",
|
||||
"songkick_enabled", "songkick_filter_enabled",
|
||||
"mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch",
|
||||
"customauth", "idtag", "deluge_paused"
|
||||
"customauth", "idtag", "deluge_paused",
|
||||
"join_enabled", "join_onsnatch"
|
||||
]
|
||||
for checked_config in checked_configs:
|
||||
if checked_config not in kwargs:
|
||||
@@ -1711,6 +1759,12 @@ class WebInterface(object):
|
||||
telegram = notifiers.TELEGRAM()
|
||||
telegram.notify("it works!", "lazers pew pew")
|
||||
|
||||
@cherrypy.expose
|
||||
def testJoin(self):
|
||||
logger.info("Testing Join notifications")
|
||||
join = notifiers.JOIN()
|
||||
join.notify("it works!", "Test message")
|
||||
|
||||
|
||||
class Artwork(object):
|
||||
@cherrypy.expose
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# PROVIDE: headphones
|
||||
# REQUIRE: DAEMON sabnzbd
|
||||
# REQUIRE: DAEMON
|
||||
# BEFORE: LOGIN
|
||||
# KEYWORD: shutdown
|
||||
#
|
||||
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
|
||||
@@ -15,56 +16,35 @@
|
||||
# as root.
|
||||
# headphones_dir: Directory where Headphones lives.
|
||||
# Default: /usr/local/headphones
|
||||
# headphones_chdir: Change to this directory before running Headphones.
|
||||
# Default is same as headphones_dir.
|
||||
# headphones_pid: The name of the pidfile to create.
|
||||
# Default is headphones.pid in headphones_dir.
|
||||
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name="headphones"
|
||||
rcvar=${name}_enable
|
||||
|
||||
load_rc_config ${name}
|
||||
|
||||
: "${headphones_enable:="NO"}"
|
||||
: "${headphones_user:="_sabnzbd"}"
|
||||
: "${headphones_dir:="/usr/local/headphones"}"
|
||||
: "${headphones_chdir:="${headphones_dir}"}"
|
||||
: "${headphones_pid:="${headphones_dir}/headphones.pid"}"
|
||||
: "${headphones_conf:="/usr/local/headphones/config.ini"}"
|
||||
|
||||
status_cmd="${name}_status"
|
||||
stop_cmd="${name}_stop"
|
||||
command="${headphones_dir}/Headphones.py"
|
||||
command_interpreter="/usr/local/bin/python"
|
||||
pidfile="/var/run/headphones/headphones.pid"
|
||||
start_precmd="headphones_start_precmd"
|
||||
headphones_flags="--daemon --nolaunch --pidfile $pidfile --config $headphones_conf $headphones_flags"
|
||||
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-f -p ${headphones_pid} python ${headphones_dir}/Headphones.py ${headphones_flags} --quiet --nolaunch"
|
||||
headphones_start_precmd() {
|
||||
if [ $($ID -u) != 0 ]; then
|
||||
err 1 "Must be root."
|
||||
fi
|
||||
|
||||
# Ensure user is root when running this script.
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
echo "Oops, you should be root before running this!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
verify_headphones_pid() {
|
||||
# Make sure the pid corresponds to the Headphones process.
|
||||
pid=$(cat "${headphones_pid}" 2>/dev/null)
|
||||
pgrep -F "${headphones_pid}" -q "python ${headphones_dir}/Headphones.py"
|
||||
return $?
|
||||
}
|
||||
|
||||
# Try to stop Headphones cleanly by calling shutdown over http.
|
||||
headphones_stop() {
|
||||
echo "Stopping $name"
|
||||
verify_headphones_pid
|
||||
if [ -n "${pid}" ]; then
|
||||
wait_for_pids "${pid}"
|
||||
echo "Stopped"
|
||||
fi
|
||||
}
|
||||
|
||||
headphones_status() {
|
||||
verify_headphones_pid && echo "$name is running as ${pid}" || echo "$name is not running"
|
||||
if [ ! -d /var/run/headphones ]; then
|
||||
install -do $headphones_user /var/run/headphones
|
||||
fi
|
||||
}
|
||||
|
||||
load_rc_config ${name}
|
||||
run_rc_command "$1"
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
## PYTHON_BIN= #$DAEMON, the location of the python binary, the default is /usr/bin/python
|
||||
## HP_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for headphones, i.e. " --config=/home/headphones/config.ini"
|
||||
## HP_PORT= #$PORT_OPTS, hardcoded port for the webserver, overrides value in config.ini
|
||||
## HP_HOST= #$HOST_OPTS, host for the webserver, overrides value in config.ini
|
||||
##
|
||||
## EXAMPLE if want to run as different user
|
||||
## add HP_USER=username to /etc/default/headphones
|
||||
@@ -105,7 +106,12 @@ load_settings() {
|
||||
PORT_OPTS=" --port=${HP_PORT} "
|
||||
}
|
||||
|
||||
DAEMON_OPTS=" Headphones.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS}${EXTRA_DAEMON_OPTS}"
|
||||
# Host config
|
||||
[ -n "$HP_HOST" ] && {
|
||||
HOST_OPTS=" --host=${HP_HOST} "
|
||||
}
|
||||
|
||||
DAEMON_OPTS=" Headphones.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS} ${HOST_OPTS} ${EXTRA_DAEMON_OPTS}"
|
||||
|
||||
SETTINGS_LOADED=TRUE
|
||||
fi
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2014 Adrian Sampson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,94 +0,0 @@
|
||||
.. image:: https://travis-ci.org/sampsyo/beets.svg?branch=master
|
||||
:target: https://travis-ci.org/sampsyo/beets
|
||||
|
||||
.. image:: http://img.shields.io/coveralls/sampsyo/beets.svg
|
||||
:target: https://coveralls.io/r/sampsyo/beets
|
||||
|
||||
.. image:: http://img.shields.io/pypi/v/beets.svg
|
||||
:target: https://pypi.python.org/pypi/beets
|
||||
|
||||
Beets is the media library management system for obsessive-compulsive music
|
||||
geeks.
|
||||
|
||||
The purpose of beets is to get your music collection right once and for all.
|
||||
It catalogs your collection, automatically improving its metadata as it goes.
|
||||
It then provides a bouquet of tools for manipulating and accessing your music.
|
||||
|
||||
Here's an example of beets' brainy tag corrector doing its thing::
|
||||
|
||||
$ beet import ~/music/ladytron
|
||||
Tagging:
|
||||
Ladytron - Witching Hour
|
||||
(Similarity: 98.4%)
|
||||
* Last One Standing -> The Last One Standing
|
||||
* Beauty -> Beauty*2
|
||||
* White Light Generation -> Whitelightgenerator
|
||||
* All the Way -> All the Way...
|
||||
|
||||
Because beets is designed as a library, it can do almost anything you can
|
||||
imagine for your music collection. Via `plugins`_, beets becomes a panacea:
|
||||
|
||||
- Fetch or calculate all the metadata you could possibly need: `album art`_,
|
||||
`lyrics`_, `genres`_, `tempos`_, `ReplayGain`_ levels, or `acoustic
|
||||
fingerprints`_.
|
||||
- Get metadata from `MusicBrainz`_, `Discogs`_, or `Beatport`_. Or guess
|
||||
metadata using songs' filenames or their acoustic fingerprints.
|
||||
- `Transcode audio`_ to any format you like.
|
||||
- Check your library for `duplicate tracks and albums`_ or for `albums that
|
||||
are missing tracks`_.
|
||||
- Clean up crufty tags left behind by other, less-awesome tools.
|
||||
- Embed and extract album art from files' metadata.
|
||||
- Browse your music library graphically through a Web browser and play it in any
|
||||
browser that supports `HTML5 Audio`_.
|
||||
- Analyze music files' metadata from the command line.
|
||||
- Listen to your library with a music player that speaks the `MPD`_ protocol
|
||||
and works with a staggering variety of interfaces.
|
||||
|
||||
If beets doesn't do what you want yet, `writing your own plugin`_ is
|
||||
shockingly simple if you know a little Python.
|
||||
|
||||
.. _plugins: http://beets.readthedocs.org/page/plugins/
|
||||
.. _MPD: http://www.musicpd.org/
|
||||
.. _MusicBrainz music collection: http://musicbrainz.org/doc/Collections/
|
||||
.. _writing your own plugin:
|
||||
http://beets.readthedocs.org/page/dev/plugins.html
|
||||
.. _HTML5 Audio:
|
||||
http://www.w3.org/TR/html-markup/audio.html
|
||||
.. _albums that are missing tracks:
|
||||
http://beets.readthedocs.org/page/plugins/missing.html
|
||||
.. _duplicate tracks and albums:
|
||||
http://beets.readthedocs.org/page/plugins/duplicates.html
|
||||
.. _Transcode audio:
|
||||
http://beets.readthedocs.org/page/plugins/convert.html
|
||||
.. _Beatport: http://www.beatport.com/
|
||||
.. _Discogs: http://www.discogs.com/
|
||||
.. _acoustic fingerprints:
|
||||
http://beets.readthedocs.org/page/plugins/chroma.html
|
||||
.. _ReplayGain: http://beets.readthedocs.org/page/plugins/replaygain.html
|
||||
.. _tempos: http://beets.readthedocs.org/page/plugins/echonest.html
|
||||
.. _genres: http://beets.readthedocs.org/page/plugins/lastgenre.html
|
||||
.. _album art: http://beets.readthedocs.org/page/plugins/fetchart.html
|
||||
.. _lyrics: http://beets.readthedocs.org/page/plugins/lyrics.html
|
||||
.. _MusicBrainz: http://musicbrainz.org/
|
||||
|
||||
Read More
|
||||
---------
|
||||
|
||||
Learn more about beets at `its Web site`_. Follow `@b33ts`_ on Twitter for
|
||||
news and updates.
|
||||
|
||||
You can install beets by typing ``pip install beets``. Then check out the
|
||||
`Getting Started`_ guide.
|
||||
|
||||
.. _its Web site: http://beets.radbox.org/
|
||||
.. _Getting Started: http://beets.readthedocs.org/page/guides/main.html
|
||||
.. _@b33ts: http://twitter.com/b33ts/
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
Beets is by `Adrian Sampson`_ with a supporting cast of thousands. For help,
|
||||
please contact the `mailing list`_.
|
||||
|
||||
.. _mailing list: https://groups.google.com/forum/#!forum/beets-users
|
||||
.. _Adrian Sampson: http://homes.cs.washington.edu/~asampson/
|
||||
35
lib/beets/__init__.py
Normal file → Executable file
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of beets.
|
||||
# Copyright 2014, Adrian Sampson.
|
||||
# Copyright 2016, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
@@ -12,17 +13,33 @@
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# This particular version has been slightly modified to work with Headphones
|
||||
# https://github.com/rembo10/headphones
|
||||
|
||||
__version__ = '1.3.10-headphones'
|
||||
__author__ = 'Adrian Sampson <adrian@radbox.org>'
|
||||
from __future__ import division, absolute_import, print_function
|
||||
|
||||
import os
|
||||
|
||||
import beets.library
|
||||
from beets.util import confit
|
||||
|
||||
Library = beets.library.Library
|
||||
# This particular version has been slightly modified to work with Headphones
|
||||
# https://github.com/rembo10/headphones
|
||||
__version__ = u'1.4.4-headphones'
|
||||
__author__ = u'Adrian Sampson <adrian@radbox.org>'
|
||||
|
||||
config = confit.LazyConfig(os.path.dirname(__file__), __name__)
|
||||
|
||||
class IncludeLazyConfig(confit.LazyConfig):
|
||||
"""A version of Confit's LazyConfig that also merges in data from
|
||||
YAML files specified in an `include` setting.
|
||||
"""
|
||||
def read(self, user=True, defaults=True):
|
||||
super(IncludeLazyConfig, self).read(user, defaults)
|
||||
|
||||
try:
|
||||
for view in self['include']:
|
||||
filename = view.as_filename()
|
||||
if os.path.isfile(filename):
|
||||
self.set_file(filename)
|
||||
except confit.NotFoundError:
|
||||
pass
|
||||
|
||||
# headphones
|
||||
#config = IncludeLazyConfig('beets', __name__)
|
||||
config = IncludeLazyConfig(os.path.dirname(__file__), __name__)
|
||||
|
||||
26
lib/beets/__main__.py
Executable file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of beets.
|
||||
# Copyright 2017, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
"""The __main__ module lets you run the beets CLI interface by typing
|
||||
`python -m beets`.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import, print_function
|
||||
|
||||
import sys
|
||||
from .ui import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
222
lib/beets/art.py
Executable file
@@ -0,0 +1,222 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of beets.
|
||||
# Copyright 2016, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
"""High-level utilities for manipulating image files associated with
|
||||
music and items' embedded album art.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import, print_function
|
||||
|
||||
import subprocess
|
||||
import platform
|
||||
from tempfile import NamedTemporaryFile
|
||||
import os
|
||||
|
||||
from beets.util import displayable_path, syspath, bytestring_path
|
||||
from beets.util.artresizer import ArtResizer
|
||||
from beets import mediafile
|
||||
|
||||
|
||||
def mediafile_image(image_path, maxwidth=None):
|
||||
"""Return a `mediafile.Image` object for the path.
|
||||
"""
|
||||
|
||||
with open(syspath(image_path), 'rb') as f:
|
||||
data = f.read()
|
||||
return mediafile.Image(data, type=mediafile.ImageType.front)
|
||||
|
||||
|
||||
def get_art(log, item):
|
||||
# Extract the art.
|
||||
try:
|
||||
mf = mediafile.MediaFile(syspath(item.path))
|
||||
except mediafile.UnreadableFileError as exc:
|
||||
log.warning(u'Could not extract art from {0}: {1}',
|
||||
displayable_path(item.path), exc)
|
||||
return
|
||||
|
||||
return mf.art
|
||||
|
||||
|
||||
def embed_item(log, item, imagepath, maxwidth=None, itempath=None,
|
||||
compare_threshold=0, ifempty=False, as_album=False):
|
||||
"""Embed an image into the item's media file.
|
||||
"""
|
||||
# Conditions and filters.
|
||||
if compare_threshold:
|
||||
if not check_art_similarity(log, item, imagepath, compare_threshold):
|
||||
log.info(u'Image not similar; skipping.')
|
||||
return
|
||||
if ifempty and get_art(log, item):
|
||||
log.info(u'media file already contained art')
|
||||
return
|
||||
if maxwidth and not as_album:
|
||||
imagepath = resize_image(log, imagepath, maxwidth)
|
||||
|
||||
# Get the `Image` object from the file.
|
||||
try:
|
||||
log.debug(u'embedding {0}', displayable_path(imagepath))
|
||||
image = mediafile_image(imagepath, maxwidth)
|
||||
except IOError as exc:
|
||||
log.warning(u'could not read image file: {0}', exc)
|
||||
return
|
||||
|
||||
# Make sure the image kind is safe (some formats only support PNG
|
||||
# and JPEG).
|
||||
if image.mime_type not in ('image/jpeg', 'image/png'):
|
||||
log.info('not embedding image of unsupported type: {}',
|
||||
image.mime_type)
|
||||
return
|
||||
|
||||
item.try_write(path=itempath, tags={'images': [image]})
|
||||
|
||||
|
||||
def embed_album(log, album, maxwidth=None, quiet=False,
|
||||
compare_threshold=0, ifempty=False):
|
||||
"""Embed album art into all of the album's items.
|
||||
"""
|
||||
imagepath = album.artpath
|
||||
if not imagepath:
|
||||
log.info(u'No album art present for {0}', album)
|
||||
return
|
||||
if not os.path.isfile(syspath(imagepath)):
|
||||
log.info(u'Album art not found at {0} for {1}',
|
||||
displayable_path(imagepath), album)
|
||||
return
|
||||
if maxwidth:
|
||||
imagepath = resize_image(log, imagepath, maxwidth)
|
||||
|
||||
log.info(u'Embedding album art into {0}', album)
|
||||
|
||||
for item in album.items():
|
||||
embed_item(log, item, imagepath, maxwidth, None,
|
||||
compare_threshold, ifempty, as_album=True)
|
||||
|
||||
|
||||
def resize_image(log, imagepath, maxwidth):
|
||||
"""Returns path to an image resized to maxwidth.
|
||||
"""
|
||||
log.debug(u'Resizing album art to {0} pixels wide', maxwidth)
|
||||
imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath))
|
||||
return imagepath
|
||||
|
||||
|
||||
def check_art_similarity(log, item, imagepath, compare_threshold):
|
||||
"""A boolean indicating if an image is similar to embedded item art.
|
||||
"""
|
||||
with NamedTemporaryFile(delete=True) as f:
|
||||
art = extract(log, f.name, item)
|
||||
|
||||
if art:
|
||||
is_windows = platform.system() == "Windows"
|
||||
|
||||
# Converting images to grayscale tends to minimize the weight
|
||||
# of colors in the diff score. So we first convert both images
|
||||
# to grayscale and then pipe them into the `compare` command.
|
||||
# On Windows, ImageMagick doesn't support the magic \\?\ prefix
|
||||
# on paths, so we pass `prefix=False` to `syspath`.
|
||||
convert_cmd = ['convert', syspath(imagepath, prefix=False),
|
||||
syspath(art, prefix=False),
|
||||
'-colorspace', 'gray', 'MIFF:-']
|
||||
compare_cmd = ['compare', '-metric', 'PHASH', '-', 'null:']
|
||||
log.debug(u'comparing images with pipeline {} | {}',
|
||||
convert_cmd, compare_cmd)
|
||||
convert_proc = subprocess.Popen(
|
||||
convert_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
close_fds=not is_windows,
|
||||
)
|
||||
compare_proc = subprocess.Popen(
|
||||
compare_cmd,
|
||||
stdin=convert_proc.stdout,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
close_fds=not is_windows,
|
||||
)
|
||||
|
||||
# Check the convert output. We're not interested in the
|
||||
# standard output; that gets piped to the next stage.
|
||||
convert_proc.stdout.close()
|
||||
convert_stderr = convert_proc.stderr.read()
|
||||
convert_proc.stderr.close()
|
||||
convert_proc.wait()
|
||||
if convert_proc.returncode:
|
||||
log.debug(
|
||||
u'ImageMagick convert failed with status {}: {!r}',
|
||||
convert_proc.returncode,
|
||||
convert_stderr,
|
||||
)
|
||||
return
|
||||
|
||||
# Check the compare output.
|
||||
stdout, stderr = compare_proc.communicate()
|
||||
if compare_proc.returncode:
|
||||
if compare_proc.returncode != 1:
|
||||
log.debug(u'ImageMagick compare failed: {0}, {1}',
|
||||
displayable_path(imagepath),
|
||||
displayable_path(art))
|
||||
return
|
||||
out_str = stderr
|
||||
else:
|
||||
out_str = stdout
|
||||
|
||||
try:
|
||||
phash_diff = float(out_str)
|
||||
except ValueError:
|
||||
log.debug(u'IM output is not a number: {0!r}', out_str)
|
||||
return
|
||||
|
||||
log.debug(u'ImageMagick compare score: {0}', phash_diff)
|
||||
return phash_diff <= compare_threshold
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def extract(log, outpath, item):
|
||||
art = get_art(log, item)
|
||||
outpath = bytestring_path(outpath)
|
||||
if not art:
|
||||
log.info(u'No album art present in {0}, skipping.', item)
|
||||
return
|
||||
|
||||
# Add an extension to the filename.
|
||||
ext = mediafile.image_extension(art)
|
||||
if not ext:
|
||||
log.warning(u'Unknown image type in {0}.',
|
||||
displayable_path(item.path))
|
||||
return
|
||||
outpath += bytestring_path('.' + ext)
|
||||
|
||||
log.info(u'Extracting album art from: {0} to: {1}',
|
||||
item, displayable_path(outpath))
|
||||
with open(syspath(outpath), 'wb') as f:
|
||||
f.write(art)
|
||||
return outpath
|
||||
|
||||
|
||||
def extract_first(log, outpath, items):
|
||||
for item in items:
|
||||
real_path = extract(log, outpath, item)
|
||||
if real_path:
|
||||
return real_path
|
||||
|
||||
|
||||
def clear(log, lib, query):
|
||||
items = lib.items(query)
|
||||
log.info(u'Clearing album art from {0} items', len(items))
|
||||
for item in items:
|
||||
log.debug(u'Clearing art for {0}', item)
|
||||
item.try_write(tags={'images': None})
|
||||
42
lib/beets/autotag/__init__.py
Normal file → Executable file
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of beets.
|
||||
# Copyright 2013, Adrian Sampson.
|
||||
# Copyright 2016, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
@@ -14,13 +15,15 @@
|
||||
|
||||
"""Facilities for automatically determining files' correct metadata.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from __future__ import division, absolute_import, print_function
|
||||
|
||||
from beets import logging
|
||||
from beets import config
|
||||
|
||||
# Parts of external interface.
|
||||
from .hooks import AlbumInfo, TrackInfo, AlbumMatch, TrackMatch # noqa
|
||||
from .match import tag_item, tag_album # noqa
|
||||
from .match import tag_item, tag_album, Proposal # noqa
|
||||
from .match import Recommendation # noqa
|
||||
|
||||
# Global logger.
|
||||
@@ -39,6 +42,16 @@ def apply_item_metadata(item, track_info):
|
||||
item.mb_trackid = track_info.track_id
|
||||
if track_info.artist_id:
|
||||
item.mb_artistid = track_info.artist_id
|
||||
if track_info.data_source:
|
||||
item.data_source = track_info.data_source
|
||||
|
||||
if track_info.lyricist is not None:
|
||||
item.lyricist = track_info.lyricist
|
||||
if track_info.composer is not None:
|
||||
item.composer = track_info.composer
|
||||
if track_info.arranger is not None:
|
||||
item.arranger = track_info.arranger
|
||||
|
||||
# At the moment, the other metadata is left intact (including album
|
||||
# and track number). Perhaps these should be emptied?
|
||||
|
||||
@@ -47,7 +60,7 @@ def apply_metadata(album_info, mapping):
|
||||
"""Set the items' metadata to match an AlbumInfo object using a
|
||||
mapping from Items to TrackInfo objects.
|
||||
"""
|
||||
for item, track_info in mapping.iteritems():
|
||||
for item, track_info in mapping.items():
|
||||
# Album, artist, track count.
|
||||
if track_info.artist:
|
||||
item.artist = track_info.artist
|
||||
@@ -90,7 +103,12 @@ def apply_metadata(album_info, mapping):
|
||||
item.title = track_info.title
|
||||
|
||||
if config['per_disc_numbering']:
|
||||
item.track = track_info.medium_index or track_info.index
|
||||
# We want to let the track number be zero, but if the medium index
|
||||
# is not provided we need to fall back to the overall index.
|
||||
if track_info.medium_index is not None:
|
||||
item.track = track_info.medium_index
|
||||
else:
|
||||
item.track = track_info.index
|
||||
item.tracktotal = track_info.medium_total or len(album_info.tracks)
|
||||
else:
|
||||
item.track = track_info.index
|
||||
@@ -122,7 +140,8 @@ def apply_metadata(album_info, mapping):
|
||||
'language',
|
||||
'country',
|
||||
'albumstatus',
|
||||
'albumdisambig'):
|
||||
'albumdisambig',
|
||||
'data_source',):
|
||||
value = getattr(album_info, field)
|
||||
if value is not None:
|
||||
item[field] = value
|
||||
@@ -132,5 +151,14 @@ def apply_metadata(album_info, mapping):
|
||||
if track_info.media is not None:
|
||||
item.media = track_info.media
|
||||
|
||||
if track_info.lyricist is not None:
|
||||
item.lyricist = track_info.lyricist
|
||||
if track_info.composer is not None:
|
||||
item.composer = track_info.composer
|
||||
if track_info.arranger is not None:
|
||||
item.arranger = track_info.arranger
|
||||
|
||||
item.track_alt = track_info.track_alt
|
||||
|
||||
# Headphones seal of approval
|
||||
item.comments = 'tagged by headphones/beets'
|
||||
item.comments = 'tagged by headphones/beets'
|
||||
|
||||