Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b48f22bd9 | ||
|
|
79d508fb07 | ||
|
|
ae8fadd587 | ||
|
|
5d7fdc7a82 | ||
|
|
1ded166765 | ||
|
|
22eb21a4ef | ||
|
|
4283b381b6 | ||
|
|
2cdc6669da | ||
|
|
de8fac347d | ||
|
|
da9287d7cf | ||
|
|
f9d73da31b | ||
|
|
3cc280666e | ||
|
|
0828643c09 | ||
|
|
9b3d7f3bbb | ||
|
|
68c551c828 | ||
|
|
b7af246340 | ||
|
|
4c3b60be68 | ||
|
|
539db608e2 | ||
|
|
0b6f263985 | ||
|
|
a10ec8dce4 | ||
|
|
867f75a681 | ||
|
|
c8057676db | ||
|
|
9197f27a50 | ||
|
|
e7062a6710 | ||
|
|
9c2bfcf7d8 | ||
|
|
4d575787cd | ||
|
|
bdf55f8662 | ||
|
|
cd57668b92 | ||
|
|
f20520541d | ||
|
|
48dd832d4e | ||
|
|
af10088dc8 | ||
|
|
cf9737e804 | ||
|
|
4f118eaaef | ||
|
|
79277ddc33 | ||
|
|
3ee2598f4c | ||
|
|
1e1b8a9351 | ||
|
|
0cc2dd7b31 | ||
|
|
80fe81e345 | ||
|
|
0c98085018 | ||
|
|
e96a6fc7c8 | ||
|
|
ee7254ddbe | ||
|
|
3e44baaded | ||
|
|
914ab65577 | ||
|
|
4d674689b1 | ||
|
|
ae328a15aa | ||
|
|
c612a228b6 | ||
|
|
334493d81e | ||
|
|
50de38ce82 | ||
|
|
36434b57ee | ||
|
|
3110659633 | ||
|
|
ecadf8cdab | ||
|
|
6c0b451882 | ||
|
|
755944f15c | ||
|
|
0acb5a413a | ||
|
|
e3c7790039 | ||
|
|
d802948a35 | ||
|
|
b2f5919b26 | ||
|
|
74d5333bdf | ||
|
|
54edacdfe7 | ||
|
|
cd2860f4e3 | ||
|
|
bc0ce99adf | ||
|
|
3bfdc9f069 | ||
|
|
39e589bc08 | ||
|
|
cef1563b4b | ||
|
|
04e8767ea9 | ||
|
|
2df13ad823 | ||
|
|
18d85d12dd | ||
|
|
1e3cbb64ca | ||
|
|
d6e1a16286 | ||
|
|
979b06ae55 | ||
|
|
7fec54289b | ||
|
|
49d632a156 | ||
|
|
a2a14e185a | ||
|
|
50851a4953 | ||
|
|
61a3abbf20 | ||
|
|
e8f436a5db | ||
|
|
34b8a11145 | ||
|
|
f0fe23899a | ||
|
|
4eabc1c614 | ||
|
|
b9d41ad779 | ||
|
|
8e1cd3155f | ||
|
|
624a3d6cc1 | ||
|
|
a042fd44a2 | ||
|
|
2e5ce520eb | ||
|
|
e9407f5dbc | ||
|
|
8eb79701a6 | ||
|
|
4c95f99d80 | ||
|
|
e1e361a46e | ||
|
|
437d33e7a8 | ||
|
|
2832166842 | ||
|
|
2841f36d8d | ||
|
|
de15f6b632 | ||
|
|
5bb6ec04a9 | ||
|
|
1da1f5124a | ||
|
|
f8b82337a2 | ||
|
|
3efd3931f5 | ||
|
|
95402144d1 | ||
|
|
220a8931d1 | ||
|
|
a96b186825 | ||
|
|
de3b73dfa5 | ||
|
|
263564f15f | ||
|
|
a5ca14e6d6 | ||
|
|
861747c235 | ||
|
|
4095bf213e | ||
|
|
f3020207c0 | ||
|
|
1cee19be74 | ||
|
|
f877caff06 | ||
|
|
9c0297326e | ||
|
|
1ed12770c2 | ||
|
|
519fdc3015 | ||
|
|
91ed781acb | ||
|
|
bf5d07efec | ||
|
|
33fdb3d7bd | ||
|
|
3f67396a55 | ||
|
|
3680c2b349 | ||
|
|
ff7ff79f85 | ||
|
|
debc65db5d | ||
|
|
a02cc4d7b9 | ||
|
|
578ae2c2d8 | ||
|
|
85749c4e94 | ||
|
|
e415de7698 | ||
|
|
a9bbd34a52 | ||
|
|
7797cd0681 | ||
|
|
9ccb0f85dc | ||
|
|
50185ab3d3 | ||
|
|
697e695f7d | ||
|
|
36ad37f005 | ||
|
|
a45894b802 | ||
|
|
dd1e4bfe3b | ||
|
|
88366443fd | ||
|
|
6a1aca66b1 | ||
|
|
ef6a203cda | ||
|
|
83c50ccd4f | ||
|
|
3e4c5cb5e2 | ||
|
|
8c1621f604 | ||
|
|
bc465edd91 | ||
|
|
3d1a9e8947 | ||
|
|
9d6a7d1d36 | ||
|
|
3cfc434e8a | ||
|
|
e7017305d8 | ||
|
|
a0fa658956 | ||
|
|
709073358b | ||
|
|
e6b74be1f6 | ||
|
|
c29dda3af6 | ||
|
|
335586893c | ||
|
|
44bb1bea08 | ||
|
|
9f378ec79c | ||
|
|
5501d84611 | ||
|
|
34746400e6 | ||
|
|
cf502aa880 | ||
|
|
2ca498d74f | ||
|
|
432018a2a4 | ||
|
|
784ec2927e | ||
|
|
fa1cb27e04 | ||
|
|
7e915c5902 | ||
|
|
7f48b021ec | ||
|
|
e8009c00d2 | ||
|
|
772fb524c7 | ||
|
|
b538e5b321 | ||
|
|
decc548086 | ||
|
|
82695311c4 | ||
|
|
6d40178ce2 | ||
|
|
4d1107ee54 | ||
|
|
8fd9958d70 | ||
|
|
f8900795da | ||
|
|
b8e8e752f0 | ||
|
|
d04cdcd8fa | ||
|
|
edc243a245 | ||
|
|
36f75f44e9 | ||
|
|
ed923a2a94 | ||
|
|
95cc0670e2 | ||
|
|
a62b55c40f | ||
|
|
7440b77377 | ||
|
|
372c6ffd3b | ||
|
|
14d1524971 | ||
|
|
9cddcabad4 | ||
|
|
656e7161cb | ||
|
|
7c12761066 |
@@ -11,7 +11,6 @@ cache:
|
|||||||
- lib
|
- lib
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- "2.6"
|
|
||||||
- "2.7"
|
- "2.7"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.5.19
|
||||||
|
Released 27 March 2018
|
||||||
|
|
||||||
|
Highlights:
|
||||||
|
* Improved: Windows encoding fixes
|
||||||
|
* Improved: Performance enhancements
|
||||||
|
* Improved: Many more since the last release. Check the list
|
||||||
|
|
||||||
|
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.18...v0.5.19).
|
||||||
|
|
||||||
## v0.5.18
|
## v0.5.18
|
||||||
Released 01 December 2016
|
Released 01 December 2016
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ def main():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
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):
|
except (locale.Error, IOError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
## Headphones
|
##  Headphones
|
||||||
|
|
||||||
[](https://travis-ci.org/rembo10/headphones)
|
[](https://travis-ci.org/rembo10/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 |
@@ -133,6 +133,12 @@
|
|||||||
</label>
|
</label>
|
||||||
<input type="text" name="mb_ignore_age" value="${config['mb_ignore_age']}" size="4">days
|
<input type="text" name="mb_ignore_age" value="${config['mb_ignore_age']}" size="4">days
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row checkbox">
|
||||||
|
<label title="Ignore MusicBrainz album updates missing a release date.">
|
||||||
|
Ignore Undated Releases
|
||||||
|
</label>
|
||||||
|
<input type="checkbox" name="mb_ignore_age_missing" id="mb_ignore_age_missing" value="${config['mb_ignore_age_missing']}">
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -312,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_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_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_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>
|
||||||
<fieldset id="torrent_blackhole_options">
|
<fieldset id="torrent_blackhole_options">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -386,6 +393,26 @@
|
|||||||
<input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30">
|
<input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30">
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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">
|
<fieldset id="deluge_options">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Deluge WebUI Host and Port</label>
|
<label>Deluge WebUI Host and Port</label>
|
||||||
@@ -628,53 +655,53 @@
|
|||||||
<label>Seed Ratio</label>
|
<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">
|
<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>
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
<div class="config">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Username</label>
|
<label>Session Cookie (Optional - Advanced)</label>
|
||||||
<input type="text" name="whatcd_username" value="${config['whatcd_username']}" size="36">
|
<input type="text" class="override-float" name="rutracker_cookie" value="${config['rutracker_cookie']}" size="10" title="bb_session cookie (Advanced)">
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label>Password</label>
|
|
||||||
<input type="password" name="whatcd_password" value="${config['whatcd_password'] | h}" size="36">
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label>URL</label>
|
|
||||||
<input type="text" name="whatcd_url" value="${config['whatcd_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">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="row checkbox left">
|
<div class="row checkbox left">
|
||||||
<input id="use_pth" type="checkbox" class="bigcheck" name="use_pth" value="1" ${config['use_pth']} /><label for="use_pth"><span class="option">PassTheHeadphones.me</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>
|
||||||
<div class="config">
|
<div class="config">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Username</label>
|
<label>Username</label>
|
||||||
<input type="text" name="pth_username" value="${config['pth_username']}" size="36">
|
<input type="text" name="apollo_username" value="${config['apollo_username']}" size="36">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Password</label>
|
<label>Password</label>
|
||||||
<input type="password" name="pth_password" value="${config['pth_password']}" size="36">
|
<input type="password" name="apollo_password" value="${config['apollo_password'] | h}" size="36">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>URL</label>
|
<label>URL</label>
|
||||||
<input type="text" name="pth_url" value="${config['pth_url']}" size="36">
|
<input type="text" name="apollo_url" value="${config['apollo_url']}" size="36">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Seed Ratio</label>
|
<label>Seed Ratio</label>
|
||||||
<input type="text" class="override-float" name="pth_ratio" value="${config['pth_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>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -699,8 +726,8 @@
|
|||||||
<div class="config" id="torznab1">
|
<div class="config" id="torznab1">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Torznab Host</label>
|
<label>Torznab Host</label>
|
||||||
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="30">
|
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="36">
|
||||||
<small>e.g. http://localhost:9117/torznab/iptorrents</small>
|
<small>e.g. http://localhost:9117/api/v2.0/indexers/demonoid/results/torznab/</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Torznab API</label>
|
<label>Torznab API</label>
|
||||||
@@ -723,7 +750,7 @@
|
|||||||
<div class="config" id="torznab${torznab_number}">
|
<div class="config" id="torznab${torznab_number}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Torznab Host</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Torznab API</label>
|
<label>Torznab API</label>
|
||||||
@@ -928,30 +955,42 @@
|
|||||||
<input type="checkbox" name="keep_nfo" value="1" ${config['keep_nfo']} />
|
<input type="checkbox" name="keep_nfo" value="1" ${config['keep_nfo']} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<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">
|
<div class="row checkbox left clearfix nopad">
|
||||||
<label>
|
<label>
|
||||||
Add album art jpeg to album folder
|
Add album art jpeg to album folder
|
||||||
<input type="checkbox" name="add_album_art" id="add_album_art" value="1" ${config['add_album_art']}>
|
<input type="checkbox" name="add_album_art" id="add_album_art" value="1" ${config['add_album_art']}>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="album_art_options" style="padding-left: 20px">
|
<div id="album_art_options" style="padding-left: 20px">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg
|
as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg
|
||||||
</div>
|
</div>
|
||||||
<small>Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').</small>
|
<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>
|
||||||
<div class="row checkbox left clearfix nopad">
|
|
||||||
<label>
|
<div id="album_art_size_options" style="padding-left: 20px">
|
||||||
Embed album art in each file
|
<div class="row">
|
||||||
<input type="checkbox" name="embed_album_art" value="1" ${config['embed_album_art']}>
|
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.">\
|
||||||
</label>
|
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 checkbox left clearfix nopad">
|
|
||||||
<label>
|
|
||||||
Embed lyrics
|
|
||||||
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>
|
<label>
|
||||||
Destination Directory:
|
Destination Directory:
|
||||||
@@ -1302,6 +1341,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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>
|
<fieldset>
|
||||||
<div class="row checkbox left">
|
<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>
|
<input type="checkbox" class="bigcheck" name="telegram_enabled" id="telegram" value="1" ${config['telegram_enabled']} /><label for="telegram"><span class="option">Telegram</span></label>
|
||||||
@@ -1319,6 +1378,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -1614,6 +1690,10 @@
|
|||||||
<label>Cache Directory</label>
|
<label>Cache Directory</label>
|
||||||
<input type="text" name="cache_dir" value="${config['cache_dir']}" size="50">
|
<input type="text" name="cache_dir" value="${config['cache_dir']}" size="50">
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -1858,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"))
|
if ($("#add_album_art").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#album_art_options").show();
|
$("#album_art_options").show();
|
||||||
|
$("#album_art_size_options").show();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1871,10 +1978,15 @@
|
|||||||
if ($("#add_album_art").is(":checked"))
|
if ($("#add_album_art").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#album_art_options").slideDown();
|
$("#album_art_options").slideDown();
|
||||||
|
$("#album_art_size_options").slideDown();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$("#album_art_options").slideUp();
|
$("#album_art_options").slideUp();
|
||||||
|
if (!$("#embed_album_art").is(":checked"))
|
||||||
|
{
|
||||||
|
$("#album_art_size_options").slideUp();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2079,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"))
|
if ($("#twitter").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#twitteroptions").show();
|
$("#twitteroptions").show();
|
||||||
@@ -2099,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"))
|
if ($("#telegram").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#telegramoptions").show();
|
$("#telegramoptions").show();
|
||||||
@@ -2275,26 +2429,30 @@
|
|||||||
|
|
||||||
if ($("#torrent_downloader_blackhole").is(":checked"))
|
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();
|
$("#torrent_blackhole_options").show();
|
||||||
}
|
}
|
||||||
if ($("#torrent_downloader_transmission").is(":checked"))
|
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();
|
$("#transmission_options").show();
|
||||||
}
|
}
|
||||||
if ($("#torrent_downloader_utorrent").is(":checked"))
|
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();
|
$("#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"))
|
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();
|
$("#deluge_options").show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$('input[type=radio]').change(function(){
|
$('input[type=radio]').change(function(){
|
||||||
if ($("#preferred_bitrate").is(":checked"))
|
if ($("#preferred_bitrate").is(":checked"))
|
||||||
{
|
{
|
||||||
@@ -2330,19 +2488,23 @@
|
|||||||
}
|
}
|
||||||
if ($("#torrent_downloader_blackhole").is(":checked"))
|
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"))
|
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"))
|
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"))
|
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() });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2439,15 +2601,15 @@
|
|||||||
initConfigCheckbox("#use_mininova");
|
initConfigCheckbox("#use_mininova");
|
||||||
initConfigCheckbox("#use_waffles");
|
initConfigCheckbox("#use_waffles");
|
||||||
initConfigCheckbox("#use_rutracker");
|
initConfigCheckbox("#use_rutracker");
|
||||||
initConfigCheckbox("#use_whatcd");
|
initConfigCheckbox("#use_apollo");
|
||||||
initConfigCheckbox("#use_pth");
|
initConfigCheckbox("#use_redacted");
|
||||||
initConfigCheckbox("#use_strike");
|
initConfigCheckbox("#use_strike");
|
||||||
initConfigCheckbox("#api_enabled");
|
initConfigCheckbox("#api_enabled");
|
||||||
initConfigCheckbox("#enable_https");
|
initConfigCheckbox("#enable_https");
|
||||||
initConfigCheckbox("#customauth");
|
initConfigCheckbox("#customauth");
|
||||||
|
initConfigCheckbox("#mb_ignore_age_missing");
|
||||||
initConfigCheckbox("#use_tquattrecentonze");
|
initConfigCheckbox("#use_tquattrecentonze");
|
||||||
|
|
||||||
|
|
||||||
$('#twitterStep1').click(function () {
|
$('#twitterStep1').click(function () {
|
||||||
$.get("/twitterStep1", function (data) {window.open(data); })
|
$.get("/twitterStep1", function (data) {window.open(data); })
|
||||||
.done(function () { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>Confirm Authorization. Check pop-up blocker if no response.</div>"); });
|
.done(function () { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>Confirm Authorization. Check pop-up blocker if no response.</div>"); });
|
||||||
|
|||||||
|
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({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
data: dataString,
|
data: dataString,
|
||||||
|
type: 'POST',
|
||||||
beforeSend: function(jqXHR, settings) {
|
beforeSend: function(jqXHR, settings) {
|
||||||
// Start loader etc.
|
// Start loader etc.
|
||||||
feedback.prepend(loader);
|
feedback.prepend(loader);
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ CURRENT_VERSION = None
|
|||||||
LATEST_VERSION = None
|
LATEST_VERSION = None
|
||||||
COMMITS_BEHIND = None
|
COMMITS_BEHIND = None
|
||||||
|
|
||||||
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma"]
|
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma", "opus"]
|
||||||
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff"]
|
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff", "aif"]
|
||||||
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
|
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
|
||||||
|
|
||||||
MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
|
MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
|
||||||
@@ -306,9 +306,10 @@ def initialize_scheduler():
|
|||||||
minutes=minutes)
|
minutes=minutes)
|
||||||
|
|
||||||
# Remove Torrent + data if Post Processed and finished Seeding
|
# Remove Torrent + data if Post Processed and finished Seeding
|
||||||
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
if headphones.CONFIG.TORRENT_DOWNLOADER != 0:
|
||||||
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
||||||
minutes=minutes)
|
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
||||||
|
minutes=minutes)
|
||||||
|
|
||||||
# Start scheduler
|
# Start scheduler
|
||||||
if start_jobs and len(SCHED.get_jobs()):
|
if start_jobs and len(SCHED.get_jobs()):
|
||||||
@@ -376,7 +377,7 @@ def dbcheck():
|
|||||||
c.execute(
|
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)')
|
'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(
|
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
|
# Matched is a temporary value used to see if there was a match found in
|
||||||
# alltracks
|
# alltracks
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -416,6 +417,8 @@ def dbcheck():
|
|||||||
'CREATE INDEX IF NOT EXISTS tracks_Location ON tracks(Location ASC)')
|
'CREATE INDEX IF NOT EXISTS tracks_Location ON tracks(Location ASC)')
|
||||||
c.execute(
|
c.execute(
|
||||||
'CREATE INDEX IF NOT EXISTS alltracks_Location ON alltracks(Location ASC)')
|
'CREATE INDEX IF NOT EXISTS alltracks_Location ON alltracks(Location ASC)')
|
||||||
|
c.execute(
|
||||||
|
'CREATE INDEX IF NOT EXISTS tracks_artistid ON tracks(ArtistID ASC)')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c.execute('SELECT IncludeExtras from artists')
|
c.execute('SELECT IncludeExtras from artists')
|
||||||
@@ -613,6 +616,20 @@ def dbcheck():
|
|||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
c.execute('ALTER TABLE artists ADD COLUMN MetaCritic TEXT DEFAULT NULL')
|
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()
|
conn.commit()
|
||||||
c.close()
|
c.close()
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,164 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from headphones import 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):
|
def getAlbumArt(albumid):
|
||||||
myDB = db.DBConnection()
|
|
||||||
asin = myDB.action(
|
|
||||||
'SELECT AlbumASIN from albums WHERE AlbumID=?', [albumid]).fetchone()[0]
|
|
||||||
|
|
||||||
if asin:
|
artwork_path = None
|
||||||
return 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % asin
|
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):
|
def getCachedArt(albumid):
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ def switch(AlbumID, ReleaseID):
|
|||||||
|
|
||||||
# Update have track counts on index
|
# Update have track counts on index
|
||||||
totaltracks = len(myDB.select(
|
totaltracks = len(myDB.select(
|
||||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")',
|
'SELECT TrackTitle from tracks AS tr INNER JOIN albums AS al ON al.AlbumID = tr.AlbumID WHERE al.ArtistID=? '
|
||||||
[newalbumdata['ArtistID']]))
|
'AND al.Status != "Ignored"', [newalbumdata['ArtistID']]))
|
||||||
havetracks = len(myDB.select(
|
havetracks = len(myDB.select(
|
||||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
||||||
[newalbumdata['ArtistID']]))
|
[newalbumdata['ArtistID']]))
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import headphones
|
import headphones
|
||||||
from headphones import db, helpers, logger, lastfm, request
|
from headphones import db, helpers, logger, lastfm, request, mb
|
||||||
|
|
||||||
LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
|
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)
|
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:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -315,18 +323,31 @@ class Cache(object):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
dbalbum = myDB.action(
|
dbalbum = myDB.action(
|
||||||
'SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?',
|
'SELECT ArtistName, AlbumTitle, ReleaseID, Type FROM albums WHERE AlbumID=?',
|
||||||
[self.id]).fetchone()
|
[self.id]).fetchone()
|
||||||
if dbalbum['ReleaseID'] != self.id:
|
if dbalbum['ReleaseID'] != self.id:
|
||||||
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'],
|
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'],
|
||||||
api_key=LASTFM_API_KEY)
|
api_key=LASTFM_API_KEY)
|
||||||
if not data:
|
if not data:
|
||||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
data = lastfm.request_lastfm("album.getinfo",
|
||||||
album=dbalbum['AlbumTitle'],
|
artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
|
||||||
|
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||||
api_key=LASTFM_API_KEY)
|
api_key=LASTFM_API_KEY)
|
||||||
else:
|
else:
|
||||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
if dbalbum['Type'] != "part of":
|
||||||
album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
|
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:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -34,11 +34,18 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'ADD_ALBUM_ART': (int, 'General', 0),
|
'ADD_ALBUM_ART': (int, 'General', 0),
|
||||||
'ADVANCEDENCODER': (str, 'General', ''),
|
'ADVANCEDENCODER': (str, 'General', ''),
|
||||||
'ALBUM_ART_FORMAT': (str, 'General', 'folder'),
|
'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
|
# This is used in importer.py to determine how complete an album needs to
|
||||||
# be - to be considered "downloaded". Percentage from 0-100
|
# be - to be considered "downloaded". Percentage from 0-100
|
||||||
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
|
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
|
||||||
'API_ENABLED': (int, 'General', 0),
|
'API_ENABLED': (int, 'General', 0),
|
||||||
'API_KEY': (str, 'General', ''),
|
'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_ALL': (int, 'General', 0),
|
||||||
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
|
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
|
||||||
'AUTOWANT_UPCOMING': (int, 'General', 1),
|
'AUTOWANT_UPCOMING': (int, 'General', 1),
|
||||||
@@ -138,12 +145,17 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'IGNORED_FILES': (list, 'Advanced', []), # path
|
'IGNORED_FILES': (list, 'Advanced', []), # path
|
||||||
'INCLUDE_EXTRAS': (int, 'General', 0),
|
'INCLUDE_EXTRAS': (int, 'General', 0),
|
||||||
'INTERFACE': (str, 'General', 'default'),
|
'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'),
|
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
||||||
'KAT': (int, 'Kat', 0),
|
'KAT': (int, 'Kat', 0),
|
||||||
'KAT_PROXY_URL': (str, 'Kat', ''),
|
'KAT_PROXY_URL': (str, 'Kat', ''),
|
||||||
'KAT_RATIO': (str, 'Kat', ''),
|
'KAT_RATIO': (str, 'Kat', ''),
|
||||||
'KEEP_NFO': (int, 'General', 0),
|
'KEEP_NFO': (int, 'General', 0),
|
||||||
'KEEP_TORRENT_FILES': (int, 'General', 0),
|
'KEEP_TORRENT_FILES': (int, 'General', 0),
|
||||||
|
'KEEP_TORRENT_FILES_DIR': (path, 'General', ''),
|
||||||
'LASTFM_USERNAME': (str, 'General', ''),
|
'LASTFM_USERNAME': (str, 'General', ''),
|
||||||
'LAUNCH_BROWSER': (int, 'General', 1),
|
'LAUNCH_BROWSER': (int, 'General', 1),
|
||||||
'LIBRARYSCAN': (int, 'General', 1),
|
'LIBRARYSCAN': (int, 'General', 1),
|
||||||
@@ -155,6 +167,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'LOSSLESS_BITRATE_TO': (int, 'General', 0),
|
'LOSSLESS_BITRATE_TO': (int, 'General', 0),
|
||||||
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
|
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
|
||||||
'MB_IGNORE_AGE': (int, 'General', 365),
|
'MB_IGNORE_AGE': (int, 'General', 365),
|
||||||
|
'MB_IGNORE_AGE_MISSING': (int, 'General', 0),
|
||||||
'MININOVA': (int, 'Mininova', 0),
|
'MININOVA': (int, 'Mininova', 0),
|
||||||
'MININOVA_RATIO': (str, 'Mininova', ''),
|
'MININOVA_RATIO': (str, 'Mininova', ''),
|
||||||
'MIRROR': (str, 'General', 'musicbrainz.org'),
|
'MIRROR': (str, 'General', 'musicbrainz.org'),
|
||||||
@@ -226,6 +239,10 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
||||||
'PUSHOVER_ONSNATCH': (int, 'Pushover', 0),
|
'PUSHOVER_ONSNATCH': (int, 'Pushover', 0),
|
||||||
'PUSHOVER_PRIORITY': (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_FILES': (int, 'General', 0),
|
||||||
'RENAME_UNPROCESSED': (bool_int, 'General', 1),
|
'RENAME_UNPROCESSED': (bool_int, 'General', 1),
|
||||||
'RENAME_FROZEN': (bool_int, 'General', 1),
|
'RENAME_FROZEN': (bool_int, 'General', 1),
|
||||||
@@ -236,6 +253,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
|
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
|
||||||
'RUTRACKER_RATIO': (str, 'Rutracker', ''),
|
'RUTRACKER_RATIO': (str, 'Rutracker', ''),
|
||||||
'RUTRACKER_USER': (str, 'Rutracker', ''),
|
'RUTRACKER_USER': (str, 'Rutracker', ''),
|
||||||
|
'RUTRACKER_COOKIE': (str, 'Rutracker', ''),
|
||||||
'SAB_APIKEY': (str, 'SABnzbd', ''),
|
'SAB_APIKEY': (str, 'SABnzbd', ''),
|
||||||
'SAB_CATEGORY': (str, 'SABnzbd', ''),
|
'SAB_CATEGORY': (str, 'SABnzbd', ''),
|
||||||
'SAB_HOST': (str, 'SABnzbd', ''),
|
'SAB_HOST': (str, 'SABnzbd', ''),
|
||||||
@@ -243,6 +261,11 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'SAB_USERNAME': (str, 'SABnzbd', ''),
|
'SAB_USERNAME': (str, 'SABnzbd', ''),
|
||||||
'SAMPLINGFREQUENCY': (int, 'General', 44100),
|
'SAMPLINGFREQUENCY': (int, 'General', 44100),
|
||||||
'SEARCH_INTERVAL': (int, 'General', 1440),
|
'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', ''),
|
'SOFT_CHROOT': (path, 'General', ''),
|
||||||
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
|
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
|
||||||
'SONGKICK_ENABLED': (int, 'Songkick', 1),
|
'SONGKICK_ENABLED': (int, 'Songkick', 1),
|
||||||
@@ -289,16 +312,10 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'WAFFLES_PASSKEY': (str, 'Waffles', ''),
|
'WAFFLES_PASSKEY': (str, 'Waffles', ''),
|
||||||
'WAFFLES_RATIO': (str, 'Waffles', ''),
|
'WAFFLES_RATIO': (str, 'Waffles', ''),
|
||||||
'WAFFLES_UID': (str, 'Waffles', ''),
|
'WAFFLES_UID': (str, 'Waffles', ''),
|
||||||
'WHATCD': (int, 'What.cd', 0),
|
'REDACTED': (int, 'Redacted', 0),
|
||||||
'WHATCD_PASSWORD': (str, 'What.cd', ''),
|
'REDACTED_USERNAME': (str, 'Redacted', ''),
|
||||||
'WHATCD_RATIO': (str, 'What.cd', ''),
|
'REDACTED_PASSWORD': (str, 'Redacted', ''),
|
||||||
'WHATCD_USERNAME': (str, 'What.cd', ''),
|
'REDACTED_RATIO': (str, 'Redacted', ''),
|
||||||
'WHATCD_URL': (str, 'What.cd', 'https://what.cd'),
|
|
||||||
'PTH': (int, 'PassTheHeadphones.me', 0),
|
|
||||||
'PTH_PASSWORD': (str, 'PassTheHeadphones.me', ''),
|
|
||||||
'PTH_RATIO': (str, 'PassTheHeadphones.me', ''),
|
|
||||||
'PTH_USERNAME': (str, 'PassTheHeadphones.me', ''),
|
|
||||||
'PTH_URL': (str, 'PassTheHeadphones.me', 'https://passtheheadphones.me'),
|
|
||||||
'XBMC_ENABLED': (int, 'XBMC', 0),
|
'XBMC_ENABLED': (int, 'XBMC', 0),
|
||||||
'XBMC_HOST': (str, 'XBMC', ''),
|
'XBMC_HOST': (str, 'XBMC', ''),
|
||||||
'XBMC_NOTIFY': (int, 'XBMC', 0),
|
'XBMC_NOTIFY': (int, 'XBMC', 0),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Headphones.
|
# This file is part of Headphones.
|
||||||
#
|
#
|
||||||
# Headphones is free software: you can redistribute it and/or modify
|
# Headphones is free software: you can redistribute it and/or modify
|
||||||
@@ -49,6 +51,7 @@ delugeweb_auth = {}
|
|||||||
delugeweb_url = ''
|
delugeweb_url = ''
|
||||||
deluge_verify_cert = False
|
deluge_verify_cert = False
|
||||||
scrub_logs = True
|
scrub_logs = True
|
||||||
|
headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
|
||||||
|
|
||||||
|
|
||||||
def _scrubber(text):
|
def _scrubber(text):
|
||||||
@@ -80,7 +83,7 @@ def addTorrent(link, data=None, name=None):
|
|||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
retid = False
|
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/']
|
url_waffles = ['https://waffles.ch/', 'http://waffles.ch/']
|
||||||
|
|
||||||
if link.lower().startswith('magnet:'):
|
if link.lower().startswith('magnet:'):
|
||||||
@@ -94,7 +97,7 @@ def addTorrent(link, data=None, name=None):
|
|||||||
if link.lower().startswith(tuple(url_waffles)):
|
if link.lower().startswith(tuple(url_waffles)):
|
||||||
if 'rss=' not in link:
|
if 'rss=' not in link:
|
||||||
link = link + '&rss=1'
|
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')
|
logger.debug('Deluge: Using different User-Agent for this site')
|
||||||
user_agent = 'Headphones'
|
user_agent = 'Headphones'
|
||||||
# This method will make Deluge download the file
|
# This method will make Deluge download the file
|
||||||
@@ -104,11 +107,11 @@ def addTorrent(link, data=None, name=None):
|
|||||||
# return addTorrent(local_torrent_path)
|
# return addTorrent(local_torrent_path)
|
||||||
else:
|
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'
|
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 = ''
|
torrentfile = ''
|
||||||
logger.debug('Deluge: Trying to download (GET)')
|
logger.debug('Deluge: Trying to download (GET)')
|
||||||
try:
|
try:
|
||||||
r = requests.get(link, headers=headers)
|
r = requests.get(link, headers=get_headers)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
logger.debug('Deluge: 200 OK')
|
logger.debug('Deluge: 200 OK')
|
||||||
# .text will ruin the encoding for some torrents
|
# .text will ruin the encoding for some torrents
|
||||||
@@ -135,7 +138,10 @@ def addTorrent(link, data=None, name=None):
|
|||||||
# remove '.torrent' suffix
|
# remove '.torrent' suffix
|
||||||
if name[-len('.torrent'):] == '.torrent':
|
if name[-len('.torrent'):] == '.torrent':
|
||||||
name = name[:-len('.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',
|
result = {'type': 'torrent',
|
||||||
'name': name,
|
'name': name,
|
||||||
'content': torrentfile}
|
'content': torrentfile}
|
||||||
@@ -164,7 +170,10 @@ def addTorrent(link, data=None, name=None):
|
|||||||
# remove '.torrent' suffix
|
# remove '.torrent' suffix
|
||||||
if name[-len('.torrent'):] == '.torrent':
|
if name[-len('.torrent'):] == '.torrent':
|
||||||
name = name[:-len('.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',
|
result = {'type': 'torrent',
|
||||||
'name': name,
|
'name': name,
|
||||||
'content': torrentfile}
|
'content': torrentfile}
|
||||||
@@ -199,7 +208,7 @@ def getTorrentFolder(result):
|
|||||||
],
|
],
|
||||||
"id": 21})
|
"id": 21})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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']
|
result['total_done'] = json.loads(response.text)['result']['total_done']
|
||||||
|
|
||||||
tries = 0
|
tries = 0
|
||||||
@@ -207,7 +216,7 @@ def getTorrentFolder(result):
|
|||||||
tries += 1
|
tries += 1
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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']
|
result['total_done'] = json.loads(response.text)['result']['total_done']
|
||||||
|
|
||||||
post_data = json.dumps({"method": "web.get_torrent_status",
|
post_data = json.dumps({"method": "web.get_torrent_status",
|
||||||
@@ -226,7 +235,7 @@ def getTorrentFolder(result):
|
|||||||
"id": 23})
|
"id": 23})
|
||||||
|
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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['save_path'] = json.loads(response.text)['result']['save_path']
|
||||||
result['name'] = json.loads(response.text)['result']['name']
|
result['name'] = json.loads(response.text)['result']['name']
|
||||||
@@ -242,18 +251,45 @@ def removeTorrent(torrentid, remove_data=False):
|
|||||||
_get_auth()
|
_get_auth()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = False
|
logger.debug('Deluge: Checking if torrent %s finished seeding' % str(torrentid))
|
||||||
post_data = json.dumps({"method": "core.remove_torrent",
|
post_data = json.dumps({"method": "web.get_torrent_status",
|
||||||
"params": [
|
"params": [
|
||||||
torrentid,
|
torrentid,
|
||||||
remove_data
|
[
|
||||||
],
|
"name",
|
||||||
"id": 25})
|
"ratio",
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
"state"
|
||||||
verify=deluge_verify_cert)
|
]
|
||||||
result = json.loads(response.text)['result']
|
],
|
||||||
|
"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:
|
except Exception as e:
|
||||||
logger.error('Deluge: Removing torrent failed: %s' % str(e))
|
logger.error('Deluge: Removing torrent failed: %s' % str(e))
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
@@ -293,12 +329,12 @@ def _get_auth():
|
|||||||
"id": 1})
|
"id": 1})
|
||||||
try:
|
try:
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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:
|
except requests.ConnectionError:
|
||||||
try:
|
try:
|
||||||
logger.debug('Deluge: Connection failed, let\'s try HTTPS just in case')
|
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,
|
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
|
# 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!')
|
logger.error('Deluge: Switching to HTTPS, but certificate won\'t be verified because NO CERTIFICATE WAS CONFIGURED!')
|
||||||
delugeweb_url = delugeweb_url.replace('http:', 'https:')
|
delugeweb_url = delugeweb_url.replace('http:', 'https:')
|
||||||
@@ -323,7 +359,7 @@ def _get_auth():
|
|||||||
"id": 10})
|
"id": 10})
|
||||||
try:
|
try:
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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:
|
except Exception as e:
|
||||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
@@ -340,7 +376,7 @@ def _get_auth():
|
|||||||
"id": 11})
|
"id": 11})
|
||||||
try:
|
try:
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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:
|
except Exception as e:
|
||||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
@@ -348,7 +384,8 @@ def _get_auth():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
delugeweb_hosts = json.loads(response.text)['result']
|
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')
|
logger.error('Deluge: WebUI does not contain daemons')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -358,7 +395,7 @@ def _get_auth():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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:
|
except Exception as e:
|
||||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
@@ -371,7 +408,7 @@ def _get_auth():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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:
|
except Exception as e:
|
||||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
@@ -396,7 +433,7 @@ def _add_torrent_magnet(result):
|
|||||||
"params": [result['url'], {}],
|
"params": [result['url'], {}],
|
||||||
"id": 2})
|
"id": 2})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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']
|
result['hash'] = json.loads(response.text)['result']
|
||||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||||
return json.loads(response.text)['result']
|
return json.loads(response.text)['result']
|
||||||
@@ -416,7 +453,7 @@ def _add_torrent_url(result):
|
|||||||
"params": [result['url'], {}],
|
"params": [result['url'], {}],
|
||||||
"id": 32})
|
"id": 32})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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']
|
result['location'] = json.loads(response.text)['result']
|
||||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||||
return json.loads(response.text)['result']
|
return json.loads(response.text)['result']
|
||||||
@@ -434,10 +471,11 @@ def _add_torrent_file(result):
|
|||||||
try:
|
try:
|
||||||
# content is torrent file contents that needs to be encoded to base64
|
# content is torrent file contents that needs to be encoded to base64
|
||||||
post_data = json.dumps({"method": "core.add_torrent_file",
|
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})
|
"id": 2})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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']
|
result['hash'] = json.loads(response.text)['result']
|
||||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||||
return json.loads(response.text)['result']
|
return json.loads(response.text)['result']
|
||||||
@@ -445,11 +483,13 @@ def _add_torrent_file(result):
|
|||||||
try:
|
try:
|
||||||
# content is torrent file contents that needs to be encoded to base64
|
# content is torrent file contents that needs to be encoded to base64
|
||||||
# this time let's try leaving the encoding as is
|
# 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",
|
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})
|
"id": 22})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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']
|
result['hash'] = json.loads(response.text)['result']
|
||||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||||
return json.loads(response.text)['result']
|
return json.loads(response.text)['result']
|
||||||
@@ -481,7 +521,7 @@ def setTorrentLabel(result):
|
|||||||
"params": [],
|
"params": [],
|
||||||
"id": 3})
|
"id": 3})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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']
|
labels = json.loads(response.text)['result']
|
||||||
|
|
||||||
if labels is not None:
|
if labels is not None:
|
||||||
@@ -492,7 +532,7 @@ def setTorrentLabel(result):
|
|||||||
"params": [label],
|
"params": [label],
|
||||||
"id": 4})
|
"id": 4})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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)
|
logger.debug('Deluge: %s label added to Deluge' % label)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Deluge: Setting label failed: %s' % str(e))
|
logger.error('Deluge: Setting label failed: %s' % str(e))
|
||||||
@@ -504,7 +544,7 @@ def setTorrentLabel(result):
|
|||||||
"params": [result['hash'], label],
|
"params": [result['hash'], label],
|
||||||
"id": 5})
|
"id": 5})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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)
|
logger.debug('Deluge: %s label added to torrent' % label)
|
||||||
else:
|
else:
|
||||||
logger.debug('Deluge: Label plugin not detected')
|
logger.debug('Deluge: Label plugin not detected')
|
||||||
@@ -528,12 +568,12 @@ def setSeedRatio(result):
|
|||||||
"params": [result['hash'], True],
|
"params": [result['hash'], True],
|
||||||
"id": 5})
|
"id": 5})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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",
|
post_data = json.dumps({"method": "core.set_torrent_stop_ratio",
|
||||||
"params": [result['hash'], float(ratio)],
|
"params": [result['hash'], float(ratio)],
|
||||||
"id": 6})
|
"id": 6})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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']
|
return not json.loads(response.text)['error']
|
||||||
|
|
||||||
@@ -556,7 +596,7 @@ def setTorrentPath(result):
|
|||||||
"params": [result['hash'], True],
|
"params": [result['hash'], True],
|
||||||
"id": 7})
|
"id": 7})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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:
|
if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
|
||||||
move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
|
move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
|
||||||
@@ -570,7 +610,7 @@ def setTorrentPath(result):
|
|||||||
"params": [result['hash'], move_to],
|
"params": [result['hash'], move_to],
|
||||||
"id": 8})
|
"id": 8})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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']
|
return not json.loads(response.text)['error']
|
||||||
|
|
||||||
@@ -593,7 +633,7 @@ def setTorrentPause(result):
|
|||||||
"params": [[result['hash']]],
|
"params": [[result['hash']]],
|
||||||
"id": 9})
|
"id": 9})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
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']
|
return not json.loads(response.text)['error']
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ import datetime
|
|||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import glob
|
||||||
|
|
||||||
|
from beets import logging as beetslogging
|
||||||
|
import six
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import re
|
import re
|
||||||
@@ -217,8 +223,7 @@ def replace_illegal_chars(string, type="file"):
|
|||||||
if type == "file":
|
if type == "file":
|
||||||
string = re.sub('[\?"*:|<>/]', '_', string)
|
string = re.sub('[\?"*:|<>/]', '_', string)
|
||||||
if type == "folder":
|
if type == "folder":
|
||||||
string = re.sub('[:\?<>"|]', '_', string)
|
string = re.sub('[:\?<>"|*]', '_', string)
|
||||||
|
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
@@ -255,6 +260,13 @@ _XLATE_SPECIAL = {
|
|||||||
u'&': ' and ', # expand & to ' and '
|
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):
|
def _translate(s, dictionary):
|
||||||
# type: (basestring,Mapping[basestring,basestring])->basestring
|
# type: (basestring,Mapping[basestring,basestring])->basestring
|
||||||
@@ -320,9 +332,27 @@ def clean_name(s):
|
|||||||
# 6. trim
|
# 6. trim
|
||||||
u = u.strip()
|
u = u.strip()
|
||||||
# 7. lowercase
|
# 7. lowercase
|
||||||
|
u = u.lower()
|
||||||
return u
|
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):
|
def cleanTitle(title):
|
||||||
title = re.sub('[\.\-\/\_]', ' ', title).lower()
|
title = re.sub('[\.\-\/\_]', ' ', title).lower()
|
||||||
|
|
||||||
@@ -621,26 +651,68 @@ def get_downloaded_track_list(albumpath):
|
|||||||
return downloaded_track_list
|
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
|
from headphones import logger
|
||||||
new_folder = os.path.join(albumpath,
|
|
||||||
'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
|
# Create temp dir
|
||||||
logger.info(
|
if headphones.CONFIG.KEEP_TORRENT_FILES_DIR:
|
||||||
"Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
|
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:
|
try:
|
||||||
shutil.copytree(albumpath, new_folder)
|
file_name = os.path.basename(os.path.normpath(albumpath))
|
||||||
return new_folder
|
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:
|
except Exception as e:
|
||||||
logger.warn("Cannot copy/move files to temp folder: " +
|
logger.error("Cannot create temp directory: " + tempdir.decode(
|
||||||
new_folder.decode(headphones.SYS_ENCODING, 'replace') +
|
headphones.SYS_ENCODING, 'replace') + ". Error: " + str(e))
|
||||||
". Not continuing. 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
|
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.
|
Attempts to check and split audio files by a cue for the given directory.
|
||||||
"""
|
"""
|
||||||
@@ -662,6 +734,15 @@ def cue_split(albumpath):
|
|||||||
# Split cue
|
# Split cue
|
||||||
if cue_count and cue_count >= count and cue_dirs:
|
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
|
from headphones import logger, cuesplit
|
||||||
logger.info("Attempting to split audio files by cue")
|
logger.info("Attempting to split audio files by cue")
|
||||||
|
|
||||||
@@ -672,12 +753,12 @@ def cue_split(albumpath):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
os.chdir(cwd)
|
os.chdir(cwd)
|
||||||
logger.warn("Cue not split: " + str(e))
|
logger.warn("Cue not split: " + str(e))
|
||||||
return False
|
return None
|
||||||
|
|
||||||
os.chdir(cwd)
|
os.chdir(cwd)
|
||||||
return True
|
return albumpath
|
||||||
|
|
||||||
return False
|
return None
|
||||||
|
|
||||||
|
|
||||||
def extract_logline(s):
|
def extract_logline(s):
|
||||||
@@ -898,3 +979,24 @@ def create_https_certificates(ssl_cert, ssl_key):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
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)
|
||||||
|
|||||||
@@ -262,8 +262,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
if check_release_date is None or check_release_date == u"None":
|
if check_release_date is None or check_release_date == u"None":
|
||||||
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
|
if headphones.CONFIG.MB_IGNORE_AGE_MISSING is not 1:
|
||||||
new_releases = mb.get_new_releases(rgid, includeExtras, True)
|
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
|
||||||
|
new_releases = mb.get_new_releases(rgid, includeExtras, True)
|
||||||
|
else:
|
||||||
|
logger.info("[%s] Skipping update of: %s (No Release Date)" % (artist['artist_name'], rg['title']))
|
||||||
|
new_releases = 0
|
||||||
else:
|
else:
|
||||||
if len(check_release_date) == 10:
|
if len(check_release_date) == 10:
|
||||||
release_date = check_release_date
|
release_date = check_release_date
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ def request_lastfm(method, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if "error" in data:
|
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
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -149,7 +149,10 @@ def getTagTopArtists(tag, limit=50):
|
|||||||
logger.debug("Fetched %d artists from Last.FM", len(artists))
|
logger.debug("Fetched %d artists from Last.FM", len(artists))
|
||||||
|
|
||||||
for artist in 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):
|
if not any(artist_mbid in x for x in results):
|
||||||
artistlist.append(artist_mbid)
|
artistlist.append(artist_mbid)
|
||||||
|
|||||||
@@ -770,3 +770,26 @@ def findAlbumID(artist=None, album=None):
|
|||||||
return False
|
return False
|
||||||
rgid = unicode(results[0]['id'])
|
rgid = unicode(results[0]['id'])
|
||||||
return rgid
|
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
|
# cut down on api calls. If it's ineffective then we'll switch to search
|
||||||
|
|
||||||
replacements = {" & ": " ", ".": ""}
|
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 = helpers.replace_all(artist_name.lower(), replacements)
|
||||||
|
|
||||||
mc_artist_name = mc_artist_name.replace(" ", "-")
|
mc_artist_name = mc_artist_name.replace(" ", "-")
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from urllib import urlencode
|
from urllib import urlencode, quote_plus
|
||||||
import urllib
|
import urllib
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
@@ -148,7 +148,9 @@ class PROWL(object):
|
|||||||
|
|
||||||
http_handler.request("POST",
|
http_handler.request("POST",
|
||||||
"/publicapi/add",
|
"/publicapi/add",
|
||||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
headers={
|
||||||
|
'Content-type':
|
||||||
|
"application/x-www-form-urlencoded"},
|
||||||
body=urlencode(data))
|
body=urlencode(data))
|
||||||
response = http_handler.getresponse()
|
response = http_handler.getresponse()
|
||||||
request_status = response.status
|
request_status = response.status
|
||||||
@@ -203,28 +205,34 @@ class XBMC(object):
|
|||||||
url = host + '/xbmcCmds/xbmcHttp/?' + url_command
|
url = host + '/xbmcCmds/xbmcHttp/?' + url_command
|
||||||
|
|
||||||
if self.password:
|
if self.password:
|
||||||
return request.request_content(url, auth=(self.username, self.password))
|
return request.request_content(url,
|
||||||
|
auth=(self.username, self.password))
|
||||||
else:
|
else:
|
||||||
return request.request_content(url)
|
return request.request_content(url)
|
||||||
|
|
||||||
def _sendjson(self, host, method, params={}):
|
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'}
|
headers = {'Content-Type': 'application/json'}
|
||||||
url = host + '/jsonrpc'
|
url = host + '/jsonrpc'
|
||||||
|
|
||||||
if self.password:
|
if self.password:
|
||||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
response = request.request_json(
|
||||||
headers=headers, auth=(self.username, self.password))
|
url, method="post",
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers=headers, auth=(
|
||||||
|
self.username, self.password))
|
||||||
else:
|
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)
|
headers=headers)
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
return response[0]['result']
|
return response[0]['result']
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
# From what I read you can't update the music library on a per directory or per path basis
|
# From what I read you can't update the music library on a per
|
||||||
# so need to update the whole thing
|
# directory or per path basis so need to update the whole thing
|
||||||
|
|
||||||
hosts = [x.strip() for x in self.hosts.split(',')]
|
hosts = [x.strip() for x in self.hosts.split(',')]
|
||||||
|
|
||||||
@@ -247,18 +255,23 @@ class XBMC(object):
|
|||||||
logger.info('Sending notification command to XMBC @ ' + host)
|
logger.info('Sending notification command to XMBC @ ' + host)
|
||||||
try:
|
try:
|
||||||
version = self._sendjson(host, 'Application.GetProperties',
|
version = self._sendjson(host, 'Application.GetProperties',
|
||||||
{'properties': ['version']})['version']['major']
|
{'properties': ['version']})[
|
||||||
|
'version']['major']
|
||||||
|
|
||||||
if version < 12: # Eden
|
if version < 12: # Eden
|
||||||
notification = header + "," + message + "," + time + "," + albumartpath
|
notification = header + "," + message + "," + time + \
|
||||||
|
"," + albumartpath
|
||||||
notifycommand = {'command': 'ExecBuiltIn',
|
notifycommand = {'command': 'ExecBuiltIn',
|
||||||
'parameter': 'Notification(' + notification + ')'}
|
'parameter': 'Notification(' +
|
||||||
|
notification + ')'}
|
||||||
request = self._sendhttp(host, notifycommand)
|
request = self._sendhttp(host, notifycommand)
|
||||||
|
|
||||||
else: # Frodo
|
else: # Frodo
|
||||||
params = {'title': header, 'message': message, 'displaytime': int(time),
|
params = {'title': header, 'message': message,
|
||||||
|
'displaytime': int(time),
|
||||||
'image': albumartpath}
|
'image': albumartpath}
|
||||||
request = self._sendjson(host, 'GUI.ShowNotification', params)
|
request = self._sendjson(host, 'GUI.ShowNotification',
|
||||||
|
params)
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
raise Exception
|
raise Exception
|
||||||
@@ -323,22 +336,28 @@ class Plex(object):
|
|||||||
url = host + '/xbmcCmds/xbmcHttp/?' + command
|
url = host + '/xbmcCmds/xbmcHttp/?' + command
|
||||||
|
|
||||||
if self.password:
|
if self.password:
|
||||||
response = request.request_response(url, auth=(self.username, self.password))
|
response = request.request_response(url, auth=(
|
||||||
|
self.username, self.password))
|
||||||
else:
|
else:
|
||||||
response = request.request_response(url)
|
response = request.request_response(url)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _sendjson(self, host, method, params={}):
|
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'}
|
headers = {'Content-Type': 'application/json'}
|
||||||
url = host + '/jsonrpc'
|
url = host + '/jsonrpc'
|
||||||
|
|
||||||
if self.password:
|
if self.password:
|
||||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
response = request.request_json(
|
||||||
headers=headers, auth=(self.username, self.password))
|
url, method="post",
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers=headers, auth=(
|
||||||
|
self.username, self.password))
|
||||||
else:
|
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)
|
headers=headers)
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
@@ -346,13 +365,14 @@ class Plex(object):
|
|||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
|
|
||||||
# From what I read you can't update the music library on a per directory or per path basis
|
# From what I read you can't update the music library on a per
|
||||||
# so need to update the whole thing
|
# directory or per path basis so need to update the whole thing
|
||||||
|
|
||||||
hosts = [x.strip() for x in self.server_hosts.split(',')]
|
hosts = [x.strip() for x in self.server_hosts.split(',')]
|
||||||
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
logger.info('Sending library update command to Plex Media Server@ ' + host)
|
logger.info(
|
||||||
|
'Sending library update command to Plex Media Server@ ' + host)
|
||||||
url = "%s/library/sections" % host
|
url = "%s/library/sections" % host
|
||||||
if self.token:
|
if self.token:
|
||||||
params = {'X-Plex-Token': self.token}
|
params = {'X-Plex-Token': self.token}
|
||||||
@@ -369,7 +389,8 @@ class Plex(object):
|
|||||||
|
|
||||||
for s in sections:
|
for s in sections:
|
||||||
if s.getAttribute('type') == "artist":
|
if s.getAttribute('type') == "artist":
|
||||||
url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key'))
|
url = "%s/library/sections/%s/refresh" % (
|
||||||
|
host, s.getAttribute('key'))
|
||||||
request.request_response(url, params=params)
|
request.request_response(url, params=params)
|
||||||
|
|
||||||
def notify(self, artist, album, albumartpath):
|
def notify(self, artist, album, albumartpath):
|
||||||
@@ -381,27 +402,35 @@ class Plex(object):
|
|||||||
time = "3000" # in ms
|
time = "3000" # in ms
|
||||||
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
logger.info('Sending notification command to Plex client @ ' + host)
|
logger.info(
|
||||||
|
'Sending notification command to Plex client @ ' + host)
|
||||||
try:
|
try:
|
||||||
version = self._sendjson(host, 'Application.GetProperties',
|
version = self._sendjson(host, 'Application.GetProperties',
|
||||||
{'properties': ['version']})['version']['major']
|
{'properties': ['version']})[
|
||||||
|
'version']['major']
|
||||||
|
|
||||||
if version < 12: # Eden
|
if version < 12: # Eden
|
||||||
notification = header + "," + message + "," + time + "," + albumartpath
|
notification = header + "," + message + "," + time + \
|
||||||
|
"," + albumartpath
|
||||||
notifycommand = {'command': 'ExecBuiltIn',
|
notifycommand = {'command': 'ExecBuiltIn',
|
||||||
'parameter': 'Notification(' + notification + ')'}
|
'parameter': 'Notification(' +
|
||||||
|
notification + ')'}
|
||||||
request = self._sendhttp(host, notifycommand)
|
request = self._sendhttp(host, notifycommand)
|
||||||
|
|
||||||
else: # Frodo
|
else: # Frodo
|
||||||
params = {'title': header, 'message': message, 'displaytime': int(time),
|
params = {'title': header, 'message': message,
|
||||||
|
'displaytime': int(time),
|
||||||
'image': albumartpath}
|
'image': albumartpath}
|
||||||
request = self._sendjson(host, 'GUI.ShowNotification', params)
|
request = self._sendjson(host, 'GUI.ShowNotification',
|
||||||
|
params)
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
raise Exception
|
raise Exception
|
||||||
|
|
||||||
except 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):
|
class NMA(object):
|
||||||
@@ -419,7 +448,8 @@ class NMA(object):
|
|||||||
message = "Headphones has snatched: " + snatched
|
message = "Headphones has snatched: " + snatched
|
||||||
else:
|
else:
|
||||||
event = artist + ' - ' + album + ' complete!'
|
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 event: " + event)
|
||||||
logger.debug(u"NMA message: " + message)
|
logger.debug(u"NMA message: " + message)
|
||||||
@@ -433,7 +463,8 @@ class NMA(object):
|
|||||||
if len(keys) > 1:
|
if len(keys) > 1:
|
||||||
batch = True
|
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':
|
if not response[api][u'code'] == u'200':
|
||||||
logger.error(u'Could not send notification to NotifyMyAndroid')
|
logger.error(u'Could not send notification to NotifyMyAndroid')
|
||||||
@@ -461,9 +492,11 @@ class PUSHBULLET(object):
|
|||||||
data['device_iden'] = self.deviceid
|
data['device_iden'] = self.deviceid
|
||||||
|
|
||||||
headers = {'Content-type': "application/json",
|
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:
|
if response:
|
||||||
logger.info(u"PushBullet notifications sent.")
|
logger.info(u"PushBullet notifications sent.")
|
||||||
@@ -492,7 +525,9 @@ class PUSHALOT(object):
|
|||||||
|
|
||||||
http_handler.request("POST",
|
http_handler.request("POST",
|
||||||
"/api/sendmessage",
|
"/api/sendmessage",
|
||||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
headers={
|
||||||
|
'Content-type':
|
||||||
|
"application/x-www-form-urlencoded"},
|
||||||
body=urlencode(data))
|
body=urlencode(data))
|
||||||
response = http_handler.getresponse()
|
response = http_handler.getresponse()
|
||||||
request_status = response.status
|
request_status = response.status
|
||||||
@@ -512,6 +547,49 @@ class PUSHALOT(object):
|
|||||||
return False
|
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):
|
class Synoindex(object):
|
||||||
def __init__(self, util_loc='/usr/syno/bin/synoindex'):
|
def __init__(self, util_loc='/usr/syno/bin/synoindex'):
|
||||||
self.util_loc = util_loc
|
self.util_loc = util_loc
|
||||||
@@ -524,7 +602,8 @@ class Synoindex(object):
|
|||||||
|
|
||||||
if not self.util_exists():
|
if not self.util_exists():
|
||||||
logger.warn(
|
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
|
return
|
||||||
|
|
||||||
if os.path.isfile(path):
|
if os.path.isfile(path):
|
||||||
@@ -533,16 +612,19 @@ class Synoindex(object):
|
|||||||
cmd_arg = '-A'
|
cmd_arg = '-A'
|
||||||
else:
|
else:
|
||||||
logger.warn(
|
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
|
return
|
||||||
|
|
||||||
cmd = [self.util_loc, cmd_arg, path]
|
cmd = [self.util_loc, cmd_arg, path]
|
||||||
logger.info("Calling synoindex command: %s" % str(cmd))
|
logger.info("Calling synoindex command: %s" % str(cmd))
|
||||||
try:
|
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)
|
cwd=headphones.PROG_DIR)
|
||||||
out, error = p.communicate()
|
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:
|
except OSError, e:
|
||||||
logger.warn("Error sending notification: %s" % str(e))
|
logger.warn("Error sending notification: %s" % str(e))
|
||||||
|
|
||||||
@@ -580,7 +662,8 @@ class PUSHOVER(object):
|
|||||||
|
|
||||||
headers = {'Content-type': "application/x-www-form-urlencoded"}
|
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:
|
if response:
|
||||||
logger.info(u"Pushover notifications sent.")
|
logger.info(u"Pushover notifications sent.")
|
||||||
@@ -614,20 +697,25 @@ class TwitterNotifier(object):
|
|||||||
def notify_snatch(self, title):
|
def notify_snatch(self, title):
|
||||||
if headphones.CONFIG.TWITTER_ONSNATCH:
|
if headphones.CONFIG.TWITTER_ONSNATCH:
|
||||||
self._notifyTwitter(
|
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):
|
def notify_download(self, title):
|
||||||
if headphones.CONFIG.TWITTER_ENABLED:
|
if headphones.CONFIG.TWITTER_ENABLED:
|
||||||
self._notifyTwitter(common.notifyStrings[
|
self._notifyTwitter(common.notifyStrings[
|
||||||
common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now())
|
common.NOTIFY_DOWNLOAD] + ': ' +
|
||||||
|
title + ' at ' + helpers.now())
|
||||||
|
|
||||||
def test_notify(self):
|
def test_notify(self):
|
||||||
return self._notifyTwitter(
|
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):
|
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)
|
oauth_client = oauth.Client(oauth_consumer)
|
||||||
|
|
||||||
logger.info('Requesting temp token from Twitter')
|
logger.info('Requesting temp token from Twitter')
|
||||||
@@ -635,32 +723,42 @@ class TwitterNotifier(object):
|
|||||||
resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET')
|
resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET')
|
||||||
|
|
||||||
if resp['status'] != '200':
|
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:
|
else:
|
||||||
request_token = dict(parse_qsl(content))
|
request_token = dict(parse_qsl(content))
|
||||||
|
|
||||||
headphones.CONFIG.TWITTER_USERNAME = request_token['oauth_token']
|
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):
|
def _get_credentials(self, key):
|
||||||
request_token = {}
|
request_token = {}
|
||||||
|
|
||||||
request_token['oauth_token'] = headphones.CONFIG.TWITTER_USERNAME
|
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'
|
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)
|
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))
|
logger.info('oauth_consumer: ' + str(oauth_consumer))
|
||||||
oauth_client = oauth.Client(oauth_consumer, token)
|
oauth_client = oauth.Client(oauth_consumer, token)
|
||||||
logger.info('oauth_client: ' + str(oauth_client))
|
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)
|
body='oauth_verifier=%s' % key)
|
||||||
logger.info('resp, content: ' + str(resp) + ',' + str(content))
|
logger.info('resp, content: ' + str(resp) + ',' + str(content))
|
||||||
|
|
||||||
@@ -669,14 +767,18 @@ class TwitterNotifier(object):
|
|||||||
|
|
||||||
logger.info('resp[status] = ' + str(resp['status']))
|
logger.info('resp[status] = ' + str(resp['status']))
|
||||||
if resp['status'] != '200':
|
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)
|
logger.ERROR)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logger.info('Your Twitter Access Token key: %s' % access_token['oauth_token'])
|
logger.info('Your Twitter Access Token key: %s' % access_token[
|
||||||
logger.info('Access Token secret: %s' % access_token['oauth_token_secret'])
|
'oauth_token'])
|
||||||
|
logger.info(
|
||||||
|
'Access Token secret: %s' % access_token['oauth_token_secret'])
|
||||||
headphones.CONFIG.TWITTER_USERNAME = access_token['oauth_token']
|
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
|
return True
|
||||||
|
|
||||||
def _send_tweet(self, message=None):
|
def _send_tweet(self, message=None):
|
||||||
@@ -688,7 +790,8 @@ class TwitterNotifier(object):
|
|||||||
|
|
||||||
logger.info(u"Sending tweet: " + message)
|
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:
|
try:
|
||||||
api.PostUpdate(message)
|
api.PostUpdate(message)
|
||||||
@@ -741,7 +844,8 @@ class OSX_NOTIFY(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
NSUserNotification = self.objc.lookUpClass('NSUserNotification')
|
NSUserNotification = self.objc.lookUpClass('NSUserNotification')
|
||||||
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter')
|
NSUserNotificationCenter = self.objc.lookUpClass(
|
||||||
|
'NSUserNotificationCenter')
|
||||||
NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')
|
NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')
|
||||||
|
|
||||||
if not NSUserNotification or not NSUserNotificationCenter:
|
if not NSUserNotification or not NSUserNotificationCenter:
|
||||||
@@ -756,14 +860,17 @@ class OSX_NOTIFY(object):
|
|||||||
if text:
|
if text:
|
||||||
notification.setInformativeText_(text)
|
notification.setInformativeText_(text)
|
||||||
if sound:
|
if sound:
|
||||||
notification.setSoundName_("NSUserNotificationDefaultSoundName")
|
notification.setSoundName_(
|
||||||
|
"NSUserNotificationDefaultSoundName")
|
||||||
if image:
|
if image:
|
||||||
source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image)
|
source_img = self.AppKit.NSImage.alloc().\
|
||||||
|
initByReferencingFile_(image)
|
||||||
notification.setContentImage_(source_img)
|
notification.setContentImage_(source_img)
|
||||||
# notification.set_identityImage_(source_img)
|
# notification.set_identityImage_(source_img)
|
||||||
notification.setHasActionButton_(False)
|
notification.setHasActionButton_(False)
|
||||||
|
|
||||||
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
notification_center = NSUserNotificationCenter.\
|
||||||
|
defaultUserNotificationCenter()
|
||||||
notification_center.deliverNotification_(notification)
|
notification_center.deliverNotification_(notification)
|
||||||
|
|
||||||
del pool
|
del pool
|
||||||
@@ -784,13 +891,16 @@ class BOXCAR(object):
|
|||||||
def notify(self, title, message, rgid=None):
|
def notify(self, title, message, rgid=None):
|
||||||
try:
|
try:
|
||||||
if rgid:
|
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({
|
data = urllib.urlencode({
|
||||||
'user_credentials': headphones.CONFIG.BOXCAR_TOKEN,
|
'user_credentials': headphones.CONFIG.BOXCAR_TOKEN,
|
||||||
'notification[title]': title.encode('utf-8'),
|
'notification[title]': title.encode('utf-8'),
|
||||||
'notification[long_message]': message.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)
|
req = urllib2.Request(self.url)
|
||||||
@@ -818,8 +928,9 @@ class SubSonicNotifier(object):
|
|||||||
self.host = self.host + "/"
|
self.host = self.host + "/"
|
||||||
|
|
||||||
# Invoke request
|
# Invoke request
|
||||||
request.request_response(self.host + "musicFolderSettings.view?scanNow",
|
request.request_response(
|
||||||
auth=(self.username, self.password))
|
self.host + "musicFolderSettings.view?scanNow",
|
||||||
|
auth=(self.username, self.password))
|
||||||
|
|
||||||
|
|
||||||
class Email(object):
|
class Email(object):
|
||||||
@@ -827,13 +938,15 @@ class Email(object):
|
|||||||
|
|
||||||
message = MIMEText(message, 'plain', "utf-8")
|
message = MIMEText(message, 'plain', "utf-8")
|
||||||
message['Subject'] = subject
|
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
|
message['To'] = headphones.CONFIG.EMAIL_TO
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if headphones.CONFIG.EMAIL_SSL:
|
if headphones.CONFIG.EMAIL_SSL:
|
||||||
mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER,
|
mailserver = smtplib.SMTP_SSL(
|
||||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||||
|
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||||
else:
|
else:
|
||||||
mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER,
|
mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||||
@@ -847,7 +960,8 @@ class Email(object):
|
|||||||
mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER,
|
mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER,
|
||||||
headphones.CONFIG.EMAIL_SMTP_PASSWORD)
|
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())
|
message.as_string())
|
||||||
mailserver.quit()
|
mailserver.quit()
|
||||||
return True
|
return True
|
||||||
@@ -858,7 +972,6 @@ class Email(object):
|
|||||||
|
|
||||||
|
|
||||||
class TELEGRAM(object):
|
class TELEGRAM(object):
|
||||||
|
|
||||||
def notify(self, message, status):
|
def notify(self, message, status):
|
||||||
if not headphones.CONFIG.TELEGRAM_ENABLED:
|
if not headphones.CONFIG.TELEGRAM_ENABLED:
|
||||||
return
|
return
|
||||||
@@ -876,15 +989,49 @@ class TELEGRAM(object):
|
|||||||
|
|
||||||
# Send message to user using Telegram's Bot API
|
# Send message to user using Telegram's Bot API
|
||||||
try:
|
try:
|
||||||
response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload)
|
response = requests.post(TELEGRAM_API % (token, "sendMessage"),
|
||||||
|
data=payload)
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
logger.info(u'Telegram notify failed: ' + str(e))
|
logger.info(u'Telegram notify failed: ' + str(e))
|
||||||
|
|
||||||
# Error logging
|
# Error logging
|
||||||
sent_successfuly = True
|
sent_successfuly = True
|
||||||
if not response.status_code == 200:
|
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
|
sent_successfuly = False
|
||||||
|
|
||||||
logger.info(u"Telegram notifications sent.")
|
logger.info(u"Telegram notifications sent.")
|
||||||
return sent_successfuly
|
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 uuid
|
||||||
import threading
|
import threading
|
||||||
import itertools
|
import itertools
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -25,11 +24,12 @@ import beets
|
|||||||
import headphones
|
import headphones
|
||||||
from beets import autotag
|
from beets import autotag
|
||||||
from beets import config as beetsconfig
|
from beets import config as beetsconfig
|
||||||
|
from beets import logging as beetslogging
|
||||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||||
from beetsplug import lyrics as beetslyrics
|
from beetsplug import lyrics as beetslyrics
|
||||||
from headphones import notifiers, utorrent, transmission, deluge
|
from headphones import notifiers, utorrent, transmission, deluge, qbittorrent
|
||||||
from headphones import db, albumart, librarysync
|
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
|
from headphones import metadata
|
||||||
|
|
||||||
postprocessor_lock = threading.Lock()
|
postprocessor_lock = threading.Lock()
|
||||||
@@ -44,6 +44,8 @@ def checkFolder():
|
|||||||
|
|
||||||
for album in snatched:
|
for album in snatched:
|
||||||
if album['FolderName']:
|
if album['FolderName']:
|
||||||
|
folder_name = album['FolderName']
|
||||||
|
single = False
|
||||||
if album['Kind'] == 'nzb':
|
if album['Kind'] == 'nzb':
|
||||||
download_dir = headphones.CONFIG.DOWNLOAD_DIR
|
download_dir = headphones.CONFIG.DOWNLOAD_DIR
|
||||||
else:
|
else:
|
||||||
@@ -52,21 +54,32 @@ def checkFolder():
|
|||||||
else:
|
else:
|
||||||
download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
|
download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
|
||||||
|
|
||||||
album_path = os.path.join(download_dir, album['FolderName']).encode(
|
# Get folder from torrent hash
|
||||||
headphones.SYS_ENCODING, 'replace')
|
if album['TorrentHash'] and headphones.CONFIG.TORRENT_DOWNLOADER:
|
||||||
logger.debug("Checking if %s exists" % album_path)
|
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):
|
if folder_name:
|
||||||
logger.info('Found "' + album['FolderName'] + '" in ' + album[
|
album_path = os.path.join(download_dir, folder_name).encode(
|
||||||
'Kind'] + ' download folder. Verifying....')
|
headphones.SYS_ENCODING, 'replace')
|
||||||
verify(album['AlbumID'], album_path, album['Kind'])
|
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:
|
else:
|
||||||
logger.info("No folder name found for " + album['Title'])
|
logger.info("No folder name found for " + album['Title'])
|
||||||
|
|
||||||
logger.debug("Checking download folder finished.")
|
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()
|
myDB = db.DBConnection()
|
||||||
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
|
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
|
||||||
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
|
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")
|
'replace') + " isn't complete yet. Will try again on the next run")
|
||||||
return
|
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(
|
if headphones.CONFIG.CUE_SPLIT and downloaded_cuecount and downloaded_cuecount >= len(
|
||||||
downloaded_track_list):
|
downloaded_track_list):
|
||||||
if headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent":
|
new_folder = None
|
||||||
albumpath = helpers.preserve_torrent_directory(albumpath)
|
new_albumpath = albumpath
|
||||||
if albumpath and helpers.cue_split(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)
|
downloaded_track_list = helpers.get_downloaded_track_list(albumpath)
|
||||||
|
keep_original_folder = False
|
||||||
|
|
||||||
# test #1: metadata - usually works
|
# test #1: metadata - usually works
|
||||||
logger.debug('Verifying metadata...')
|
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:
|
if metaartist == dbartist and metaalbum == dbalbum:
|
||||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||||
keep_original_folder)
|
keep_original_folder, forced, single)
|
||||||
return
|
return
|
||||||
|
|
||||||
# test #2: filenames
|
# test #2: filenames
|
||||||
@@ -262,7 +300,7 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
|||||||
|
|
||||||
if dbtrack in filetrack:
|
if dbtrack in filetrack:
|
||||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||||
keep_original_folder)
|
keep_original_folder, forced, single)
|
||||||
return
|
return
|
||||||
|
|
||||||
# test #3: number of songs and duration
|
# 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)
|
delta = abs(downloaded_track_duration - db_track_duration)
|
||||||
if delta < 240:
|
if delta < 240:
|
||||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||||
keep_original_folder)
|
keep_original_folder, forced, single)
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.warn(u'Could not identify album: %s. It may not be the intended album.',
|
logger.warn(u'Could not identify album: %s. It may not be the intended album.',
|
||||||
albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
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(
|
myDB.action(
|
||||||
'UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?',
|
'UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
|
||||||
[albumid])
|
|
||||||
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
|
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
|
||||||
if not processed:
|
if not processed:
|
||||||
if headphones.CONFIG.RENAME_UNPROCESSED:
|
if headphones.CONFIG.RENAME_UNPROCESSED and not keep_original_folder:
|
||||||
renameUnprocessedFolder(albumpath, tag="Unprocessed")
|
renameUnprocessedFolder(albumpath, tag="Unprocessed")
|
||||||
else:
|
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'))
|
albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||||
|
|
||||||
|
|
||||||
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None,
|
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None,
|
||||||
keep_original_folder=False):
|
keep_original_folder=False, forced=False, single=False):
|
||||||
logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle']))
|
logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle']))
|
||||||
new_folder = None
|
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:
|
# Preserve the torrent dir
|
||||||
new_folder = tempfile.mkdtemp(prefix="headphones_")
|
if keep_original_folder or single:
|
||||||
subdir = os.path.join(new_folder, "headphones")
|
temp_path = helpers.preserve_torrent_directory(albumpath, forced, single)
|
||||||
logger.info("Copying files to " + subdir.decode(headphones.SYS_ENCODING, 'replace') + " subfolder to preserve downloaded files for seeding")
|
if not temp_path:
|
||||||
try:
|
markAsUnprocessed(albumid, albumpath, keep_original_folder)
|
||||||
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)
|
|
||||||
return
|
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.
|
# Need to update the downloaded track list with the new location.
|
||||||
# Could probably just throw in the "headphones-modified" folder,
|
# 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
|
# but this is good to make sure we're not counting files that may have failed to move
|
||||||
|
if new_folder:
|
||||||
downloaded_track_list = []
|
downloaded_track_list = []
|
||||||
|
|
||||||
for r, d, f in os.walk(albumpath):
|
for r, d, f in os.walk(albumpath):
|
||||||
for files in f:
|
for files in f:
|
||||||
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
|
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
|
||||||
@@ -352,7 +393,8 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
|||||||
downloaded_track.decode(headphones.SYS_ENCODING, "replace"))
|
downloaded_track.decode(headphones.SYS_ENCODING, "replace"))
|
||||||
return
|
return
|
||||||
except IOError:
|
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:
|
if new_folder:
|
||||||
shutil.rmtree(new_folder)
|
shutil.rmtree(new_folder)
|
||||||
return
|
return
|
||||||
@@ -387,21 +429,13 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
|||||||
shutil.rmtree(new_folder)
|
shutil.rmtree(new_folder)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# get artwork and path
|
||||||
|
album_art_path = None
|
||||||
artwork = None
|
artwork = None
|
||||||
album_art_path = albumart.getAlbumArt(albumid)
|
if headphones.CONFIG.EMBED_ALBUM_ART or headphones.CONFIG.ADD_ALBUM_ART or \
|
||||||
if headphones.CONFIG.EMBED_ALBUM_ART or headphones.CONFIG.ADD_ALBUM_ART:
|
(headphones.CONFIG.PLEX_ENABLED and headphones.CONFIG.PLEX_NOTIFY) or \
|
||||||
|
(headphones.CONFIG.XBMC_ENABLED and headphones.CONFIG.XBMC_NOTIFY):
|
||||||
if album_art_path:
|
album_art_path, artwork = albumart.getAlbumArt(albumid)
|
||||||
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 and artwork:
|
if headphones.CONFIG.EMBED_ALBUM_ART and artwork:
|
||||||
embedAlbumArt(artwork, downloaded_track_list)
|
embedAlbumArt(artwork, downloaded_track_list)
|
||||||
@@ -446,12 +480,12 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
|||||||
[albumid])
|
[albumid])
|
||||||
|
|
||||||
# Check if torrent has finished seeding
|
# 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(
|
seed_snatched = myDB.action(
|
||||||
'SELECT * from snatched WHERE Status="Seed_Snatched" and AlbumID=?',
|
'SELECT * from snatched WHERE Status="Seed_Snatched" and AlbumID=?',
|
||||||
[albumid]).fetchone()
|
[albumid]).fetchone()
|
||||||
if seed_snatched:
|
if seed_snatched:
|
||||||
hash = seed_snatched['FolderName']
|
hash = seed_snatched['TorrentHash']
|
||||||
torrent_removed = False
|
torrent_removed = False
|
||||||
logger.info(u'%s - %s. Checking if torrent has finished seeding and can be removed' % (
|
logger.info(u'%s - %s. Checking if torrent has finished seeding and can be removed' % (
|
||||||
release['ArtistName'], release['AlbumTitle']))
|
release['ArtistName'], release['AlbumTitle']))
|
||||||
@@ -459,8 +493,10 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
|||||||
torrent_removed = transmission.removeTorrent(hash, True)
|
torrent_removed = transmission.removeTorrent(hash, True)
|
||||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
|
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
|
||||||
torrent_removed = deluge.removeTorrent(hash, True)
|
torrent_removed = deluge.removeTorrent(hash, True)
|
||||||
else:
|
elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||||
torrent_removed = utorrent.removeTorrent(hash, True)
|
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
|
# Torrent removed, delete the snatched record, else update Status for scheduled job to check
|
||||||
if torrent_removed:
|
if torrent_removed:
|
||||||
@@ -538,6 +574,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
|||||||
pushbullet = notifiers.PUSHBULLET()
|
pushbullet = notifiers.PUSHBULLET()
|
||||||
pushbullet.notify(pushmessage, statusmessage)
|
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:
|
if headphones.CONFIG.TELEGRAM_ENABLED:
|
||||||
logger.info(u"Telegram request")
|
logger.info(u"Telegram request")
|
||||||
telegram = notifiers.TELEGRAM()
|
telegram = notifiers.TELEGRAM()
|
||||||
@@ -918,12 +959,29 @@ def correctMetadata(albumid, release, downloaded_track_list):
|
|||||||
if not items:
|
if not items:
|
||||||
continue
|
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:
|
try:
|
||||||
cur_artist, cur_album, candidates, rec = autotag.tag_album(items,
|
beetslog = beetslogging.getLogger('beets')
|
||||||
search_artist=helpers.latinToAscii(
|
beetslog.set_global_level(beetslogging.DEBUG) if headphones.VERBOSE else beetslog.set_global_level(
|
||||||
release['ArtistName']),
|
beetslogging.CRITICAL)
|
||||||
search_album=helpers.latinToAscii(
|
with helpers.capture_beets_log() as logs:
|
||||||
release['AlbumTitle']))
|
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:
|
except Exception as e:
|
||||||
logger.error('Error getting recommendation: %s. Not writing metadata', e)
|
logger.error('Error getting recommendation: %s. Not writing metadata', e)
|
||||||
return False
|
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',
|
'Found a match in the database: %s. Verifying to make sure it is the correct album',
|
||||||
snatched['Title'])
|
snatched['Title'])
|
||||||
verify(snatched['AlbumID'], folder, snatched['Kind'],
|
verify(snatched['AlbumID'], folder, snatched['Kind'],
|
||||||
keep_original_folder=keep_original_folder)
|
forced=True, keep_original_folder=keep_original_folder)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Attempt 2: strip release group id from filename
|
# 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:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
'Found a (possibly) valid Musicbrainz release group id in album folder name.')
|
'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
|
continue
|
||||||
|
|
||||||
# Attempt 3a: parse the folder name into a valid format
|
# 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(
|
logger.info(
|
||||||
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
||||||
release['ArtistName'], release['AlbumTitle'])
|
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
|
continue
|
||||||
else:
|
else:
|
||||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name,
|
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
|
rgid = None
|
||||||
|
|
||||||
if rgid:
|
if rgid:
|
||||||
verify(rgid, folder, keep_original_folder=keep_original_folder)
|
verify(rgid, folder, forced=True, keep_original_folder=keep_original_folder)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
||||||
@@ -1257,12 +1316,23 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
|||||||
except Exception:
|
except Exception:
|
||||||
name = album = None
|
name = album = None
|
||||||
|
|
||||||
# Check if there's a cue to split
|
# Not found from meta data, check if there's a cue to split and try meta data again
|
||||||
if headphones.CONFIG.CUE_SPLIT and not name and not album and helpers.cue_split(folder):
|
kind = None
|
||||||
try:
|
if headphones.CONFIG.CUE_SPLIT and not name and not album:
|
||||||
name, album, year = helpers.extract_metadata(folder)
|
cue_folder = helpers.cue_split(folder, keep_original_folder=keep_original_folder)
|
||||||
except Exception:
|
if cue_folder:
|
||||||
name = album = None
|
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:
|
if name and album:
|
||||||
release = myDB.action(
|
release = myDB.action(
|
||||||
@@ -1272,7 +1342,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
|||||||
logger.info(
|
logger.info(
|
||||||
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
||||||
release['ArtistName'], release['AlbumTitle'])
|
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
|
continue
|
||||||
else:
|
else:
|
||||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name,
|
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
|
rgid = None
|
||||||
|
|
||||||
if rgid:
|
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
|
continue
|
||||||
else:
|
else:
|
||||||
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
||||||
@@ -1301,7 +1371,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
|||||||
logger.info(
|
logger.info(
|
||||||
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
||||||
release['ArtistName'], release['AlbumTitle'])
|
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
|
continue
|
||||||
else:
|
else:
|
||||||
logger.info('Querying MusicBrainz for the release group id for: %s',
|
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
|
rgid = None
|
||||||
|
|
||||||
if rgid:
|
if rgid:
|
||||||
verify(rgid, folder, keep_original_folder=keep_original_folder)
|
verify(rgid, folder, forced=True, keep_original_folder=keep_original_folder)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
||||||
|
|||||||
@@ -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 re
|
||||||
|
|
||||||
import requests as requests
|
import requests as requests
|
||||||
|
# from requests.auth import HTTPDigestAuth
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
import headphones
|
import headphones
|
||||||
@@ -24,7 +25,7 @@ class Rutracker(object):
|
|||||||
return self.loggedin
|
return self.loggedin
|
||||||
|
|
||||||
def still_logged_in(self, html):
|
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
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
@@ -34,7 +35,7 @@ class Rutracker(object):
|
|||||||
Logs in user
|
Logs in user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
loginpage = 'http://login.rutracker.org/forum/login.php'
|
loginpage = 'http://rutracker.org/forum/login.php'
|
||||||
post_params = {
|
post_params = {
|
||||||
'login_username': headphones.CONFIG.RUTRACKER_USER,
|
'login_username': headphones.CONFIG.RUTRACKER_USER,
|
||||||
'login_password': headphones.CONFIG.RUTRACKER_PASSWORD,
|
'login_password': headphones.CONFIG.RUTRACKER_PASSWORD,
|
||||||
@@ -46,10 +47,14 @@ class Rutracker(object):
|
|||||||
try:
|
try:
|
||||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
|
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
|
||||||
# try again
|
# try again
|
||||||
if not self.has_bb_data_cookie(r):
|
if not self.has_bb_session_cookie(r):
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
|
if headphones.CONFIG.RUTRACKER_COOKIE:
|
||||||
if self.has_bb_data_cookie(r):
|
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:
|
||||||
|
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
|
||||||
|
if self.has_bb_session_cookie(r):
|
||||||
self.loggedin = True
|
self.loggedin = True
|
||||||
logger.info("Successfully logged in to rutracker")
|
logger.info("Successfully logged in to rutracker")
|
||||||
else:
|
else:
|
||||||
@@ -62,11 +67,11 @@ class Rutracker(object):
|
|||||||
self.loggedin = False
|
self.loggedin = False
|
||||||
return self.loggedin
|
return self.loggedin
|
||||||
|
|
||||||
def has_bb_data_cookie(self, response):
|
def has_bb_session_cookie(self, response):
|
||||||
if 'bb_data' in response.cookies.keys():
|
if 'bb_session' in response.cookies.keys():
|
||||||
return True
|
return True
|
||||||
# Rutracker randomly send a 302 redirect code, cookie may be present in response history
|
# Rutracker randomly send a 302 redirect code, cookie may be present in response history
|
||||||
return next(('bb_data' in r.cookies.keys() for r in response.history), False)
|
return next(('bb_session' in r.cookies.keys() for r in response.history), False)
|
||||||
|
|
||||||
def searchurl(self, artist, album, year, format):
|
def searchurl(self, artist, album, year, format):
|
||||||
"""
|
"""
|
||||||
@@ -93,7 +98,11 @@ class Rutracker(object):
|
|||||||
|
|
||||||
# sort by size, descending.
|
# sort by size, descending.
|
||||||
sort = '&o=7&s=2'
|
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)
|
logger.info("Searching rutracker using term: %s", searchterm)
|
||||||
return searchurl
|
return searchurl
|
||||||
|
|
||||||
@@ -169,7 +178,7 @@ class Rutracker(object):
|
|||||||
return the .torrent data
|
return the .torrent data
|
||||||
"""
|
"""
|
||||||
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
|
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}
|
cookie = {'bb_dl': torrent_id}
|
||||||
try:
|
try:
|
||||||
headers = {'Referer': url}
|
headers = {'Referer': url}
|
||||||
@@ -216,3 +225,37 @@ class Rutracker(object):
|
|||||||
self.session.post(url, params={'action': 'add-file'}, files=files)
|
self.session.post(url, params={'action': 'add-file'}, files=files)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception('Error adding file to utorrent %s', 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)
|
||||||
|
|||||||
@@ -32,22 +32,26 @@ import re
|
|||||||
from pygazelle import api as gazelleapi
|
from pygazelle import api as gazelleapi
|
||||||
from pygazelle import encoding as gazelleencoding
|
from pygazelle import encoding as gazelleencoding
|
||||||
from pygazelle import format as gazelleformat
|
from pygazelle import format as gazelleformat
|
||||||
|
from pygazelle import release_type as gazellerelease_type
|
||||||
import headphones
|
import headphones
|
||||||
from headphones.common import USER_AGENT
|
from headphones.common import USER_AGENT
|
||||||
from headphones import logger, db, helpers, classes, sab, nzbget, request
|
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
|
from bencode import bencode, bdecode
|
||||||
|
|
||||||
# Magnet to torrent services, for Black hole. Stolen from CouchPotato.
|
# Magnet to torrent services, for Black hole. Stolen from CouchPotato.
|
||||||
TORRENT_TO_MAGNET_SERVICES = [
|
TORRENT_TO_MAGNET_SERVICES = [
|
||||||
# 'https://zoink.it/torrent/%s.torrent',
|
# 'https://zoink.it/torrent/%s.torrent',
|
||||||
# 'http://torrage.com/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
|
# Persistent Apollo.rip API object
|
||||||
gazelle = None
|
apolloobj = None
|
||||||
ruobj = None
|
ruobj = None
|
||||||
|
# Persistent RED API object
|
||||||
|
redobj = None
|
||||||
|
|
||||||
|
|
||||||
def fix_url(s, charset="utf-8"):
|
def fix_url(s, charset="utf-8"):
|
||||||
@@ -160,10 +164,10 @@ def get_seed_ratio(provider):
|
|||||||
seed_ratio = headphones.CONFIG.RUTRACKER_RATIO
|
seed_ratio = headphones.CONFIG.RUTRACKER_RATIO
|
||||||
elif provider == 'Kick Ass Torrents':
|
elif provider == 'Kick Ass Torrents':
|
||||||
seed_ratio = headphones.CONFIG.KAT_RATIO
|
seed_ratio = headphones.CONFIG.KAT_RATIO
|
||||||
elif provider == 'What.cd':
|
elif provider == 'Apollo.rip':
|
||||||
seed_ratio = headphones.CONFIG.WHATCD_RATIO
|
seed_ratio = headphones.CONFIG.APOLLO_RATIO
|
||||||
elif provider == 'PassTheHeadphones.Me':
|
elif provider == 'Redacted':
|
||||||
seed_ratio = headphones.CONFIG.PTH_RATIO
|
seed_ratio = headphones.CONFIG.REDACTED_RATIO
|
||||||
elif provider == 'The Pirate Bay':
|
elif provider == 'The Pirate Bay':
|
||||||
seed_ratio = headphones.CONFIG.PIRATEBAY_RATIO
|
seed_ratio = headphones.CONFIG.PIRATEBAY_RATIO
|
||||||
elif provider == 'Old Pirate Bay':
|
elif provider == 'Old Pirate Bay':
|
||||||
@@ -274,9 +278,10 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
|
|||||||
headphones.CONFIG.MININOVA or
|
headphones.CONFIG.MININOVA or
|
||||||
headphones.CONFIG.WAFFLES or
|
headphones.CONFIG.WAFFLES or
|
||||||
headphones.CONFIG.RUTRACKER or
|
headphones.CONFIG.RUTRACKER or
|
||||||
headphones.CONFIG.WHATCD or
|
headphones.CONFIG.APOLLO or
|
||||||
headphones.CONFIG.PTH or
|
headphones.CONFIG.REDACTED or
|
||||||
headphones.CONFIG.STRIKE)
|
headphones.CONFIG.STRIKE or
|
||||||
|
headphones.CONFIG.TQUATTRECENTONZE)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
myDB = db.DBConnection()
|
myDB = db.DBConnection()
|
||||||
@@ -737,7 +742,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
}
|
}
|
||||||
|
|
||||||
data = request.request_json(
|
data = request.request_json(
|
||||||
url='http://api.omgwtfnzbs.org/json/',
|
url='http://api.omgwtfnzbs.me/json/',
|
||||||
params=params, headers=headers
|
params=params, headers=headers
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -874,12 +879,11 @@ def send_to_downloader(data, bestqual, album):
|
|||||||
services = TORRENT_TO_MAGNET_SERVICES[:]
|
services = TORRENT_TO_MAGNET_SERVICES[:]
|
||||||
random.shuffle(services)
|
random.shuffle(services)
|
||||||
headers = {'User-Agent': USER_AGENT}
|
headers = {'User-Agent': USER_AGENT}
|
||||||
headers['Referer'] = 'https://torcache.net/'
|
|
||||||
|
|
||||||
for service in services:
|
for service in services:
|
||||||
|
|
||||||
data = request.request_content(service % torrent_hash, headers=headers)
|
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):
|
if not torrent_to_file(download_path, data):
|
||||||
return
|
return
|
||||||
# Extract folder name from torrent
|
# Extract folder name from torrent
|
||||||
@@ -925,11 +929,11 @@ def send_to_downloader(data, bestqual, album):
|
|||||||
logger.error("Error sending torrent to Transmission. Are you sure it's running?")
|
logger.error("Error sending torrent to Transmission. Are you sure it's running?")
|
||||||
return
|
return
|
||||||
|
|
||||||
folder_name = transmission.getTorrentFolder(torrentid)
|
folder_name = transmission.getName(torrentid)
|
||||||
if folder_name:
|
if folder_name:
|
||||||
logger.info('Torrent folder name: %s' % folder_name)
|
logger.info('Torrent name: %s' % folder_name)
|
||||||
else:
|
else:
|
||||||
logger.error('Torrent folder name could not be determined')
|
logger.error('Torrent name could not be determined')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Set Seed Ratio
|
# Set Seed Ratio
|
||||||
@@ -979,7 +983,7 @@ def send_to_downloader(data, bestqual, album):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Error sending torrent to Deluge: %s' % str(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")
|
logger.info("Sending torrent to uTorrent")
|
||||||
|
|
||||||
# Add torrent
|
# Add torrent
|
||||||
@@ -1010,19 +1014,42 @@ def send_to_downloader(data, bestqual, album):
|
|||||||
seed_ratio = get_seed_ratio(bestqual[3])
|
seed_ratio = get_seed_ratio(bestqual[3])
|
||||||
if seed_ratio is not None:
|
if seed_ratio is not None:
|
||||||
utorrent.setSeedRatio(torrentid, seed_ratio)
|
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 = db.DBConnection()
|
||||||
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']])
|
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,
|
[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
|
# 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:
|
if seed_ratio is not None and seed_ratio != 0 and torrentid:
|
||||||
myDB.action(
|
myDB.action(
|
||||||
'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)',
|
'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)',
|
||||||
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", torrentid,
|
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", folder_name,
|
||||||
kind])
|
kind, torrentid])
|
||||||
|
|
||||||
# notify
|
# notify
|
||||||
artist = album[1]
|
artist = album[1]
|
||||||
@@ -1050,6 +1077,14 @@ def send_to_downloader(data, bestqual, album):
|
|||||||
logger.info(u"Sending PushBullet notification")
|
logger.info(u"Sending PushBullet notification")
|
||||||
pushbullet = notifiers.PUSHBULLET()
|
pushbullet = notifiers.PUSHBULLET()
|
||||||
pushbullet.notify(name, "Download started")
|
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:
|
if headphones.CONFIG.TELEGRAM_ENABLED and headphones.CONFIG.TELEGRAM_ONSNATCH:
|
||||||
logger.info(u"Sending Telegram notification")
|
logger.info(u"Sending Telegram notification")
|
||||||
telegram = notifiers.TELEGRAM()
|
telegram = notifiers.TELEGRAM()
|
||||||
@@ -1176,7 +1211,8 @@ def verifyresult(title, artistterm, term, lossless):
|
|||||||
|
|
||||||
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||||
choose_specific_download=False):
|
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
|
global ruobj # and rutracker
|
||||||
|
|
||||||
reldate = album['ReleaseDate']
|
reldate = album['ReleaseDate']
|
||||||
@@ -1256,10 +1292,13 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
|
|
||||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||||
categories = "3040"
|
categories = "3040"
|
||||||
|
maxsize = 10000000000
|
||||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
|
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
|
||||||
categories = "3040,3010"
|
categories = "3040,3010,3050"
|
||||||
|
maxsize = 10000000000
|
||||||
else:
|
else:
|
||||||
categories = "3010"
|
categories = "3010,3050"
|
||||||
|
maxsize = 300000000
|
||||||
|
|
||||||
if album['Type'] == 'Other':
|
if album['Type'] == 'Other':
|
||||||
categories = "3030"
|
categories = "3030"
|
||||||
@@ -1269,8 +1308,12 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
|
|
||||||
provider = torznab_host[0]
|
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
|
# 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}
|
headers = {'User-Agent': USER_AGENT}
|
||||||
params = {
|
params = {
|
||||||
@@ -1281,25 +1324,47 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
"q": term
|
"q": term
|
||||||
}
|
}
|
||||||
|
|
||||||
data = request.request_feed(
|
data = request.request_soup(
|
||||||
url=torznab_host[0] + '/api?',
|
url=torznab_host[0],
|
||||||
params=params, headers=headers
|
params=params, headers=headers
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process feed
|
# Process feed
|
||||||
if data:
|
if data:
|
||||||
if not len(data.entries):
|
items = data.find_all('item')
|
||||||
logger.info(u"No results found from %s for %s", torznab_host[0], term)
|
if not items:
|
||||||
|
logger.info(u"No results found from %s for %s", provider, term)
|
||||||
else:
|
else:
|
||||||
for item in data.entries:
|
for item in items:
|
||||||
try:
|
try:
|
||||||
url = item.link
|
title = item.title.get_text()
|
||||||
title = item.title
|
url = item.find("link").next_sibling.strip()
|
||||||
size = int(item.links[1]['length'])
|
seeders = int(item.find("torznab:attr", attrs={"name": "seeders"}).get('value'))
|
||||||
|
|
||||||
|
# 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.size.string)
|
||||||
|
|
||||||
if all(word.lower() in title.lower() for word in term.split()):
|
if all(word.lower() in title.lower() for word in term.split()):
|
||||||
logger.info(
|
if size < maxsize and minimumseeders < seeders:
|
||||||
'Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||||
resultlist.append((title, size, url, provider, 'torrent', True))
|
resultlist.append((title, size, url, provider, 'torrent', True))
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
'%s is larger than the maxsize or has too little seeders for this category, '
|
||||||
|
'skipping. (Size: %i bytes, Seeders: %d)',
|
||||||
|
title, size, seeders)
|
||||||
else:
|
else:
|
||||||
logger.info('Skipping %s, not all search term words found' % title)
|
logger.info('Skipping %s, not all search term words found' % title)
|
||||||
|
|
||||||
@@ -1315,7 +1380,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
if headphones.CONFIG.KAT_PROXY_URL:
|
if headphones.CONFIG.KAT_PROXY_URL:
|
||||||
providerurl = fix_url(set_proxy(headphones.CONFIG.KAT_PROXY_URL))
|
providerurl = fix_url(set_proxy(headphones.CONFIG.KAT_PROXY_URL))
|
||||||
else:
|
else:
|
||||||
providerurl = fix_url("https://kat.cr")
|
providerurl = fix_url("https://katcr.co/new/")
|
||||||
|
|
||||||
# Build URL
|
# Build URL
|
||||||
providerurl = providerurl + "/usearch/" + ka_term
|
providerurl = providerurl + "/usearch/" + ka_term
|
||||||
@@ -1391,8 +1456,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
query_items = [usersearchterm]
|
query_items = [usersearchterm]
|
||||||
|
|
||||||
query_items.extend(['format:(%s)' % format,
|
query_items.extend(['format:(%s)' % format,
|
||||||
'size:[0 TO %d]' % maxsize,
|
'size:[0 TO %d]' % maxsize])
|
||||||
'-seeders:0']) # cut out dead torrents
|
# (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:
|
if bitrate:
|
||||||
query_items.append('bitrate:"%s"' % bitrate)
|
query_items.append('bitrate:"%s"' % bitrate)
|
||||||
@@ -1468,9 +1534,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
if rulist:
|
if rulist:
|
||||||
resultlist.extend(rulist)
|
resultlist.extend(rulist)
|
||||||
|
|
||||||
if headphones.CONFIG.WHATCD:
|
if headphones.CONFIG.APOLLO:
|
||||||
provider = "What.cd"
|
provider = "Apollo.rip"
|
||||||
providerurl = "http://what.cd/"
|
providerurl = "http://apollo.rip/"
|
||||||
|
|
||||||
bitrate = None
|
bitrate = None
|
||||||
bitrate_string = bitrate
|
bitrate_string = bitrate
|
||||||
@@ -1493,7 +1559,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
bitrate_string = encoding_string
|
bitrate_string = encoding_string
|
||||||
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
||||||
logger.info(
|
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
|
maxsize = 10000000000
|
||||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
|
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
|
||||||
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
||||||
@@ -1502,31 +1568,60 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
search_formats = [gazelleformat.MP3]
|
search_formats = [gazelleformat.MP3]
|
||||||
maxsize = 300000000
|
maxsize = 300000000
|
||||||
|
|
||||||
if not gazelle or not gazelle.logged_in():
|
if not apolloobj or not apolloobj.logged_in():
|
||||||
try:
|
try:
|
||||||
logger.info(u"Attempting to log in to What.cd...")
|
logger.info(u"Attempting to log in to Apollo.rip...")
|
||||||
gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.WHATCD_USERNAME,
|
apolloobj = gazelleapi.GazelleAPI(headphones.CONFIG.APOLLO_USERNAME,
|
||||||
headphones.CONFIG.WHATCD_PASSWORD,
|
headphones.CONFIG.APOLLO_PASSWORD,
|
||||||
headphones.CONFIG.WHATCD_URL)
|
headphones.CONFIG.APOLLO_URL)
|
||||||
gazelle._login()
|
apolloobj._login()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
gazelle = None
|
apolloobj = None
|
||||||
logger.error(u"What.cd credentials incorrect or site is down. Error: %s %s" % (
|
logger.error(u"Apollo.rip credentials incorrect or site is down. Error: %s %s" % (
|
||||||
e.__class__.__name__, str(e)))
|
e.__class__.__name__, str(e)))
|
||||||
|
|
||||||
if gazelle and gazelle.logged_in():
|
if apolloobj and apolloobj.logged_in():
|
||||||
logger.info(u"Searching %s..." % provider)
|
logger.info(u"Searching %s..." % provider)
|
||||||
all_torrents = []
|
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:
|
for search_format in search_formats:
|
||||||
if usersearchterm:
|
if usersearchterm:
|
||||||
all_torrents.extend(
|
all_torrents.extend(
|
||||||
gazelle.search_torrents(searchstr=usersearchterm, format=search_format,
|
apolloobj.search_torrents(searchstr=usersearchterm, format=search_format,
|
||||||
encoding=bitrate_string)['results'])
|
encoding=bitrate_string, releasetype=album_type)['results'])
|
||||||
else:
|
else:
|
||||||
all_torrents.extend(gazelle.search_torrents(artistname=semi_clean_artist_term,
|
all_torrents.extend(apolloobj.search_torrents(artistname=semi_clean_artist_term,
|
||||||
groupname=semi_clean_album_term,
|
groupname=semi_clean_album_term,
|
||||||
format=search_format,
|
format=search_format,
|
||||||
encoding=bitrate_string)['results'])
|
encoding=bitrate_string,
|
||||||
|
releasetype=album_type)['results'])
|
||||||
|
|
||||||
# filter on format, size, and num seeders
|
# filter on format, size, and num seeders
|
||||||
logger.info(u"Filtering torrents by format, maximum size, and minimum seeders...")
|
logger.info(u"Filtering torrents by format, maximum size, and minimum seeders...")
|
||||||
@@ -1563,14 +1658,14 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
||||||
resultlist.append((torrent.file_path,
|
resultlist.append((torrent.file_path,
|
||||||
torrent.size,
|
torrent.size,
|
||||||
gazelle.generate_torrent_link(torrent.id),
|
apolloobj.generate_torrent_link(torrent.id),
|
||||||
provider,
|
provider,
|
||||||
'torrent', True))
|
'torrent', True))
|
||||||
|
|
||||||
# PassTheHeadphones.me - Using same logic as What.CD as it's also Gazelle, so should really make this into something reusable
|
# Redacted - Using same logic as What.CD as it's also Gazelle, so should really make this into something reusable
|
||||||
if headphones.CONFIG.PTH:
|
if headphones.CONFIG.REDACTED:
|
||||||
provider = "PassTheHeadphones.me"
|
provider = "Redacted"
|
||||||
providerurl = "https://passtheheadphones.me/"
|
providerurl = "https://redacted.ch"
|
||||||
|
|
||||||
bitrate = None
|
bitrate = None
|
||||||
bitrate_string = bitrate
|
bitrate_string = bitrate
|
||||||
@@ -1593,7 +1688,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
bitrate_string = encoding_string
|
bitrate_string = encoding_string
|
||||||
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
||||||
logger.info(
|
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 RED filters, so not using it as a search parameter.")
|
||||||
maxsize = 10000000000
|
maxsize = 10000000000
|
||||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
|
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
|
||||||
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
||||||
@@ -1602,28 +1697,28 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
search_formats = [gazelleformat.MP3]
|
search_formats = [gazelleformat.MP3]
|
||||||
maxsize = 300000000
|
maxsize = 300000000
|
||||||
|
|
||||||
if not gazelle or not gazelle.logged_in():
|
if not redobj or not redobj.logged_in():
|
||||||
try:
|
try:
|
||||||
logger.info(u"Attempting to log in to PassTheHeadphones.me...")
|
logger.info(u"Attempting to log in to Redacted...")
|
||||||
gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.PTH_USERNAME,
|
redobj = gazelleapi.GazelleAPI(headphones.CONFIG.REDACTED_USERNAME,
|
||||||
headphones.CONFIG.PTH_PASSWORD,
|
headphones.CONFIG.REDACTED_PASSWORD,
|
||||||
headphones.CONFIG.PTH_URL)
|
providerurl)
|
||||||
gazelle._login()
|
redobj._login()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
gazelle = None
|
redobj = None
|
||||||
logger.error(u"PassTheHeadphones credentials incorrect or site is down. Error: %s %s" % (
|
logger.error(u"Redacted credentials incorrect or site is down. Error: %s %s" % (
|
||||||
e.__class__.__name__, str(e)))
|
e.__class__.__name__, str(e)))
|
||||||
|
|
||||||
if gazelle and gazelle.logged_in():
|
if redobj and redobj.logged_in():
|
||||||
logger.info(u"Searching %s..." % provider)
|
logger.info(u"Searching %s..." % provider)
|
||||||
all_torrents = []
|
all_torrents = []
|
||||||
for search_format in search_formats:
|
for search_format in search_formats:
|
||||||
if usersearchterm:
|
if usersearchterm:
|
||||||
all_torrents.extend(
|
all_torrents.extend(
|
||||||
gazelle.search_torrents(searchstr=usersearchterm, format=search_format,
|
redobj.search_torrents(searchstr=usersearchterm, format=search_format,
|
||||||
encoding=bitrate_string)['results'])
|
encoding=bitrate_string)['results'])
|
||||||
else:
|
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,
|
groupname=semi_clean_album_term,
|
||||||
format=search_format,
|
format=search_format,
|
||||||
encoding=bitrate_string)['results'])
|
encoding=bitrate_string)['results'])
|
||||||
@@ -1663,20 +1758,20 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
||||||
resultlist.append((torrent.file_path,
|
resultlist.append((torrent.file_path,
|
||||||
torrent.size,
|
torrent.size,
|
||||||
gazelle.generate_torrent_link(torrent.id),
|
redobj.generate_torrent_link(torrent.id),
|
||||||
provider,
|
provider,
|
||||||
'torrent', True))
|
'torrent', True))
|
||||||
|
|
||||||
# Pirate Bay
|
# Pirate Bay
|
||||||
if headphones.CONFIG.PIRATEBAY:
|
if headphones.CONFIG.PIRATEBAY:
|
||||||
provider = "The Pirate Bay"
|
provider = "The Pirate Bay"
|
||||||
tpb_term = term.replace("!", "")
|
tpb_term = term.replace("!", "").replace("'", " ")
|
||||||
|
|
||||||
# Use proxy if specified
|
# Use proxy if specified
|
||||||
if headphones.CONFIG.PIRATEBAY_PROXY_URL:
|
if headphones.CONFIG.PIRATEBAY_PROXY_URL:
|
||||||
providerurl = fix_url(set_proxy(headphones.CONFIG.PIRATEBAY_PROXY_URL))
|
providerurl = fix_url(set_proxy(headphones.CONFIG.PIRATEBAY_PROXY_URL))
|
||||||
else:
|
else:
|
||||||
providerurl = fix_url("https://thepiratebay.se")
|
providerurl = fix_url("https://thepiratebay.org")
|
||||||
|
|
||||||
# Build URL
|
# Build URL
|
||||||
providerurl = providerurl + "/search/" + tpb_term + "/0/7/" # 7 is sort by seeders
|
providerurl = providerurl + "/search/" + tpb_term + "/0/7/" # 7 is sort by seeders
|
||||||
@@ -1904,7 +1999,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
if headphones.CONFIG.TQUATTRECENTONZE:
|
if headphones.CONFIG.TQUATTRECENTONZE:
|
||||||
username = headphones.CONFIG.TQUATTRECENTONZE_USER
|
username = headphones.CONFIG.TQUATTRECENTONZE_USER
|
||||||
password = headphones.CONFIG.TQUATTRECENTONZE_PASSWORD
|
password = headphones.CONFIG.TQUATTRECENTONZE_PASSWORD
|
||||||
API_URL = "http://api.t411.li"
|
API_URL = "http://api.t411.ai"
|
||||||
AUTH_URL = API_URL + '/auth'
|
AUTH_URL = API_URL + '/auth'
|
||||||
DL_URL = API_URL + '/torrents/download/'
|
DL_URL = API_URL + '/torrents/download/'
|
||||||
provider = "t411"
|
provider = "t411"
|
||||||
@@ -2008,9 +2103,9 @@ def preprocess(resultlist):
|
|||||||
if result[3] == 'Kick Ass Torrents':
|
if result[3] == 'Kick Ass Torrents':
|
||||||
headers['Referer'] = 'https://torcache.net/'
|
headers['Referer'] = 'https://torcache.net/'
|
||||||
headers['User-Agent'] = USER_AGENT
|
headers['User-Agent'] = USER_AGENT
|
||||||
elif result[3] == 'What.cd':
|
elif result[3] == 'Apollo.rip':
|
||||||
headers['User-Agent'] = 'Headphones'
|
headers['User-Agent'] = 'Headphones'
|
||||||
elif result[3] == 'PassTheHeadphones.me':
|
elif result[3] == 'Redacted':
|
||||||
headers['User-Agent'] = 'Headphones'
|
headers['User-Agent'] = 'Headphones'
|
||||||
elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay":
|
elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay":
|
||||||
headers[
|
headers[
|
||||||
|
|||||||
@@ -13,13 +13,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from headphones import db, utorrent, transmission, logger
|
from headphones import db, utorrent, transmission, deluge, qbittorrent, logger
|
||||||
import headphones
|
import headphones
|
||||||
|
|
||||||
postprocessor_lock = threading.Lock()
|
|
||||||
|
|
||||||
|
|
||||||
def checkTorrentFinished():
|
def checkTorrentFinished():
|
||||||
"""
|
"""
|
||||||
@@ -28,21 +25,25 @@ def checkTorrentFinished():
|
|||||||
|
|
||||||
logger.info("Checking if any torrents have finished seeding and can be removed")
|
logger.info("Checking if any torrents have finished seeding and can be removed")
|
||||||
|
|
||||||
with postprocessor_lock:
|
myDB = db.DBConnection()
|
||||||
myDB = db.DBConnection()
|
results = myDB.select('SELECT * from snatched WHERE Status="Seed_Processed"')
|
||||||
results = myDB.select('SELECT * from snatched WHERE Status="Seed_Processed"')
|
|
||||||
|
|
||||||
for album in results:
|
for album in results:
|
||||||
hash = album['FolderName']
|
hash = album['TorrentHash']
|
||||||
albumid = album['AlbumID']
|
albumid = album['AlbumID']
|
||||||
torrent_removed = False
|
torrent_removed = False
|
||||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
|
|
||||||
torrent_removed = transmission.removeTorrent(hash, True)
|
|
||||||
else:
|
|
||||||
torrent_removed = utorrent.removeTorrent(hash, True)
|
|
||||||
|
|
||||||
if torrent_removed:
|
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
|
||||||
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?',
|
torrent_removed = transmission.removeTorrent(hash, True)
|
||||||
[albumid])
|
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")
|
logger.info("Checking finished torrents completed")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import time
|
|||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
import urlparse
|
import urlparse
|
||||||
|
import os
|
||||||
|
|
||||||
from headphones import logger, request
|
from headphones import logger, request
|
||||||
import headphones
|
import headphones
|
||||||
@@ -33,7 +34,7 @@ _session_id = None
|
|||||||
def addTorrent(link, data=None):
|
def addTorrent(link, data=None):
|
||||||
method = 'torrent-add'
|
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:
|
if data:
|
||||||
metainfo = str(base64.b64encode(data))
|
metainfo = str(base64.b64encode(data))
|
||||||
else:
|
else:
|
||||||
@@ -64,7 +65,31 @@ def addTorrent(link, data=None):
|
|||||||
return False
|
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'
|
method = 'torrent-get'
|
||||||
arguments = {'ids': torrentid, 'fields': ['name', 'percentDone']}
|
arguments = {'ids': torrentid, 'fields': ['name', 'percentDone']}
|
||||||
|
|
||||||
|
|||||||
@@ -39,14 +39,15 @@ def runGit(args):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + headphones.PROG_DIR)
|
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)
|
cwd=headphones.PROG_DIR)
|
||||||
output, err = p.communicate()
|
output, err = p.communicate()
|
||||||
output = output.strip()
|
output = output.strip()
|
||||||
|
|
||||||
logger.debug('Git output: ' + output)
|
logger.debug('Git output: ' + output)
|
||||||
except OSError:
|
except OSError as e:
|
||||||
logger.debug('Command failed: %s', cmd)
|
logger.debug('Command failed: %s. Error: %s' % (cmd, e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if 'not found' in output or "not recognized as an internal or external command" in output:
|
if 'not found' in output or "not recognized as an internal or external command" in output:
|
||||||
|
|||||||
@@ -252,7 +252,10 @@ class WebInterface(object):
|
|||||||
namecheck = myDB.select('SELECT ArtistName from artists where ArtistID=?', [ArtistID])
|
namecheck = myDB.select('SELECT ArtistName from artists where ArtistID=?', [ArtistID])
|
||||||
for name in namecheck:
|
for name in namecheck:
|
||||||
artistname = name['ArtistName']
|
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])
|
myDB.action('DELETE from artists WHERE ArtistID=?', [ArtistID])
|
||||||
|
|
||||||
from headphones import cache
|
from headphones import cache
|
||||||
@@ -345,14 +348,18 @@ class WebInterface(object):
|
|||||||
|
|
||||||
dirs = set(dirs)
|
dirs = set(dirs)
|
||||||
|
|
||||||
for dir in dirs:
|
try:
|
||||||
artistfolder = os.path.join(dir, folder)
|
for dir in dirs:
|
||||||
if not os.path.isdir(artistfolder.encode(headphones.SYS_ENCODING)):
|
artistfolder = os.path.join(dir, folder)
|
||||||
logger.debug("Cannot find directory: " + artistfolder)
|
if not os.path.isdir(artistfolder.encode(headphones.SYS_ENCODING)):
|
||||||
continue
|
logger.debug("Cannot find directory: " + artistfolder)
|
||||||
threading.Thread(target=librarysync.libraryScan,
|
continue
|
||||||
kwargs={"dir": artistfolder, "artistScan": True, "ArtistID": ArtistID,
|
threading.Thread(target=librarysync.libraryScan,
|
||||||
"ArtistName": artist_name}).start()
|
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)
|
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@@ -534,7 +541,7 @@ class WebInterface(object):
|
|||||||
myDB = db.DBConnection()
|
myDB = db.DBConnection()
|
||||||
upcoming = myDB.select(
|
upcoming = myDB.select(
|
||||||
"SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate ASC")
|
"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,
|
return serve_template(templatename="upcoming.html", title="Upcoming", upcoming=upcoming,
|
||||||
wanted=wanted)
|
wanted=wanted)
|
||||||
|
|
||||||
@@ -871,11 +878,19 @@ class WebInterface(object):
|
|||||||
def musicScan(self, path, scan=0, redirect=None, autoadd=0, libraryscan=0):
|
def musicScan(self, path, scan=0, redirect=None, autoadd=0, libraryscan=0):
|
||||||
headphones.CONFIG.LIBRARYSCAN = libraryscan
|
headphones.CONFIG.LIBRARYSCAN = libraryscan
|
||||||
headphones.CONFIG.AUTO_ADD_ARTISTS = autoadd
|
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:
|
if scan:
|
||||||
try:
|
try:
|
||||||
threading.Thread(target=librarysync.libraryScan).start()
|
threading.Thread(target=librarysync.libraryScan, kwargs=params).start()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Unable to complete the scan: %s' % e)
|
logger.error('Unable to complete the scan: %s' % e)
|
||||||
if redirect:
|
if redirect:
|
||||||
@@ -1145,6 +1160,7 @@ class WebInterface(object):
|
|||||||
"download_scan_interval": headphones.CONFIG.DOWNLOAD_SCAN_INTERVAL,
|
"download_scan_interval": headphones.CONFIG.DOWNLOAD_SCAN_INTERVAL,
|
||||||
"update_db_interval": headphones.CONFIG.UPDATE_DB_INTERVAL,
|
"update_db_interval": headphones.CONFIG.UPDATE_DB_INTERVAL,
|
||||||
"mb_ignore_age": headphones.CONFIG.MB_IGNORE_AGE,
|
"mb_ignore_age": headphones.CONFIG.MB_IGNORE_AGE,
|
||||||
|
"mb_ignore_age_missing": headphones.CONFIG.MB_IGNORE_AGE_MISSING,
|
||||||
"search_interval": headphones.CONFIG.SEARCH_INTERVAL,
|
"search_interval": headphones.CONFIG.SEARCH_INTERVAL,
|
||||||
"libraryscan_interval": headphones.CONFIG.LIBRARYSCAN_INTERVAL,
|
"libraryscan_interval": headphones.CONFIG.LIBRARYSCAN_INTERVAL,
|
||||||
"sab_host": headphones.CONFIG.SAB_HOST,
|
"sab_host": headphones.CONFIG.SAB_HOST,
|
||||||
@@ -1157,6 +1173,10 @@ class WebInterface(object):
|
|||||||
"nzbget_password": headphones.CONFIG.NZBGET_PASSWORD,
|
"nzbget_password": headphones.CONFIG.NZBGET_PASSWORD,
|
||||||
"nzbget_category": headphones.CONFIG.NZBGET_CATEGORY,
|
"nzbget_category": headphones.CONFIG.NZBGET_CATEGORY,
|
||||||
"nzbget_priority": headphones.CONFIG.NZBGET_PRIORITY,
|
"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_host": headphones.CONFIG.TRANSMISSION_HOST,
|
||||||
"transmission_username": headphones.CONFIG.TRANSMISSION_USERNAME,
|
"transmission_username": headphones.CONFIG.TRANSMISSION_USERNAME,
|
||||||
"transmission_password": headphones.CONFIG.TRANSMISSION_PASSWORD,
|
"transmission_password": headphones.CONFIG.TRANSMISSION_PASSWORD,
|
||||||
@@ -1177,6 +1197,7 @@ class WebInterface(object):
|
|||||||
"torrent_downloader_transmission": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 1),
|
"torrent_downloader_transmission": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 1),
|
||||||
"torrent_downloader_utorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 2),
|
"torrent_downloader_utorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 2),
|
||||||
"torrent_downloader_deluge": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 3),
|
"torrent_downloader_deluge": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 3),
|
||||||
|
"torrent_downloader_qbittorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 4),
|
||||||
"download_dir": headphones.CONFIG.DOWNLOAD_DIR,
|
"download_dir": headphones.CONFIG.DOWNLOAD_DIR,
|
||||||
"use_blackhole": checked(headphones.CONFIG.BLACKHOLE),
|
"use_blackhole": checked(headphones.CONFIG.BLACKHOLE),
|
||||||
"blackhole_dir": headphones.CONFIG.BLACKHOLE_DIR,
|
"blackhole_dir": headphones.CONFIG.BLACKHOLE_DIR,
|
||||||
@@ -1224,16 +1245,16 @@ class WebInterface(object):
|
|||||||
"rutracker_user": headphones.CONFIG.RUTRACKER_USER,
|
"rutracker_user": headphones.CONFIG.RUTRACKER_USER,
|
||||||
"rutracker_password": headphones.CONFIG.RUTRACKER_PASSWORD,
|
"rutracker_password": headphones.CONFIG.RUTRACKER_PASSWORD,
|
||||||
"rutracker_ratio": headphones.CONFIG.RUTRACKER_RATIO,
|
"rutracker_ratio": headphones.CONFIG.RUTRACKER_RATIO,
|
||||||
"use_whatcd": checked(headphones.CONFIG.WHATCD),
|
"rutracker_cookie": headphones.CONFIG.RUTRACKER_COOKIE,
|
||||||
"whatcd_username": headphones.CONFIG.WHATCD_USERNAME,
|
"use_apollo": checked(headphones.CONFIG.APOLLO),
|
||||||
"whatcd_password": headphones.CONFIG.WHATCD_PASSWORD,
|
"apollo_username": headphones.CONFIG.APOLLO_USERNAME,
|
||||||
"whatcd_ratio": headphones.CONFIG.WHATCD_RATIO,
|
"apollo_password": headphones.CONFIG.APOLLO_PASSWORD,
|
||||||
"whatcd_url": headphones.CONFIG.WHATCD_URL,
|
"apollo_ratio": headphones.CONFIG.APOLLO_RATIO,
|
||||||
"use_pth": checked(headphones.CONFIG.PTH),
|
"apollo_url": headphones.CONFIG.APOLLO_URL,
|
||||||
"pth_username": headphones.CONFIG.PTH_USERNAME,
|
"use_redacted": checked(headphones.CONFIG.REDACTED),
|
||||||
"pth_password": headphones.CONFIG.PTH_PASSWORD,
|
"redacted_username": headphones.CONFIG.REDACTED_USERNAME,
|
||||||
"pth_ratio": headphones.CONFIG.PTH_RATIO,
|
"redacted_password": headphones.CONFIG.REDACTED_PASSWORD,
|
||||||
"pth_url": headphones.CONFIG.PTH_URL,
|
"redacted_ratio": headphones.CONFIG.REDACTED_RATIO,
|
||||||
"use_strike": checked(headphones.CONFIG.STRIKE),
|
"use_strike": checked(headphones.CONFIG.STRIKE),
|
||||||
"strike_ratio": headphones.CONFIG.STRIKE_RATIO,
|
"strike_ratio": headphones.CONFIG.STRIKE_RATIO,
|
||||||
"use_tquattrecentonze": checked(headphones.CONFIG.TQUATTRECENTONZE),
|
"use_tquattrecentonze": checked(headphones.CONFIG.TQUATTRECENTONZE),
|
||||||
@@ -1262,6 +1283,8 @@ class WebInterface(object):
|
|||||||
"keep_nfo": checked(headphones.CONFIG.KEEP_NFO),
|
"keep_nfo": checked(headphones.CONFIG.KEEP_NFO),
|
||||||
"add_album_art": checked(headphones.CONFIG.ADD_ALBUM_ART),
|
"add_album_art": checked(headphones.CONFIG.ADD_ALBUM_ART),
|
||||||
"album_art_format": headphones.CONFIG.ALBUM_ART_FORMAT,
|
"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_album_art": checked(headphones.CONFIG.EMBED_ALBUM_ART),
|
||||||
"embed_lyrics": checked(headphones.CONFIG.EMBED_LYRICS),
|
"embed_lyrics": checked(headphones.CONFIG.EMBED_LYRICS),
|
||||||
"replace_existing_folders": checked(headphones.CONFIG.REPLACE_EXISTING_FOLDERS),
|
"replace_existing_folders": checked(headphones.CONFIG.REPLACE_EXISTING_FOLDERS),
|
||||||
@@ -1288,6 +1311,7 @@ class WebInterface(object):
|
|||||||
"magnet_links_3": radio(headphones.CONFIG.MAGNET_LINKS, 3),
|
"magnet_links_3": radio(headphones.CONFIG.MAGNET_LINKS, 3),
|
||||||
"log_dir": headphones.CONFIG.LOG_DIR,
|
"log_dir": headphones.CONFIG.LOG_DIR,
|
||||||
"cache_dir": headphones.CONFIG.CACHE_DIR,
|
"cache_dir": headphones.CONFIG.CACHE_DIR,
|
||||||
|
"keep_torrent_files_dir": headphones.CONFIG.KEEP_TORRENT_FILES_DIR,
|
||||||
"interface_list": interface_list,
|
"interface_list": interface_list,
|
||||||
"music_encoder": checked(headphones.CONFIG.MUSIC_ENCODER),
|
"music_encoder": checked(headphones.CONFIG.MUSIC_ENCODER),
|
||||||
"encoder": headphones.CONFIG.ENCODER,
|
"encoder": headphones.CONFIG.ENCODER,
|
||||||
@@ -1388,7 +1412,16 @@ class WebInterface(object):
|
|||||||
"email_ssl": checked(headphones.CONFIG.EMAIL_SSL),
|
"email_ssl": checked(headphones.CONFIG.EMAIL_SSL),
|
||||||
"email_tls": checked(headphones.CONFIG.EMAIL_TLS),
|
"email_tls": checked(headphones.CONFIG.EMAIL_TLS),
|
||||||
"email_onsnatch": checked(headphones.CONFIG.EMAIL_ONSNATCH),
|
"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():
|
for k, v in config.iteritems():
|
||||||
@@ -1435,8 +1468,8 @@ class WebInterface(object):
|
|||||||
"use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled",
|
"use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled",
|
||||||
"use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay",
|
"use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay",
|
||||||
"use_mininova", "use_waffles", "use_rutracker",
|
"use_mininova", "use_waffles", "use_rutracker",
|
||||||
"use_whatcd", "use_strike", "use_tquattrecentonze", "preferred_bitrate_allow_lossless", "detect_bitrate",
|
"use_apollo", "use_redacted", "use_strike", "use_tquattrecentonze", "preferred_bitrate_allow_lossless",
|
||||||
"ignore_clean_releases", "freeze_db", "cue_split", "move_files",
|
"detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files",
|
||||||
"rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art",
|
"rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art",
|
||||||
"embed_album_art", "embed_lyrics",
|
"embed_album_art", "embed_lyrics",
|
||||||
"replace_existing_folders", "keep_original_folder", "file_underscores",
|
"replace_existing_folders", "keep_original_folder", "file_underscores",
|
||||||
@@ -1456,7 +1489,8 @@ class WebInterface(object):
|
|||||||
"osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch",
|
"osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch",
|
||||||
"songkick_enabled", "songkick_filter_enabled",
|
"songkick_enabled", "songkick_filter_enabled",
|
||||||
"mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch",
|
"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:
|
for checked_config in checked_configs:
|
||||||
if checked_config not in kwargs:
|
if checked_config not in kwargs:
|
||||||
@@ -1725,6 +1759,12 @@ class WebInterface(object):
|
|||||||
telegram = notifiers.TELEGRAM()
|
telegram = notifiers.TELEGRAM()
|
||||||
telegram.notify("it works!", "lazers pew pew")
|
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):
|
class Artwork(object):
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
name="headphones"
|
name="headphones"
|
||||||
rcvar=${name}_enable
|
rcvar=${name}_enable
|
||||||
|
load_rc_config ${name}
|
||||||
|
|
||||||
: "${headphones_enable:="NO"}"
|
: "${headphones_enable:="NO"}"
|
||||||
: "${headphones_user:="_sabnzbd"}"
|
: "${headphones_user:="_sabnzbd"}"
|
||||||
@@ -30,7 +31,7 @@ rcvar=${name}_enable
|
|||||||
: "${headphones_conf:="/usr/local/headphones/config.ini"}"
|
: "${headphones_conf:="/usr/local/headphones/config.ini"}"
|
||||||
|
|
||||||
command="${headphones_dir}/Headphones.py"
|
command="${headphones_dir}/Headphones.py"
|
||||||
command_interpreter="/usr/bin/python"
|
command_interpreter="/usr/local/bin/python"
|
||||||
pidfile="/var/run/headphones/headphones.pid"
|
pidfile="/var/run/headphones/headphones.pid"
|
||||||
start_precmd="headphones_start_precmd"
|
start_precmd="headphones_start_precmd"
|
||||||
headphones_flags="--daemon --nolaunch --pidfile $pidfile --config $headphones_conf $headphones_flags"
|
headphones_flags="--daemon --nolaunch --pidfile $pidfile --config $headphones_conf $headphones_flags"
|
||||||
|
|||||||
@@ -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/
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# This file is part of beets.
|
# 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
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
# a copy of this software and associated documentation files (the
|
# a copy of this software and associated documentation files (the
|
||||||
@@ -12,17 +13,33 @@
|
|||||||
# The above copyright notice and this permission notice shall be
|
# The above copyright notice and this permission notice shall be
|
||||||
# included in all copies or substantial portions of the Software.
|
# included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
# This particular version has been slightly modified to work with Headphones
|
from __future__ import division, absolute_import, print_function
|
||||||
# https://github.com/rembo10/headphones
|
|
||||||
|
|
||||||
__version__ = '1.3.10-headphones'
|
|
||||||
__author__ = 'Adrian Sampson <adrian@radbox.org>'
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import beets.library
|
|
||||||
from beets.util import confit
|
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__)
|
||||||
|
|||||||
@@ -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:])
|
||||||
@@ -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})
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# This file is part of beets.
|
# 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
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
# a copy of this software and associated documentation files (the
|
# a copy of this software and associated documentation files (the
|
||||||
@@ -14,13 +15,15 @@
|
|||||||
|
|
||||||
"""Facilities for automatically determining files' correct metadata.
|
"""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
|
from beets import config
|
||||||
|
|
||||||
# Parts of external interface.
|
# Parts of external interface.
|
||||||
from .hooks import AlbumInfo, TrackInfo, AlbumMatch, TrackMatch # noqa
|
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
|
from .match import Recommendation # noqa
|
||||||
|
|
||||||
# Global logger.
|
# Global logger.
|
||||||
@@ -39,6 +42,16 @@ def apply_item_metadata(item, track_info):
|
|||||||
item.mb_trackid = track_info.track_id
|
item.mb_trackid = track_info.track_id
|
||||||
if track_info.artist_id:
|
if track_info.artist_id:
|
||||||
item.mb_artistid = 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
|
# At the moment, the other metadata is left intact (including album
|
||||||
# and track number). Perhaps these should be emptied?
|
# 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
|
"""Set the items' metadata to match an AlbumInfo object using a
|
||||||
mapping from Items to TrackInfo objects.
|
mapping from Items to TrackInfo objects.
|
||||||
"""
|
"""
|
||||||
for item, track_info in mapping.iteritems():
|
for item, track_info in mapping.items():
|
||||||
# Album, artist, track count.
|
# Album, artist, track count.
|
||||||
if track_info.artist:
|
if track_info.artist:
|
||||||
item.artist = track_info.artist
|
item.artist = track_info.artist
|
||||||
@@ -90,7 +103,12 @@ def apply_metadata(album_info, mapping):
|
|||||||
item.title = track_info.title
|
item.title = track_info.title
|
||||||
|
|
||||||
if config['per_disc_numbering']:
|
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)
|
item.tracktotal = track_info.medium_total or len(album_info.tracks)
|
||||||
else:
|
else:
|
||||||
item.track = track_info.index
|
item.track = track_info.index
|
||||||
@@ -122,7 +140,8 @@ def apply_metadata(album_info, mapping):
|
|||||||
'language',
|
'language',
|
||||||
'country',
|
'country',
|
||||||
'albumstatus',
|
'albumstatus',
|
||||||
'albumdisambig'):
|
'albumdisambig',
|
||||||
|
'data_source',):
|
||||||
value = getattr(album_info, field)
|
value = getattr(album_info, field)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
item[field] = value
|
item[field] = value
|
||||||
@@ -132,5 +151,14 @@ def apply_metadata(album_info, mapping):
|
|||||||
if track_info.media is not None:
|
if track_info.media is not None:
|
||||||
item.media = track_info.media
|
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
|
# Headphones seal of approval
|
||||||
item.comments = 'tagged by headphones/beets'
|
item.comments = 'tagged by headphones/beets'
|
||||||