Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3199605be | ||
|
|
9a7006ad14 | ||
|
|
38197a5871 | ||
|
|
ee58f62b2b | ||
|
|
58edc604b3 | ||
|
|
ba33a5e655 | ||
|
|
379fd3d0b8 | ||
|
|
374b106522 | ||
|
|
bf74f57535 | ||
|
|
f1a03e5fb6 | ||
|
|
50cbe2e943 | ||
|
|
219f855dfc | ||
|
|
f18334d87c | ||
|
|
72fce3cdeb | ||
|
|
5283b48736 | ||
|
|
507d8aa7d2 | ||
|
|
4bce833c1e | ||
|
|
f7a23bb4d0 | ||
|
|
5d145b6f8a | ||
|
|
3ab31f1da7 | ||
|
|
cf85d94a37 | ||
|
|
a8a53353ee | ||
|
|
476ee7b312 | ||
|
|
14bef1e63d | ||
|
|
dc22bb006d | ||
|
|
b49cd8a813 | ||
|
|
d7275e5a99 | ||
|
|
aa1867cf06 | ||
|
|
19b766a7ce | ||
|
|
b45f3565da | ||
|
|
22525668fd | ||
|
|
af79bc47e8 | ||
|
|
6208fe0e69 | ||
|
|
f66a37abbd | ||
|
|
4b52ae445d | ||
|
|
8a2c3169e2 | ||
|
|
805eb7dd13 | ||
|
|
81486d2cd2 | ||
|
|
d1237b052a | ||
|
|
a4ef757d12 | ||
|
|
bb8eb19dbf | ||
|
|
602e115a29 | ||
|
|
e2c335db2d | ||
|
|
44583d42dd | ||
|
|
43d465ca22 | ||
|
|
bd60935493 | ||
|
|
0264b1fb0d | ||
|
|
24f22834ff | ||
|
|
aed2e63984 | ||
|
|
694c17a40b | ||
|
|
6c34bc46e9 | ||
|
|
0b2600b783 | ||
|
|
693e5b1624 | ||
|
|
287278c049 | ||
|
|
496560909f | ||
|
|
22b1bf36ab | ||
|
|
b506a1797d | ||
|
|
ff8417b410 | ||
|
|
741cb466ef | ||
|
|
a99a6abde2 | ||
|
|
4631b3cbbf | ||
|
|
7230428a31 | ||
|
|
2b5b2bd506 | ||
|
|
99c3e8498d | ||
|
|
472f657c88 | ||
|
|
368e30067b | ||
|
|
053fd40e76 | ||
|
|
7af7f6be1e | ||
|
|
5fd20e50fd | ||
|
|
43df2956fb | ||
|
|
0c96997005 | ||
|
|
81651b119a | ||
|
|
ecad4d39eb | ||
|
|
0d3be4866f | ||
|
|
eeed89811b | ||
|
|
d9bff5dffe | ||
|
|
3b48f22bd9 | ||
|
|
79d508fb07 | ||
|
|
ae8fadd587 | ||
|
|
5d7fdc7a82 | ||
|
|
1ded166765 | ||
|
|
22eb21a4ef | ||
|
|
4283b381b6 | ||
|
|
2cdc6669da | ||
|
|
e3983d615d | ||
|
|
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 | ||
|
|
b71c0d8b93 | ||
|
|
0ecfe499fc | ||
|
|
68d85841ea | ||
|
|
1ffcc177a4 | ||
|
|
2230c5bc0c | ||
|
|
352b009e91 | ||
|
|
b6b33e1b1e | ||
|
|
dff1ac2e41 | ||
|
|
36f75f44e9 | ||
|
|
92601e0eb8 | ||
|
|
6e82423947 | ||
|
|
0a805a01f3 | ||
|
|
579b5a2f2f | ||
|
|
c77f43be5e | ||
|
|
f10d1834dd | ||
|
|
b4c7ed2227 | ||
|
|
0086980b1b | ||
|
|
89e1793978 | ||
|
|
5a598fe611 | ||
|
|
b58f670694 | ||
|
|
5103436d17 | ||
|
|
c9a1796e56 | ||
|
|
7428b56dd5 | ||
|
|
42294d261d | ||
|
|
31a99a908a | ||
|
|
aa503f0fe8 | ||
|
|
d3d48d27df | ||
|
|
bd8eb74662 | ||
|
|
99680e9a0f | ||
|
|
0cc58bdb46 | ||
|
|
09435b8786 | ||
|
|
5d2753a34e | ||
|
|
8eb3ece2f9 | ||
|
|
ed923a2a94 | ||
|
|
95cc0670e2 | ||
|
|
a62b55c40f | ||
|
|
7440b77377 | ||
|
|
372c6ffd3b | ||
|
|
14d1524971 | ||
|
|
9cddcabad4 | ||
|
|
656e7161cb | ||
|
|
7c12761066 | ||
|
|
d3584a5ae9 | ||
|
|
c0c64621e6 | ||
|
|
323d364572 | ||
|
|
9a681a9422 | ||
|
|
a7a709df51 | ||
|
|
3f23b5553a | ||
|
|
5a2082ede5 | ||
|
|
e29f3a0a9a | ||
|
|
f4bebf6a41 | ||
|
|
564df8136c |
@@ -11,7 +11,6 @@ cache:
|
||||
- lib
|
||||
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
|
||||
install:
|
||||
|
||||
@@ -1,5 +1,63 @@
|
||||
# Changelog
|
||||
|
||||
## v0.5.20
|
||||
Released 15 October 2021
|
||||
|
||||
Highlights:
|
||||
* Improved: Last.fm & metacrtic api fixes
|
||||
* Improved: Various database fixes
|
||||
* Added: qBitTorrent
|
||||
* Added: Telegram notifications
|
||||
* Improved: Many more since the last release. Check the list
|
||||
|
||||
Many thanks to @AdeHub for keeping the ship afloat
|
||||
and to everyone else who contributed.
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.19...v0.5.20).
|
||||
|
||||
## 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
|
||||
Released 01 December 2016
|
||||
|
||||
Highlights:
|
||||
* Added: PassTheHeadphones support
|
||||
* Fixed: Special characters in password fields breaking on config page
|
||||
* Improved: Updated t411 url
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.17...v0.5.18).
|
||||
|
||||
|
||||
## v0.5.17
|
||||
Released 10 November 2016
|
||||
|
||||
Highlights:
|
||||
* Added: t411 support
|
||||
* Fixed: Rutracker login
|
||||
* Fixed: Deluge empty password
|
||||
* Fixed: FreeBSD init script
|
||||
* Improved: Musicbrainz searching
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.16...v0.5.17).
|
||||
|
||||
## v0.5.16
|
||||
Released 10 June 2016
|
||||
|
||||
Hotfix update
|
||||
|
||||
Highlights:
|
||||
* Fixed: Waffles url
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.15...v0.5.16).
|
||||
|
||||
## v0.5.15
|
||||
Released 07 June 2016
|
||||
|
||||
|
||||
@@ -54,7 +54,10 @@ def main():
|
||||
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
headphones.SYS_ENCODING = locale.getpreferredencoding()
|
||||
if headphones.SYS_PLATFORM == 'win32':
|
||||
headphones.SYS_ENCODING = sys.getdefaultencoding().upper()
|
||||
else:
|
||||
headphones.SYS_ENCODING = locale.getpreferredencoding()
|
||||
except (locale.Error, IOError):
|
||||
pass
|
||||
|
||||
@@ -81,6 +84,8 @@ def main():
|
||||
help='Prevent browser from launching on startup')
|
||||
parser.add_argument(
|
||||
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
|
||||
parser.add_argument(
|
||||
'--host', help='Specify a host (default - localhost)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -170,6 +175,13 @@ def main():
|
||||
else:
|
||||
http_port = int(headphones.CONFIG.HTTP_PORT)
|
||||
|
||||
# Force the http host if neccessary
|
||||
if args.host:
|
||||
http_host = args.host
|
||||
logger.info('Using forced web server host: %s', http_host)
|
||||
else:
|
||||
http_host = headphones.CONFIG.HTTP_HOST
|
||||
|
||||
# Check if pyOpenSSL is installed. It is required for certificate generation
|
||||
# and for CherryPy.
|
||||
if headphones.CONFIG.ENABLE_HTTPS:
|
||||
@@ -183,7 +195,7 @@ def main():
|
||||
# Try to start the server. Will exit here is address is already in use.
|
||||
web_config = {
|
||||
'http_port': http_port,
|
||||
'http_host': headphones.CONFIG.HTTP_HOST,
|
||||
'http_host': http_host,
|
||||
'http_root': headphones.CONFIG.HTTP_ROOT,
|
||||
'http_proxy': headphones.CONFIG.HTTP_PROXY,
|
||||
'enable_https': headphones.CONFIG.ENABLE_HTTPS,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## Headphones
|
||||
##  Headphones
|
||||
|
||||
[](https://travis-ci.org/rembo10/headphones)
|
||||
[](https://travis-ci.org/rembo10/headphones)
|
||||
|
||||
|
Before Width: | Height: | Size: 612 B After Width: | Height: | Size: 576 B |
|
Before Width: | Height: | Size: 807 B After Width: | Height: | Size: 772 B |
|
Before Width: | Height: | Size: 635 B After Width: | Height: | Size: 601 B |
|
Before Width: | Height: | Size: 852 B After Width: | Height: | Size: 817 B |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 212 B After Width: | Height: | Size: 79 B |
|
Before Width: | Height: | Size: 220 B After Width: | Height: | Size: 86 B |
|
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 86 B |
|
Before Width: | Height: | Size: 220 B After Width: | Height: | Size: 86 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 125 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 121 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 121 B |
|
Before Width: | Height: | Size: 253 B After Width: | Height: | Size: 114 B |
|
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 120 B |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -324,7 +324,7 @@
|
||||
if (++x === 10) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
var sts = $("#artistname").text();
|
||||
var sts = $("#artistname").text().trim();
|
||||
if (wasLoading == true || sts == "Loading"){
|
||||
location.reload();
|
||||
$('#albumnamespinner').remove()
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<label title="Password for web server authentication. Leave empty to disable.">
|
||||
HTTP Password
|
||||
</label>
|
||||
<input type="password" name="http_password" value="${config['http_password']}" size="30">
|
||||
<input type="password" name="http_password" value="${config['http_password'] | h}" size="30">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="launch_browser" value="1" ${config['launch_browser']} />
|
||||
@@ -133,6 +133,12 @@
|
||||
</label>
|
||||
<input type="text" name="mb_ignore_age" value="${config['mb_ignore_age']}" size="4">days
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<label title="Ignore MusicBrainz album updates missing a release date.">
|
||||
Ignore Undated Releases
|
||||
</label>
|
||||
<input type="checkbox" id="mb_ignore_age_missing" name="mb_ignore_age_missing" value="1" ${config['mb_ignore_age_missing']}
|
||||
</div>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -173,7 +179,7 @@
|
||||
<label title="SABnzbd password. Leave empty if not applicable.">
|
||||
SABnzbd Password
|
||||
</label>
|
||||
<input type="password" name="sab_password" value="${config['sab_password']}" size="20">
|
||||
<input type="password" name="sab_password" value="${config['sab_password'] | h}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label title="SABnzbd API key. Can be found in SABnzbd settings.">
|
||||
@@ -207,7 +213,7 @@
|
||||
<label title="NZBGet password. Leave empty if not applicable">
|
||||
NZBget Password
|
||||
</label>
|
||||
<input type="password" name="nzbget_password" value="${config['nzbget_password']}" size="20">
|
||||
<input type="password" name="nzbget_password" value="${config['nzbget_password'] | h}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label title="Name of NZBget category to add downloads to.">
|
||||
@@ -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_utorrent" value="2" ${config['torrent_downloader_utorrent']}> uTorrent (Beta)
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="3" ${config['torrent_downloader_deluge']}> Deluge (Beta)
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_qbittorrent" value="4" ${config['torrent_downloader_qbittorrent']}> QBitTorrent
|
||||
</fieldset>
|
||||
<fieldset id="torrent_blackhole_options">
|
||||
<div class="row">
|
||||
@@ -359,7 +366,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Transmission Password</label>
|
||||
<input type="password" name="transmission_password" value="${config['transmission_password']}" size="30">
|
||||
<input type="password" name="transmission_password" value="${config['transmission_password'] | h}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<small>Note: With Transmission, you can specify a different download directory for downloads sent from Headphones.
|
||||
@@ -379,13 +386,33 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>uTorrent Password</label>
|
||||
<input type="password" name="utorrent_password" value="${config['utorrent_password']}" size="30">
|
||||
<input type="password" name="utorrent_password" value="${config['utorrent_password'] | h}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>uTorrent Label</label>
|
||||
<input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="qbittorrent_options">
|
||||
<small class="heading"><i class="fa fa-info-circle"></i> Note: Works with WebAPI Rev 11 and later (QBitTorrent 3.4.0 and later) </small>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Host</label>
|
||||
<input type="text" name="qbittorrent_host" value="${config['qbittorrent_host']}" size="30">
|
||||
<small>usually http://localhost:8081</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Username</label>
|
||||
<input type="text" name="qbittorrent_username" value="${config['qbittorrent_username']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Password</label>
|
||||
<input type="password" name="qbittorrent_password" value="${config['qbittorrent_password']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Label</label>
|
||||
<input type="text" name="qbittorrent_label" value="${config['qbittorrent_label']}" size="30">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="deluge_options">
|
||||
<div class="row">
|
||||
<label>Deluge WebUI Host and Port</label>
|
||||
@@ -400,7 +427,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Deluge Password</label>
|
||||
<input type="password" name="deluge_password" value="${config['deluge_password']}" size="30">
|
||||
<input type="password" name="deluge_password" value="${config['deluge_password'] | h}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<small>Note: With Deluge, you can specify a different download directory for downloads sent from Headphones.
|
||||
@@ -466,7 +493,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label>
|
||||
<input class="hppass" type="password" value="${config['hppass']}" size="20">
|
||||
<input class="hppass" type="password" value="${config['hppass'] | h}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Don't have an account? Sign up!</a>
|
||||
@@ -574,23 +601,6 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input id="use_kat" type="checkbox" class="bigcheck" name="use_kat" value="1" ${config['use_kat']} /><label for="use_kat"><span class="option">Kick Ass Torrents</span></label>
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="row">
|
||||
<label>Proxy URL</label>
|
||||
<input type="text" name="kat_proxy_url" value="${config['kat_proxy_url']}" size="36">
|
||||
<small>Optional. Leave empty for default.</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Seed Ratio</label>
|
||||
<input type="text" class="override-float" name="kat_ratio" value="${config['kat_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_waffles" type="checkbox" class="bigcheck" name="use_waffles" value="1" ${config['use_waffles']} /><label for="use_waffles"><span class="option">Waffles.ch</span></label>
|
||||
@@ -622,43 +632,63 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label>
|
||||
<input type="password" name="rutracker_password" value="${config['rutracker_password']}" size="36">
|
||||
<input type="password" name="rutracker_password" value="${config['rutracker_password'] | h}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Seed Ratio</label>
|
||||
<input type="text" class="override-float" name="rutracker_ratio" value="${config['rutracker_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Session Cookie (Optional - Advanced)</label>
|
||||
<input type="text" class="override-float" name="rutracker_cookie" value="${config['rutracker_cookie']}" size="10" title="bb_session cookie (Advanced)">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input id="use_whatcd" type="checkbox" class="bigcheck" name="use_whatcd" value="1" ${config['use_whatcd']} /><label for="use_whatcd"><span class="option">What.cd</span></label>
|
||||
<input id="use_orpheus" type="checkbox" class="bigcheck" name="use_orpheus" value="1" ${config['use_orpheus']} /><label for="use_orpheus"><span class="option">Orpheus.network</span></label>
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="row">
|
||||
<label>Username</label>
|
||||
<input type="text" name="whatcd_username" value="${config['whatcd_username']}" size="36">
|
||||
<input type="text" name="orpheus_username" value="${config['orpheus_username']}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label>
|
||||
<input type="password" name="whatcd_password" value="${config['whatcd_password']}" size="36">
|
||||
<input type="password" name="orpheus_password" value="${config['orpheus_password'] | h}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>URL</label>
|
||||
<input type="text" name="orpheus_url" value="${config['orpheus_url']}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Seed Ratio</label>
|
||||
<input type="text" class="override-float" name="whatcd_ratio" value="${config['whatcd_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
|
||||
<input type="text" class="override-float" name="orpheus_ratio" value="${config['orpheus_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_strike" type="checkbox" class="bigcheck" name="use_strike" value="1" ${config['use_strike']} /><label for="use_strike"><span class="option">Strike Search</span></label>
|
||||
<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="strike_ratio" value="${config['strike_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="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 class="row">
|
||||
<label>Use freeleech tokens when available</label>
|
||||
<input type="checkbox" name="redacted_use_fltoken" value="1" ${config['redacted_use_fltoken']}>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -671,13 +701,17 @@
|
||||
<div class="config" id="torznab1">
|
||||
<div class="row">
|
||||
<label>Torznab Host</label>
|
||||
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="30">
|
||||
<small>e.g. http://localhost:9117/torznab/iptorrents</small>
|
||||
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="36">
|
||||
<small>e.g. http://localhost:9117/api/v2.0/indexers/demonoid/results/torznab/</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab API</label>
|
||||
<input type="text" name="torznab_apikey" value="${config['torznab_apikey']}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab Seed Ratio</label>
|
||||
<input type="text" class="override-float" name="torznab_ratio" value="${config['torznab_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding.">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input id="torznab_enabled" type="checkbox" name="torznab_enabled" value="1" ${config['torznab_enabled']} /><label>Enabled</label>
|
||||
</div>
|
||||
@@ -687,7 +721,7 @@
|
||||
%>
|
||||
%for torznab in config['extra_torznabs']:
|
||||
<%
|
||||
if torznab[2] == '1' or torznab[2] == 1:
|
||||
if torznab[3] == '1' or torznab[3] == 1:
|
||||
torznab_enabled = "checked"
|
||||
else:
|
||||
torznab_enabled = ""
|
||||
@@ -695,12 +729,16 @@
|
||||
<div class="config" id="torznab${torznab_number}">
|
||||
<div class="row">
|
||||
<label>Torznab Host</label>
|
||||
<input type="text" name="torznab_host${torznab_number}" value="${torznab[0]}" size="30">
|
||||
<input type="text" name="torznab_host${torznab_number}" value="${torznab[0]}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab API</label>
|
||||
<input type="text" name="torznab_api${torznab_number}" value="${torznab[1]}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab Seed Ratio</label>
|
||||
<input type="text" class="override-float" name="torznab_ratio${torznab_number}" value="${torznab[2]}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding.">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input id="torznab_enabled" type="checkbox" name="torznab_enabled${torznab_number}" value="1" ${torznab_enabled} /><label>Enabled</label>
|
||||
</div>
|
||||
@@ -732,18 +770,6 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input id="use_mininova" type="checkbox" class="bigcheck" name="use_mininova" value="1" ${config['use_mininova']} /><label for="use_mininova"><span class="option">Mininova</span></label>
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="row">
|
||||
<label>Seed Ratio</label>
|
||||
<input type="text" class="override-float" name="mininova_ratio" value="${config['mininova_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>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -884,30 +910,42 @@
|
||||
<input type="checkbox" name="keep_nfo" value="1" ${config['keep_nfo']} />
|
||||
</label>
|
||||
</div>
|
||||
<div class="row checkbox left clearfix nopad">
|
||||
<label>
|
||||
Embed lyrics
|
||||
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row checkbox left clearfix nopad">
|
||||
<label>
|
||||
Embed album art in each file
|
||||
<input type="checkbox" name="embed_album_art" id="embed_album_art" value="1" ${config['embed_album_art']}>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row checkbox left clearfix nopad">
|
||||
<label>
|
||||
Add album art jpeg to album folder
|
||||
<input type="checkbox" name="add_album_art" id="add_album_art" value="1" ${config['add_album_art']}>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="album_art_options" style="padding-left: 20px">
|
||||
<div class="row">
|
||||
as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg
|
||||
</div>
|
||||
<small>Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').</small>
|
||||
</div>
|
||||
<div class="row checkbox left clearfix nopad">
|
||||
<label>
|
||||
Embed album art in each file
|
||||
<input type="checkbox" name="embed_album_art" value="1" ${config['embed_album_art']}>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row checkbox left clearfix nopad">
|
||||
<label>
|
||||
Embed lyrics
|
||||
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}>
|
||||
</label>
|
||||
|
||||
<div id="album_art_size_options" style="padding-left: 20px">
|
||||
<div class="row">
|
||||
Album art min width <input type="text" class="override-float" name="album_art_min_width" value="${config['album_art_min_width']}" size="4" title="Only images with a pixel width greater or equal are considered as valid album art candidates. Default: 0.">\
|
||||
Album art max width <input type="text" class="override-float" name="album_art_max_width" value="${config['album_art_max_width']}" size="4" title="A maximum image width to downscale fetched images if they are too big.">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>
|
||||
Destination Directory:
|
||||
@@ -964,7 +1002,7 @@
|
||||
<label>SMTP User</label><input type="text" name="email_smtp_user" value="${config['email_smtp_user']}" size="254">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>SMTP Password</label><input type="password" name="email_smtp_password" value="${config['email_smtp_password']}" size="50">
|
||||
<label>SMTP Password</label><input type="password" name="email_smtp_password" value="${config['email_smtp_password'] | h}" size="50">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="text" class="override-float" name="email_smtp_port" value="${config['email_smtp_port']}" size="4"><label>SMTP Port</label>
|
||||
@@ -990,7 +1028,7 @@
|
||||
<label>Growl Host:Port</label><input type="text" name="growl_host" value="${config['growl_host']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Growl Password</label><input type="password" name="growl_password" value="${config['growl_password']}" size="30">
|
||||
<label>Growl Password</label><input type="password" name="growl_password" value="${config['growl_password'] | h}" size="30">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="growl_onsnatch" value="1" ${config['growl_onsnatch']} /><label>Notify on snatch?</label>
|
||||
@@ -1012,7 +1050,7 @@
|
||||
<label>Username</label><input type="text" name="xbmc_username" value="${config['xbmc_username']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label><input type="password" name="xbmc_password" value="${config['xbmc_password']}" size="30">
|
||||
<label>Password</label><input type="password" name="xbmc_password" value="${config['xbmc_password'] | h}" size="30">
|
||||
</div>
|
||||
<div class="checkbox row">
|
||||
<input type="checkbox" name="xbmc_update" value="1" ${config['xbmc_update']} /><label>Update Library</label>
|
||||
@@ -1126,7 +1164,7 @@
|
||||
<small>Username of your Plex client API (blank for none)</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Plex Password</label><input type="password" name="plex_password" value="${config['plex_password']}" size="30">
|
||||
<label>Plex Password</label><input type="password" name="plex_password" value="${config['plex_password'] | h}" size="30">
|
||||
<small>Password of your Plex client API (blank for none)</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -1226,7 +1264,7 @@
|
||||
<label>Subsonic Username</label><input type="text" name="subsonic_username" value="${config['subsonic_username']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Subsonic Password</label><input type="password" name="subsonic_password" value="${config['subsonic_password']}" size="30">
|
||||
<label>Subsonic Password</label><input type="password" name="subsonic_password" value="${config['subsonic_password'] | h}" size="30">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -1258,6 +1296,26 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input type="checkbox" class="bigcheck" name="slack_enabled" id="slack" value="1" ${config['slack_enabled']} /><label for="slack"><span class="option">Slack</span></label>
|
||||
</div>
|
||||
<div id="slackoptions">
|
||||
<div class="row">
|
||||
<label>Slack Webhook</label><input type="text" name="slack_url" value="${config['slack_url']}" size="50">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Channel</label><input type="text" name="slack_channel" value="${config['slack_channel']}" size="50">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<label>Emoji</label><input type="text" name="slack_emoji" value="${config['slack_emoji']}" size="50">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="slack_onsnatch" value="1" ${config['slack_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input type="checkbox" class="bigcheck" name="telegram_enabled" id="telegram" value="1" ${config['telegram_enabled']} /><label for="telegram"><span class="option">Telegram</span></label>
|
||||
@@ -1275,6 +1333,23 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row checkbox left">
|
||||
<input type="checkbox" class="bigcheck" name="join_enabled" id="join" value="1" ${config['join_enabled']} /><label for="join"><span class="option">Join</span></label>
|
||||
</div>
|
||||
<div id="joinoptions">
|
||||
<div class="row">
|
||||
<label>Join API Key</label><input type="text" name="join_apikey" value="${config['join_apikey']}" size="50">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Device ID(s)</label><input type="text" name="join_deviceid" value="${config['join_deviceid']}" size="50"><small>Comma separated list. Leave blank to send to all devices</small>
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="join_onsnatch" value="1" ${config['join_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -1570,6 +1645,10 @@
|
||||
<label>Cache Directory</label>
|
||||
<input type="text" name="cache_dir" value="${config['cache_dir']}" size="50">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Post Processing Temporary Directory</label>
|
||||
<input type="text" name="keep_torrent_files_dir" value="${config['keep_torrent_files_dir']}" size="50" title="Enter the directory to preserve files for seeding, default is system temp directory">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
@@ -1626,7 +1705,7 @@
|
||||
<label>Username</label><input type="text" class="customuser" name="customuser" value="${config['customuser']}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label><input type="password" class="custompass" name="custompass" value="${config['custompass']}" size="15"><br>
|
||||
<label>Password</label><input type="password" class="custompass" name="custompass" value="${config['custompass'] | h}" size="15"><br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -1639,7 +1718,7 @@
|
||||
<label>Username</label><input type="text" class="hpuser" name="hpuser" value="${config['hpuser']}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label><input type="password" class="hppass" name="hppass" value="${config['hppass']}" size="20"><br>
|
||||
<label>Password</label><input type="password" class="hppass" name="hppass" value="${config['hppass'] | h}" size="20"><br>
|
||||
<a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Get an Account!</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1814,9 +1893,36 @@
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#embed_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_size_options").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!$("#add_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_size_options").hide();
|
||||
}
|
||||
}
|
||||
|
||||
$("#embed_album_art").click(function(){
|
||||
if ($("#embed_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_size_options").slideDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!$("#add_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_size_options").slideUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#add_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_options").show();
|
||||
$("#album_art_size_options").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1827,10 +1933,15 @@
|
||||
if ($("#add_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_options").slideDown();
|
||||
$("#album_art_size_options").slideDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#album_art_options").slideUp();
|
||||
if (!$("#embed_album_art").is(":checked"))
|
||||
{
|
||||
$("#album_art_size_options").slideUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2035,6 +2146,27 @@
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#join").is(":checked"))
|
||||
{
|
||||
$("#joinoptions").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#joinoptions").hide();
|
||||
}
|
||||
|
||||
|
||||
$("#join").click(function(){
|
||||
if ($("#join").is(":checked"))
|
||||
{
|
||||
$("#joinoptions").slideDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#joinoptions").slideUp();
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#twitter").is(":checked"))
|
||||
{
|
||||
$("#twitteroptions").show();
|
||||
@@ -2055,6 +2187,27 @@
|
||||
}
|
||||
});
|
||||
|
||||
if ($("#slack").is(":checked"))
|
||||
{
|
||||
$("#slackoptions").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#slackoptions").hide();
|
||||
}
|
||||
|
||||
$("#slack").click(function(){
|
||||
if ($("#slack").is(":checked"))
|
||||
{
|
||||
$("#slackoptions").slideDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#slackoptions").slideUp();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if ($("#telegram").is(":checked"))
|
||||
{
|
||||
$("#telegramoptions").show();
|
||||
@@ -2231,26 +2384,30 @@
|
||||
|
||||
if ($("#torrent_downloader_blackhole").is(":checked"))
|
||||
{
|
||||
$("#transmission_options,#utorrent_options,#deluge_options").hide();
|
||||
$("#transmission_options,#utorrent_options,#deluge_options,#qbittorrent_options").hide();
|
||||
$("#torrent_blackhole_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_transmission").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").hide();
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options,#qbittorrent_options").hide();
|
||||
$("#transmission_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_utorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options").hide();
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options,#qbittorrent_options").hide();
|
||||
$("#utorrent_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_qbittorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#deluge_options").hide();
|
||||
$("#qbittorrent_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_deluge").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options").hide();
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#qbittorrent_options").hide();
|
||||
$("#deluge_options").show();
|
||||
}
|
||||
|
||||
|
||||
$('input[type=radio]').change(function(){
|
||||
if ($("#preferred_bitrate").is(":checked"))
|
||||
{
|
||||
@@ -2286,19 +2443,23 @@
|
||||
}
|
||||
if ($("#torrent_downloader_blackhole").is(":checked"))
|
||||
{
|
||||
$("#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
|
||||
$("#transmission_options,#utorrent_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_transmission").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_utorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_qbittorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#qbittorrent_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_deluge").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#utorrent_options,#transmission_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
|
||||
$("#torrent_blackhole_options,#utorrent_options,#transmission_options,#qbittorrent_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2360,7 +2521,7 @@
|
||||
|
||||
$("#add_torznab").click(function() {
|
||||
var intId = $("#torznab_providers > div").size() + deletedTorznabs + 1;
|
||||
var formfields = $("<div class=\"config\" id=\"torznab" + intId + "\"><div class=\"row\"><label>Torznab Host</label><input type=\"text\" name=\"torznab_host" + intId + "\" size=\"30\"></div><div class=\"row\"><label>Torznab API</label><input type=\"text\" name=\"torznab_api" + intId + "\" size=\"36\"></div><div class=\"row checkbox\"><input type=\"checkbox\" name=\"torznab_enabled" + intId + "\" value=\"1\" checked /><label>Enabled</label></div>");
|
||||
var formfields = $("<div class=\"config\" id=\"torznab" + intId + "\"><div class=\"row\"><label>Torznab Host</label><input type=\"text\" name=\"torznab_host" + intId + "\" size=\"30\"></div><div class=\"row\"><label>Torznab API</label><input type=\"text\" name=\"torznab_api" + intId + "\" size=\"36\"></div><div class=\"row\"><label>Torznab Seed Ratio</label><input type=\"text\" class=\"override-float\" name=\"torznab_ratio" + intId + "\" size=\"10\"></div><div class=\"row checkbox\"><input type=\"checkbox\" name=\"torznab_enabled" + intId + "\" value=\"1\" checked /><label>Enabled</label></div>");
|
||||
var removeTorznabButton = $("<div class=\"row\"><input type=\"button\" class=\"remove_torznab\" value=\"Remove\" /></div>");
|
||||
removeTorznabButton.click(function() {
|
||||
$(this).parent().remove();
|
||||
@@ -2389,18 +2550,16 @@
|
||||
initConfigCheckbox("#use_nzbsorg");
|
||||
initConfigCheckbox("#use_omgwtfnzbs");
|
||||
initConfigCheckbox("#use_torznab");
|
||||
initConfigCheckbox("#use_kat");
|
||||
initConfigCheckbox("#use_piratebay");
|
||||
initConfigCheckbox("#use_oldpiratebay");
|
||||
initConfigCheckbox("#use_mininova");
|
||||
initConfigCheckbox("#use_waffles");
|
||||
initConfigCheckbox("#use_rutracker");
|
||||
initConfigCheckbox("#use_whatcd");
|
||||
initConfigCheckbox("#use_strike");
|
||||
initConfigCheckbox("#use_orpheus");
|
||||
initConfigCheckbox("#use_redacted");
|
||||
initConfigCheckbox("#api_enabled");
|
||||
initConfigCheckbox("#enable_https");
|
||||
initConfigCheckbox("#customauth");
|
||||
|
||||
initConfigCheckbox("#mb_ignore_age_missing");
|
||||
|
||||
$('#twitterStep1').click(function () {
|
||||
$.get("/twitterStep1", function (data) {window.open(data); })
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 860 B |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 95 B After Width: | Height: | Size: 89 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 93 B |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 299 B |
|
Before Width: | Height: | Size: 757 B After Width: | Height: | Size: 749 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 860 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 176 B |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 251 B |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 264 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 99 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 485 B |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 345 B After Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 386 B |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 688 B |
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 688 B |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 87 B |
|
Before Width: | Height: | Size: 135 B After Width: | Height: | Size: 101 B |
|
Before Width: | Height: | Size: 975 B After Width: | Height: | Size: 83 B |
|
Before Width: | Height: | Size: 838 B After Width: | Height: | Size: 56 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 116 B |
@@ -36,7 +36,7 @@
|
||||
"aTargets": [0],
|
||||
"mData":"ArtistID",
|
||||
"mRender": function ( data, type, full ) {
|
||||
return '<div id="artistImg"><img class="albumArt" alt="" id="'+ data + '" data-src="artwork/thumbs/artist/' + data + '"/></div>';
|
||||
return '<div id="artistImg"><img class="albumArt" height="50" width="50" alt="" id="'+ data + '" data-src="artwork/thumbs/artist/' + data + '"/></div>';
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1003 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1010 B |
|
Before Width: | Height: | Size: 107 B After Width: | Height: | Size: 106 B |
|
Before Width: | Height: | Size: 106 B After Width: | Height: | Size: 98 B |
|
Before Width: | Height: | Size: 111 B After Width: | Height: | Size: 107 B |
|
Before Width: | Height: | Size: 352 B After Width: | Height: | Size: 326 B |
|
Before Width: | Height: | Size: 103 B After Width: | Height: | Size: 98 B |
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 203 B After Width: | Height: | Size: 158 B |
|
Before Width: | Height: | Size: 176 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
@@ -312,6 +312,7 @@ function doAjaxCall(url,elem,reload,form) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
data: dataString,
|
||||
type: 'POST',
|
||||
beforeSend: function(jqXHR, settings) {
|
||||
// Start loader etc.
|
||||
feedback.prepend(loader);
|
||||
|
||||
@@ -86,8 +86,8 @@ CURRENT_VERSION = None
|
||||
LATEST_VERSION = None
|
||||
COMMITS_BEHIND = None
|
||||
|
||||
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma"]
|
||||
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff"]
|
||||
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma", "opus"]
|
||||
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff", "aif"]
|
||||
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
|
||||
|
||||
MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
|
||||
@@ -306,9 +306,10 @@ def initialize_scheduler():
|
||||
minutes=minutes)
|
||||
|
||||
# Remove Torrent + data if Post Processed and finished Seeding
|
||||
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
||||
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
||||
minutes=minutes)
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER != 0:
|
||||
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
||||
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
||||
minutes=minutes)
|
||||
|
||||
# Start scheduler
|
||||
if start_jobs and len(SCHED.get_jobs()):
|
||||
@@ -359,6 +360,8 @@ def sig_handler(signum=None, frame=None):
|
||||
|
||||
|
||||
def dbcheck():
|
||||
logger.debug("SQLite Version: %s", sqlite3.sqlite_version)
|
||||
logger.debug("DB-API Version: %s", sqlite3.version)
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
@@ -376,7 +379,7 @@ def dbcheck():
|
||||
c.execute(
|
||||
'CREATE TABLE IF NOT EXISTS alltracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)')
|
||||
c.execute(
|
||||
'CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT, Kind TEXT)')
|
||||
'CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT, Kind TEXT, TorrentHash TEXT)')
|
||||
# Matched is a temporary value used to see if there was a match found in
|
||||
# alltracks
|
||||
c.execute(
|
||||
@@ -416,6 +419,33 @@ def dbcheck():
|
||||
'CREATE INDEX IF NOT EXISTS tracks_Location ON tracks(Location ASC)')
|
||||
c.execute(
|
||||
'CREATE INDEX IF NOT EXISTS alltracks_Location ON alltracks(Location ASC)')
|
||||
c.execute(
|
||||
'CREATE INDEX IF NOT EXISTS tracks_artistid ON tracks(ArtistID ASC)')
|
||||
|
||||
# Speed up album page
|
||||
c.execute('CREATE INDEX IF NOT EXISTS allalbums_albumid ON allalbums(AlbumID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS alltracks_albumid ON alltracks(AlbumID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS releases_albumid ON releases(ReleaseGroupID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS descriptions_albumid ON descriptions(ReleaseGroupID ASC)')
|
||||
|
||||
# Speed up artist deletion
|
||||
c.execute('CREATE INDEX IF NOT EXISTS allalbums_artistid ON allalbums(ArtistID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS alltracks_artistid ON alltracks(ArtistID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS descriptions_artistid ON descriptions(ArtistID ASC)')
|
||||
|
||||
# Speed up Artist refresh hybrid release
|
||||
c.execute('CREATE INDEX IF NOT EXISTS albums_releaseid ON albums(ReleaseID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS tracks_releaseid ON tracks(ReleaseID ASC)')
|
||||
|
||||
# Speed up scanning and track matching
|
||||
c.execute('CREATE INDEX IF NOT EXISTS artist_artistname ON artists(ArtistName COLLATE NOCASE ASC)')
|
||||
|
||||
# General speed up
|
||||
c.execute('CREATE INDEX IF NOT EXISTS artist_artistsortname ON artists(ArtistSortName COLLATE NOCASE ASC)')
|
||||
|
||||
c.execute(
|
||||
"""CREATE INDEX IF NOT EXISTS have_matched_artist_album ON have(Matched ASC, ArtistName COLLATE NOCASE ASC, AlbumTitle COLLATE NOCASE ASC)""")
|
||||
c.execute('DROP INDEX IF EXISTS have_matched')
|
||||
|
||||
try:
|
||||
c.execute('SELECT IncludeExtras from artists')
|
||||
@@ -613,6 +643,20 @@ def dbcheck():
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE artists ADD COLUMN MetaCritic TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT TorrentHash from snatched')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE snatched ADD COLUMN TorrentHash TEXT')
|
||||
c.execute('UPDATE snatched SET TorrentHash = FolderName WHERE Status LIKE "Seed_%"')
|
||||
|
||||
# One off script to set CleanName to lower case
|
||||
clean_name_mixed = c.execute('SELECT CleanName FROM have ORDER BY Date Desc').fetchone()
|
||||
if clean_name_mixed and clean_name_mixed[0] != clean_name_mixed[0].lower():
|
||||
logger.info("Updating track clean name, this could take some time...")
|
||||
c.execute('UPDATE tracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||
c.execute('UPDATE alltracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||
c.execute('UPDATE have SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||
|
||||
conn.commit()
|
||||
c.close()
|
||||
|
||||
|
||||
@@ -13,16 +13,164 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from headphones import request, db, logger
|
||||
import struct
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from io import BytesIO
|
||||
|
||||
import headphones
|
||||
from headphones import db, request, logger
|
||||
|
||||
|
||||
def getAlbumArt(albumid):
|
||||
myDB = db.DBConnection()
|
||||
asin = myDB.action(
|
||||
'SELECT AlbumASIN from albums WHERE AlbumID=?', [albumid]).fetchone()[0]
|
||||
|
||||
if asin:
|
||||
return 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % asin
|
||||
artwork_path = None
|
||||
artwork = None
|
||||
|
||||
# CAA
|
||||
logger.info("Searching for artwork at CAA")
|
||||
artwork_path = 'http://coverartarchive.org/release-group/%s/front' % albumid
|
||||
artwork = getartwork(artwork_path)
|
||||
if artwork:
|
||||
logger.info("Artwork found at CAA")
|
||||
return artwork_path, artwork
|
||||
|
||||
# Amazon
|
||||
logger.info("Searching for artwork at Amazon")
|
||||
myDB = db.DBConnection()
|
||||
dbalbum = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, ReleaseID, AlbumASIN FROM albums WHERE AlbumID=?',
|
||||
[albumid]).fetchone()
|
||||
if dbalbum['AlbumASIN']:
|
||||
artwork_path = 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % dbalbum['AlbumASIN']
|
||||
artwork = getartwork(artwork_path)
|
||||
if artwork:
|
||||
logger.info("Artwork found at Amazon")
|
||||
return artwork_path, artwork
|
||||
|
||||
# last.fm
|
||||
from headphones import lastfm
|
||||
logger.info("Searching for artwork at last.fm")
|
||||
if dbalbum['ReleaseID'] != albumid:
|
||||
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'])
|
||||
if not data:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'])
|
||||
else:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'])
|
||||
|
||||
if data:
|
||||
try:
|
||||
images = data['album']['image']
|
||||
for image in images:
|
||||
if image['size'] == 'extralarge':
|
||||
artwork_path = image['#text']
|
||||
elif image['size'] == 'mega':
|
||||
artwork_path = image['#text']
|
||||
break
|
||||
except KeyError:
|
||||
artwork_path = None
|
||||
|
||||
if artwork_path:
|
||||
artwork = getartwork(artwork_path)
|
||||
if artwork:
|
||||
logger.info("Artwork found at last.fm")
|
||||
return artwork_path, artwork
|
||||
|
||||
logger.info("No suitable album art found.")
|
||||
return None, None
|
||||
|
||||
|
||||
def jpeg(bites):
|
||||
fhandle = BytesIO(bites)
|
||||
try:
|
||||
fhandle.seek(0)
|
||||
size = 2
|
||||
ftype = 0
|
||||
while not 0xc0 <= ftype <= 0xcf:
|
||||
fhandle.seek(size, 1)
|
||||
byte = fhandle.read(1)
|
||||
while ord(byte) == 0xff:
|
||||
byte = fhandle.read(1)
|
||||
ftype = ord(byte)
|
||||
size = struct.unpack('>H', fhandle.read(2))[0] - 2
|
||||
fhandle.seek(1, 1)
|
||||
height, width = struct.unpack('>HH', fhandle.read(4))
|
||||
return width, height
|
||||
except struct.error:
|
||||
return None, None
|
||||
except TypeError:
|
||||
return None, None
|
||||
|
||||
|
||||
def png(bites):
|
||||
try:
|
||||
check = struct.unpack('>i', bites[4:8])[0]
|
||||
if check != 0x0d0a1a0a:
|
||||
return None, None
|
||||
return struct.unpack('>ii', bites[16:24])
|
||||
except struct.error:
|
||||
return None, None
|
||||
|
||||
|
||||
def get_image_data(bites):
|
||||
type = None
|
||||
width = None
|
||||
height = None
|
||||
if len(bites) < 24:
|
||||
return None, None, None
|
||||
|
||||
peek = bites[0:2]
|
||||
if peek == b'\xff\xd8':
|
||||
width, height = jpeg(bites)
|
||||
type = 'jpg'
|
||||
elif peek == b'\x89P':
|
||||
width, height = png(bites)
|
||||
type = 'png'
|
||||
return type, width, height
|
||||
|
||||
|
||||
def getartwork(artwork_path):
|
||||
artwork = bytes()
|
||||
minwidth = 0
|
||||
maxwidth = 0
|
||||
if headphones.CONFIG.ALBUM_ART_MIN_WIDTH:
|
||||
minwidth = int(headphones.CONFIG.ALBUM_ART_MIN_WIDTH)
|
||||
if headphones.CONFIG.ALBUM_ART_MAX_WIDTH:
|
||||
maxwidth = int(headphones.CONFIG.ALBUM_ART_MAX_WIDTH)
|
||||
|
||||
resp = request.request_response(artwork_path, timeout=20, stream=True, whitelist_status_code=404)
|
||||
|
||||
if resp:
|
||||
img_width = None
|
||||
for chunk in resp.iter_content(chunk_size=1024):
|
||||
artwork += chunk
|
||||
if not img_width and (minwidth or maxwidth):
|
||||
img_type, img_width, img_height = get_image_data(artwork)
|
||||
# Check min/max
|
||||
if img_width and (minwidth or maxwidth):
|
||||
if minwidth and img_width < minwidth:
|
||||
logger.info("Artwork is too small. Type: %s. Width: %s. Height: %s",
|
||||
img_type, img_width, img_height)
|
||||
artwork = None
|
||||
break
|
||||
elif maxwidth and img_width > maxwidth:
|
||||
# Downsize using proxy service to max width
|
||||
artwork_path = '{0}?{1}'.format('http://images.weserv.nl/', urlencode({
|
||||
'url': artwork_path.replace('http://', ''),
|
||||
'w': maxwidth,
|
||||
}))
|
||||
artwork = bytes()
|
||||
r = request.request_response(artwork_path, timeout=20, stream=True, whitelist_status_code=404)
|
||||
if r:
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
artwork += chunk
|
||||
r.close()
|
||||
logger.info("Artwork is greater than the maximum width, downsized using proxy service")
|
||||
break
|
||||
resp.close()
|
||||
|
||||
return artwork
|
||||
|
||||
|
||||
def getCachedArt(albumid):
|
||||
|
||||
@@ -85,8 +85,8 @@ def switch(AlbumID, ReleaseID):
|
||||
|
||||
# Update have track counts on index
|
||||
totaltracks = len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")',
|
||||
[newalbumdata['ArtistID']]))
|
||||
'SELECT TrackTitle from tracks AS tr INNER JOIN albums AS al ON al.AlbumID = tr.AlbumID WHERE al.ArtistID=? '
|
||||
'AND al.Status != "Ignored"', [newalbumdata['ArtistID']]))
|
||||
havetracks = len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
||||
[newalbumdata['ArtistID']]))
|
||||
|
||||
@@ -82,7 +82,7 @@ class Api(object):
|
||||
def fetchData(self):
|
||||
|
||||
if self.data == 'OK':
|
||||
logger.info('Recieved API command: %s', self.cmd)
|
||||
logger.info('Received API command: %s', self.cmd)
|
||||
methodToCall = getattr(self, "_" + self.cmd)
|
||||
methodToCall(**self.kwargs)
|
||||
if 'callback' not in self.kwargs:
|
||||
|
||||
@@ -14,11 +14,16 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
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 = "8d983789c771afaeb7412ac358d4bad0"
|
||||
|
||||
FANART_URL = 'https://webservice.fanart.tv/v3/music/'
|
||||
FANART_PROJECT_KEY = '22b73c9603eba09d0c855f2d2bdba31c'
|
||||
FANART_CLIENT_KEY = '919b389a18a3f0b2c916090022ab3c7a'
|
||||
|
||||
|
||||
class Cache(object):
|
||||
@@ -96,6 +101,11 @@ class Cache(object):
|
||||
def _is_current(self, filename=None, date=None):
|
||||
|
||||
if filename:
|
||||
|
||||
# 2019 last.fm no longer allows access to artist images, for now we'll keep the cached artist image if it exists and get new from fanart.tv
|
||||
if self.id_type == 'artist' and 'fanart' not in filename:
|
||||
return True
|
||||
|
||||
base_filename = os.path.basename(filename)
|
||||
date = base_filename.split('.')[1]
|
||||
|
||||
@@ -211,20 +221,34 @@ class Cache(object):
|
||||
if ArtistID:
|
||||
|
||||
self.id_type = 'artist'
|
||||
data = lastfm.request_lastfm("artist.getinfo", mbid=ArtistID, api_key=LASTFM_API_KEY)
|
||||
|
||||
# 2019 last.fm no longer allows access to artist images, try fanart.tv instead
|
||||
image_url = None
|
||||
thumb_url = None
|
||||
data = request.request_json(FANART_URL + ArtistID, whitelist_status_code=404,
|
||||
headers={'api-key': FANART_PROJECT_KEY, 'client-key': FANART_CLIENT_KEY})
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
try:
|
||||
image_url = data['artist']['image'][-1]['#text']
|
||||
except (KeyError, IndexError):
|
||||
logger.debug('No artist image found')
|
||||
image_url = None
|
||||
if data.get('artistthumb'):
|
||||
image_url = data['artistthumb'][0]['url']
|
||||
elif data.get('artistbackground'):
|
||||
image_url = data['artistbackground'][0]['url']
|
||||
# elif data.get('hdmusiclogo'):
|
||||
# image_url = data['hdmusiclogo'][0]['url']
|
||||
|
||||
thumb_url = self._get_thumb_url(data)
|
||||
if not thumb_url:
|
||||
logger.debug('No artist thumbnail image found')
|
||||
# fallback to 1st album cover if none of the above
|
||||
elif 'albums' in data:
|
||||
for mbid, art in data.get('albums', dict()).items():
|
||||
if 'albumcover' in art:
|
||||
image_url = art['albumcover'][0]['url']
|
||||
break
|
||||
|
||||
if image_url:
|
||||
thumb_url = image_url
|
||||
else:
|
||||
logger.debug('No artist image found on fanart.tv for Artist Id: %s', self.id)
|
||||
|
||||
else:
|
||||
|
||||
@@ -283,6 +307,7 @@ class Cache(object):
|
||||
"""
|
||||
|
||||
myDB = db.DBConnection()
|
||||
fanart = False
|
||||
|
||||
# Since lastfm uses release ids rather than release group ids for albums, we have to do a artist + album search for albums
|
||||
# Exception is when adding albums manually, then we should use release id
|
||||
@@ -290,6 +315,14 @@ class Cache(object):
|
||||
|
||||
data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=LASTFM_API_KEY)
|
||||
|
||||
# Try with name if not found
|
||||
if not data:
|
||||
dbartist = myDB.action('SELECT ArtistName, Type FROM artists WHERE ArtistID=?', [self.id]).fetchone()
|
||||
if dbartist:
|
||||
data = lastfm.request_lastfm("artist.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(dbartist['ArtistName']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
@@ -303,30 +336,70 @@ class Cache(object):
|
||||
except KeyError:
|
||||
logger.debug('No artist bio found')
|
||||
self.info_content = None
|
||||
try:
|
||||
image_url = data['artist']['image'][-1]['#text']
|
||||
except KeyError:
|
||||
logger.debug('No artist image found')
|
||||
image_url = None
|
||||
|
||||
thumb_url = self._get_thumb_url(data)
|
||||
if not thumb_url:
|
||||
logger.debug('No artist thumbnail image found')
|
||||
# 2019 last.fm no longer allows access to artist images, try fanart.tv instead
|
||||
image_url = None
|
||||
thumb_url = None
|
||||
data = request.request_json(FANART_URL + self.id, whitelist_status_code=404,
|
||||
headers={'api-key': FANART_PROJECT_KEY, 'client-key': FANART_CLIENT_KEY})
|
||||
|
||||
if data.get('artistthumb'):
|
||||
image_url = data['artistthumb'][0]['url']
|
||||
elif data.get('artistbackground'):
|
||||
image_url = data['artistbackground'][0]['url']
|
||||
# elif data.get('hdmusiclogo'):
|
||||
# image_url = data['hdmusiclogo'][0]['url']
|
||||
|
||||
# fallback to 1st album cover if none of the above
|
||||
elif 'albums' in data:
|
||||
for mbid, art in data.get('albums', dict()).items():
|
||||
if 'albumcover' in art:
|
||||
image_url = art['albumcover'][0]['url']
|
||||
break
|
||||
|
||||
# finally, use 1st album cover from last.fm
|
||||
if image_url:
|
||||
fanart = True
|
||||
thumb_url = image_url
|
||||
else:
|
||||
dbalbum = myDB.action(
|
||||
'SELECT ArtworkURL, ThumbURL FROM albums WHERE ArtworkURL IS NOT NULL AND ArtistID=?',
|
||||
[self.id]).fetchone()
|
||||
if dbalbum:
|
||||
fanart = True
|
||||
image_url = dbalbum['ArtworkURL']
|
||||
thumb_url = dbalbum['ThumbURL']
|
||||
|
||||
if not image_url:
|
||||
logger.debug('No artist image found on fanart.tv for Artist Id: %s', self.id)
|
||||
|
||||
else:
|
||||
dbalbum = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?',
|
||||
'SELECT ArtistName, AlbumTitle, ReleaseID, Type FROM albums WHERE AlbumID=?',
|
||||
[self.id]).fetchone()
|
||||
if dbalbum['ReleaseID'] != self.id:
|
||||
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'],
|
||||
api_key=LASTFM_API_KEY)
|
||||
if not data:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'],
|
||||
data = lastfm.request_lastfm("album.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
|
||||
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
else:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
|
||||
if dbalbum['Type'] != "part of":
|
||||
data = lastfm.request_lastfm("album.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
|
||||
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
else:
|
||||
|
||||
# Series, use actual artist for the release-group
|
||||
artist = mb.getArtistForReleaseGroup(self.id)
|
||||
if artist:
|
||||
data = lastfm.request_lastfm("album.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(artist),
|
||||
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
@@ -406,7 +479,11 @@ class Cache(object):
|
||||
|
||||
ext = os.path.splitext(image_url)[1]
|
||||
|
||||
artwork_path = os.path.join(self.path_to_art_cache,
|
||||
if fanart:
|
||||
artwork_path = os.path.join(self.path_to_art_cache,
|
||||
self.id + '_fanart_' + '.' + helpers.today() + ext)
|
||||
else:
|
||||
artwork_path = os.path.join(self.path_to_art_cache,
|
||||
self.id + '.' + helpers.today() + ext)
|
||||
try:
|
||||
with open(artwork_path, 'wb') as f:
|
||||
@@ -422,7 +499,9 @@ class Cache(object):
|
||||
# as it's missing/outdated.
|
||||
if thumb_url and self.query_type in ['thumb', 'artwork'] and not (
|
||||
self.thumb_files and self._is_current(self.thumb_files[0])):
|
||||
artwork = request.request_content(thumb_url, timeout=20)
|
||||
|
||||
if not (self.query_type == 'artwork' and 'fanart' in thumb_url and artwork):
|
||||
artwork = request.request_content(thumb_url, timeout=20)
|
||||
|
||||
if artwork:
|
||||
# Make sure the artwork dir exists:
|
||||
@@ -445,11 +524,34 @@ class Cache(object):
|
||||
|
||||
ext = os.path.splitext(image_url)[1]
|
||||
|
||||
thumb_path = os.path.join(self.path_to_art_cache,
|
||||
'T_' + self.id + '.' + helpers.today() + ext)
|
||||
if fanart:
|
||||
thumb_path = os.path.join(self.path_to_art_cache,
|
||||
'T_' + self.id + '_fanart_' + '.' + helpers.today() + ext)
|
||||
else:
|
||||
thumb_path = os.path.join(self.path_to_art_cache,
|
||||
'T_' + self.id + '.' + helpers.today() + ext)
|
||||
try:
|
||||
with open(thumb_path, 'wb') as f:
|
||||
f.write(artwork)
|
||||
if self.id_type != 'artist':
|
||||
with open(thumb_path, 'wb') as f:
|
||||
f.write(artwork)
|
||||
else:
|
||||
|
||||
# 2019 last.fm no longer allows access to artist images, use the fanart.tv image to create a thumb
|
||||
artwork_thumb = None
|
||||
if 'fanart' in thumb_url:
|
||||
# Create thumb using image resizing service
|
||||
artwork_path = '{0}?{1}'.format('http://images.weserv.nl/', urlencode({
|
||||
'url': thumb_url.replace('http://', ''),
|
||||
'w': 300,
|
||||
}))
|
||||
artwork_thumb = request.request_content(artwork_path, timeout=20, whitelist_status_code=404)
|
||||
|
||||
if artwork_thumb:
|
||||
with open(thumb_path, 'wb') as f:
|
||||
f.write(artwork_thumb)
|
||||
else:
|
||||
with open(thumb_path, 'wb') as f:
|
||||
f.write(artwork)
|
||||
|
||||
os.chmod(thumb_path, int(headphones.CONFIG.FILE_PERMISSIONS, 8))
|
||||
except (OSError, IOError) as e:
|
||||
@@ -465,7 +567,7 @@ def getArtwork(ArtistID=None, AlbumID=None):
|
||||
if not artwork_path:
|
||||
return None
|
||||
|
||||
if artwork_path.startswith('http://'):
|
||||
if artwork_path.startswith(('http://', 'https://')):
|
||||
return artwork_path
|
||||
else:
|
||||
artwork_file = os.path.basename(artwork_path)
|
||||
@@ -479,7 +581,7 @@ def getThumb(ArtistID=None, AlbumID=None):
|
||||
if not artwork_path:
|
||||
return None
|
||||
|
||||
if artwork_path.startswith('http://'):
|
||||
if artwork_path.startswith(('http://', 'https://')):
|
||||
return artwork_path
|
||||
else:
|
||||
thumbnail_file = os.path.basename(artwork_path)
|
||||
|
||||
@@ -34,11 +34,18 @@ _CONFIG_DEFINITIONS = {
|
||||
'ADD_ALBUM_ART': (int, 'General', 0),
|
||||
'ADVANCEDENCODER': (str, 'General', ''),
|
||||
'ALBUM_ART_FORMAT': (str, 'General', 'folder'),
|
||||
'ALBUM_ART_MIN_WIDTH': (str, 'General', ''),
|
||||
'ALBUM_ART_MAX_WIDTH': (str, 'General', ''),
|
||||
# This is used in importer.py to determine how complete an album needs to
|
||||
# be - to be considered "downloaded". Percentage from 0-100
|
||||
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
|
||||
'API_ENABLED': (int, 'General', 0),
|
||||
'API_KEY': (str, 'General', ''),
|
||||
'ORPHEUS': (int, 'Orpheus.network', 0),
|
||||
'ORPHEUS_PASSWORD': (str, 'Orpheus.network', ''),
|
||||
'ORPHEUS_RATIO': (str, 'Orpheus.network', ''),
|
||||
'ORPHEUS_USERNAME': (str, 'Orpheus.network', ''),
|
||||
'ORPHEUS_URL': (str, 'Orpheus.network', 'https://orpheus.network'),
|
||||
'AUTOWANT_ALL': (int, 'General', 0),
|
||||
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
|
||||
'AUTOWANT_UPCOMING': (int, 'General', 1),
|
||||
@@ -138,12 +145,14 @@ _CONFIG_DEFINITIONS = {
|
||||
'IGNORED_FILES': (list, 'Advanced', []), # path
|
||||
'INCLUDE_EXTRAS': (int, 'General', 0),
|
||||
'INTERFACE': (str, 'General', 'default'),
|
||||
'JOIN_APIKEY': (str, 'Join', ''),
|
||||
'JOIN_DEVICEID': (str, 'Join', ''),
|
||||
'JOIN_ENABLED': (int, 'Join', 0),
|
||||
'JOIN_ONSNATCH': (int, 'Join', 0),
|
||||
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
||||
'KAT': (int, 'Kat', 0),
|
||||
'KAT_PROXY_URL': (str, 'Kat', ''),
|
||||
'KAT_RATIO': (str, 'Kat', ''),
|
||||
'KEEP_NFO': (int, 'General', 0),
|
||||
'KEEP_TORRENT_FILES': (int, 'General', 0),
|
||||
'KEEP_TORRENT_FILES_DIR': (path, 'General', ''),
|
||||
'LASTFM_USERNAME': (str, 'General', ''),
|
||||
'LAUNCH_BROWSER': (int, 'General', 1),
|
||||
'LIBRARYSCAN': (int, 'General', 1),
|
||||
@@ -155,8 +164,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'LOSSLESS_BITRATE_TO': (int, 'General', 0),
|
||||
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
|
||||
'MB_IGNORE_AGE': (int, 'General', 365),
|
||||
'MININOVA': (int, 'Mininova', 0),
|
||||
'MININOVA_RATIO': (str, 'Mininova', ''),
|
||||
'MB_IGNORE_AGE_MISSING': (int, 'General', 0),
|
||||
'MIRROR': (str, 'General', 'musicbrainz.org'),
|
||||
'MOVE_FILES': (int, 'General', 0),
|
||||
'MPC_ENABLED': (bool_int, 'MPC', False),
|
||||
@@ -226,6 +234,10 @@ _CONFIG_DEFINITIONS = {
|
||||
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
||||
'PUSHOVER_ONSNATCH': (int, 'Pushover', 0),
|
||||
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
||||
'QBITTORRENT_HOST': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_LABEL': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_PASSWORD': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_USERNAME': (str, 'QBitTorrent', ''),
|
||||
'RENAME_FILES': (int, 'General', 0),
|
||||
'RENAME_UNPROCESSED': (bool_int, 'General', 1),
|
||||
'RENAME_FROZEN': (bool_int, 'General', 1),
|
||||
@@ -236,6 +248,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
|
||||
'RUTRACKER_RATIO': (str, 'Rutracker', ''),
|
||||
'RUTRACKER_USER': (str, 'Rutracker', ''),
|
||||
'RUTRACKER_COOKIE': (str, 'Rutracker', ''),
|
||||
'SAB_APIKEY': (str, 'SABnzbd', ''),
|
||||
'SAB_CATEGORY': (str, 'SABnzbd', ''),
|
||||
'SAB_HOST': (str, 'SABnzbd', ''),
|
||||
@@ -243,13 +256,16 @@ _CONFIG_DEFINITIONS = {
|
||||
'SAB_USERNAME': (str, 'SABnzbd', ''),
|
||||
'SAMPLINGFREQUENCY': (int, 'General', 44100),
|
||||
'SEARCH_INTERVAL': (int, 'General', 1440),
|
||||
'SLACK_ENABLED': (int, 'Slack', 0),
|
||||
'SLACK_URL': (str, 'Slack', ''),
|
||||
'SLACK_CHANNEL': (str, 'Slack', ''),
|
||||
'SLACK_EMOJI': (str, 'Slack', ''),
|
||||
'SLACK_ONSNATCH': (int, 'Slack', 0),
|
||||
'SOFT_CHROOT': (path, 'General', ''),
|
||||
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
|
||||
'SONGKICK_ENABLED': (int, 'Songkick', 1),
|
||||
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
|
||||
'SONGKICK_LOCATION': (str, 'Songkick', ''),
|
||||
'STRIKE': (int, 'Strike', 0),
|
||||
'STRIKE_RATIO': (str, 'Strike', ''),
|
||||
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
|
||||
'SUBSONIC_HOST': (str, 'Subsonic', ''),
|
||||
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
|
||||
@@ -266,6 +282,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'TORZNAB_APIKEY': (str, 'Torznab', ''),
|
||||
'TORZNAB_ENABLED': (int, 'Torznab', 1),
|
||||
'TORZNAB_HOST': (str, 'Torznab', ''),
|
||||
'TORZNAB_RATIO': (str, 'Torznab', ''),
|
||||
'TRANSMISSION_HOST': (str, 'Transmission', ''),
|
||||
'TRANSMISSION_PASSWORD': (str, 'Transmission', ''),
|
||||
'TRANSMISSION_USERNAME': (str, 'Transmission', ''),
|
||||
@@ -286,10 +303,11 @@ _CONFIG_DEFINITIONS = {
|
||||
'WAFFLES_PASSKEY': (str, 'Waffles', ''),
|
||||
'WAFFLES_RATIO': (str, 'Waffles', ''),
|
||||
'WAFFLES_UID': (str, 'Waffles', ''),
|
||||
'WHATCD': (int, 'What.cd', 0),
|
||||
'WHATCD_PASSWORD': (str, 'What.cd', ''),
|
||||
'WHATCD_RATIO': (str, 'What.cd', ''),
|
||||
'WHATCD_USERNAME': (str, 'What.cd', ''),
|
||||
'REDACTED': (int, 'Redacted', 0),
|
||||
'REDACTED_USERNAME': (str, 'Redacted', ''),
|
||||
'REDACTED_PASSWORD': (str, 'Redacted', ''),
|
||||
'REDACTED_RATIO': (str, 'Redacted', ''),
|
||||
'REDACTED_USE_FLTOKEN': (int, 'Redacted', 0),
|
||||
'XBMC_ENABLED': (int, 'XBMC', 0),
|
||||
'XBMC_HOST': (str, 'XBMC', ''),
|
||||
'XBMC_NOTIFY': (int, 'XBMC', 0),
|
||||
@@ -394,8 +412,8 @@ class Config(object):
|
||||
def get_extra_torznabs(self):
|
||||
""" Return the extra torznab tuples """
|
||||
extra_torznabs = list(
|
||||
itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 3)
|
||||
for i in range(3)])
|
||||
itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 4)
|
||||
for i in range(4)])
|
||||
)
|
||||
return extra_torznabs
|
||||
|
||||
@@ -466,4 +484,17 @@ class Config(object):
|
||||
if self.CONFIG_VERSION == '5':
|
||||
if self.OPEN_MAGNET_LINKS:
|
||||
self.MAGNET_LINKS = 2
|
||||
self.CONFIG_VERSION = '5'
|
||||
|
||||
# Add Seed Ratio to Torznabs
|
||||
if self.EXTRA_TORZNABS:
|
||||
extra_torznabs = list(
|
||||
itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 3)
|
||||
for i in range(3)])
|
||||
)
|
||||
new_torznabs = []
|
||||
for torznab in extra_torznabs:
|
||||
new_torznabs.extend([torznab[0], torznab[1], u'', torznab[2]])
|
||||
if new_torznabs:
|
||||
self.EXTRA_TORZNABS = new_torznabs
|
||||
|
||||
self.CONFIG_VERSION = '6'
|
||||
|
||||
@@ -355,86 +355,89 @@ class ConfigApiTest(TestCase):
|
||||
# TORZNABS
|
||||
# TODO : here is copypaste from of NEZNABS tests. Make tests better, plz refactor them
|
||||
#
|
||||
@TestArgs(
|
||||
('', []),
|
||||
('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
|
||||
(['ABC', 'DEF'], []),
|
||||
([1], []),
|
||||
([1, 2], []),
|
||||
([1, 2, 3], [(1, 2, 3)]),
|
||||
|
||||
([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
|
||||
([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
|
||||
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc', 'Ddddda'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||
)
|
||||
def test_get_extra_torznabs(self, conf_value, expected):
|
||||
""" Config: get_extra_torznabs """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
|
||||
# set up mocks:
|
||||
# 'EXTRA_TORZNABS': (list, '', ''),
|
||||
self.config_mock["Torznab"] = {"extra_torznabs": conf_value}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
res = c.get_extra_torznabs()
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(res, expected)
|
||||
|
||||
def test_clear_extra_torznabs(self):
|
||||
""" Config: clear_extra_torznabs """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
random_value = -1292721
|
||||
self.config_mock["Torznab"] = {"extra_torznabs": [1, 2, 3]}
|
||||
self.config_mock["Torznab"] = {"do_not_touch": random_value}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
res = c.clear_extra_torznabs()
|
||||
|
||||
# assertions:
|
||||
self.assertIsNone(res)
|
||||
self.assertEqual(self.config_mock["Torznab"]["extra_torznabs"], [])
|
||||
self.assertEqual(self.config_mock["Torznab"]["do_not_touch"], random_value)
|
||||
|
||||
@TestArgs(
|
||||
([], [''], ['']),
|
||||
([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
|
||||
|
||||
([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
|
||||
)
|
||||
def test_add_extra_torznab(self, initial, added, expected):
|
||||
""" Config: add_extra_torznab """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
c.add_extra_torznab(added)
|
||||
act = self.config_mock["Torznab"]["extra_torznabs"]
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(act, expected)
|
||||
|
||||
@TestArgs(
|
||||
(None),
|
||||
([]),
|
||||
([1, 2, 3]),
|
||||
([True]),
|
||||
)
|
||||
def test_add_extra_torznab_raise_on_none(self, initial):
|
||||
""" Config: add_extra_torznab should raise on None adding"""
|
||||
path = '/tmp/notexist'
|
||||
|
||||
self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
with self.assertRaises(TypeError):
|
||||
c.add_extra_torznab(None)
|
||||
pass
|
||||
# TODO: Fix tests for following:
|
||||
# CONFIG_VERSION == '5' each entry = 'host, api, enabled'
|
||||
# CONFIG_VERSION > '5' each entry = 'host, api, seed ratio, enabled'
|
||||
# @TestArgs(
|
||||
# ('', []),
|
||||
# ('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
|
||||
# (['ABC', 'DEF'], []),
|
||||
# ([1], []),
|
||||
# ([1, 2], []),
|
||||
# ([1, 2, 3], [(1, 2, 3)]),
|
||||
#
|
||||
# ([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
|
||||
# ([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
|
||||
# ([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||
# ([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc', 'Ddddda'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||
# )
|
||||
# def test_get_extra_torznabs(self, conf_value, expected):
|
||||
# """ Config: get_extra_torznabs """
|
||||
# path = '/tmp/notexist'
|
||||
#
|
||||
# # itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
|
||||
# # set up mocks:
|
||||
# # 'EXTRA_TORZNABS': (list, '', ''),
|
||||
# self.config_mock["Torznab"] = {"extra_torznabs": conf_value}
|
||||
#
|
||||
# # call methods
|
||||
# c = headphones.config.Config(path)
|
||||
# res = c.get_extra_torznabs()
|
||||
#
|
||||
# # assertions:
|
||||
# self.assertEqual(res, expected)
|
||||
#
|
||||
# def test_clear_extra_torznabs(self):
|
||||
# """ Config: clear_extra_torznabs """
|
||||
# path = '/tmp/notexist'
|
||||
#
|
||||
# random_value = -1292721
|
||||
# self.config_mock["Torznab"] = {"extra_torznabs": [1, 2, 3]}
|
||||
# self.config_mock["Torznab"] = {"do_not_touch": random_value}
|
||||
#
|
||||
# # call methods
|
||||
# c = headphones.config.Config(path)
|
||||
# res = c.clear_extra_torznabs()
|
||||
#
|
||||
# # assertions:
|
||||
# self.assertIsNone(res)
|
||||
# self.assertEqual(self.config_mock["Torznab"]["extra_torznabs"], [])
|
||||
# self.assertEqual(self.config_mock["Torznab"]["do_not_touch"], random_value)
|
||||
#
|
||||
# @TestArgs(
|
||||
# ([], [''], ['']),
|
||||
# ([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
|
||||
#
|
||||
# ([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
|
||||
# )
|
||||
# def test_add_extra_torznab(self, initial, added, expected):
|
||||
# """ Config: add_extra_torznab """
|
||||
# path = '/tmp/notexist'
|
||||
#
|
||||
# self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
||||
#
|
||||
# # call methods
|
||||
# c = headphones.config.Config(path)
|
||||
# c.add_extra_torznab(added)
|
||||
# act = self.config_mock["Torznab"]["extra_torznabs"]
|
||||
#
|
||||
# # assertions:
|
||||
# self.assertEqual(act, expected)
|
||||
#
|
||||
# @TestArgs(
|
||||
# (None),
|
||||
# ([]),
|
||||
# ([1, 2, 3]),
|
||||
# ([True]),
|
||||
# )
|
||||
# def test_add_extra_torznab_raise_on_none(self, initial):
|
||||
# """ Config: add_extra_torznab should raise on None adding"""
|
||||
# path = '/tmp/notexist'
|
||||
#
|
||||
# self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
||||
#
|
||||
# # call methods
|
||||
# c = headphones.config.Config(path)
|
||||
# with self.assertRaises(TypeError):
|
||||
# c.add_extra_torznab(None)
|
||||
# pass
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import pprint
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from headphones import logger
|
||||
|
||||
|
||||
def cry():
|
||||
"""
|
||||
Logs thread traces.
|
||||
"""
|
||||
tmap = {}
|
||||
main_thread = None
|
||||
# get a map of threads by their ID so we can print their names
|
||||
# during the traceback dump
|
||||
for t in threading.enumerate():
|
||||
if t.ident:
|
||||
tmap[t.ident] = t
|
||||
else:
|
||||
main_thread = t
|
||||
|
||||
# Loop over each thread's current frame, writing info about it
|
||||
for tid, frame in sys._current_frames().iteritems():
|
||||
thread = tmap.get(tid, main_thread)
|
||||
|
||||
lines = []
|
||||
lines.append('%s\n' % thread.getName())
|
||||
lines.append('========================================\n')
|
||||
lines += traceback.format_stack(frame)
|
||||
lines.append('========================================\n')
|
||||
lines.append('LOCAL VARIABLES:\n')
|
||||
lines.append('========================================\n')
|
||||
lines.append(pprint.pformat(frame.f_locals))
|
||||
lines.append('\n\n')
|
||||
logger.info("".join(lines))
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import time
|
||||
|
||||
import sqlite3
|
||||
|
||||
import os
|
||||
@@ -45,36 +47,97 @@ class DBConnection:
|
||||
self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
|
||||
# don't wait for the disk to finish writing
|
||||
self.connection.execute("PRAGMA synchronous = OFF")
|
||||
# journal disabled since we never do rollbacks
|
||||
# default set to Write-Ahead Logging WAL
|
||||
self.connection.execute("PRAGMA journal_mode = %s" % headphones.CONFIG.JOURNAL_MODE)
|
||||
# 64mb of cache memory,probably need to make it user configurable
|
||||
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize() * 1024))
|
||||
self.connection.row_factory = sqlite3.Row
|
||||
|
||||
def action(self, query, args=None):
|
||||
def action(self, query, args=None, upsert_insert_qry=None):
|
||||
|
||||
if query is None:
|
||||
return
|
||||
|
||||
sqlResult = None
|
||||
attempts = 0
|
||||
dberror = None
|
||||
|
||||
try:
|
||||
with self.connection as c:
|
||||
if args is None:
|
||||
sqlResult = c.execute(query)
|
||||
while attempts < 10:
|
||||
try:
|
||||
with self.connection as c:
|
||||
|
||||
# log that previous attempt was locked and we're trying again
|
||||
if dberror:
|
||||
if args is None:
|
||||
logger.debug('SQL: Database was previously locked, trying again. Attempt number %i. Query: %s', attempts + 1, query)
|
||||
else:
|
||||
logger.debug('SQL: Database was previously locked, trying again. Attempt number %i. Query: %s. Args: %s', attempts + 1, query, args)
|
||||
|
||||
# debugging
|
||||
# try:
|
||||
# explain_query = 'EXPLAIN QUERY PLAN ' + query
|
||||
# if not args:
|
||||
# sql_results = c.execute(explain_query)
|
||||
# else:
|
||||
# sql_results = c.execute(explain_query, args)
|
||||
# if not args:
|
||||
# print(explain_query)
|
||||
# else:
|
||||
# print(explain_query + ' ' + str(args))
|
||||
# explain_results = sql_results
|
||||
# for row in explain_results:
|
||||
# print row
|
||||
# except Exception as e:
|
||||
# print(e)
|
||||
|
||||
# Execute query
|
||||
|
||||
# time0 = time.time()
|
||||
|
||||
if args is None:
|
||||
sqlResult = c.execute(query)
|
||||
# logger.debug('SQL: ' + query)
|
||||
else:
|
||||
sqlResult = c.execute(query, args)
|
||||
# logger.debug('SQL: %s. Args: %s', query, args)
|
||||
|
||||
# INSERT part of upsert query
|
||||
if upsert_insert_qry:
|
||||
sqlResult = c.execute(upsert_insert_qry, args)
|
||||
# logger.debug('SQL: %s. Args: %s', upsert_insert_qry, args)
|
||||
|
||||
# debugging: loose test to log queries taking longer than 5 seconds
|
||||
# seconds = time.time() - time0
|
||||
# if seconds > 5:
|
||||
# if args is None:
|
||||
# logger.debug("SQL: Query ran for %s seconds: %s", seconds, query)
|
||||
# else:
|
||||
# logger.debug("SQL: Query ran for %s seconds: %s with args %s", seconds, query, args)
|
||||
|
||||
break
|
||||
|
||||
except sqlite3.OperationalError, e:
|
||||
if "unable to open database file" in e.message or "database is locked" in e.message:
|
||||
dberror = e
|
||||
if args is None:
|
||||
logger.debug('Database error: %s. Query: %s', e, query)
|
||||
else:
|
||||
logger.debug('Database error: %s. Query: %s. Args: %s', e, query, args)
|
||||
attempts += 1
|
||||
time.sleep(1)
|
||||
else:
|
||||
sqlResult = c.execute(query, args)
|
||||
|
||||
except sqlite3.OperationalError, e:
|
||||
if "unable to open database file" in e.message or "database is locked" in e.message:
|
||||
logger.warn('Database Error: %s', e)
|
||||
else:
|
||||
logger.error('Database error: %s', e)
|
||||
logger.error('Database error: %s', e)
|
||||
raise
|
||||
except sqlite3.DatabaseError, e:
|
||||
logger.error('Fatal Error executing %s :: %s', query, e)
|
||||
raise
|
||||
|
||||
except sqlite3.DatabaseError, e:
|
||||
logger.error('Fatal Error executing %s :: %s', query, e)
|
||||
raise
|
||||
# log if no results returned due to lock
|
||||
if not sqlResult and attempts:
|
||||
if args is None:
|
||||
logger.warn('SQL: Query failed due to database error: %s. Query: %s', dberror, query)
|
||||
else:
|
||||
logger.warn('SQL: Query failed due to database error: %s. Query: %s. Args: %s', dberror, query, args)
|
||||
|
||||
return sqlResult
|
||||
|
||||
@@ -88,24 +151,19 @@ class DBConnection:
|
||||
return sqlResults
|
||||
|
||||
def upsert(self, tableName, valueDict, keyDict):
|
||||
|
||||
"""
|
||||
Transactions an Update or Insert to a table based on key.
|
||||
If the table is not updated then the 'WHERE changes' will be 0 and the table inserted
|
||||
"""
|
||||
def genParams(myDict):
|
||||
return [x + " = ?" for x in myDict.keys()]
|
||||
|
||||
changesBefore = self.connection.total_changes
|
||||
update_query = "UPDATE " + tableName + " SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
|
||||
|
||||
update_query = "UPDATE " + tableName + " SET " + ", ".join(
|
||||
genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
|
||||
insert_query = ("INSERT INTO " + tableName + " (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + " SELECT " + ", ".join(
|
||||
["?"] * len(valueDict.keys() + keyDict.keys())) + " WHERE changes()=0")
|
||||
|
||||
self.action(update_query, valueDict.values() + keyDict.values())
|
||||
|
||||
if self.connection.total_changes == changesBefore:
|
||||
insert_query = (
|
||||
"INSERT INTO " + tableName + " (" + ", ".join(
|
||||
valueDict.keys() + keyDict.keys()) + ")" +
|
||||
" VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")"
|
||||
)
|
||||
try:
|
||||
self.action(insert_query, valueDict.values() + keyDict.values())
|
||||
except sqlite3.IntegrityError:
|
||||
logger.info('Queries failed: %s and %s', update_query, insert_query)
|
||||
try:
|
||||
self.action(update_query, valueDict.values() + keyDict.values(), upsert_insert_qry=insert_query)
|
||||
except sqlite3.IntegrityError:
|
||||
logger.info('Queries failed: %s and %s', update_query, insert_query)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of Headphones.
|
||||
#
|
||||
# Headphones is free software: you can redistribute it and/or modify
|
||||
@@ -49,6 +51,7 @@ delugeweb_auth = {}
|
||||
delugeweb_url = ''
|
||||
deluge_verify_cert = False
|
||||
scrub_logs = True
|
||||
headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
def _scrubber(text):
|
||||
@@ -80,7 +83,7 @@ def addTorrent(link, data=None, name=None):
|
||||
|
||||
result = {}
|
||||
retid = False
|
||||
url_what = ['https://what.cd/', 'http://what.cd/']
|
||||
url_orpheus = ['https://orpheus.network/', 'http://orpheus.network/']
|
||||
url_waffles = ['https://waffles.ch/', 'http://waffles.ch/']
|
||||
|
||||
if link.lower().startswith('magnet:'):
|
||||
@@ -94,7 +97,7 @@ def addTorrent(link, data=None, name=None):
|
||||
if link.lower().startswith(tuple(url_waffles)):
|
||||
if 'rss=' not in link:
|
||||
link = link + '&rss=1'
|
||||
if link.lower().startswith(tuple(url_what)):
|
||||
if link.lower().startswith(tuple(url_orpheus)):
|
||||
logger.debug('Deluge: Using different User-Agent for this site')
|
||||
user_agent = 'Headphones'
|
||||
# This method will make Deluge download the file
|
||||
@@ -104,11 +107,11 @@ def addTorrent(link, data=None, name=None):
|
||||
# return addTorrent(local_torrent_path)
|
||||
else:
|
||||
user_agent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
|
||||
headers = {'User-Agent': user_agent}
|
||||
get_headers = {'User-Agent': user_agent}
|
||||
torrentfile = ''
|
||||
logger.debug('Deluge: Trying to download (GET)')
|
||||
try:
|
||||
r = requests.get(link, headers=headers)
|
||||
r = requests.get(link, headers=get_headers)
|
||||
if r.status_code == 200:
|
||||
logger.debug('Deluge: 200 OK')
|
||||
# .text will ruin the encoding for some torrents
|
||||
@@ -135,7 +138,10 @@ def addTorrent(link, data=None, name=None):
|
||||
# remove '.torrent' suffix
|
||||
if name[-len('.torrent'):] == '.torrent':
|
||||
name = name[:-len('.torrent')]
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||
try:
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||
except:
|
||||
logger.debug('Deluge: Sending Deluge torrent with problematic name and some content')
|
||||
result = {'type': 'torrent',
|
||||
'name': name,
|
||||
'content': torrentfile}
|
||||
@@ -164,7 +170,10 @@ def addTorrent(link, data=None, name=None):
|
||||
# remove '.torrent' suffix
|
||||
if name[-len('.torrent'):] == '.torrent':
|
||||
name = name[:-len('.torrent')]
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||
try:
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||
except UnicodeDecodeError:
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name.decode('utf-8'), str(torrentfile)[:40]))
|
||||
result = {'type': 'torrent',
|
||||
'name': name,
|
||||
'content': torrentfile}
|
||||
@@ -199,7 +208,7 @@ def getTorrentFolder(result):
|
||||
],
|
||||
"id": 21})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['total_done'] = json.loads(response.text)['result']['total_done']
|
||||
|
||||
tries = 0
|
||||
@@ -207,7 +216,7 @@ def getTorrentFolder(result):
|
||||
tries += 1
|
||||
time.sleep(5)
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['total_done'] = json.loads(response.text)['result']['total_done']
|
||||
|
||||
post_data = json.dumps({"method": "web.get_torrent_status",
|
||||
@@ -226,7 +235,7 @@ def getTorrentFolder(result):
|
||||
"id": 23})
|
||||
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
result['save_path'] = json.loads(response.text)['result']['save_path']
|
||||
result['name'] = json.loads(response.text)['result']['name']
|
||||
@@ -242,18 +251,45 @@ def removeTorrent(torrentid, remove_data=False):
|
||||
_get_auth()
|
||||
|
||||
try:
|
||||
result = False
|
||||
post_data = json.dumps({"method": "core.remove_torrent",
|
||||
logger.debug('Deluge: Checking if torrent %s finished seeding' % str(torrentid))
|
||||
post_data = json.dumps({"method": "web.get_torrent_status",
|
||||
"params": [
|
||||
torrentid,
|
||||
remove_data
|
||||
],
|
||||
"id": 25})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
result = json.loads(response.text)['result']
|
||||
[
|
||||
"name",
|
||||
"ratio",
|
||||
"state"
|
||||
]
|
||||
],
|
||||
"id": 26})
|
||||
|
||||
return result
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
try:
|
||||
state = json.loads(response.text)['result']['state']
|
||||
except KeyError as e:
|
||||
logger.debug('Deluge: "state" KeyError when trying to remove torrent %s' % str(torrentid))
|
||||
return False
|
||||
|
||||
not_finished = ["queued", "seeding", "downloading", "checking", "error"]
|
||||
result = False
|
||||
if state.lower() in not_finished:
|
||||
logger.debug('Deluge: Torrent %s is either queued or seeding, not removing yet' % str(torrentid))
|
||||
return False
|
||||
else:
|
||||
logger.debug('Deluge: Removing torrent %s' % str(torrentid))
|
||||
post_data = json.dumps({"method": "core.remove_torrent",
|
||||
"params": [
|
||||
torrentid,
|
||||
remove_data
|
||||
],
|
||||
"id": 25})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result = json.loads(response.text)['result']
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Removing torrent failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
@@ -269,7 +305,8 @@ def _get_auth():
|
||||
delugeweb_host = headphones.CONFIG.DELUGE_HOST
|
||||
delugeweb_cert = headphones.CONFIG.DELUGE_CERT
|
||||
delugeweb_password = headphones.CONFIG.DELUGE_PASSWORD
|
||||
logger.debug('Deluge: Using password %s******%s' % (delugeweb_password[0], delugeweb_password[-1]))
|
||||
if len(delugeweb_password) > 0:
|
||||
logger.debug('Deluge: Using password %s******%s' % (delugeweb_password[0], delugeweb_password[-1]))
|
||||
|
||||
if not delugeweb_host.startswith('http'):
|
||||
delugeweb_host = 'http://%s' % delugeweb_host
|
||||
@@ -292,12 +329,12 @@ def _get_auth():
|
||||
"id": 1})
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
except requests.ConnectionError:
|
||||
try:
|
||||
logger.debug('Deluge: Connection failed, let\'s try HTTPS just in case')
|
||||
response = requests.post(delugeweb_url.replace('http:', 'https:'), data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
# If the previous line didn't fail, change delugeweb_url for the rest of this session
|
||||
logger.error('Deluge: Switching to HTTPS, but certificate won\'t be verified because NO CERTIFICATE WAS CONFIGURED!')
|
||||
delugeweb_url = delugeweb_url.replace('http:', 'https:')
|
||||
@@ -322,7 +359,7 @@ def _get_auth():
|
||||
"id": 10})
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
@@ -339,7 +376,7 @@ def _get_auth():
|
||||
"id": 11})
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
@@ -347,7 +384,8 @@ def _get_auth():
|
||||
return None
|
||||
|
||||
delugeweb_hosts = json.loads(response.text)['result']
|
||||
if len(delugeweb_hosts) == 0:
|
||||
# Check if delugeweb_hosts is None before checking its length
|
||||
if not delugeweb_hosts or len(delugeweb_hosts) == 0:
|
||||
logger.error('Deluge: WebUI does not contain daemons')
|
||||
return None
|
||||
|
||||
@@ -357,7 +395,7 @@ def _get_auth():
|
||||
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
@@ -370,7 +408,7 @@ def _get_auth():
|
||||
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
@@ -395,7 +433,7 @@ def _add_torrent_magnet(result):
|
||||
"params": [result['url'], {}],
|
||||
"id": 2})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['hash'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
@@ -415,7 +453,7 @@ def _add_torrent_url(result):
|
||||
"params": [result['url'], {}],
|
||||
"id": 32})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['location'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
@@ -433,10 +471,11 @@ def _add_torrent_file(result):
|
||||
try:
|
||||
# content is torrent file contents that needs to be encoded to base64
|
||||
post_data = json.dumps({"method": "core.add_torrent_file",
|
||||
"params": [result['name'] + '.torrent', b64encode(result['content'].encode('utf8')), {}],
|
||||
"params": [result['name'] + '.torrent',
|
||||
b64encode(result['content'].encode('utf8')), {}],
|
||||
"id": 2})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['hash'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
@@ -444,11 +483,13 @@ def _add_torrent_file(result):
|
||||
try:
|
||||
# content is torrent file contents that needs to be encoded to base64
|
||||
# this time let's try leaving the encoding as is
|
||||
logger.debug('Deluge: There was a decoding issue, let\'s try again')
|
||||
post_data = json.dumps({"method": "core.add_torrent_file",
|
||||
"params": [result['name'] + '.torrent', b64encode(result['content']), {}],
|
||||
"params": [result['name'].decode('utf8') + '.torrent',
|
||||
b64encode(result['content']), {}],
|
||||
"id": 22})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
result['hash'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
@@ -480,7 +521,7 @@ def setTorrentLabel(result):
|
||||
"params": [],
|
||||
"id": 3})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
labels = json.loads(response.text)['result']
|
||||
|
||||
if labels is not None:
|
||||
@@ -491,7 +532,7 @@ def setTorrentLabel(result):
|
||||
"params": [label],
|
||||
"id": 4})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
logger.debug('Deluge: %s label added to Deluge' % label)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Setting label failed: %s' % str(e))
|
||||
@@ -503,7 +544,7 @@ def setTorrentLabel(result):
|
||||
"params": [result['hash'], label],
|
||||
"id": 5})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
logger.debug('Deluge: %s label added to torrent' % label)
|
||||
else:
|
||||
logger.debug('Deluge: Label plugin not detected')
|
||||
@@ -527,12 +568,12 @@ def setSeedRatio(result):
|
||||
"params": [result['hash'], True],
|
||||
"id": 5})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
post_data = json.dumps({"method": "core.set_torrent_stop_ratio",
|
||||
"params": [result['hash'], float(ratio)],
|
||||
"id": 6})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
return not json.loads(response.text)['error']
|
||||
|
||||
@@ -555,7 +596,7 @@ def setTorrentPath(result):
|
||||
"params": [result['hash'], True],
|
||||
"id": 7})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
|
||||
move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
|
||||
@@ -569,7 +610,7 @@ def setTorrentPath(result):
|
||||
"params": [result['hash'], move_to],
|
||||
"id": 8})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
return not json.loads(response.text)['error']
|
||||
|
||||
@@ -592,7 +633,7 @@ def setTorrentPause(result):
|
||||
"params": [[result['hash']]],
|
||||
"id": 9})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
verify=deluge_verify_cert, headers=headers)
|
||||
|
||||
return not json.loads(response.text)['error']
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ import datetime
|
||||
import shutil
|
||||
import time
|
||||
import sys
|
||||
import tempfile
|
||||
import glob
|
||||
|
||||
from beets import logging as beetslogging
|
||||
import six
|
||||
from contextlib import contextmanager
|
||||
|
||||
import fnmatch
|
||||
import re
|
||||
@@ -191,9 +197,15 @@ def piratesize(size):
|
||||
return size
|
||||
|
||||
|
||||
def replace_all(text, dic, normalize=False):
|
||||
def pattern_substitute(pattern, dic, normalize=False):
|
||||
"""
|
||||
Execute path rendering/substitution based on replacement dictionary
|
||||
e.g. pattern = $Artist/$Album
|
||||
dic = {Artist: 'My artist', Album: 'My album'}
|
||||
returns My artist/My album
|
||||
"""
|
||||
from headphones import pathrender
|
||||
if not text:
|
||||
if not pattern:
|
||||
return ''
|
||||
|
||||
if normalize:
|
||||
@@ -210,15 +222,23 @@ def replace_all(text, dic, normalize=False):
|
||||
j.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
new_dic[i] = j
|
||||
dic = new_dic
|
||||
return pathrender.render(text, dic)[0]
|
||||
return pathrender.render(pattern, dic)[0]
|
||||
|
||||
|
||||
def replace_all(text, dic):
|
||||
if not text:
|
||||
return ''
|
||||
|
||||
for i, j in dic.iteritems():
|
||||
text = text.replace(i, j)
|
||||
return text
|
||||
|
||||
|
||||
def replace_illegal_chars(string, type="file"):
|
||||
if type == "file":
|
||||
string = re.sub('[\?"*:|<>/]', '_', string)
|
||||
if type == "folder":
|
||||
string = re.sub('[:\?<>"|]', '_', string)
|
||||
|
||||
string = re.sub('[:\?<>"|*]', '_', string)
|
||||
return string
|
||||
|
||||
|
||||
@@ -251,9 +271,17 @@ _XLATE_SPECIAL = {
|
||||
# Translation table.
|
||||
# Cover additional special characters processing normalization.
|
||||
u"'": '', # replace apostrophe with nothing
|
||||
u"’": '', # replace musicbrainz style apostrophe with nothing
|
||||
u'&': ' and ', # expand & to ' and '
|
||||
}
|
||||
|
||||
_XLATE_MUSICBRAINZ = {
|
||||
# Translation table for Musicbrainz.
|
||||
u"…": '...', # HORIZONTAL ELLIPSIS (U+2026)
|
||||
u"’": "'", # APOSTROPHE (U+0027)
|
||||
u"‐": "-", # EN DASH (U+2013)
|
||||
}
|
||||
|
||||
|
||||
def _translate(s, dictionary):
|
||||
# type: (basestring,Mapping[basestring,basestring])->basestring
|
||||
@@ -319,9 +347,27 @@ def clean_name(s):
|
||||
# 6. trim
|
||||
u = u.strip()
|
||||
# 7. lowercase
|
||||
u = u.lower()
|
||||
return u
|
||||
|
||||
|
||||
def clean_musicbrainz_name(s, return_as_string=True):
|
||||
# type: (basestring)->unicode
|
||||
"""Substitute special Musicbrainz characters.
|
||||
:param s: string to clean up, probably unicode.
|
||||
:return: cleaned-up version of input string.
|
||||
"""
|
||||
if not isinstance(s, unicode):
|
||||
u = unicode(s, 'ascii', 'replace')
|
||||
else:
|
||||
u = s
|
||||
u = _translate(u, _XLATE_MUSICBRAINZ)
|
||||
if return_as_string:
|
||||
return u.encode('utf-8')
|
||||
else:
|
||||
return u
|
||||
|
||||
|
||||
def cleanTitle(title):
|
||||
title = re.sub('[\.\-\/\_]', ' ', title).lower()
|
||||
|
||||
@@ -620,26 +666,68 @@ def get_downloaded_track_list(albumpath):
|
||||
return downloaded_track_list
|
||||
|
||||
|
||||
def preserve_torrent_directory(albumpath):
|
||||
def preserve_torrent_directory(albumpath, forced=False, single=False):
|
||||
"""
|
||||
Copy torrent directory to headphones-modified to keep files for seeding.
|
||||
Copy torrent directory to temp headphones_ directory to keep files for seeding.
|
||||
"""
|
||||
from headphones import logger
|
||||
new_folder = os.path.join(albumpath,
|
||||
'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info(
|
||||
"Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
|
||||
|
||||
# Create temp dir
|
||||
if headphones.CONFIG.KEEP_TORRENT_FILES_DIR:
|
||||
tempdir = headphones.CONFIG.KEEP_TORRENT_FILES_DIR
|
||||
else:
|
||||
tempdir = tempfile.gettempdir()
|
||||
|
||||
logger.info("Preparing to copy to a temporary directory for post processing: " + albumpath.decode(
|
||||
headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
try:
|
||||
shutil.copytree(albumpath, new_folder)
|
||||
return new_folder
|
||||
file_name = os.path.basename(os.path.normpath(albumpath))
|
||||
if not single:
|
||||
prefix = "headphones_" + file_name + "_@hp@_"
|
||||
else:
|
||||
prefix = "headphones_" + os.path.splitext(file_name)[0] + "_@hp@_"
|
||||
new_folder = tempfile.mkdtemp(prefix=prefix, dir=tempdir)
|
||||
except Exception as e:
|
||||
logger.warn("Cannot copy/move files to temp folder: " +
|
||||
new_folder.decode(headphones.SYS_ENCODING, 'replace') +
|
||||
". Not continuing. Error: " + str(e))
|
||||
logger.error("Cannot create temp directory: " + tempdir.decode(
|
||||
headphones.SYS_ENCODING, 'replace') + ". Error: " + str(e))
|
||||
return None
|
||||
|
||||
# Attempt to stop multiple temp dirs being created for the same albumpath
|
||||
if not forced:
|
||||
try:
|
||||
workdir = os.path.join(tempdir, prefix)
|
||||
workdir = re.sub(r'\[', '[[]', workdir)
|
||||
workdir = re.sub(r'(?<!\[)\]', '[]]', workdir)
|
||||
if len(glob.glob(workdir + '*/')) >= 3:
|
||||
logger.error(
|
||||
"Looks like a temp directory has previously been created for this albumpath, not continuing " + workdir.decode(
|
||||
headphones.SYS_ENCODING, 'replace'))
|
||||
shutil.rmtree(new_folder)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warn("Cannot determine if already copied/processed, will copy anyway: Warning: " + str(e))
|
||||
|
||||
# Copy to temp dir
|
||||
try:
|
||||
subdir = os.path.join(new_folder, "headphones")
|
||||
logger.info("Copying files to " + subdir.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
if not single:
|
||||
shutil.copytree(albumpath, subdir)
|
||||
else:
|
||||
os.makedirs(subdir)
|
||||
shutil.copy(albumpath, subdir)
|
||||
# Update the album path with the new location
|
||||
return subdir
|
||||
except Exception as e:
|
||||
logger.warn("Cannot copy/move files to temp directory: " + new_folder.decode(headphones.SYS_ENCODING,
|
||||
'replace') + ". Not continuing. Error: " + str(
|
||||
e))
|
||||
shutil.rmtree(new_folder)
|
||||
return None
|
||||
|
||||
|
||||
def cue_split(albumpath):
|
||||
def cue_split(albumpath, keep_original_folder=False):
|
||||
"""
|
||||
Attempts to check and split audio files by a cue for the given directory.
|
||||
"""
|
||||
@@ -661,6 +749,15 @@ def cue_split(albumpath):
|
||||
# Split cue
|
||||
if cue_count and cue_count >= count and cue_dirs:
|
||||
|
||||
# Copy to temp directory
|
||||
if keep_original_folder:
|
||||
temppath = preserve_torrent_directory(albumpath)
|
||||
if temppath:
|
||||
cue_dirs = [cue_dir.replace(albumpath, temppath) for cue_dir in cue_dirs]
|
||||
albumpath = temppath
|
||||
else:
|
||||
return None
|
||||
|
||||
from headphones import logger, cuesplit
|
||||
logger.info("Attempting to split audio files by cue")
|
||||
|
||||
@@ -671,12 +768,12 @@ def cue_split(albumpath):
|
||||
except Exception as e:
|
||||
os.chdir(cwd)
|
||||
logger.warn("Cue not split: " + str(e))
|
||||
return False
|
||||
return None
|
||||
|
||||
os.chdir(cwd)
|
||||
return True
|
||||
return albumpath
|
||||
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def extract_logline(s):
|
||||
@@ -728,9 +825,11 @@ def smartMove(src, dest, delete=True):
|
||||
|
||||
source_dir = os.path.dirname(src)
|
||||
filename = os.path.basename(src)
|
||||
source_path = os.path.join(source_dir, filename)
|
||||
dest_path = os.path.join(dest, filename)
|
||||
|
||||
if os.path.isfile(os.path.join(dest, filename)):
|
||||
logger.info('Destination file exists: %s', os.path.join(dest, filename))
|
||||
if os.path.isfile(dest_path):
|
||||
logger.info('Destination file exists: %s', dest_path)
|
||||
title = os.path.splitext(filename)[0]
|
||||
ext = os.path.splitext(filename)[1]
|
||||
i = 1
|
||||
@@ -748,15 +847,29 @@ def smartMove(src, dest, delete=True):
|
||||
src.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
break
|
||||
|
||||
try:
|
||||
if delete:
|
||||
shutil.move(os.path.join(source_dir, filename), os.path.join(dest, filename))
|
||||
else:
|
||||
shutil.copy(os.path.join(source_dir, filename), os.path.join(dest, filename))
|
||||
if delete:
|
||||
try:
|
||||
logger.info('Moving "%s" to "%s"', source_path, dest_path)
|
||||
shutil.move(source_path, dest_path)
|
||||
except Exception as e:
|
||||
exists = os.path.exists(dest_path)
|
||||
if exists and os.path.getsize(source_path) == os.path.getsize(dest_path):
|
||||
logger.warn('Successfully moved file "%s", but something went wrong: %s',
|
||||
filename.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
os.unlink(source_path)
|
||||
else:
|
||||
# remove faultly copied file
|
||||
if exists:
|
||||
os.unlink(dest_path)
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
logger.info('Copying "%s" to "%s"', source_path, dest_path)
|
||||
shutil.copy(source_path, dest_path)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'),
|
||||
e)
|
||||
except Exception as e:
|
||||
logger.warn('Error copying file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'),
|
||||
e)
|
||||
|
||||
|
||||
def walk_directory(basedir, followlinks=True):
|
||||
@@ -897,3 +1010,24 @@ def create_https_certificates(ssl_cert, ssl_key):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BeetsLogCapture(beetslogging.Handler):
|
||||
|
||||
def __init__(self):
|
||||
beetslogging.Handler.__init__(self)
|
||||
self.messages = []
|
||||
|
||||
def emit(self, record):
|
||||
self.messages.append(six.text_type(record.msg))
|
||||
|
||||
|
||||
@contextmanager
|
||||
def capture_beets_log(logger='beets'):
|
||||
capture = BeetsLogCapture()
|
||||
log = beetslogging.getLogger(logger)
|
||||
log.addHandler(capture)
|
||||
try:
|
||||
yield capture.messages
|
||||
finally:
|
||||
log.removeHandler(capture)
|
||||
|
||||
@@ -89,11 +89,13 @@ def artistlist_to_mbids(artistlist, forced=False):
|
||||
addArtisttoDB(artistid)
|
||||
|
||||
# Just update the tracks if it does
|
||||
else:
|
||||
havetracks = len(
|
||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len(
|
||||
myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist]))
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, artistid])
|
||||
|
||||
# not sure this is correct and we're updating during scanning in librarysync
|
||||
# else:
|
||||
# havetracks = len(
|
||||
# myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len(
|
||||
# myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist]))
|
||||
# myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, artistid])
|
||||
|
||||
# Delete it from the New Artists if the request came from there
|
||||
if forced:
|
||||
@@ -105,7 +107,7 @@ def artistlist_to_mbids(artistlist, forced=False):
|
||||
try:
|
||||
lastfm.getSimilar()
|
||||
except Exception as e:
|
||||
logger.warn('Failed to update arist information from Last.fm: %s' % e)
|
||||
logger.warn('Failed to update artist information from Last.fm: %s' % e)
|
||||
|
||||
|
||||
def addArtistIDListToDB(artistidlist):
|
||||
@@ -262,8 +264,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
|
||||
else:
|
||||
if check_release_date is None or check_release_date == u"None":
|
||||
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
|
||||
new_releases = mb.get_new_releases(rgid, includeExtras, True)
|
||||
if headphones.CONFIG.MB_IGNORE_AGE_MISSING is not 1:
|
||||
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:
|
||||
if len(check_release_date) == 10:
|
||||
release_date = check_release_date
|
||||
@@ -305,7 +311,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
# This will be used later to build a hybrid release
|
||||
fullreleaselist = []
|
||||
# Search for releases within a release group
|
||||
find_hybrid_releases = myDB.action("SELECT * from allalbums WHERE AlbumID=?",
|
||||
find_hybrid_releases = myDB.select("SELECT * from allalbums WHERE AlbumID=?",
|
||||
[rg['id']])
|
||||
|
||||
# Build the dictionary for the fullreleaselist
|
||||
@@ -512,7 +518,10 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
|
||||
logger.info(
|
||||
u"[%s] Seeing if we need album art for %s" % (artist['artist_name'], rg['title']))
|
||||
cache.getThumb(AlbumID=rg['id'])
|
||||
try:
|
||||
cache.getThumb(AlbumID=rg['id'])
|
||||
except Exception as e:
|
||||
logger.error("Error getting album art: %s", e)
|
||||
|
||||
# Start a search for the album if it's new, hasn't been marked as
|
||||
# downloaded and autowant_all is selected. This search is deferred,
|
||||
@@ -528,10 +537,16 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
finalize_update(artistid, artist['artist_name'], errors)
|
||||
|
||||
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
|
||||
cache.getThumb(ArtistID=artistid)
|
||||
try:
|
||||
cache.getThumb(ArtistID=artistid)
|
||||
except Exception as e:
|
||||
logger.error("Error getting album art: %s", e)
|
||||
|
||||
logger.info(u"Fetching Metacritic reviews for: %s" % artist['artist_name'])
|
||||
metacritic.update(artistid, artist['artist_name'], artist['releasegroups'])
|
||||
try:
|
||||
metacritic.update(artistid, artist['artist_name'], artist['releasegroups'])
|
||||
except Exception as e:
|
||||
logger.error("Error getting Metacritic reviews: %s", e)
|
||||
|
||||
if errors:
|
||||
logger.info(
|
||||
|
||||
@@ -55,7 +55,7 @@ def request_lastfm(method, **kwargs):
|
||||
return
|
||||
|
||||
if "error" in data:
|
||||
logger.error("Last.FM returned an error: %s", data["message"])
|
||||
logger.debug("Last.FM returned an error: %s", data["message"])
|
||||
return
|
||||
|
||||
return data
|
||||
@@ -149,7 +149,10 @@ def getTagTopArtists(tag, limit=50):
|
||||
logger.debug("Fetched %d artists from Last.FM", len(artists))
|
||||
|
||||
for artist in artists:
|
||||
artist_mbid = artist["mbid"]
|
||||
try:
|
||||
artist_mbid = artist["mbid"]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
if not any(artist_mbid in x for x in results):
|
||||
artistlist.append(artist_mbid)
|
||||
|
||||
@@ -50,27 +50,43 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
if not append:
|
||||
# Clean up bad filepaths
|
||||
tracks = myDB.select(
|
||||
'SELECT Location from alltracks WHERE Location IS NOT NULL UNION SELECT Location from tracks WHERE Location IS NOT NULL')
|
||||
|
||||
# Clean up bad filepaths. Queries can take some time, ensure all results are loaded before processing
|
||||
if ArtistID:
|
||||
tracks = myDB.action(
|
||||
'SELECT Location FROM alltracks WHERE ArtistID = ? AND Location IS NOT NULL UNION SELECT Location FROM tracks WHERE ArtistID = ? AND Location '
|
||||
'IS NOT NULL',
|
||||
[ArtistID, ArtistID])
|
||||
else:
|
||||
tracks = myDB.action(
|
||||
'SELECT Location FROM alltracks WHERE Location IS NOT NULL UNION SELECT Location FROM tracks WHERE Location IS NOT NULL')
|
||||
|
||||
locations = []
|
||||
for track in tracks:
|
||||
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace')
|
||||
locations.append(track['Location'])
|
||||
for location in locations:
|
||||
encoded_track_string = location.encode(headphones.SYS_ENCODING, 'replace')
|
||||
if not os.path.isfile(encoded_track_string):
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||
[None, None, None, track['Location']])
|
||||
[None, None, None, location])
|
||||
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||
[None, None, None, track['Location']])
|
||||
[None, None, None, location])
|
||||
|
||||
del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName from have')
|
||||
if ArtistName:
|
||||
del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName FROM have WHERE ArtistName = ? COLLATE NOCASE', [ArtistName])
|
||||
else:
|
||||
del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName FROM have')
|
||||
|
||||
locations = []
|
||||
for track in del_have_tracks:
|
||||
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace')
|
||||
locations.append([track['Location'], track['ArtistName']])
|
||||
for location in locations:
|
||||
encoded_track_string = location[0].encode(headphones.SYS_ENCODING, 'replace')
|
||||
if not os.path.isfile(encoded_track_string):
|
||||
if track['ArtistName']:
|
||||
if location[1]:
|
||||
# Make sure deleted files get accounted for when updating artist track counts
|
||||
new_artists.append(track['ArtistName'])
|
||||
myDB.action('DELETE FROM have WHERE Location=?', [track['Location']])
|
||||
new_artists.append(location[1])
|
||||
myDB.action('DELETE FROM have WHERE Location=?', [location[0]])
|
||||
logger.info(
|
||||
'File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(
|
||||
headphones.SYS_ENCODING, 'replace'))
|
||||
@@ -216,6 +232,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
song_count = 0
|
||||
latest_artist = []
|
||||
last_completion_percentage = 0
|
||||
prev_artist_name = None
|
||||
artistid = None
|
||||
|
||||
for song in song_list:
|
||||
|
||||
@@ -237,87 +255,72 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
# ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM
|
||||
# (IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT.
|
||||
|
||||
if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']:
|
||||
albumid = None
|
||||
|
||||
track = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
have_updated = False
|
||||
if track:
|
||||
controlValueDict = {'ArtistName': track['ArtistName'],
|
||||
'AlbumTitle': track['AlbumTitle'],
|
||||
'TrackTitle': track['TrackTitle']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
if song['ArtistName'] and song['CleanName']:
|
||||
artist_name = song['ArtistName']
|
||||
clean_name = song['CleanName']
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
newValueDict2 = {'Matched': track['AlbumID']}
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
have_updated = True
|
||||
else:
|
||||
track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?',
|
||||
[song['CleanName']]).fetchone()
|
||||
# Only update if artist is in the db
|
||||
if artist_name != prev_artist_name:
|
||||
prev_artist_name = artist_name
|
||||
artistid = None
|
||||
|
||||
artist_lookup = "\"" + artist_name.replace("\"", "\"\"") + "\""
|
||||
|
||||
try:
|
||||
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM artists WHERE ArtistName LIKE ' + artist_lookup + '')
|
||||
except:
|
||||
dbartist = None
|
||||
if not dbartist:
|
||||
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM tracks WHERE CleanName = ?', [clean_name])
|
||||
if not dbartist:
|
||||
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM alltracks WHERE CleanName = ?', [clean_name])
|
||||
if not dbartist:
|
||||
clean_artist = helpers.clean_name(artist_name)
|
||||
if clean_artist:
|
||||
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM tracks WHERE CleanName >= ? and CleanName < ?',
|
||||
[clean_artist, clean_artist + '{'])
|
||||
if not dbartist:
|
||||
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM alltracks WHERE CleanName >= ? and CleanName < ?',
|
||||
[clean_artist, clean_artist + '{'])
|
||||
|
||||
if dbartist:
|
||||
artistid = dbartist[0][0]
|
||||
|
||||
if artistid:
|
||||
|
||||
# This was previously using Artist, Album, Title with a SELECT LIKE ? and was not using an index
|
||||
# (Possible issue: https://stackoverflow.com/questions/37845854/python-sqlite3-not-using-index-with-like)
|
||||
# Now selects/updates using CleanName index (may have to revert if not working)
|
||||
|
||||
# matching on CleanName should be enough, ensure it's the same artist just in case
|
||||
|
||||
# Update tracks
|
||||
track = myDB.action('SELECT AlbumID, ArtistName FROM tracks WHERE CleanName = ? AND ArtistID = ?', [clean_name, artistid]).fetchone()
|
||||
if track:
|
||||
controlValueDict = {'CleanName': track['CleanName']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
albumid = track['AlbumID']
|
||||
myDB.action(
|
||||
'UPDATE tracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ? AND ArtistID = ?',
|
||||
[song['Location'], song['BitRate'], song['Format'], clean_name, artistid])
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
newValueDict2 = {'Matched': track['AlbumID']}
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
have_updated = True
|
||||
else:
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
newValueDict2 = {'Matched': "Failed"}
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
have_updated = True
|
||||
|
||||
alltrack = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
if alltrack:
|
||||
controlValueDict = {'ArtistName': alltrack['ArtistName'],
|
||||
'AlbumTitle': alltrack['AlbumTitle'],
|
||||
'TrackTitle': alltrack['TrackTitle']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
newValueDict2 = {'Matched': alltrack['AlbumID']}
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
else:
|
||||
alltrack = myDB.action(
|
||||
'SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?',
|
||||
[song['CleanName']]).fetchone()
|
||||
# Update alltracks
|
||||
alltrack = myDB.action('SELECT AlbumID, ArtistName FROM alltracks WHERE CleanName = ? AND ArtistID = ?', [clean_name, artistid]).fetchone()
|
||||
if alltrack:
|
||||
controlValueDict = {'CleanName': alltrack['CleanName']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
newValueDict2 = {'Matched': alltrack['AlbumID']}
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
else:
|
||||
# alltracks may not exist if adding album manually, have should only be set to failed if not already updated in tracks
|
||||
if not have_updated:
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
newValueDict2 = {'Matched': "Failed"}
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
albumid = alltrack['AlbumID']
|
||||
myDB.action(
|
||||
'UPDATE alltracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ? AND ArtistID = ?',
|
||||
[song['Location'], song['BitRate'], song['Format'], clean_name, artistid])
|
||||
|
||||
# Update have
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
if albumid:
|
||||
newValueDict2 = {'Matched': albumid}
|
||||
else:
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
newValueDict2 = {'Matched': "Failed"}
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
|
||||
# myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
|
||||
# myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
|
||||
|
||||
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING,
|
||||
'replace'))
|
||||
@@ -327,49 +330,110 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
|
||||
# Clean up the new artist list
|
||||
unique_artists = {}.fromkeys(new_artists).keys()
|
||||
current_artists = myDB.select('SELECT ArtistName, ArtistID from artists')
|
||||
|
||||
# There was a bug where artists with special characters (-,') would show up in new artists.
|
||||
artist_list = [
|
||||
x for x in unique_artists
|
||||
if helpers.clean_name(x).lower() not in [
|
||||
helpers.clean_name(y[0]).lower()
|
||||
for y in current_artists
|
||||
]
|
||||
]
|
||||
artists_checked = [
|
||||
x for x in unique_artists
|
||||
if helpers.clean_name(x).lower() in [
|
||||
helpers.clean_name(y[0]).lower()
|
||||
for y in current_artists
|
||||
]
|
||||
]
|
||||
# # Don't think we need to do this, check the db instead below
|
||||
#
|
||||
# # artist scan
|
||||
# if ArtistName:
|
||||
# current_artists = [[ArtistName]]
|
||||
# # directory scan
|
||||
# else:
|
||||
# current_artists = myDB.select('SELECT ArtistName, ArtistID FROM artists WHERE ArtistName IS NOT NULL')
|
||||
#
|
||||
# # There was a bug where artists with special characters (-,') would show up in new artists.
|
||||
#
|
||||
# # artist_list = scanned artists not in the db
|
||||
# artist_list = [
|
||||
# x for x in unique_artists
|
||||
# if helpers.clean_name(x).lower() not in [
|
||||
# helpers.clean_name(y[0]).lower()
|
||||
# for y in current_artists
|
||||
# ]
|
||||
# ]
|
||||
#
|
||||
# # artists_checked = scanned artists that exist in the db
|
||||
# artists_checked = [
|
||||
# x for x in unique_artists
|
||||
# if helpers.clean_name(x).lower() in [
|
||||
# helpers.clean_name(y[0]).lower()
|
||||
# for y in current_artists
|
||||
# ]
|
||||
# ]
|
||||
|
||||
# Update track counts
|
||||
for artist in artists_checked:
|
||||
# Have tracks are selected from tracks table and not all tracks because of duplicates
|
||||
# We update the track count upon an album switch to compliment this
|
||||
havetracks = (
|
||||
len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL',
|
||||
[artist])) + len(myDB.select(
|
||||
'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
||||
[artist]))
|
||||
)
|
||||
# Note: some people complain about having "artist have tracks" > # of tracks total in artist official releases
|
||||
# (can fix by getting rid of second len statement)
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist])
|
||||
new_artist_list = []
|
||||
|
||||
logger.info('Found %i new artists' % len(artist_list))
|
||||
for artist in unique_artists:
|
||||
|
||||
if artist_list:
|
||||
if not artist:
|
||||
continue
|
||||
|
||||
logger.info('Processing artist: %s' % artist)
|
||||
|
||||
# check if artist is already in the db
|
||||
artist_lookup = "\"" + artist.replace("\"", "\"\"") + "\""
|
||||
|
||||
try:
|
||||
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM artists WHERE ArtistName LIKE ' + artist_lookup + '')
|
||||
except:
|
||||
dbartist = None
|
||||
if not dbartist:
|
||||
clean_artist = helpers.clean_name(artist)
|
||||
if clean_artist:
|
||||
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM tracks WHERE CleanName >= ? and CleanName < ?',
|
||||
[clean_artist, clean_artist + '{'])
|
||||
if not dbartist:
|
||||
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM alltracks WHERE CleanName >= ? and CleanName < ?',
|
||||
[clean_artist, clean_artist + '{'])
|
||||
|
||||
# new artist not in db, add to list
|
||||
if not dbartist:
|
||||
new_artist_list.append(artist)
|
||||
else:
|
||||
|
||||
# artist in db, update have track counts
|
||||
artistid = dbartist[0][0]
|
||||
|
||||
# Have tracks are selected from tracks table and not all tracks because of duplicates
|
||||
# We update the track count upon an album switch to compliment this
|
||||
|
||||
# havetracks = (
|
||||
# len(myDB.select(
|
||||
# 'SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL',
|
||||
# [artist])) + len(myDB.select(
|
||||
# 'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
||||
# [artist]))
|
||||
# )
|
||||
|
||||
try:
|
||||
havetracks = (
|
||||
len(myDB.select(
|
||||
'SELECT ArtistID From tracks WHERE ArtistID = ? AND Location IS NOT NULL',
|
||||
[artistid])) + len(myDB.select(
|
||||
'SELECT ArtistName FROM have WHERE ArtistName LIKE ' + artist_lookup + ' AND Matched = "Failed"'))
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warn('Error updating counts for artist: %s: %s' % (artist, e))
|
||||
|
||||
# Note: some people complain about having "artist have tracks" > # of tracks total in artist official releases
|
||||
# (can fix by getting rid of second len statement)
|
||||
|
||||
if havetracks:
|
||||
myDB.action('UPDATE artists SET HaveTracks = ? WHERE ArtistID = ?', [havetracks, artistid])
|
||||
|
||||
# Update albums to downloaded
|
||||
update_album_status(ArtistID=artistid)
|
||||
|
||||
logger.info('Found %i new artists' % len(new_artist_list))
|
||||
|
||||
# Add scanned artists not in the db
|
||||
if new_artist_list:
|
||||
if headphones.CONFIG.AUTO_ADD_ARTISTS:
|
||||
logger.info('Importing %i new artists' % len(artist_list))
|
||||
importer.artistlist_to_mbids(artist_list)
|
||||
logger.info('Importing %i new artists' % len(new_artist_list))
|
||||
importer.artistlist_to_mbids(new_artist_list)
|
||||
else:
|
||||
logger.info('To add these artists, go to Manage->Manage New Artists')
|
||||
# myDB.action('DELETE from newartists')
|
||||
for artist in artist_list:
|
||||
for artist in new_artist_list:
|
||||
myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist])
|
||||
|
||||
if headphones.CONFIG.DETECT_BITRATE and bitrates:
|
||||
@@ -379,50 +443,78 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
# If we're appending a new album to the database, update the artists total track counts
|
||||
logger.info('Updating artist track counts')
|
||||
|
||||
havetracks = len(
|
||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
||||
[ArtistID])) + len(myDB.select(
|
||||
'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
||||
[ArtistName]))
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
|
||||
artist_lookup = "\"" + ArtistName.replace("\"", "\"\"") + "\""
|
||||
try:
|
||||
havetracks = len(
|
||||
myDB.select('SELECT ArtistID FROM tracks WHERE ArtistID = ? AND Location IS NOT NULL',
|
||||
[ArtistID])) + len(myDB.select(
|
||||
'SELECT ArtistName FROM have WHERE ArtistName LIKE ' + artist_lookup + ' AND Matched = "Failed"'))
|
||||
except Exception as e:
|
||||
logger.warn('Error updating counts for artist: %s: %s' % (ArtistName, e))
|
||||
|
||||
if not append:
|
||||
update_album_status()
|
||||
if havetracks:
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
|
||||
|
||||
# Moved above to call for each artist
|
||||
# if not append:
|
||||
# update_album_status()
|
||||
|
||||
if not append and not artistScan:
|
||||
lastfm.getSimilar()
|
||||
|
||||
logger.info('Library scan complete')
|
||||
if ArtistName:
|
||||
logger.info('Scanning complete for artist: %s', ArtistName)
|
||||
else:
|
||||
logger.info('Library scan complete')
|
||||
|
||||
|
||||
# ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED
|
||||
|
||||
# Think the above comment relates to calling from Manage Unmatched
|
||||
|
||||
def update_album_status(AlbumID=None):
|
||||
# This used to select and update all albums and would clobber the db, changed to run by ArtistID.
|
||||
|
||||
def update_album_status(AlbumID=None, ArtistID=None):
|
||||
myDB = db.DBConnection()
|
||||
logger.info('Counting matched tracks to mark albums as skipped/downloaded')
|
||||
# logger.info('Counting matched tracks to mark albums as skipped/downloaded')
|
||||
|
||||
if AlbumID:
|
||||
album_status_updater = myDB.action(
|
||||
'SELECT AlbumID, AlbumTitle, Status from albums WHERE AlbumID=?', [AlbumID])
|
||||
'SELECT'
|
||||
' a.AlbumID, a.ArtistName, a.AlbumTitle, a.Status, AVG(t.Location IS NOT NULL) * 100 AS album_completion '
|
||||
'FROM'
|
||||
' albums AS a '
|
||||
'JOIN tracks AS t ON t.AlbumID = a.AlbumID '
|
||||
'WHERE'
|
||||
' a.AlbumID = ? AND a.Status != "Downloaded" '
|
||||
'GROUP BY'
|
||||
' a.AlbumID '
|
||||
'HAVING'
|
||||
' AVG(t.Location IS NOT NULL) * 100 >= ?',
|
||||
[AlbumID, headphones.CONFIG.ALBUM_COMPLETION_PCT]
|
||||
)
|
||||
else:
|
||||
album_status_updater = myDB.action('SELECT AlbumID, AlbumTitle, Status from albums')
|
||||
for album in album_status_updater:
|
||||
track_counter = myDB.action('SELECT Location from tracks where AlbumID=?',
|
||||
[album['AlbumID']])
|
||||
total_tracks = 0
|
||||
have_tracks = 0
|
||||
for track in track_counter:
|
||||
total_tracks += 1
|
||||
if track['Location']:
|
||||
have_tracks += 1
|
||||
if total_tracks != 0:
|
||||
album_completion = float(float(have_tracks) / float(total_tracks)) * 100
|
||||
else:
|
||||
album_completion = 0
|
||||
logger.info('Album %s does not have any tracks in database' % album['AlbumTitle'])
|
||||
album_status_updater = myDB.action(
|
||||
'SELECT'
|
||||
' a.AlbumID, a.ArtistID, a.ArtistName, a.AlbumTitle, a.Status, AVG(t.Location IS NOT NULL) * 100 AS album_completion '
|
||||
'FROM'
|
||||
' albums AS a '
|
||||
'JOIN tracks AS t ON t.AlbumID = a.AlbumID '
|
||||
'WHERE'
|
||||
' a.ArtistID = ? AND a.Status != "Downloaded" '
|
||||
'GROUP BY'
|
||||
' a.AlbumID '
|
||||
'HAVING'
|
||||
' AVG(t.Location IS NOT NULL) * 100 >= ?',
|
||||
[ArtistID, headphones.CONFIG.ALBUM_COMPLETION_PCT]
|
||||
)
|
||||
|
||||
if album_completion >= headphones.CONFIG.ALBUM_COMPLETION_PCT:
|
||||
new_album_status = "Downloaded"
|
||||
new_album_status = "Downloaded"
|
||||
|
||||
albums = []
|
||||
for album in album_status_updater:
|
||||
albums.append([album['AlbumID'], album['ArtistName'], album['AlbumTitle']])
|
||||
for album in albums:
|
||||
|
||||
# I don't think we want to change Downloaded->Skipped.....
|
||||
# I think we can only automatically change Skipped->Downloaded when updating
|
||||
@@ -433,10 +525,13 @@ def update_album_status(AlbumID=None):
|
||||
# new_album_status = "Skipped"
|
||||
# else:
|
||||
# new_album_status = album['Status']
|
||||
else:
|
||||
new_album_status = album['Status']
|
||||
# else:
|
||||
# new_album_status = album['Status']
|
||||
#
|
||||
# myDB.upsert("albums", {'Status': new_album_status}, {'AlbumID': album['AlbumID']})
|
||||
# if new_album_status != album['Status']:
|
||||
# logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status))
|
||||
# logger.info('Album status update complete')
|
||||
|
||||
myDB.upsert("albums", {'Status': new_album_status}, {'AlbumID': album['AlbumID']})
|
||||
if new_album_status != album['Status']:
|
||||
logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status))
|
||||
logger.info('Album status update complete')
|
||||
myDB.action('UPDATE albums SET Status = ? WHERE AlbumID = ?', [new_album_status, album[0]])
|
||||
logger.info('Album: %s - %s. Status updated to %s' % (album[1], album[2], new_album_status))
|
||||
|
||||
@@ -91,10 +91,6 @@ def findArtist(name, limit=1):
|
||||
artistlist = []
|
||||
artistResults = None
|
||||
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
|
||||
criteria = {'artist': name.lower()}
|
||||
|
||||
with mb_lock:
|
||||
@@ -156,16 +152,13 @@ def findRelease(name, limit=1, artist=None):
|
||||
if not artist and ':' in name:
|
||||
name, artist = name.rsplit(":", 1)
|
||||
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
if artist and any((c in chars) for c in artist):
|
||||
artist = '"' + artist + '"'
|
||||
criteria = {'release': name.lower()}
|
||||
if artist:
|
||||
criteria['artist'] = artist.lower()
|
||||
|
||||
with mb_lock:
|
||||
try:
|
||||
releaseResults = musicbrainzngs.search_releases(query=name, limit=limit, artist=artist)[
|
||||
'release-list']
|
||||
releaseResults = musicbrainzngs.search_releases(limit=limit, **criteria)['release-list']
|
||||
except musicbrainzngs.WebServiceError as e: # need to update exceptions
|
||||
logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
@@ -234,10 +227,6 @@ def findSeries(name, limit=1):
|
||||
serieslist = []
|
||||
seriesResults = None
|
||||
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
|
||||
criteria = {'series': name.lower()}
|
||||
|
||||
with mb_lock:
|
||||
@@ -759,19 +748,12 @@ def findArtistbyAlbum(name):
|
||||
|
||||
def findAlbumID(artist=None, album=None):
|
||||
results = None
|
||||
chars = set('!?*-')
|
||||
|
||||
try:
|
||||
if album and artist:
|
||||
if any((c in chars) for c in album):
|
||||
album = '"' + album + '"'
|
||||
if any((c in chars) for c in artist):
|
||||
artist = '"' + artist + '"'
|
||||
criteria = {'release': album.lower()}
|
||||
criteria['artist'] = artist.lower()
|
||||
else:
|
||||
if any((c in chars) for c in album):
|
||||
album = '"' + album + '"'
|
||||
criteria = {'release': album.lower()}
|
||||
with mb_lock:
|
||||
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get(
|
||||
@@ -788,3 +770,50 @@ def findAlbumID(artist=None, album=None):
|
||||
return False
|
||||
rgid = unicode(results[0]['id'])
|
||||
return rgid
|
||||
|
||||
|
||||
def getArtistForReleaseGroup(rgid):
|
||||
"""
|
||||
Returns artist name for a release group
|
||||
Used for series where we store the series instead of the artist
|
||||
"""
|
||||
releaseGroup = None
|
||||
try:
|
||||
with mb_lock:
|
||||
releaseGroup = musicbrainzngs.get_release_group_by_id(
|
||||
rgid, ["artists"])
|
||||
releaseGroup = releaseGroup['release-group']
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn(
|
||||
'Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (
|
||||
rgid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not releaseGroup:
|
||||
return False
|
||||
else:
|
||||
return releaseGroup['artist-credit'][0]['artist']['name']
|
||||
|
||||
|
||||
def getArtistRelationships(artistid):
|
||||
"""
|
||||
Returns list of relationship urls. e.g. Discogs, Wikipedia etc.
|
||||
"""
|
||||
urls = []
|
||||
artist = None
|
||||
try:
|
||||
with mb_lock:
|
||||
info = musicbrainzngs.get_artist_by_id(artistid, includes='url-rels')
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn(
|
||||
'Attempt to query MusicBrainz for %s failed "%s"' % (artistid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
if 'artist' in info:
|
||||
artist = info['artist']
|
||||
if 'url-relation-list' in artist:
|
||||
for l in artist['url-relation-list']:
|
||||
urls.append({
|
||||
'type': l['type'],
|
||||
'url': l['target']
|
||||
})
|
||||
return urls
|
||||
|
||||
@@ -27,8 +27,9 @@ def update(artistid, artist_name, release_groups):
|
||||
# cut down on api calls. If it's ineffective then we'll switch to search
|
||||
|
||||
replacements = {" & ": " ", ".": ""}
|
||||
mc_artist_name = helpers.clean_musicbrainz_name(artist_name, return_as_string=False)
|
||||
mc_artist_name = mc_artist_name.replace("'", " ")
|
||||
mc_artist_name = helpers.replace_all(artist_name.lower(), replacements)
|
||||
|
||||
mc_artist_name = mc_artist_name.replace(" ", "-")
|
||||
|
||||
headers = {
|
||||
@@ -36,7 +37,7 @@ def update(artistid, artist_name, release_groups):
|
||||
|
||||
url = "http://www.metacritic.com/person/" + mc_artist_name + "?filter-options=music&sort_options=date&num_items=100"
|
||||
|
||||
res = request.request_soup(url, headers=headers)
|
||||
res = request.request_soup(url, headers=headers, whitelist_status_code=404)
|
||||
|
||||
rows = None
|
||||
|
||||
|
||||
@@ -289,13 +289,19 @@ def album_metadata(path, release, common_tags):
|
||||
first_char = sort_name[0]
|
||||
|
||||
orig_folder = u''
|
||||
for r, d, f in os.walk(path):
|
||||
try:
|
||||
orig_folder = os.path.basename(
|
||||
os.path.normpath(r).decode(headphones.SYS_ENCODING, 'replace'))
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
# Get from temp path
|
||||
if "_@hp@_" in path:
|
||||
orig_folder = path.rsplit("headphones_", 1)[1].split("_@hp@_")[0]
|
||||
orig_folder = orig_folder.decode(headphones.SYS_ENCODING, 'replace')
|
||||
else:
|
||||
for r, d, f in os.walk(path):
|
||||
try:
|
||||
orig_folder = os.path.basename(
|
||||
os.path.normpath(r).decode(headphones.SYS_ENCODING, 'replace'))
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
override_values = {
|
||||
Vars.ARTIST: artist,
|
||||
|
||||
@@ -168,8 +168,8 @@ class MetadataTest(TestCase):
|
||||
if not _h.SYS_ENCODING:
|
||||
_h.SYS_ENCODING = 'UTF-8'
|
||||
|
||||
res = _hp.replace_all(
|
||||
res = _hp.pattern_substitute(
|
||||
"/music/$First/$Artist/$Artist - $Album{ [$Year]}", md, True)
|
||||
|
||||
self.assertEqual(res, u"/music/A/artist/artist - Album",
|
||||
"check correct rendering of None via replace_all()")
|
||||
"check correct rendering of None via pattern_substitute()")
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from urllib import urlencode
|
||||
from urllib import urlencode, quote_plus
|
||||
import urllib
|
||||
import subprocess
|
||||
import json
|
||||
@@ -23,6 +23,7 @@ import email.utils
|
||||
from httplib import HTTPSConnection
|
||||
from urlparse import parse_qsl
|
||||
import urllib2
|
||||
import requests as requests
|
||||
|
||||
import os.path
|
||||
from headphones import logger, helpers, common, request
|
||||
@@ -148,7 +149,9 @@ class PROWL(object):
|
||||
|
||||
http_handler.request("POST",
|
||||
"/publicapi/add",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
headers={
|
||||
'Content-type':
|
||||
"application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
@@ -203,28 +206,34 @@ class XBMC(object):
|
||||
url = host + '/xbmcCmds/xbmcHttp/?' + url_command
|
||||
|
||||
if self.password:
|
||||
return request.request_content(url, auth=(self.username, self.password))
|
||||
return request.request_content(url,
|
||||
auth=(self.username, self.password))
|
||||
else:
|
||||
return request.request_content(url)
|
||||
|
||||
def _sendjson(self, host, method, params={}):
|
||||
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
data = [
|
||||
{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
url = host + '/jsonrpc'
|
||||
|
||||
if self.password:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers, auth=(self.username, self.password))
|
||||
response = request.request_json(
|
||||
url, method="post",
|
||||
data=json.dumps(data),
|
||||
headers=headers, auth=(
|
||||
self.username, self.password))
|
||||
else:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
response = request.request_json(url, method="post",
|
||||
data=json.dumps(data),
|
||||
headers=headers)
|
||||
|
||||
if response:
|
||||
return response[0]['result']
|
||||
|
||||
def update(self):
|
||||
# From what I read you can't update the music library on a per directory or per path basis
|
||||
# so need to update the whole thing
|
||||
# From what I read you can't update the music library on a per
|
||||
# directory or per path basis so need to update the whole thing
|
||||
|
||||
hosts = [x.strip() for x in self.hosts.split(',')]
|
||||
|
||||
@@ -247,18 +256,23 @@ class XBMC(object):
|
||||
logger.info('Sending notification command to XMBC @ ' + host)
|
||||
try:
|
||||
version = self._sendjson(host, 'Application.GetProperties',
|
||||
{'properties': ['version']})['version']['major']
|
||||
{'properties': ['version']})[
|
||||
'version']['major']
|
||||
|
||||
if version < 12: # Eden
|
||||
notification = header + "," + message + "," + time + "," + albumartpath
|
||||
notification = header + "," + message + "," + time + \
|
||||
"," + albumartpath
|
||||
notifycommand = {'command': 'ExecBuiltIn',
|
||||
'parameter': 'Notification(' + notification + ')'}
|
||||
'parameter': 'Notification(' +
|
||||
notification + ')'}
|
||||
request = self._sendhttp(host, notifycommand)
|
||||
|
||||
else: # Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time),
|
||||
params = {'title': header, 'message': message,
|
||||
'displaytime': int(time),
|
||||
'image': albumartpath}
|
||||
request = self._sendjson(host, 'GUI.ShowNotification', params)
|
||||
request = self._sendjson(host, 'GUI.ShowNotification',
|
||||
params)
|
||||
|
||||
if not request:
|
||||
raise Exception
|
||||
@@ -323,22 +337,28 @@ class Plex(object):
|
||||
url = host + '/xbmcCmds/xbmcHttp/?' + command
|
||||
|
||||
if self.password:
|
||||
response = request.request_response(url, auth=(self.username, self.password))
|
||||
response = request.request_response(url, auth=(
|
||||
self.username, self.password))
|
||||
else:
|
||||
response = request.request_response(url)
|
||||
|
||||
return response
|
||||
|
||||
def _sendjson(self, host, method, params={}):
|
||||
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
data = [
|
||||
{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
url = host + '/jsonrpc'
|
||||
|
||||
if self.password:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers, auth=(self.username, self.password))
|
||||
response = request.request_json(
|
||||
url, method="post",
|
||||
data=json.dumps(data),
|
||||
headers=headers, auth=(
|
||||
self.username, self.password))
|
||||
else:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
response = request.request_json(url, method="post",
|
||||
data=json.dumps(data),
|
||||
headers=headers)
|
||||
|
||||
if response:
|
||||
@@ -346,32 +366,77 @@ class Plex(object):
|
||||
|
||||
def update(self):
|
||||
|
||||
# From what I read you can't update the music library on a per directory or per path basis
|
||||
# so need to update the whole thing
|
||||
# Get token from user credentials
|
||||
if not self.token:
|
||||
loginpage = 'https://plex.tv/users/sign_in.json'
|
||||
post_params = {
|
||||
'user[login]': self.username,
|
||||
'user[password]': self.password
|
||||
}
|
||||
headers = {
|
||||
'X-Plex-Device-Name': 'Headphones',
|
||||
'X-Plex-Product': 'Headphones',
|
||||
'X-Plex-Client-Identifier': common.USER_AGENT,
|
||||
'X-Plex-Version': ''
|
||||
}
|
||||
|
||||
logger.info("Getting plex.tv credentials for user %s", self.username)
|
||||
|
||||
try:
|
||||
r = requests.post(loginpage, data=post_params, headers=headers)
|
||||
r.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
logger.error("Error getting plex.tv credentials, check settings: %s", e)
|
||||
return False
|
||||
|
||||
try:
|
||||
data = r.json()
|
||||
except ValueError as e:
|
||||
logger.error("Error getting plex.tv credentials: %s", e)
|
||||
return False
|
||||
|
||||
try:
|
||||
self.token = data['user']['authentication_token']
|
||||
except KeyError as e:
|
||||
logger.error("Error getting plex.tv credentials: %s", e)
|
||||
return False
|
||||
|
||||
# From what I read you can't update the music library on a per
|
||||
# directory or per path basis so need to update the whole thing
|
||||
|
||||
hosts = [x.strip() for x in self.server_hosts.split(',')]
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending library update command to Plex Media Server@ ' + host)
|
||||
logger.info(
|
||||
'Sending library update command to Plex Media Server@ ' + host)
|
||||
url = "%s/library/sections" % host
|
||||
if self.token:
|
||||
params = {'X-Plex-Token': self.token}
|
||||
else:
|
||||
params = False
|
||||
|
||||
r = request.request_minidom(url, params=params)
|
||||
try:
|
||||
r = request.request_minidom(url, params=params)
|
||||
if not r:
|
||||
logger.warn("Error getting Plex Media Server details, check settings (possibly incorrect token)")
|
||||
return False
|
||||
|
||||
sections = r.getElementsByTagName('Directory')
|
||||
sections = r.getElementsByTagName('Directory')
|
||||
|
||||
if not sections:
|
||||
logger.info(u"Plex Media Server not running on: " + host)
|
||||
if not sections:
|
||||
logger.info(u"Plex Media Server not running on: " + host)
|
||||
return False
|
||||
|
||||
for s in sections:
|
||||
if s.getAttribute('type') == "artist":
|
||||
url = "%s/library/sections/%s/refresh" % (
|
||||
host, s.getAttribute('key'))
|
||||
request.request_response(url, params=params)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting Plex Media Server details: %s" % e)
|
||||
return False
|
||||
|
||||
for s in sections:
|
||||
if s.getAttribute('type') == "artist":
|
||||
url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key'))
|
||||
request.request_response(url, params=params)
|
||||
|
||||
def notify(self, artist, album, albumartpath):
|
||||
|
||||
hosts = [x.strip() for x in self.client_hosts.split(',')]
|
||||
@@ -381,27 +446,35 @@ class Plex(object):
|
||||
time = "3000" # in ms
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending notification command to Plex client @ ' + host)
|
||||
logger.info(
|
||||
'Sending notification command to Plex client @ ' + host)
|
||||
try:
|
||||
version = self._sendjson(host, 'Application.GetProperties',
|
||||
{'properties': ['version']})['version']['major']
|
||||
{'properties': ['version']})[
|
||||
'version']['major']
|
||||
|
||||
if version < 12: # Eden
|
||||
notification = header + "," + message + "," + time + "," + albumartpath
|
||||
notification = header + "," + message + "," + time + \
|
||||
"," + albumartpath
|
||||
notifycommand = {'command': 'ExecBuiltIn',
|
||||
'parameter': 'Notification(' + notification + ')'}
|
||||
'parameter': 'Notification(' +
|
||||
notification + ')'}
|
||||
request = self._sendhttp(host, notifycommand)
|
||||
|
||||
else: # Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time),
|
||||
params = {'title': header, 'message': message,
|
||||
'displaytime': int(time),
|
||||
'image': albumartpath}
|
||||
request = self._sendjson(host, 'GUI.ShowNotification', params)
|
||||
request = self._sendjson(host, 'GUI.ShowNotification',
|
||||
params)
|
||||
|
||||
if not request:
|
||||
raise Exception
|
||||
|
||||
except Exception:
|
||||
logger.error('Error sending notification request to Plex client @ ' + host)
|
||||
logger.error(
|
||||
'Error sending notification request to Plex client @ ' +
|
||||
host)
|
||||
|
||||
|
||||
class NMA(object):
|
||||
@@ -419,7 +492,8 @@ class NMA(object):
|
||||
message = "Headphones has snatched: " + snatched
|
||||
else:
|
||||
event = artist + ' - ' + album + ' complete!'
|
||||
message = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']'
|
||||
message = "Headphones has downloaded and postprocessed: " + \
|
||||
artist + ' [' + album + ']'
|
||||
|
||||
logger.debug(u"NMA event: " + event)
|
||||
logger.debug(u"NMA message: " + message)
|
||||
@@ -433,7 +507,8 @@ class NMA(object):
|
||||
if len(keys) > 1:
|
||||
batch = True
|
||||
|
||||
response = p.push(title, event, message, priority=nma_priority, batch_mode=batch)
|
||||
response = p.push(title, event, message, priority=nma_priority,
|
||||
batch_mode=batch)
|
||||
|
||||
if not response[api][u'code'] == u'200':
|
||||
logger.error(u'Could not send notification to NotifyMyAndroid')
|
||||
@@ -461,9 +536,11 @@ class PUSHBULLET(object):
|
||||
data['device_iden'] = self.deviceid
|
||||
|
||||
headers = {'Content-type': "application/json",
|
||||
'Authorization': 'Bearer ' + headphones.CONFIG.PUSHBULLET_APIKEY}
|
||||
'Authorization': 'Bearer ' +
|
||||
headphones.CONFIG.PUSHBULLET_APIKEY}
|
||||
|
||||
response = request.request_json(url, method="post", headers=headers, data=json.dumps(data))
|
||||
response = request.request_json(url, method="post", headers=headers,
|
||||
data=json.dumps(data))
|
||||
|
||||
if response:
|
||||
logger.info(u"PushBullet notifications sent.")
|
||||
@@ -492,7 +569,9 @@ class PUSHALOT(object):
|
||||
|
||||
http_handler.request("POST",
|
||||
"/api/sendmessage",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
headers={
|
||||
'Content-type':
|
||||
"application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
@@ -512,6 +591,49 @@ class PUSHALOT(object):
|
||||
return False
|
||||
|
||||
|
||||
class JOIN(object):
|
||||
def __init__(self):
|
||||
|
||||
self.enabled = headphones.CONFIG.JOIN_ENABLED
|
||||
self.apikey = headphones.CONFIG.JOIN_APIKEY
|
||||
self.deviceid = headphones.CONFIG.JOIN_DEVICEID
|
||||
self.url = 'https://joinjoaomgcd.appspot.com/_ah/' \
|
||||
'api/messaging/v1/sendPush?apikey={apikey}' \
|
||||
'&title={title}&text={text}' \
|
||||
'&icon={icon}'
|
||||
|
||||
def notify(self, message, event):
|
||||
if not headphones.CONFIG.JOIN_ENABLED or \
|
||||
not headphones.CONFIG.JOIN_APIKEY:
|
||||
return
|
||||
|
||||
icon = "https://cdn.rawgit.com/Headphones/" \
|
||||
"headphones/develop/data/images/headphoneslogo.png"
|
||||
|
||||
if not self.deviceid:
|
||||
self.deviceid = "group.all"
|
||||
l = [x.strip() for x in self.deviceid.split(',')]
|
||||
if len(l) > 1:
|
||||
self.url += '&deviceIds={deviceid}'
|
||||
else:
|
||||
self.url += '&deviceId={deviceid}'
|
||||
|
||||
response = urllib2.urlopen(self.url.format(apikey=self.apikey,
|
||||
title=quote_plus(event),
|
||||
text=quote_plus(
|
||||
message.encode(
|
||||
"utf-8")),
|
||||
icon=icon,
|
||||
deviceid=self.deviceid))
|
||||
|
||||
if response:
|
||||
logger.info(u"Join notifications sent.")
|
||||
return True
|
||||
else:
|
||||
logger.error(u"Join notification failed.")
|
||||
return False
|
||||
|
||||
|
||||
class Synoindex(object):
|
||||
def __init__(self, util_loc='/usr/syno/bin/synoindex'):
|
||||
self.util_loc = util_loc
|
||||
@@ -524,7 +646,8 @@ class Synoindex(object):
|
||||
|
||||
if not self.util_exists():
|
||||
logger.warn(
|
||||
"Error sending notification: synoindex utility not found at %s" % self.util_loc)
|
||||
"Error sending notification: synoindex utility "
|
||||
"not found at %s" % self.util_loc)
|
||||
return
|
||||
|
||||
if os.path.isfile(path):
|
||||
@@ -533,16 +656,19 @@ class Synoindex(object):
|
||||
cmd_arg = '-A'
|
||||
else:
|
||||
logger.warn(
|
||||
"Error sending notification: Path passed to synoindex was not a file or folder.")
|
||||
"Error sending notification: Path passed to synoindex "
|
||||
"was not a file or folder.")
|
||||
return
|
||||
|
||||
cmd = [self.util_loc, cmd_arg, path]
|
||||
logger.info("Calling synoindex command: %s" % str(cmd))
|
||||
try:
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=headphones.PROG_DIR)
|
||||
out, error = p.communicate()
|
||||
# synoindex never returns any codes other than '0', highly irritating
|
||||
# synoindex never returns any codes other than '0',
|
||||
# highly irritating
|
||||
except OSError, e:
|
||||
logger.warn("Error sending notification: %s" % str(e))
|
||||
|
||||
@@ -580,7 +706,8 @@ class PUSHOVER(object):
|
||||
|
||||
headers = {'Content-type': "application/x-www-form-urlencoded"}
|
||||
|
||||
response = request.request_response(url, method="POST", headers=headers, data=data)
|
||||
response = request.request_response(url, method="POST",
|
||||
headers=headers, data=data)
|
||||
|
||||
if response:
|
||||
logger.info(u"Pushover notifications sent.")
|
||||
@@ -614,20 +741,25 @@ class TwitterNotifier(object):
|
||||
def notify_snatch(self, title):
|
||||
if headphones.CONFIG.TWITTER_ONSNATCH:
|
||||
self._notifyTwitter(
|
||||
common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now())
|
||||
common.notifyStrings[
|
||||
common.NOTIFY_SNATCH] + ': ' + title + ' at ' +
|
||||
helpers.now())
|
||||
|
||||
def notify_download(self, title):
|
||||
if headphones.CONFIG.TWITTER_ENABLED:
|
||||
self._notifyTwitter(common.notifyStrings[
|
||||
common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now())
|
||||
common.NOTIFY_DOWNLOAD] + ': ' +
|
||||
title + ' at ' + helpers.now())
|
||||
|
||||
def test_notify(self):
|
||||
return self._notifyTwitter(
|
||||
"This is a test notification from Headphones at " + helpers.now(), force=True)
|
||||
"This is a test notification from Headphones at " + helpers.now(),
|
||||
force=True)
|
||||
|
||||
def _get_authorization(self):
|
||||
|
||||
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
|
||||
oauth_consumer = oauth.Consumer(key=self.consumer_key,
|
||||
secret=self.consumer_secret)
|
||||
oauth_client = oauth.Client(oauth_consumer)
|
||||
|
||||
logger.info('Requesting temp token from Twitter')
|
||||
@@ -635,32 +767,42 @@ class TwitterNotifier(object):
|
||||
resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET')
|
||||
|
||||
if resp['status'] != '200':
|
||||
logger.info('Invalid respond from Twitter requesting temp token: %s' % resp['status'])
|
||||
logger.info(
|
||||
'Invalid respond from Twitter requesting temp token: %s' %
|
||||
resp['status'])
|
||||
else:
|
||||
request_token = dict(parse_qsl(content))
|
||||
|
||||
headphones.CONFIG.TWITTER_USERNAME = request_token['oauth_token']
|
||||
headphones.CONFIG.TWITTER_PASSWORD = request_token['oauth_token_secret']
|
||||
headphones.CONFIG.TWITTER_PASSWORD = request_token[
|
||||
'oauth_token_secret']
|
||||
|
||||
return self.AUTHORIZATION_URL + "?oauth_token=" + request_token['oauth_token']
|
||||
return self.AUTHORIZATION_URL + "?oauth_token=" + request_token[
|
||||
'oauth_token']
|
||||
|
||||
def _get_credentials(self, key):
|
||||
request_token = {}
|
||||
|
||||
request_token['oauth_token'] = headphones.CONFIG.TWITTER_USERNAME
|
||||
request_token['oauth_token_secret'] = headphones.CONFIG.TWITTER_PASSWORD
|
||||
request_token[
|
||||
'oauth_token_secret'] = headphones.CONFIG.TWITTER_PASSWORD
|
||||
request_token['oauth_callback_confirmed'] = 'true'
|
||||
|
||||
token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret'])
|
||||
token = oauth.Token(request_token['oauth_token'],
|
||||
request_token['oauth_token_secret'])
|
||||
token.set_verifier(key)
|
||||
|
||||
logger.info('Generating and signing request for an access token using key ' + key)
|
||||
logger.info(
|
||||
'Generating and signing request for an access token using key ' +
|
||||
key)
|
||||
|
||||
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
|
||||
oauth_consumer = oauth.Consumer(key=self.consumer_key,
|
||||
secret=self.consumer_secret)
|
||||
logger.info('oauth_consumer: ' + str(oauth_consumer))
|
||||
oauth_client = oauth.Client(oauth_consumer, token)
|
||||
logger.info('oauth_client: ' + str(oauth_client))
|
||||
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST',
|
||||
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL,
|
||||
method='POST',
|
||||
body='oauth_verifier=%s' % key)
|
||||
logger.info('resp, content: ' + str(resp) + ',' + str(content))
|
||||
|
||||
@@ -669,14 +811,18 @@ class TwitterNotifier(object):
|
||||
|
||||
logger.info('resp[status] = ' + str(resp['status']))
|
||||
if resp['status'] != '200':
|
||||
logger.info('The request for a token with did not succeed: ' + str(resp['status']),
|
||||
logger.info('The request for a token with did not succeed: ' + str(
|
||||
resp['status']),
|
||||
logger.ERROR)
|
||||
return False
|
||||
else:
|
||||
logger.info('Your Twitter Access Token key: %s' % access_token['oauth_token'])
|
||||
logger.info('Access Token secret: %s' % access_token['oauth_token_secret'])
|
||||
logger.info('Your Twitter Access Token key: %s' % access_token[
|
||||
'oauth_token'])
|
||||
logger.info(
|
||||
'Access Token secret: %s' % access_token['oauth_token_secret'])
|
||||
headphones.CONFIG.TWITTER_USERNAME = access_token['oauth_token']
|
||||
headphones.CONFIG.TWITTER_PASSWORD = access_token['oauth_token_secret']
|
||||
headphones.CONFIG.TWITTER_PASSWORD = access_token[
|
||||
'oauth_token_secret']
|
||||
return True
|
||||
|
||||
def _send_tweet(self, message=None):
|
||||
@@ -688,7 +834,8 @@ class TwitterNotifier(object):
|
||||
|
||||
logger.info(u"Sending tweet: " + message)
|
||||
|
||||
api = twitter.Api(username, password, access_token_key, access_token_secret)
|
||||
api = twitter.Api(username, password, access_token_key,
|
||||
access_token_secret)
|
||||
|
||||
try:
|
||||
api.PostUpdate(message)
|
||||
@@ -714,7 +861,7 @@ class OSX_NOTIFY(object):
|
||||
self.AppKit = __import__("AppKit")
|
||||
except:
|
||||
logger.warn('OS X Notification: Cannot import objc or AppKit')
|
||||
return False
|
||||
pass
|
||||
|
||||
def swizzle(self, cls, SEL, func):
|
||||
old_IMP = getattr(cls, SEL, None)
|
||||
@@ -741,7 +888,8 @@ class OSX_NOTIFY(object):
|
||||
)
|
||||
|
||||
NSUserNotification = self.objc.lookUpClass('NSUserNotification')
|
||||
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter')
|
||||
NSUserNotificationCenter = self.objc.lookUpClass(
|
||||
'NSUserNotificationCenter')
|
||||
NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')
|
||||
|
||||
if not NSUserNotification or not NSUserNotificationCenter:
|
||||
@@ -756,14 +904,17 @@ class OSX_NOTIFY(object):
|
||||
if text:
|
||||
notification.setInformativeText_(text)
|
||||
if sound:
|
||||
notification.setSoundName_("NSUserNotificationDefaultSoundName")
|
||||
notification.setSoundName_(
|
||||
"NSUserNotificationDefaultSoundName")
|
||||
if image:
|
||||
source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image)
|
||||
source_img = self.AppKit.NSImage.alloc().\
|
||||
initByReferencingFile_(image)
|
||||
notification.setContentImage_(source_img)
|
||||
# notification.set_identityImage_(source_img)
|
||||
notification.setHasActionButton_(False)
|
||||
|
||||
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
||||
notification_center = NSUserNotificationCenter.\
|
||||
defaultUserNotificationCenter()
|
||||
notification_center.deliverNotification_(notification)
|
||||
|
||||
del pool
|
||||
@@ -784,13 +935,16 @@ class BOXCAR(object):
|
||||
def notify(self, title, message, rgid=None):
|
||||
try:
|
||||
if rgid:
|
||||
message += '<br></br><a href="http://musicbrainz.org/release-group/%s">MusicBrainz</a>' % rgid
|
||||
message += '<br></br><a href="http://musicbrainz.org/' \
|
||||
'release-group/%s">MusicBrainz</a>' % rgid
|
||||
|
||||
data = urllib.urlencode({
|
||||
'user_credentials': headphones.CONFIG.BOXCAR_TOKEN,
|
||||
'notification[title]': title.encode('utf-8'),
|
||||
'notification[long_message]': message.encode('utf-8'),
|
||||
'notification[sound]': "done"
|
||||
'notification[sound]': "done",
|
||||
'notification[icon_url]': "https://raw.githubusercontent.com/rembo10/headphones/master/data/images"
|
||||
"/headphoneslogo.png"
|
||||
})
|
||||
|
||||
req = urllib2.Request(self.url)
|
||||
@@ -818,8 +972,9 @@ class SubSonicNotifier(object):
|
||||
self.host = self.host + "/"
|
||||
|
||||
# Invoke request
|
||||
request.request_response(self.host + "musicFolderSettings.view?scanNow",
|
||||
auth=(self.username, self.password))
|
||||
request.request_response(
|
||||
self.host + "musicFolderSettings.view?scanNow",
|
||||
auth=(self.username, self.password))
|
||||
|
||||
|
||||
class Email(object):
|
||||
@@ -827,13 +982,16 @@ class Email(object):
|
||||
|
||||
message = MIMEText(message, 'plain', "utf-8")
|
||||
message['Subject'] = subject
|
||||
message['From'] = email.utils.formataddr(('Headphones', headphones.CONFIG.EMAIL_FROM))
|
||||
message['From'] = email.utils.formataddr(
|
||||
('Headphones', headphones.CONFIG.EMAIL_FROM))
|
||||
message['To'] = headphones.CONFIG.EMAIL_TO
|
||||
message['Date'] = email.utils.formatdate(localtime=True)
|
||||
|
||||
try:
|
||||
if headphones.CONFIG.EMAIL_SSL:
|
||||
mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
mailserver = smtplib.SMTP_SSL(
|
||||
headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
else:
|
||||
mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
@@ -847,7 +1005,8 @@ class Email(object):
|
||||
mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PASSWORD)
|
||||
|
||||
mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, headphones.CONFIG.EMAIL_TO,
|
||||
mailserver.sendmail(headphones.CONFIG.EMAIL_FROM,
|
||||
headphones.CONFIG.EMAIL_TO,
|
||||
message.as_string())
|
||||
mailserver.quit()
|
||||
return True
|
||||
@@ -858,8 +1017,7 @@ class Email(object):
|
||||
|
||||
|
||||
class TELEGRAM(object):
|
||||
|
||||
def notify(self, message, status):
|
||||
def notify(self, message, status, rgid=None, image=None):
|
||||
if not headphones.CONFIG.TELEGRAM_ENABLED:
|
||||
return
|
||||
|
||||
@@ -872,19 +1030,65 @@ class TELEGRAM(object):
|
||||
userid = headphones.CONFIG.TELEGRAM_USERID
|
||||
|
||||
# Construct message
|
||||
payload = {'chat_id': userid, 'text': status + ': ' + message}
|
||||
message = '\n\n' + message
|
||||
|
||||
# Send message to user using Telegram's Bot API
|
||||
try:
|
||||
response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload)
|
||||
except Exception, e:
|
||||
logger.info(u'Telegram notify failed: ' + str(e))
|
||||
# MusicBrainz link
|
||||
if rgid:
|
||||
message += '\n\n <a href="http://musicbrainz.org/' \
|
||||
'release-group/%s">MusicBrainz</a>' % rgid
|
||||
|
||||
# Send image
|
||||
response = None
|
||||
if image:
|
||||
image_file = {'photo': (image, open(image, "rb"))}
|
||||
payload = {'chat_id': userid, 'parse_mode': "HTML", 'caption': status + message}
|
||||
try:
|
||||
response = requests.post(TELEGRAM_API % (token, "sendPhoto"), data=payload, files=image_file)
|
||||
except Exception, e:
|
||||
logger.info(u'Telegram notify failed: ' + str(e))
|
||||
# Sent text
|
||||
else:
|
||||
payload = {'chat_id': userid, 'parse_mode': "HTML", 'text': status + message}
|
||||
try:
|
||||
response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload)
|
||||
except Exception, e:
|
||||
logger.info(u'Telegram notify failed: ' + str(e))
|
||||
|
||||
# Error logging
|
||||
sent_successfuly = True
|
||||
if not response.status_code == 200:
|
||||
logger.info(u'Could not send notification to TelegramBot (token=%s). Response: [%s]', (token, response.text))
|
||||
if response and not response.status_code == 200:
|
||||
logger.info("Could not send notification to TelegramBot (token=%s). Response: [%s]", token, response.text)
|
||||
sent_successfuly = False
|
||||
|
||||
logger.info(u"Telegram notifications sent.")
|
||||
return sent_successfuly
|
||||
|
||||
|
||||
class SLACK(object):
|
||||
def notify(self, message, status):
|
||||
if not headphones.CONFIG.SLACK_ENABLED:
|
||||
return
|
||||
|
||||
import requests
|
||||
|
||||
SLACK_URL = headphones.CONFIG.SLACK_URL
|
||||
channel = headphones.CONFIG.SLACK_CHANNEL
|
||||
emoji = headphones.CONFIG.SLACK_EMOJI
|
||||
|
||||
payload = {'channel': channel, 'text': status + ': ' + message,
|
||||
'icon_emoji': emoji}
|
||||
|
||||
try:
|
||||
response = requests.post(SLACK_URL, json=payload)
|
||||
except Exception, e:
|
||||
logger.info(u'Slack notify failed: ' + str(e))
|
||||
|
||||
sent_successfuly = True
|
||||
if not response.status_code == 200:
|
||||
logger.info(
|
||||
u'Could not send notification to Slack. Response: [%s]',
|
||||
(response.text))
|
||||
sent_successfuly = False
|
||||
|
||||
logger.info(u"Slack notifications sent.")
|
||||
return sent_successfuly
|
||||
|
||||
@@ -17,7 +17,6 @@ import shutil
|
||||
import uuid
|
||||
import threading
|
||||
import itertools
|
||||
import tempfile
|
||||
|
||||
import os
|
||||
import re
|
||||
@@ -25,11 +24,12 @@ import beets
|
||||
import headphones
|
||||
from beets import autotag
|
||||
from beets import config as beetsconfig
|
||||
from beets import logging as beetslogging
|
||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||
from beetsplug import lyrics as beetslyrics
|
||||
from headphones import notifiers, utorrent, transmission, deluge
|
||||
from headphones import notifiers, utorrent, transmission, deluge, qbittorrent
|
||||
from headphones import db, albumart, librarysync
|
||||
from headphones import logger, helpers, request, mb, music_encoder
|
||||
from headphones import logger, helpers, mb, music_encoder
|
||||
from headphones import metadata
|
||||
|
||||
postprocessor_lock = threading.Lock()
|
||||
@@ -44,6 +44,8 @@ def checkFolder():
|
||||
|
||||
for album in snatched:
|
||||
if album['FolderName']:
|
||||
folder_name = album['FolderName']
|
||||
single = False
|
||||
if album['Kind'] == 'nzb':
|
||||
download_dir = headphones.CONFIG.DOWNLOAD_DIR
|
||||
else:
|
||||
@@ -52,21 +54,32 @@ def checkFolder():
|
||||
else:
|
||||
download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
|
||||
|
||||
album_path = os.path.join(download_dir, album['FolderName']).encode(
|
||||
headphones.SYS_ENCODING, 'replace')
|
||||
logger.debug("Checking if %s exists" % album_path)
|
||||
# Get folder from torrent hash
|
||||
if album['TorrentHash'] and headphones.CONFIG.TORRENT_DOWNLOADER:
|
||||
torrent_folder_name = None
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
|
||||
torrent_folder_name, single = transmission.getFolder(album['TorrentHash'])
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 4:
|
||||
torrent_folder_name, single = qbittorrent.getFolder(album['TorrentHash'])
|
||||
if torrent_folder_name:
|
||||
folder_name = torrent_folder_name
|
||||
|
||||
if os.path.exists(album_path):
|
||||
logger.info('Found "' + album['FolderName'] + '" in ' + album[
|
||||
'Kind'] + ' download folder. Verifying....')
|
||||
verify(album['AlbumID'], album_path, album['Kind'])
|
||||
if folder_name:
|
||||
album_path = os.path.join(download_dir, folder_name).encode(
|
||||
headphones.SYS_ENCODING, 'replace')
|
||||
logger.debug("Checking if %s exists" % album_path)
|
||||
|
||||
if os.path.exists(album_path):
|
||||
logger.info('Found "' + folder_name + '" in ' + album[
|
||||
'Kind'] + ' download folder. Verifying....')
|
||||
verify(album['AlbumID'], album_path, album['Kind'], single=single)
|
||||
else:
|
||||
logger.info("No folder name found for " + album['Title'])
|
||||
|
||||
logger.debug("Checking download folder finished.")
|
||||
|
||||
|
||||
def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=False):
|
||||
def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=False, single=False):
|
||||
myDB = db.DBConnection()
|
||||
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
|
||||
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
|
||||
@@ -170,13 +183,17 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"AlbumID": albumid}
|
||||
|
||||
clean_name = helpers.clean_name(
|
||||
release_dict['artist_name'] + ' ' + release_dict['title'] + ' ' + track['title'])
|
||||
|
||||
newValueDict = {"ArtistID": release_dict['artist_id'],
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
"AlbumTitle": release_dict['title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
"TrackTitle": track['title'],
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number']
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": clean_name
|
||||
}
|
||||
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
@@ -207,13 +224,38 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
'replace') + " isn't complete yet. Will try again on the next run")
|
||||
return
|
||||
|
||||
# Split cue
|
||||
# Force single file through
|
||||
if single and not downloaded_track_list:
|
||||
downloaded_track_list.append(albumpath)
|
||||
|
||||
# Check to see if we're preserving the torrent dir
|
||||
if (headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent") or headphones.CONFIG.KEEP_ORIGINAL_FOLDER:
|
||||
keep_original_folder = True
|
||||
|
||||
# Split cue before metadata check
|
||||
if headphones.CONFIG.CUE_SPLIT and downloaded_cuecount and downloaded_cuecount >= len(
|
||||
downloaded_track_list):
|
||||
if headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent":
|
||||
albumpath = helpers.preserve_torrent_directory(albumpath)
|
||||
if albumpath and helpers.cue_split(albumpath):
|
||||
new_folder = None
|
||||
new_albumpath = albumpath
|
||||
if keep_original_folder:
|
||||
temp_path = helpers.preserve_torrent_directory(new_albumpath, forced)
|
||||
if not temp_path:
|
||||
markAsUnprocessed(albumid, new_albumpath, keep_original_folder)
|
||||
return
|
||||
else:
|
||||
new_albumpath = temp_path
|
||||
new_folder = os.path.split(new_albumpath)[0]
|
||||
Kind = "cue_split"
|
||||
cuepath = helpers.cue_split(new_albumpath)
|
||||
if not cuepath:
|
||||
if new_folder:
|
||||
shutil.rmtree(new_folder)
|
||||
markAsUnprocessed(albumid, albumpath, keep_original_folder)
|
||||
return
|
||||
else:
|
||||
albumpath = cuepath
|
||||
downloaded_track_list = helpers.get_downloaded_track_list(albumpath)
|
||||
keep_original_folder = False
|
||||
|
||||
# test #1: metadata - usually works
|
||||
logger.debug('Verifying metadata...')
|
||||
@@ -243,7 +285,7 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
|
||||
if metaartist == dbartist and metaalbum == dbalbum:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||
keep_original_folder)
|
||||
keep_original_folder, forced, single)
|
||||
return
|
||||
|
||||
# test #2: filenames
|
||||
@@ -262,7 +304,7 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
|
||||
if dbtrack in filetrack:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||
keep_original_folder)
|
||||
keep_original_folder, forced, single)
|
||||
return
|
||||
|
||||
# test #3: number of songs and duration
|
||||
@@ -295,46 +337,49 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
delta = abs(downloaded_track_duration - db_track_duration)
|
||||
if delta < 240:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||
keep_original_folder)
|
||||
keep_original_folder, forced, single)
|
||||
return
|
||||
|
||||
logger.warn(u'Could not identify album: %s. It may not be the intended album.',
|
||||
albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
markAsUnprocessed(albumid, albumpath, keep_original_folder)
|
||||
|
||||
|
||||
def markAsUnprocessed(albumid, albumpath, keep_original_folder=False):
|
||||
myDB = db.DBConnection()
|
||||
myDB.action(
|
||||
'UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?',
|
||||
[albumid])
|
||||
'UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
|
||||
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
|
||||
if not processed:
|
||||
if headphones.CONFIG.RENAME_UNPROCESSED:
|
||||
if headphones.CONFIG.RENAME_UNPROCESSED and not keep_original_folder:
|
||||
renameUnprocessedFolder(albumpath, tag="Unprocessed")
|
||||
else:
|
||||
logger.warn(u"Won't rename %s to mark as 'Unprocessed', because it is disabled.",
|
||||
logger.warn(u"Won't rename %s to mark as 'Unprocessed', because it is disabled or folder is being kept.",
|
||||
albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
|
||||
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None,
|
||||
keep_original_folder=False):
|
||||
keep_original_folder=False, forced=False, single=False):
|
||||
logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle']))
|
||||
new_folder = None
|
||||
# Check to see if we're preserving the torrent dir
|
||||
if (headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent" and 'headphones-modified' not in albumpath) or headphones.CONFIG.KEEP_ORIGINAL_FOLDER or keep_original_folder:
|
||||
new_folder = tempfile.mkdtemp(prefix="headphones_")
|
||||
subdir = os.path.join(new_folder, "headphones")
|
||||
logger.info("Copying files to " + subdir.decode(headphones.SYS_ENCODING, 'replace') + " subfolder to preserve downloaded files for seeding")
|
||||
try:
|
||||
shutil.copytree(albumpath, subdir)
|
||||
# Update the album path with the new location
|
||||
albumpath = subdir
|
||||
except Exception as e:
|
||||
logger.warn("Cannot copy/move files to temp folder: " + new_folder.decode(headphones.SYS_ENCODING, 'replace') + ". Not continuing. Error: " + str(e))
|
||||
shutil.rmtree(new_folder)
|
||||
|
||||
# Preserve the torrent dir
|
||||
if keep_original_folder or single:
|
||||
temp_path = helpers.preserve_torrent_directory(albumpath, forced, single)
|
||||
if not temp_path:
|
||||
markAsUnprocessed(albumid, albumpath, keep_original_folder)
|
||||
return
|
||||
else:
|
||||
albumpath = temp_path
|
||||
new_folder = os.path.split(albumpath)[0]
|
||||
elif Kind == "cue_split":
|
||||
new_folder = os.path.split(albumpath)[0]
|
||||
|
||||
# Need to update the downloaded track list with the new location.
|
||||
# Could probably just throw in the "headphones-modified" folder,
|
||||
# but this is good to make sure we're not counting files that may have failed to move
|
||||
# Need to update the downloaded track list with the new location.
|
||||
# Could probably just throw in the "headphones-modified" folder,
|
||||
# but this is good to make sure we're not counting files that may have failed to move
|
||||
if new_folder:
|
||||
downloaded_track_list = []
|
||||
|
||||
for r, d, f in os.walk(albumpath):
|
||||
for files in f:
|
||||
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
|
||||
@@ -352,7 +397,8 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, "replace"))
|
||||
return
|
||||
except IOError:
|
||||
logger.error("Unable to find media file: %s. Not continuing.")
|
||||
logger.error("Unable to find media file: %s. Not continuing.", downloaded_track.decode(
|
||||
headphones.SYS_ENCODING, "replace"))
|
||||
if new_folder:
|
||||
shutil.rmtree(new_folder)
|
||||
return
|
||||
@@ -387,21 +433,13 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
shutil.rmtree(new_folder)
|
||||
return
|
||||
|
||||
# get artwork and path
|
||||
album_art_path = None
|
||||
artwork = None
|
||||
album_art_path = albumart.getAlbumArt(albumid)
|
||||
if headphones.CONFIG.EMBED_ALBUM_ART or headphones.CONFIG.ADD_ALBUM_ART:
|
||||
|
||||
if album_art_path:
|
||||
artwork = request.request_content(album_art_path)
|
||||
else:
|
||||
artwork = None
|
||||
|
||||
if not album_art_path or not artwork or len(artwork) < 100:
|
||||
logger.info("No suitable album art found from Amazon. Checking Last.FM....")
|
||||
artwork = albumart.getCachedArt(albumid)
|
||||
if not artwork or len(artwork) < 100:
|
||||
artwork = False
|
||||
logger.info("No suitable album art found from Last.FM. Not adding album art")
|
||||
if headphones.CONFIG.EMBED_ALBUM_ART or headphones.CONFIG.ADD_ALBUM_ART or \
|
||||
(headphones.CONFIG.PLEX_ENABLED and headphones.CONFIG.PLEX_NOTIFY) or \
|
||||
(headphones.CONFIG.XBMC_ENABLED and headphones.CONFIG.XBMC_NOTIFY):
|
||||
album_art_path, artwork = albumart.getAlbumArt(albumid)
|
||||
|
||||
if headphones.CONFIG.EMBED_ALBUM_ART and artwork:
|
||||
embedAlbumArt(artwork, downloaded_track_list)
|
||||
@@ -446,12 +484,12 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
[albumid])
|
||||
|
||||
# Check if torrent has finished seeding
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1 or headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER != 0:
|
||||
seed_snatched = myDB.action(
|
||||
'SELECT * from snatched WHERE Status="Seed_Snatched" and AlbumID=?',
|
||||
[albumid]).fetchone()
|
||||
if seed_snatched:
|
||||
hash = seed_snatched['FolderName']
|
||||
hash = seed_snatched['TorrentHash']
|
||||
torrent_removed = False
|
||||
logger.info(u'%s - %s. Checking if torrent has finished seeding and can be removed' % (
|
||||
release['ArtistName'], release['AlbumTitle']))
|
||||
@@ -459,8 +497,10 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
torrent_removed = transmission.removeTorrent(hash, True)
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
|
||||
torrent_removed = deluge.removeTorrent(hash, True)
|
||||
else:
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
torrent_removed = utorrent.removeTorrent(hash, True)
|
||||
else:
|
||||
torrent_removed = qbittorrent.removeTorrent(hash, True)
|
||||
|
||||
# Torrent removed, delete the snatched record, else update Status for scheduled job to check
|
||||
if torrent_removed:
|
||||
@@ -538,10 +578,15 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
pushbullet = notifiers.PUSHBULLET()
|
||||
pushbullet.notify(pushmessage, statusmessage)
|
||||
|
||||
if headphones.CONFIG.JOIN_ENABLED:
|
||||
logger.info(u"Join request")
|
||||
join = notifiers.JOIN()
|
||||
join.notify(pushmessage, statusmessage)
|
||||
|
||||
if headphones.CONFIG.TELEGRAM_ENABLED:
|
||||
logger.info(u"Telegram request")
|
||||
telegram = notifiers.TELEGRAM()
|
||||
telegram.notify(pushmessage, statusmessage)
|
||||
telegram.notify(statusmessage, pushmessage)
|
||||
|
||||
if headphones.CONFIG.TWITTER_ENABLED:
|
||||
logger.info(u"Sending Twitter notification")
|
||||
@@ -615,7 +660,7 @@ def addAlbumArt(artwork, albumpath, release, metadata_dict):
|
||||
if artwork[:4] == '\x89PNG':
|
||||
ext = ".png"
|
||||
|
||||
album_art_name = helpers.replace_all(
|
||||
album_art_name = helpers.pattern_substitute(
|
||||
headphones.CONFIG.ALBUM_ART_FORMAT.strip(), md) + ext
|
||||
|
||||
album_art_name = helpers.replace_illegal_chars(album_art_name).encode(
|
||||
@@ -671,7 +716,7 @@ def moveFiles(albumpath, release, metadata_dict):
|
||||
logger.info("Moving files: %s" % albumpath)
|
||||
|
||||
md = metadata.album_metadata(albumpath, release, metadata_dict)
|
||||
folder = helpers.replace_all(
|
||||
folder = helpers.pattern_substitute(
|
||||
headphones.CONFIG.FOLDER_FORMAT.strip(), md, normalize=True)
|
||||
|
||||
if headphones.CONFIG.FILE_UNDERSCORES:
|
||||
@@ -918,12 +963,29 @@ def correctMetadata(albumid, release, downloaded_track_list):
|
||||
if not items:
|
||||
continue
|
||||
|
||||
search_ids = []
|
||||
logger.debug('Getting recommendation from beets. Artist: %s. Album: %s. Tracks: %s', release['ArtistName'],
|
||||
release['AlbumTitle'], len(items))
|
||||
|
||||
# Try with specific release, e.g. alternate release selected from albumPage
|
||||
if release['ReleaseID'] != release['AlbumID']:
|
||||
logger.debug('trying beets with specific Release ID: %s', release['ReleaseID'])
|
||||
search_ids = [release['ReleaseID']]
|
||||
|
||||
try:
|
||||
cur_artist, cur_album, candidates, rec = autotag.tag_album(items,
|
||||
search_artist=helpers.latinToAscii(
|
||||
release['ArtistName']),
|
||||
search_album=helpers.latinToAscii(
|
||||
release['AlbumTitle']))
|
||||
beetslog = beetslogging.getLogger('beets')
|
||||
beetslog.set_global_level(beetslogging.DEBUG) if headphones.VERBOSE else beetslog.set_global_level(
|
||||
beetslogging.CRITICAL)
|
||||
with helpers.capture_beets_log() as logs:
|
||||
cur_artist, cur_album, prop = autotag.tag_album(items,
|
||||
search_artist=release['ArtistName'],
|
||||
search_album=release['AlbumTitle'],
|
||||
search_ids=search_ids)
|
||||
candidates = prop.candidates
|
||||
rec = prop.recommendation
|
||||
for log in logs:
|
||||
logger.debug('Beets: %s', log)
|
||||
beetslog.set_global_level(beetslogging.NOTSET)
|
||||
except Exception as e:
|
||||
logger.error('Error getting recommendation: %s. Not writing metadata', e)
|
||||
return False
|
||||
@@ -1033,7 +1095,7 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
title = md[metadata.Vars.TITLE]
|
||||
new_file_name = helpers.cleanTitle(title) + ext
|
||||
else:
|
||||
new_file_name = helpers.replace_all(
|
||||
new_file_name = helpers.pattern_substitute(
|
||||
headphones.CONFIG.FILE_FORMAT.strip(), md
|
||||
).replace('/', '_') + ext
|
||||
|
||||
@@ -1186,7 +1248,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
'Found a match in the database: %s. Verifying to make sure it is the correct album',
|
||||
snatched['Title'])
|
||||
verify(snatched['AlbumID'], folder, snatched['Kind'],
|
||||
keep_original_folder=keep_original_folder)
|
||||
forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
|
||||
# Attempt 2: strip release group id from filename
|
||||
@@ -1213,7 +1275,8 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
else:
|
||||
logger.info(
|
||||
'Found a (possibly) valid Musicbrainz release group id in album folder name.')
|
||||
verify(rgid, folder, forced=True)
|
||||
verify(rgid, folder, forced=True,
|
||||
keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
|
||||
# Attempt 3a: parse the folder name into a valid format
|
||||
@@ -1232,7 +1295,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
logger.info(
|
||||
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
||||
release['ArtistName'], release['AlbumTitle'])
|
||||
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
|
||||
verify(release['AlbumID'], folder, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name,
|
||||
@@ -1244,7 +1307,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
rgid = None
|
||||
|
||||
if rgid:
|
||||
verify(rgid, folder, keep_original_folder=keep_original_folder)
|
||||
verify(rgid, folder, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
||||
@@ -1257,12 +1320,23 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
except Exception:
|
||||
name = album = None
|
||||
|
||||
# Check if there's a cue to split
|
||||
if headphones.CONFIG.CUE_SPLIT and not name and not album and helpers.cue_split(folder):
|
||||
try:
|
||||
name, album, year = helpers.extract_metadata(folder)
|
||||
except Exception:
|
||||
name = album = None
|
||||
# Not found from meta data, check if there's a cue to split and try meta data again
|
||||
kind = None
|
||||
if headphones.CONFIG.CUE_SPLIT and not name and not album:
|
||||
cue_folder = helpers.cue_split(folder, keep_original_folder=keep_original_folder)
|
||||
if cue_folder:
|
||||
try:
|
||||
name, album, year = helpers.extract_metadata(cue_folder)
|
||||
except Exception:
|
||||
name = album = None
|
||||
if name:
|
||||
folder = cue_folder
|
||||
if keep_original_folder:
|
||||
keep_original_folder = False
|
||||
kind = "cue_split"
|
||||
elif folder != cue_folder:
|
||||
cue_folder = os.path.split(cue_folder)[0]
|
||||
shutil.rmtree(cue_folder)
|
||||
|
||||
if name and album:
|
||||
release = myDB.action(
|
||||
@@ -1272,7 +1346,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
logger.info(
|
||||
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
||||
release['ArtistName'], release['AlbumTitle'])
|
||||
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
|
||||
verify(release['AlbumID'], folder, Kind=kind, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name,
|
||||
@@ -1284,7 +1358,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
rgid = None
|
||||
|
||||
if rgid:
|
||||
verify(rgid, folder, keep_original_folder=keep_original_folder)
|
||||
verify(rgid, folder, Kind=kind, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
||||
@@ -1301,7 +1375,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
logger.info(
|
||||
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
||||
release['ArtistName'], release['AlbumTitle'])
|
||||
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
|
||||
verify(release['AlbumID'], folder, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s',
|
||||
@@ -1313,7 +1387,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
rgid = None
|
||||
|
||||
if rgid:
|
||||
verify(rgid, folder, keep_original_folder=keep_original_folder)
|
||||
verify(rgid, folder, forced=True, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
# 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
|
||||
|
||||
from qbittorrentv2 import Client
|
||||
|
||||
|
||||
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
|
||||
|
||||
# Try new v2 api
|
||||
try:
|
||||
self.qb = Client(self.base_url)
|
||||
login_text = self.qb.login(self.username, self.password)
|
||||
if login_text:
|
||||
logger.warning("Could not login to qBittorrent v2 api, check credentials: %s", login_text)
|
||||
self.version = 2
|
||||
except Exception as e:
|
||||
logger.warning("Error with qBittorrent v2 api, check settings or update, will try v1: %s" % e)
|
||||
self.cookiejar = cookielib.CookieJar()
|
||||
self.opener = self._make_opener()
|
||||
self._get_sid(self.base_url, self.username, self.password)
|
||||
self.version = 1
|
||||
|
||||
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()
|
||||
if qbclient.version == 2:
|
||||
torrentlist = qbclient.qb.torrents(hashes=hash.lower())
|
||||
else:
|
||||
status, torrentlist = qbclient._get_list()
|
||||
for torrent in torrentlist:
|
||||
if torrent['hash'].lower() == hash.lower():
|
||||
if torrent['ratio'] >= torrent['ratio_limit'] and torrent['ratio_limit'] >= 0:
|
||||
if qbclient.version == 2:
|
||||
if remove_data:
|
||||
logger.info(
|
||||
'%s has finished seeding, removing torrent and data. '
|
||||
'Ratio: %s, Ratio Limit: %s' % (torrent['name'], torrent['ratio'], torrent['ratio_limit']))
|
||||
qbclient.qb.delete_permanently(hash)
|
||||
else:
|
||||
logger.info('%s has finished seeding, removing torrent' % torrent['name'])
|
||||
qbclient.qb.delete(hash)
|
||||
else:
|
||||
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. '
|
||||
'Ratio: %s, Ratio Limit: %s' % (torrent['name'], torrent['ratio'], torrent['ratio_limit']))
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def addTorrent(link):
|
||||
logger.debug('addTorrent(%s)' % link)
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
if qbclient.version == 2:
|
||||
return qbclient.qb.download_from_link(link, savepath=headphones.CONFIG.DOWNLOAD_TORRENT_DIR,
|
||||
category=headphones.CONFIG.QBITTORRENT_LABEL)
|
||||
else:
|
||||
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()
|
||||
if qbclient.version == 2:
|
||||
return qbclient.qb.download_from_file(data, savepath=headphones.CONFIG.DOWNLOAD_TORRENT_DIR,
|
||||
category=headphones.CONFIG.QBITTORRENT_LABEL)
|
||||
else:
|
||||
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)
|
||||
if qbclient.version == 2:
|
||||
torrentlist = qbclient.qb.torrents(hashes=hash.lower())
|
||||
else:
|
||||
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:
|
||||
if qbclient.version == 2:
|
||||
torrent_files = qbclient.qb.get_torrent_files(hash.lower())
|
||||
else:
|
||||
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]
|
||||
torrent_folder = torrent_folder.split(os.sep)[0]
|
||||
single_file = False
|
||||
except:
|
||||
torrent_folder = None
|
||||
single_file = False
|
||||
|
||||
return torrent_folder, single_file
|
||||
|
||||
|
||||
def setSeedRatio(hash, ratio):
|
||||
logger.debug('setSeedRatio(%s)' % hash)
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
|
||||
if qbclient.version == 2:
|
||||
ratio = -1 if ratio == 0 else ratio
|
||||
return qbclient.qb.set_share_ratio(hash.lower(), ratio)
|
||||
else:
|
||||
logger.warn('setSeedRatio only available with qBittorrent v2 api')
|
||||
return
|
||||
|
||||
|
||||
def apiVersion2():
|
||||
logger.debug('getApiVersion')
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
|
||||
if qbclient.version == 2:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
_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)
|
||||
@@ -93,7 +93,7 @@ def request_response(url, method="get", auto_raise=True,
|
||||
if kwargs["verify"]:
|
||||
logger.error(
|
||||
"Unable to connect to remote host because of a SSL error. "
|
||||
"It is likely that your system cannot verify the validity"
|
||||
"It is likely that your system cannot verify the validity "
|
||||
"of the certificate. The remote certificate is either "
|
||||
"self-signed, or the remote server uses SNI. See the wiki for "
|
||||
"more information on this topic.")
|
||||
|
||||
@@ -6,6 +6,7 @@ from urlparse import urlparse
|
||||
import re
|
||||
|
||||
import requests as requests
|
||||
# from requests.auth import HTTPDigestAuth
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
import headphones
|
||||
@@ -24,7 +25,7 @@ class Rutracker(object):
|
||||
return self.loggedin
|
||||
|
||||
def still_logged_in(self, html):
|
||||
if not html or "action=\"http://login.rutracker.org/forum/login.php\">" in html:
|
||||
if not html or "action=\"http://rutracker.org/forum/login.php\">" in html:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@@ -34,7 +35,7 @@ class Rutracker(object):
|
||||
Logs in user
|
||||
"""
|
||||
|
||||
loginpage = 'http://login.rutracker.org/forum/login.php'
|
||||
loginpage = 'http://rutracker.org/forum/login.php'
|
||||
post_params = {
|
||||
'login_username': headphones.CONFIG.RUTRACKER_USER,
|
||||
'login_password': headphones.CONFIG.RUTRACKER_PASSWORD,
|
||||
@@ -44,28 +45,34 @@ class Rutracker(object):
|
||||
logger.info("Attempting to log in to rutracker...")
|
||||
|
||||
try:
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout)
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
|
||||
# try again
|
||||
if 'bb_data' not in r.cookies.keys():
|
||||
if not self.has_bb_session_cookie(r):
|
||||
time.sleep(10)
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout)
|
||||
if r.status_code != 200:
|
||||
logger.error("rutracker login returned status code %s" % r.status_code)
|
||||
self.loggedin = False
|
||||
else:
|
||||
if 'bb_data' in r.cookies.keys():
|
||||
self.loggedin = True
|
||||
logger.info("Successfully logged in to rutracker")
|
||||
if headphones.CONFIG.RUTRACKER_COOKIE:
|
||||
logger.info("Attempting to log in using predefined cookie...")
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False, cookies={'bb_session': headphones.CONFIG.RUTRACKER_COOKIE})
|
||||
else:
|
||||
logger.error(
|
||||
"Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later")
|
||||
self.loggedin = False
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
|
||||
if self.has_bb_session_cookie(r):
|
||||
self.loggedin = True
|
||||
logger.info("Successfully logged in to rutracker")
|
||||
else:
|
||||
logger.error(
|
||||
"Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later")
|
||||
self.loggedin = False
|
||||
return self.loggedin
|
||||
except Exception as e:
|
||||
logger.error("Unknown error logging in to rutracker: %s" % e)
|
||||
self.loggedin = False
|
||||
return self.loggedin
|
||||
|
||||
def has_bb_session_cookie(self, response):
|
||||
if 'bb_session' in response.cookies.keys():
|
||||
return True
|
||||
# Rutracker randomly send a 302 redirect code, cookie may be present in response history
|
||||
return next(('bb_session' in r.cookies.keys() for r in response.history), False)
|
||||
|
||||
def searchurl(self, artist, album, year, format):
|
||||
"""
|
||||
Return the search url
|
||||
@@ -80,10 +87,10 @@ class Rutracker(object):
|
||||
searchterm = searchterm + year
|
||||
|
||||
if format == 'lossless':
|
||||
format = '+lossless'
|
||||
format = '+lossless||TR24'
|
||||
self.maxsize = 10000000000
|
||||
elif format == 'lossless+mp3':
|
||||
format = '+lossless||mp3||aac'
|
||||
format = '+lossless||TR24||mp3||aac'
|
||||
self.maxsize = 10000000000
|
||||
else:
|
||||
format = '+mp3||aac'
|
||||
@@ -91,8 +98,13 @@ class Rutracker(object):
|
||||
|
||||
# sort by size, descending.
|
||||
sort = '&o=7&s=2'
|
||||
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
|
||||
try:
|
||||
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
|
||||
except:
|
||||
searchterm = searchterm.encode('utf-8')
|
||||
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
|
||||
logger.info("Searching rutracker using term: %s", searchterm)
|
||||
|
||||
return searchurl
|
||||
|
||||
def search(self, searchurl):
|
||||
@@ -167,7 +179,7 @@ class Rutracker(object):
|
||||
return the .torrent data
|
||||
"""
|
||||
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
|
||||
downloadurl = 'http://dl.rutracker.org/forum/dl.php?t=' + torrent_id
|
||||
downloadurl = 'http://rutracker.org/forum/dl.php?t=' + torrent_id
|
||||
cookie = {'bb_dl': torrent_id}
|
||||
try:
|
||||
headers = {'Referer': url}
|
||||
@@ -214,3 +226,37 @@ class Rutracker(object):
|
||||
self.session.post(url, params={'action': 'add-file'}, files=files)
|
||||
except Exception as e:
|
||||
logger.exception('Error adding file to utorrent %s', e)
|
||||
|
||||
# TODO get this working in qbittorrent.py
|
||||
def qbittorrent_add_file(self, data):
|
||||
host = headphones.CONFIG.QBITTORRENT_HOST
|
||||
if not host.startswith('http'):
|
||||
host = 'http://' + host
|
||||
if host.endswith('/'):
|
||||
host = host[:-1]
|
||||
if host.endswith('/gui'):
|
||||
host = host[:-4]
|
||||
base_url = host
|
||||
|
||||
# self.session.auth = HTTPDigestAuth(headphones.CONFIG.QBITTORRENT_USERNAME, headphones.CONFIG.QBITTORRENT_PASSWORD)
|
||||
|
||||
url = base_url + '/login'
|
||||
try:
|
||||
self.session.post(url, data={'username': headphones.CONFIG.QBITTORRENT_USERNAME,
|
||||
'password': headphones.CONFIG.QBITTORRENT_PASSWORD})
|
||||
except Exception as e:
|
||||
logger.exception('Error adding file to qbittorrent %s', e)
|
||||
return
|
||||
|
||||
url = base_url + '/command/upload'
|
||||
|
||||
args = {'savepath': headphones.CONFIG.DOWNLOAD_TORRENT_DIR}
|
||||
if headphones.CONFIG.QBITTORRENT_LABEL:
|
||||
args['category'] = headphones.CONFIG.QBITTORRENT_LABEL
|
||||
|
||||
torrent_files = {'torrents': data}
|
||||
|
||||
try:
|
||||
self.session.post(url, data=args, files=torrent_files)
|
||||
except Exception as e:
|
||||
logger.exception('Error adding file to qbittorrent %s', e)
|
||||
|
||||
@@ -30,23 +30,25 @@ import re
|
||||
from pygazelle import api as gazelleapi
|
||||
from pygazelle import encoding as gazelleencoding
|
||||
from pygazelle import format as gazelleformat
|
||||
from pygazelle import release_type as gazellerelease_type
|
||||
import headphones
|
||||
from headphones.common import USER_AGENT
|
||||
from headphones import logger, db, helpers, classes, sab, nzbget, request
|
||||
from headphones import utorrent, transmission, notifiers, rutracker, deluge
|
||||
from headphones import utorrent, transmission, notifiers, rutracker, deluge, qbittorrent
|
||||
from bencode import bencode, bdecode
|
||||
|
||||
|
||||
# Magnet to torrent services, for Black hole. Stolen from CouchPotato.
|
||||
TORRENT_TO_MAGNET_SERVICES = [
|
||||
# 'https://zoink.it/torrent/%s.torrent',
|
||||
# 'http://torrage.com/torrent/%s.torrent',
|
||||
'https://torcache.net/torrent/%s.torrent',
|
||||
'http://itorrents.org/torrent/%s.torrent',
|
||||
'https://cache.torrentgalaxy.org/get/%s',
|
||||
'https://www.seedpeer.me/torrent/%s'
|
||||
]
|
||||
|
||||
# Persistent What.cd API object
|
||||
gazelle = None
|
||||
# Persistent Orpheus.network API object
|
||||
orpheusobj = None
|
||||
ruobj = None
|
||||
# Persistent RED API object
|
||||
redobj = None
|
||||
|
||||
|
||||
def fix_url(s, charset="utf-8"):
|
||||
@@ -151,26 +153,31 @@ def calculate_torrent_hash(link, data=None):
|
||||
|
||||
def get_seed_ratio(provider):
|
||||
"""
|
||||
Return the seed ratio for the specified provider, if applicable. Defaults to
|
||||
Return the seed ratio for the specified provider if applicable. Defaults to
|
||||
None in case of an error.
|
||||
"""
|
||||
|
||||
if provider == 'rutracker.org':
|
||||
seed_ratio = headphones.CONFIG.RUTRACKER_RATIO
|
||||
elif provider == 'Kick Ass Torrents':
|
||||
seed_ratio = headphones.CONFIG.KAT_RATIO
|
||||
elif provider == 'What.cd':
|
||||
seed_ratio = headphones.CONFIG.WHATCD_RATIO
|
||||
elif provider == 'Orpheus.network':
|
||||
seed_ratio = headphones.CONFIG.ORPHEUS_RATIO
|
||||
elif provider == 'Redacted':
|
||||
seed_ratio = headphones.CONFIG.REDACTED_RATIO
|
||||
elif provider == 'The Pirate Bay':
|
||||
seed_ratio = headphones.CONFIG.PIRATEBAY_RATIO
|
||||
elif provider == 'Old Pirate Bay':
|
||||
seed_ratio = headphones.CONFIG.OLDPIRATEBAY_RATIO
|
||||
elif provider == 'Waffles.ch':
|
||||
seed_ratio = headphones.CONFIG.WAFFLES_RATIO
|
||||
elif provider == 'Mininova':
|
||||
seed_ratio = headphones.CONFIG.MININOVA_RATIO
|
||||
elif provider == 'Strike':
|
||||
seed_ratio = headphones.CONFIG.STRIKE_RATIO
|
||||
elif provider.startswith("Jackett_"):
|
||||
provider = provider.split("Jackett_")[1]
|
||||
if provider in headphones.CONFIG.TORZNAB_HOST:
|
||||
seed_ratio = headphones.CONFIG.TORZNAB_RATIO
|
||||
else:
|
||||
for torznab in headphones.CONFIG.get_extra_torznabs():
|
||||
if provider in torznab[0]:
|
||||
seed_ratio = torznab[2]
|
||||
break
|
||||
else:
|
||||
seed_ratio = None
|
||||
|
||||
@@ -199,14 +206,13 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
|
||||
continue
|
||||
|
||||
if headphones.CONFIG.WAIT_UNTIL_RELEASE_DATE and album['ReleaseDate']:
|
||||
try:
|
||||
release_date = datetime.datetime.strptime(album['ReleaseDate'], "%Y-%m-%d")
|
||||
except:
|
||||
logger.warn(
|
||||
"No valid date for: %s. Skipping automatic search" % album['AlbumTitle'])
|
||||
release_date = strptime_musicbrainz(album['ReleaseDate'])
|
||||
if not release_date:
|
||||
logger.warn("No valid date for: %s. Skipping automatic search" %
|
||||
album['AlbumTitle'])
|
||||
continue
|
||||
|
||||
if release_date > datetime.datetime.today():
|
||||
elif release_date > datetime.datetime.today():
|
||||
logger.info("Skipping: %s. Waiting for release date of: %s" % (
|
||||
album['AlbumTitle'], album['ReleaseDate']))
|
||||
continue
|
||||
@@ -235,6 +241,26 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
|
||||
logger.info('Search for wanted albums complete')
|
||||
|
||||
|
||||
def strptime_musicbrainz(date_str):
|
||||
"""
|
||||
Release date as returned by Musicbrainz may contain the full date (Year-Month-Day)
|
||||
but it may as well be just Year-Month or even just the year.
|
||||
|
||||
Args:
|
||||
date_str: the date as a string (ex: "2003-05-01", "2003-03", "2003")
|
||||
|
||||
Returns:
|
||||
The more accurate datetime object we can create or None if parse failed
|
||||
"""
|
||||
acceptable_formats = ('%Y-%m-%d', '%Y-%m', '%Y')
|
||||
for date_format in acceptable_formats:
|
||||
try:
|
||||
return datetime.datetime.strptime(date_str, date_format)
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
|
||||
NZB_PROVIDERS = (headphones.CONFIG.HEADPHONES_INDEXER or
|
||||
headphones.CONFIG.NEWZNAB or
|
||||
@@ -246,14 +272,12 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
|
||||
headphones.CONFIG.NZBGET_HOST)
|
||||
|
||||
TORRENT_PROVIDERS = (headphones.CONFIG.TORZNAB or
|
||||
headphones.CONFIG.KAT or
|
||||
headphones.CONFIG.PIRATEBAY or
|
||||
headphones.CONFIG.OLDPIRATEBAY or
|
||||
headphones.CONFIG.MININOVA or
|
||||
headphones.CONFIG.WAFFLES or
|
||||
headphones.CONFIG.RUTRACKER or
|
||||
headphones.CONFIG.WHATCD or
|
||||
headphones.CONFIG.STRIKE)
|
||||
headphones.CONFIG.ORPHEUS or
|
||||
headphones.CONFIG.REDACTED)
|
||||
|
||||
results = []
|
||||
myDB = db.DBConnection()
|
||||
@@ -412,6 +436,7 @@ def sort_search_results(resultlist, album, new, albumlength):
|
||||
|
||||
resultlist = temp_list
|
||||
|
||||
# if headphones.CONFIG.PREFERRED_QUALITY == 2 and headphones.CONFIG.PREFERRED_BITRATE and result[3] != 'Orpheus.network':
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 2 and headphones.CONFIG.PREFERRED_BITRATE:
|
||||
|
||||
try:
|
||||
@@ -459,6 +484,10 @@ def sort_search_results(resultlist, album, new, albumlength):
|
||||
|
||||
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
|
||||
|
||||
# keep number of seeders order for Orpheus.network
|
||||
# if result[3] == 'Orpheus.network':
|
||||
# finallist = resultlist
|
||||
|
||||
if not len(finallist):
|
||||
logger.info('No appropriate matches found for %s - %s', album['ArtistName'],
|
||||
album['AlbumTitle'])
|
||||
@@ -714,7 +743,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None,
|
||||
}
|
||||
|
||||
data = request.request_json(
|
||||
url='http://api.omgwtfnzbs.org/json/',
|
||||
url='http://api.omgwtfnzbs.me/json/',
|
||||
params=params, headers=headers
|
||||
)
|
||||
|
||||
@@ -838,12 +867,11 @@ def send_to_downloader(data, bestqual, album):
|
||||
services = TORRENT_TO_MAGNET_SERVICES[:]
|
||||
random.shuffle(services)
|
||||
headers = {'User-Agent': USER_AGENT}
|
||||
headers['Referer'] = 'https://torcache.net/'
|
||||
|
||||
for service in services:
|
||||
|
||||
data = request.request_content(service % torrent_hash, headers=headers)
|
||||
if data and "torcache" in data:
|
||||
if data:
|
||||
if not torrent_to_file(download_path, data):
|
||||
return
|
||||
# Extract folder name from torrent
|
||||
@@ -889,11 +917,11 @@ def send_to_downloader(data, bestqual, album):
|
||||
logger.error("Error sending torrent to Transmission. Are you sure it's running?")
|
||||
return
|
||||
|
||||
folder_name = transmission.getTorrentFolder(torrentid)
|
||||
folder_name = transmission.getName(torrentid)
|
||||
if folder_name:
|
||||
logger.info('Torrent folder name: %s' % folder_name)
|
||||
logger.info('Torrent name: %s' % folder_name)
|
||||
else:
|
||||
logger.error('Torrent folder name could not be determined')
|
||||
logger.error('Torrent name could not be determined')
|
||||
return
|
||||
|
||||
# Set Seed Ratio
|
||||
@@ -943,7 +971,7 @@ def send_to_downloader(data, bestqual, album):
|
||||
except Exception as e:
|
||||
logger.error('Error sending torrent to Deluge: %s' % str(e))
|
||||
|
||||
else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
logger.info("Sending torrent to uTorrent")
|
||||
|
||||
# Add torrent
|
||||
@@ -974,19 +1002,50 @@ def send_to_downloader(data, bestqual, album):
|
||||
seed_ratio = get_seed_ratio(bestqual[3])
|
||||
if seed_ratio is not None:
|
||||
utorrent.setSeedRatio(torrentid, seed_ratio)
|
||||
else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 4:
|
||||
logger.info("Sending torrent to QBiTorrent")
|
||||
|
||||
# Add torrent
|
||||
if bestqual[3] == 'rutracker.org':
|
||||
if qbittorrent.apiVersion2:
|
||||
qbittorrent.addFile(data)
|
||||
else:
|
||||
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
|
||||
|
||||
# Set Seed Ratio
|
||||
seed_ratio = get_seed_ratio(bestqual[3])
|
||||
if seed_ratio is not None:
|
||||
qbittorrent.setSeedRatio(torrentid, seed_ratio)
|
||||
|
||||
myDB = db.DBConnection()
|
||||
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']])
|
||||
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)',
|
||||
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)',
|
||||
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name,
|
||||
kind])
|
||||
kind, torrentid])
|
||||
|
||||
# Store the torrent id so we can check later if it's finished seeding and can be removed
|
||||
if seed_ratio is not None and seed_ratio != 0 and torrentid:
|
||||
myDB.action(
|
||||
'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)',
|
||||
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", torrentid,
|
||||
kind])
|
||||
'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)',
|
||||
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", folder_name,
|
||||
kind, torrentid])
|
||||
|
||||
# notify
|
||||
artist = album[1]
|
||||
@@ -1014,10 +1073,22 @@ def send_to_downloader(data, bestqual, album):
|
||||
logger.info(u"Sending PushBullet notification")
|
||||
pushbullet = notifiers.PUSHBULLET()
|
||||
pushbullet.notify(name, "Download started")
|
||||
if headphones.CONFIG.JOIN_ENABLED and headphones.CONFIG.JOIN_ONSNATCH:
|
||||
logger.info(u"Sending Join notification")
|
||||
join = notifiers.JOIN()
|
||||
join.notify(name, "Download started")
|
||||
if headphones.CONFIG.SLACK_ENABLED and headphones.CONFIG.SLACK_ONSNATCH:
|
||||
logger.info(u"Sending Slack notification")
|
||||
slack = notifiers.SLACK()
|
||||
slack.notify(name, "Download started")
|
||||
if headphones.CONFIG.TELEGRAM_ENABLED and headphones.CONFIG.TELEGRAM_ONSNATCH:
|
||||
logger.info(u"Sending Telegram notification")
|
||||
from headphones import cache
|
||||
c = cache.Cache()
|
||||
album_art = c.get_artwork_from_cache(None, rgid)
|
||||
telegram = notifiers.TELEGRAM()
|
||||
telegram.notify(name, "Download started")
|
||||
message = 'Snatched from ' + provider + '. ' + name
|
||||
telegram.notify(message, "Snatched: " + title, rgid, image=album_art)
|
||||
if headphones.CONFIG.TWITTER_ENABLED and headphones.CONFIG.TWITTER_ONSNATCH:
|
||||
logger.info(u"Sending Twitter notification")
|
||||
twitter = notifiers.TwitterNotifier()
|
||||
@@ -1140,7 +1211,8 @@ def verifyresult(title, artistterm, term, lossless):
|
||||
|
||||
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
choose_specific_download=False):
|
||||
global gazelle # persistent what.cd api object to reduce number of login attempts
|
||||
global orpheusobj # persistent orpheus.network api object to reduce number of login attempts
|
||||
global redobj # persistent redacted api object to reduce number of login attempts
|
||||
global ruobj # and rutracker
|
||||
|
||||
reldate = album['ReleaseDate']
|
||||
@@ -1212,18 +1284,21 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
|
||||
if headphones.CONFIG.TORZNAB_HOST and headphones.CONFIG.TORZNAB_ENABLED:
|
||||
torznab_hosts.append((headphones.CONFIG.TORZNAB_HOST, headphones.CONFIG.TORZNAB_APIKEY,
|
||||
headphones.CONFIG.TORZNAB_ENABLED))
|
||||
headphones.CONFIG.TORZNAB_RATIO, headphones.CONFIG.TORZNAB_ENABLED))
|
||||
|
||||
for torznab_host in headphones.CONFIG.get_extra_torznabs():
|
||||
if torznab_host[2] == '1' or torznab_host[2] == 1:
|
||||
if torznab_host[3] == '1' or torznab_host[3] == 1:
|
||||
torznab_hosts.append(torznab_host)
|
||||
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||
categories = "3040"
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
|
||||
categories = "3040,3010"
|
||||
categories = "3040,3010,3050"
|
||||
maxsize = 10000000000
|
||||
else:
|
||||
categories = "3010"
|
||||
categories = "3010,3050"
|
||||
maxsize = 300000000
|
||||
|
||||
if album['Type'] == 'Other':
|
||||
categories = "3030"
|
||||
@@ -1233,8 +1308,12 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
|
||||
provider = torznab_host[0]
|
||||
|
||||
# Format Jackett provider
|
||||
if "api/v2.0/indexers" in torznab_host[0]:
|
||||
provider = "Jackett_" + provider.split("/indexers/", 1)[1].split('/', 1)[0]
|
||||
|
||||
# Request results
|
||||
logger.info('Parsing results from %s using search term: %s' % (torznab_host[0], term))
|
||||
logger.info('Parsing results from %s using search term: %s' % (provider, term))
|
||||
|
||||
headers = {'User-Agent': USER_AGENT}
|
||||
params = {
|
||||
@@ -1245,25 +1324,49 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
"q": term
|
||||
}
|
||||
|
||||
data = request.request_feed(
|
||||
url=torznab_host[0] + '/api?',
|
||||
data = request.request_soup(
|
||||
url=torznab_host[0],
|
||||
params=params, headers=headers
|
||||
)
|
||||
|
||||
# Process feed
|
||||
if data:
|
||||
if not len(data.entries):
|
||||
logger.info(u"No results found from %s for %s", torznab_host[0], term)
|
||||
items = data.find_all('item')
|
||||
if not items:
|
||||
logger.info(u"No results found from %s for %s", provider, term)
|
||||
else:
|
||||
for item in data.entries:
|
||||
for item in items:
|
||||
try:
|
||||
url = item.link
|
||||
title = item.title
|
||||
size = int(item.links[1]['length'])
|
||||
title = item.title.get_text()
|
||||
url = item.find("link").next_sibling.strip()
|
||||
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
|
||||
elif item.size:
|
||||
size = int(item.size.string)
|
||||
else:
|
||||
size = int(item.find("torznab:attr", attrs={"name": "size"}).get('value'))
|
||||
|
||||
if all(word.lower() in title.lower() for word in term.split()):
|
||||
logger.info(
|
||||
'Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
resultlist.append((title, size, url, provider, 'torrent', True))
|
||||
if size < maxsize and minimumseeders < seeders:
|
||||
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
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:
|
||||
logger.info('Skipping %s, not all search term words found' % title)
|
||||
|
||||
@@ -1271,69 +1374,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
logger.exception(
|
||||
"An unknown error occurred trying to parse the feed: %s" % e)
|
||||
|
||||
if headphones.CONFIG.KAT:
|
||||
provider = "Kick Ass Torrents"
|
||||
ka_term = term.replace("!", "")
|
||||
|
||||
# Use proxy if specified
|
||||
if headphones.CONFIG.KAT_PROXY_URL:
|
||||
providerurl = fix_url(set_proxy(headphones.CONFIG.KAT_PROXY_URL))
|
||||
else:
|
||||
providerurl = fix_url("https://kat.cr")
|
||||
|
||||
# Build URL
|
||||
providerurl = providerurl + "/usearch/" + ka_term
|
||||
|
||||
# Set max size and category
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||
maxsize = 10000000000
|
||||
providerurl += " category:lossless/"
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
|
||||
maxsize = 10000000000
|
||||
providerurl += " category:music/"
|
||||
else:
|
||||
maxsize = 300000000
|
||||
providerurl += " category:music/"
|
||||
|
||||
# Requesting content
|
||||
logger.info("Searching %s using term: %s" % (provider, ka_term))
|
||||
|
||||
params = {
|
||||
"field": "seeders",
|
||||
"sorder": "desc",
|
||||
"rss": "1"
|
||||
}
|
||||
|
||||
data = request.request_feed(url=providerurl, params=params,
|
||||
whitelist_status_code=[404])
|
||||
|
||||
# Process feed
|
||||
if data:
|
||||
if not len(data.entries):
|
||||
logger.info("No results found on %s using search term: %s" % (provider, ka_term))
|
||||
else:
|
||||
for item in data.entries:
|
||||
try:
|
||||
title = item['title']
|
||||
seeders = item['torrent_seeds']
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 0:
|
||||
url = item['links'][1]['href']
|
||||
else:
|
||||
url = item['torrent_magneturi']
|
||||
size = int(item['links'][1]['length'])
|
||||
if size < maxsize and minimumseeders < int(seeders):
|
||||
resultlist.append((title, size, url, provider, 'torrent', True))
|
||||
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
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, int(seeders))
|
||||
except Exception as e:
|
||||
logger.exception("Unhandled exception in the KAT parser")
|
||||
|
||||
if headphones.CONFIG.WAFFLES:
|
||||
provider = "Waffles.ch"
|
||||
providerurl = fix_url("https://www.waffles.ch/browse.php")
|
||||
providerurl = fix_url("https://waffles.ch/browse.php")
|
||||
|
||||
bitrate = None
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||
@@ -1355,8 +1398,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
query_items = [usersearchterm]
|
||||
|
||||
query_items.extend(['format:(%s)' % format,
|
||||
'size:[0 TO %d]' % maxsize,
|
||||
'-seeders:0']) # cut out dead torrents
|
||||
'size:[0 TO %d]' % maxsize])
|
||||
# (25/03/2017 Waffles back up after 5 months, all torrents currently have no seeders, remove for now)
|
||||
# '-seeders:0']) cut out dead torrents
|
||||
|
||||
if bitrate:
|
||||
query_items.append('bitrate:"%s"' % bitrate)
|
||||
@@ -1432,9 +1476,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
if rulist:
|
||||
resultlist.extend(rulist)
|
||||
|
||||
if headphones.CONFIG.WHATCD:
|
||||
provider = "What.cd"
|
||||
providerurl = "http://what.cd/"
|
||||
if headphones.CONFIG.ORPHEUS:
|
||||
provider = "Orpheus.network"
|
||||
providerurl = "http://orpheus.network/"
|
||||
|
||||
bitrate = None
|
||||
bitrate_string = bitrate
|
||||
@@ -1457,7 +1501,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
bitrate_string = encoding_string
|
||||
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
||||
logger.info(
|
||||
u"Your preferred bitrate is not one of the available What.cd filters, so not using it as a search parameter.")
|
||||
u"Your preferred bitrate is not one of the available Orpheus.network filters, so not using it as a search parameter.")
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
|
||||
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
||||
@@ -1466,27 +1510,159 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
search_formats = [gazelleformat.MP3]
|
||||
maxsize = 300000000
|
||||
|
||||
if not gazelle or not gazelle.logged_in():
|
||||
if not orpheusobj or not orpheusobj.logged_in():
|
||||
try:
|
||||
logger.info(u"Attempting to log in to What.cd...")
|
||||
gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.WHATCD_USERNAME,
|
||||
headphones.CONFIG.WHATCD_PASSWORD)
|
||||
gazelle._login()
|
||||
logger.info(u"Attempting to log in to Orpheus.network...")
|
||||
orpheusobj = gazelleapi.GazelleAPI(headphones.CONFIG.ORPHEUS_USERNAME,
|
||||
headphones.CONFIG.ORPHEUS_PASSWORD,
|
||||
headphones.CONFIG.ORPHEUS_URL)
|
||||
orpheusobj._login()
|
||||
except Exception as e:
|
||||
gazelle = None
|
||||
logger.error(u"What.cd credentials incorrect or site is down. Error: %s %s" % (
|
||||
orpheusobj = None
|
||||
logger.error(u"Orpheus.network credentials incorrect or site is down. Error: %s %s" % (
|
||||
e.__class__.__name__, str(e)))
|
||||
|
||||
if gazelle and gazelle.logged_in():
|
||||
if orpheusobj and orpheusobj.logged_in():
|
||||
logger.info(u"Searching %s..." % provider)
|
||||
all_torrents = []
|
||||
|
||||
album_type = ""
|
||||
|
||||
# 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]
|
||||
if album['Type'] == 'Other':
|
||||
album_type = [gazellerelease_type.UNKNOWN]
|
||||
|
||||
for search_format in search_formats:
|
||||
if usersearchterm:
|
||||
all_torrents.extend(
|
||||
orpheusobj.search_torrents(searchstr=usersearchterm, format=search_format,
|
||||
encoding=bitrate_string, releasetype=album_type)['results'])
|
||||
else:
|
||||
all_torrents.extend(orpheusobj.search_torrents(artistname=semi_clean_artist_term,
|
||||
groupname=semi_clean_album_term,
|
||||
format=search_format,
|
||||
encoding=bitrate_string,
|
||||
releasetype=album_type)['results'])
|
||||
|
||||
# filter on format, size, and num seeders
|
||||
logger.info(u"Filtering torrents by format, maximum size, and minimum seeders...")
|
||||
match_torrents = [t for t in all_torrents if
|
||||
t.size <= maxsize and t.seeders >= minimumseeders]
|
||||
|
||||
logger.info(
|
||||
u"Remaining torrents: %s" % ", ".join(repr(torrent) for torrent in match_torrents))
|
||||
|
||||
# sort by times d/l'd
|
||||
if not len(match_torrents):
|
||||
logger.info(u"No results found from %s for %s after filtering" % (provider, term))
|
||||
elif len(match_torrents) > 1:
|
||||
logger.info(u"Found %d matching releases from %s for %s - %s after filtering" %
|
||||
(len(match_torrents), provider, artistterm, albumterm))
|
||||
logger.info('Sorting torrents by number of seeders...')
|
||||
match_torrents.sort(key=lambda x: int(x.seeders), reverse=True)
|
||||
if gazelleformat.MP3 in search_formats:
|
||||
logger.info('Sorting torrents by seeders...')
|
||||
match_torrents.sort(key=lambda x: int(x.seeders), reverse=True)
|
||||
if search_formats and None not in search_formats:
|
||||
match_torrents.sort(
|
||||
key=lambda x: int(search_formats.index(x.format))) # prefer lossless
|
||||
# if bitrate:
|
||||
# match_torrents.sort(key=lambda x: re.match("mp3", x.getTorrentDetails(), flags=re.I), reverse=True)
|
||||
# match_torrents.sort(key=lambda x: str(bitrate) in x.getTorrentFolderName(), reverse=True)
|
||||
logger.info(
|
||||
u"New order: %s" % ", ".join(repr(torrent) for torrent in match_torrents))
|
||||
|
||||
for torrent in match_torrents:
|
||||
if not torrent.file_path:
|
||||
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
||||
resultlist.append((torrent.file_path,
|
||||
torrent.size,
|
||||
orpheusobj.generate_torrent_link(torrent.id),
|
||||
provider,
|
||||
'torrent', True))
|
||||
|
||||
# Redacted - Using same logic as What.CD as it's also Gazelle, so should really make this into something reusable
|
||||
if headphones.CONFIG.REDACTED:
|
||||
provider = "Redacted"
|
||||
providerurl = "https://redacted.ch"
|
||||
|
||||
bitrate = None
|
||||
bitrate_string = bitrate
|
||||
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly: # Lossless Only mode
|
||||
search_formats = [gazelleformat.FLAC]
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 2: # Preferred quality mode
|
||||
search_formats = [None] # should return all
|
||||
bitrate = headphones.CONFIG.PREFERRED_BITRATE
|
||||
if bitrate:
|
||||
if 225 <= int(bitrate) < 256:
|
||||
bitrate = 'V0'
|
||||
elif 200 <= int(bitrate) < 225:
|
||||
bitrate = 'V1'
|
||||
elif 175 <= int(bitrate) < 200:
|
||||
bitrate = 'V2'
|
||||
for encoding_string in gazelleencoding.ALL_ENCODINGS:
|
||||
if re.search(bitrate, encoding_string, flags=re.I):
|
||||
bitrate_string = encoding_string
|
||||
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
||||
logger.info(
|
||||
u"Your preferred bitrate is not one of the available RED filters, so not using it as a search parameter.")
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
|
||||
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
||||
maxsize = 10000000000
|
||||
else: # Highest quality excluding lossless
|
||||
search_formats = [gazelleformat.MP3]
|
||||
maxsize = 300000000
|
||||
|
||||
if not redobj or not redobj.logged_in():
|
||||
try:
|
||||
logger.info(u"Attempting to log in to Redacted...")
|
||||
redobj = gazelleapi.GazelleAPI(headphones.CONFIG.REDACTED_USERNAME,
|
||||
headphones.CONFIG.REDACTED_PASSWORD,
|
||||
providerurl)
|
||||
redobj._login()
|
||||
except Exception as e:
|
||||
redobj = None
|
||||
logger.error(u"Redacted credentials incorrect or site is down. Error: %s %s" % (
|
||||
e.__class__.__name__, str(e)))
|
||||
|
||||
if redobj and redobj.logged_in():
|
||||
logger.info(u"Searching %s..." % provider)
|
||||
all_torrents = []
|
||||
for search_format in search_formats:
|
||||
if usersearchterm:
|
||||
all_torrents.extend(
|
||||
gazelle.search_torrents(searchstr=usersearchterm, format=search_format,
|
||||
redobj.search_torrents(searchstr=usersearchterm, format=search_format,
|
||||
encoding=bitrate_string)['results'])
|
||||
else:
|
||||
all_torrents.extend(gazelle.search_torrents(artistname=semi_clean_artist_term,
|
||||
all_torrents.extend(redobj.search_torrents(artistname=semi_clean_artist_term,
|
||||
groupname=semi_clean_album_term,
|
||||
format=search_format,
|
||||
encoding=bitrate_string)['results'])
|
||||
@@ -1524,22 +1700,23 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
for torrent in match_torrents:
|
||||
if not torrent.file_path:
|
||||
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
||||
use_token = headphones.CONFIG.REDACTED_USE_FLTOKEN and torrent.can_use_token
|
||||
resultlist.append((torrent.file_path,
|
||||
torrent.size,
|
||||
gazelle.generate_torrent_link(torrent.id),
|
||||
redobj.generate_torrent_link(torrent.id, use_token),
|
||||
provider,
|
||||
'torrent', True))
|
||||
|
||||
# Pirate Bay
|
||||
if headphones.CONFIG.PIRATEBAY:
|
||||
provider = "The Pirate Bay"
|
||||
tpb_term = term.replace("!", "")
|
||||
tpb_term = term.replace("!", "").replace("'", " ")
|
||||
|
||||
# Use proxy if specified
|
||||
if headphones.CONFIG.PIRATEBAY_PROXY_URL:
|
||||
providerurl = fix_url(set_proxy(headphones.CONFIG.PIRATEBAY_PROXY_URL))
|
||||
else:
|
||||
providerurl = fix_url("https://thepiratebay.se")
|
||||
providerurl = fix_url("https://thepiratebay.org")
|
||||
|
||||
# Build URL
|
||||
providerurl = providerurl + "/search/" + tpb_term + "/0/7/" # 7 is sort by seeders
|
||||
@@ -1572,7 +1749,6 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
for item in rows:
|
||||
try:
|
||||
url = None
|
||||
rightformat = True
|
||||
title = ''.join(item.find("a", {"class": "detLink"}))
|
||||
seeds = int(''.join(item.find("td", {"align": "right"})))
|
||||
|
||||
@@ -1641,7 +1817,6 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
try:
|
||||
links = item.select("td.title-row a")
|
||||
|
||||
rightformat = True
|
||||
title = links[1].text
|
||||
seeds = int(item.select("td.seeders-row")[0].text)
|
||||
url = links[0][
|
||||
@@ -1663,113 +1838,13 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
logger.error(
|
||||
u"An unknown error occurred in the Old Pirate Bay parser: %s" % e)
|
||||
|
||||
# Strike
|
||||
if headphones.CONFIG.STRIKE:
|
||||
provider = "Strike"
|
||||
s_term = term.replace("!", "")
|
||||
providerurl = fix_url("https://getstrike.net/api/v2/torrents/search/?phrase=")
|
||||
|
||||
providerurl = providerurl + s_term + "&category=Music"
|
||||
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||
providerurl = providerurl + "&subcategory=Lossless"
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
|
||||
maxsize = 10000000000
|
||||
else:
|
||||
maxsize = 300000000
|
||||
|
||||
logger.info("Searching %s using term: %s" % (provider, s_term))
|
||||
data = request.request_json(url=providerurl,
|
||||
whitelist_status_code=[404])
|
||||
|
||||
if not data or not data.get('torrents'):
|
||||
logger.info("No results found on %s using search term: %s" % (provider, s_term))
|
||||
else:
|
||||
for item in data['torrents']:
|
||||
try:
|
||||
title = item['torrent_title']
|
||||
seeders = item['seeds']
|
||||
url = item['magnet_uri']
|
||||
size = int(item['size'])
|
||||
|
||||
if size < maxsize and minimumseeders < int(seeders):
|
||||
resultlist.append((title, size, url, provider, 'torrent', True))
|
||||
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
else:
|
||||
logger.info(
|
||||
'%s is larger than the maxsize, the wrong format or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %d)',
|
||||
title, size, int(seeders))
|
||||
except Exception as e:
|
||||
logger.exception("Unhandled exception in the Strike parser")
|
||||
|
||||
# Mininova
|
||||
if headphones.CONFIG.MININOVA:
|
||||
provider = "Mininova"
|
||||
providerurl = fix_url("http://www.mininova.org/rss/" + term + "/5")
|
||||
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||
# categories = "7" #music
|
||||
format = "2" # flac
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
|
||||
# categories = "7" #music
|
||||
format = "10" # mp3+flac
|
||||
maxsize = 10000000000
|
||||
else:
|
||||
# categories = "7" #music
|
||||
format = "8" # mp3
|
||||
maxsize = 300000000
|
||||
|
||||
# Requesting content
|
||||
logger.info('Parsing results from Mininova')
|
||||
|
||||
data = request.request_feed(
|
||||
url=providerurl,
|
||||
timeout=20
|
||||
)
|
||||
|
||||
# Process feed
|
||||
if data:
|
||||
if not len(data.entries):
|
||||
logger.info(u"No results found from %s for %s" % (provider, term))
|
||||
else:
|
||||
for item in data.entries:
|
||||
try:
|
||||
rightformat = True
|
||||
title = item.title
|
||||
sxstart = item.description.find("Ratio: ") + 7
|
||||
seeds = ""
|
||||
while item.description[sxstart:sxstart + 1] != " ":
|
||||
seeds = seeds + item.description[sxstart:sxstart + 1]
|
||||
sxstart = sxstart + 1
|
||||
url = item.links[1]['url']
|
||||
size = int(item.links[1]['length'])
|
||||
if format == "2":
|
||||
torrent = request.request_content(url)
|
||||
if not torrent or (int(torrent.find(".mp3")) > 0 and int(
|
||||
torrent.find(".flac")) < 1):
|
||||
rightformat = False
|
||||
|
||||
if rightformat and size < maxsize and minimumseeders < seeds:
|
||||
match = True
|
||||
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
else:
|
||||
match = False
|
||||
logger.info('%s is larger than the maxsize, the wrong format or has too little seeders'
|
||||
' for this category, skipping. (Size: %i bytes, Seeders: %i, Format: %s)' % (
|
||||
title, size, int(seeds), rightformat))
|
||||
|
||||
resultlist.append((title, size, url, provider, 'torrent', match))
|
||||
except Exception as e:
|
||||
logger.exception("Unhandled exception in Mininova Parser")
|
||||
|
||||
# attempt to verify that this isn't a substring result
|
||||
# when looking for "Foo - Foo" we don't want "Foobar"
|
||||
# this should be less of an issue when it isn't a self-titled album so we'll only check vs artist
|
||||
results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)]
|
||||
|
||||
# Additional filtering for size etc
|
||||
# if results and not choose_specific_download and result[3] != 'Orpheus.network':
|
||||
if results and not choose_specific_download:
|
||||
results = more_filtering(results, album, albumlength, new)
|
||||
|
||||
@@ -1783,28 +1858,47 @@ def preprocess(resultlist):
|
||||
for result in resultlist:
|
||||
if result[4] == 'torrent':
|
||||
|
||||
headers = {}
|
||||
|
||||
# rutracker always needs the torrent data
|
||||
if result[3] == 'rutracker.org':
|
||||
return ruobj.get_torrent_data(result[2]), result
|
||||
|
||||
# Get out of here if we're using Transmission
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1: # if not a magnet link still need the .torrent to generate hash... uTorrent support labeling
|
||||
# Jackett sometimes redirects
|
||||
jackett_content = None
|
||||
if result[3].startswith('Jackett_') or 'torznab' in result[3].lower():
|
||||
r = request.request_response(url=result[2], headers=headers, allow_redirects=False)
|
||||
if r:
|
||||
jackett_content = r.content
|
||||
link = r.headers.get('Location')
|
||||
if link and link != result[2]:
|
||||
if link.startswith('magnet:'):
|
||||
result = (result[0], result[1], link, result[3], "magnet", result[5])
|
||||
return "d10:magnet-uri%d:%se" % (len(link), link), result
|
||||
else:
|
||||
result = (result[0], result[1], link, result[3], result[4], result[5])
|
||||
return True, result
|
||||
|
||||
# Get out of here if we're using Transmission or Deluge
|
||||
# if not a magnet link still need the .torrent to generate hash... uTorrent support labeling
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER in [1, 3]:
|
||||
return True, result
|
||||
|
||||
# Get out of here if it's a magnet link
|
||||
if result[2].lower().startswith("magnet:"):
|
||||
return True, result
|
||||
|
||||
# Download the torrent file
|
||||
headers = {}
|
||||
|
||||
if result[3] == 'Kick Ass Torrents':
|
||||
headers['Referer'] = 'https://torcache.net/'
|
||||
headers['User-Agent'] = USER_AGENT
|
||||
elif result[3] == 'What.cd':
|
||||
if result[3] == 'Orpheus.network':
|
||||
headers['User-Agent'] = 'Headphones'
|
||||
elif result[3] == 'Redacted':
|
||||
headers['User-Agent'] = 'Headphones'
|
||||
elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay":
|
||||
headers[
|
||||
'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'] = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
|
||||
elif jackett_content:
|
||||
return jackett_content, result
|
||||
|
||||
return request.request_content(url=result[2], headers=headers), result
|
||||
|
||||
if result[4] == 'magnet':
|
||||
|
||||
@@ -13,13 +13,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import threading
|
||||
|
||||
from headphones import db, utorrent, transmission, logger
|
||||
from headphones import db, utorrent, transmission, deluge, qbittorrent, logger
|
||||
import headphones
|
||||
|
||||
postprocessor_lock = threading.Lock()
|
||||
|
||||
|
||||
def checkTorrentFinished():
|
||||
"""
|
||||
@@ -28,21 +25,25 @@ def checkTorrentFinished():
|
||||
|
||||
logger.info("Checking if any torrents have finished seeding and can be removed")
|
||||
|
||||
with postprocessor_lock:
|
||||
myDB = db.DBConnection()
|
||||
results = myDB.select('SELECT * from snatched WHERE Status="Seed_Processed"')
|
||||
myDB = db.DBConnection()
|
||||
results = myDB.select('SELECT * from snatched WHERE Status="Seed_Processed"')
|
||||
|
||||
for album in results:
|
||||
hash = album['FolderName']
|
||||
albumid = album['AlbumID']
|
||||
torrent_removed = False
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
|
||||
torrent_removed = transmission.removeTorrent(hash, True)
|
||||
else:
|
||||
torrent_removed = utorrent.removeTorrent(hash, True)
|
||||
for album in results:
|
||||
hash = album['TorrentHash']
|
||||
albumid = album['AlbumID']
|
||||
torrent_removed = False
|
||||
|
||||
if torrent_removed:
|
||||
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?',
|
||||
[albumid])
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
|
||||
torrent_removed = transmission.removeTorrent(hash, True)
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
torrent_removed = utorrent.removeTorrent(hash, True)
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3:
|
||||
torrent_removed = deluge.removeTorrent(hash, True)
|
||||
else:
|
||||
torrent_removed = qbittorrent.removeTorrent(hash, True)
|
||||
|
||||
if torrent_removed:
|
||||
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?',
|
||||
[albumid])
|
||||
|
||||
logger.info("Checking finished torrents completed")
|
||||
|
||||