Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
517d0eb327 | ||
|
|
2bacd5a0fc | ||
|
|
5a559c526d | ||
|
|
095cee9368 | ||
|
|
79cb133d1d | ||
|
|
3a9b749017 | ||
|
|
0182be2f27 | ||
|
|
b6388f7daa | ||
|
|
0b3d0242fc | ||
|
|
9551e1b04a | ||
|
|
ab4dd18be4 | ||
|
|
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
|
- lib
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- "2.6"
|
|
||||||
- "2.7"
|
- "2.7"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
|||||||
@@ -1,5 +1,63 @@
|
|||||||
# Changelog
|
# 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
|
## v0.5.15
|
||||||
Released 07 June 2016
|
Released 07 June 2016
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
if sys.version_info <= (3, 5):
|
||||||
|
sys.stdout.write("Headphones requires Python >= 3.5\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Ensure lib added to path, before any other imports
|
# Ensure lib added to path, before any other imports
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
|
||||||
|
|
||||||
@@ -54,7 +58,10 @@ def main():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
headphones.SYS_ENCODING = locale.getpreferredencoding()
|
if headphones.SYS_PLATFORM == 'win32':
|
||||||
|
headphones.SYS_ENCODING = sys.getdefaultencoding().upper()
|
||||||
|
else:
|
||||||
|
headphones.SYS_ENCODING = locale.getpreferredencoding()
|
||||||
except (locale.Error, IOError):
|
except (locale.Error, IOError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -81,6 +88,8 @@ def main():
|
|||||||
help='Prevent browser from launching on startup')
|
help='Prevent browser from launching on startup')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
|
'--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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -170,6 +179,13 @@ def main():
|
|||||||
else:
|
else:
|
||||||
http_port = int(headphones.CONFIG.HTTP_PORT)
|
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
|
# Check if pyOpenSSL is installed. It is required for certificate generation
|
||||||
# and for CherryPy.
|
# and for CherryPy.
|
||||||
if headphones.CONFIG.ENABLE_HTTPS:
|
if headphones.CONFIG.ENABLE_HTTPS:
|
||||||
@@ -183,7 +199,7 @@ def main():
|
|||||||
# Try to start the server. Will exit here is address is already in use.
|
# Try to start the server. Will exit here is address is already in use.
|
||||||
web_config = {
|
web_config = {
|
||||||
'http_port': http_port,
|
'http_port': http_port,
|
||||||
'http_host': headphones.CONFIG.HTTP_HOST,
|
'http_host': http_host,
|
||||||
'http_root': headphones.CONFIG.HTTP_ROOT,
|
'http_root': headphones.CONFIG.HTTP_ROOT,
|
||||||
'http_proxy': headphones.CONFIG.HTTP_PROXY,
|
'http_proxy': headphones.CONFIG.HTTP_PROXY,
|
||||||
'enable_https': headphones.CONFIG.ENABLE_HTTPS,
|
'enable_https': headphones.CONFIG.ENABLE_HTTPS,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
## Headphones
|
##  Headphones
|
||||||
|
|
||||||
[](https://travis-ci.org/rembo10/headphones)
|
[](https://travis-ci.org/rembo10/headphones)
|
||||||
[](https://travis-ci.org/rembo10/headphones)
|
[](https://travis-ci.org/rembo10/headphones)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import sys
|
|||||||
# Ensure that we use the Headphones provided libraries.
|
# Ensure that we use the Headphones provided libraries.
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../lib"))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../lib"))
|
||||||
|
|
||||||
import urlparse
|
import urllib.parse
|
||||||
|
|
||||||
|
|
||||||
def can_import(module):
|
def can_import(module):
|
||||||
@@ -89,7 +89,7 @@ def main():
|
|||||||
url = sys.argv[1]
|
url = sys.argv[1]
|
||||||
|
|
||||||
# Check if it is a HTTPS website.
|
# Check if it is a HTTPS website.
|
||||||
parts = urlparse.urlparse(url)
|
parts = urllib.parse.urlparse(url)
|
||||||
|
|
||||||
if parts.scheme.lower() != "https":
|
if parts.scheme.lower() != "https":
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
|
|||||||
|
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) {
|
if (++x === 10) {
|
||||||
clearInterval(refreshInterval);
|
clearInterval(refreshInterval);
|
||||||
}
|
}
|
||||||
var sts = $("#artistname").text();
|
var sts = $("#artistname").text().trim();
|
||||||
if (wasLoading == true || sts == "Loading"){
|
if (wasLoading == true || sts == "Loading"){
|
||||||
location.reload();
|
location.reload();
|
||||||
$('#albumnamespinner').remove()
|
$('#albumnamespinner').remove()
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<label title="Password for web server authentication. Leave empty to disable.">
|
<label title="Password for web server authentication. Leave empty to disable.">
|
||||||
HTTP Password
|
HTTP Password
|
||||||
</label>
|
</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>
|
||||||
<div class="row checkbox">
|
<div class="row checkbox">
|
||||||
<input type="checkbox" name="launch_browser" value="1" ${config['launch_browser']} />
|
<input type="checkbox" name="launch_browser" value="1" ${config['launch_browser']} />
|
||||||
@@ -133,6 +133,12 @@
|
|||||||
</label>
|
</label>
|
||||||
<input type="text" name="mb_ignore_age" value="${config['mb_ignore_age']}" size="4">days
|
<input type="text" name="mb_ignore_age" value="${config['mb_ignore_age']}" size="4">days
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row checkbox">
|
||||||
|
<label title="Ignore MusicBrainz album updates missing a release date.">
|
||||||
|
Ignore Undated Releases
|
||||||
|
</label>
|
||||||
|
<input type="checkbox" id="mb_ignore_age_missing" name="mb_ignore_age_missing" value="1" ${config['mb_ignore_age_missing']}
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -173,7 +179,7 @@
|
|||||||
<label title="SABnzbd password. Leave empty if not applicable.">
|
<label title="SABnzbd password. Leave empty if not applicable.">
|
||||||
SABnzbd Password
|
SABnzbd Password
|
||||||
</label>
|
</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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label title="SABnzbd API key. Can be found in SABnzbd settings.">
|
<label title="SABnzbd API key. Can be found in SABnzbd settings.">
|
||||||
@@ -207,7 +213,7 @@
|
|||||||
<label title="NZBGet password. Leave empty if not applicable">
|
<label title="NZBGet password. Leave empty if not applicable">
|
||||||
NZBget Password
|
NZBget Password
|
||||||
</label>
|
</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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label title="Name of NZBget category to add downloads to.">
|
<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_transmission" value="1" ${config['torrent_downloader_transmission']}> Transmission
|
||||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_utorrent" value="2" ${config['torrent_downloader_utorrent']}> uTorrent (Beta)
|
<input type="radio" name="torrent_downloader" id="torrent_downloader_utorrent" value="2" ${config['torrent_downloader_utorrent']}> uTorrent (Beta)
|
||||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="3" ${config['torrent_downloader_deluge']}> Deluge (Beta)
|
<input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="3" ${config['torrent_downloader_deluge']}> Deluge (Beta)
|
||||||
|
<input type="radio" name="torrent_downloader" id="torrent_downloader_qbittorrent" value="4" ${config['torrent_downloader_qbittorrent']}> QBitTorrent
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset id="torrent_blackhole_options">
|
<fieldset id="torrent_blackhole_options">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -359,7 +366,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Transmission Password</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<small>Note: With Transmission, you can specify a different download directory for downloads sent from Headphones.
|
<small>Note: With Transmission, you can specify a different download directory for downloads sent from Headphones.
|
||||||
@@ -379,13 +386,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>uTorrent Password</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>uTorrent Label</label>
|
<label>uTorrent Label</label>
|
||||||
<input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30">
|
<input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30">
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset id="qbittorrent_options">
|
||||||
|
<small class="heading"><i class="fa fa-info-circle"></i> Note: Works with WebAPI Rev 11 and later (QBitTorrent 3.4.0 and later) </small>
|
||||||
|
<div class="row">
|
||||||
|
<label>QBitTorrent Host</label>
|
||||||
|
<input type="text" name="qbittorrent_host" value="${config['qbittorrent_host']}" size="30">
|
||||||
|
<small>usually http://localhost:8081</small>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>QBitTorrent Username</label>
|
||||||
|
<input type="text" name="qbittorrent_username" value="${config['qbittorrent_username']}" size="30">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>QBitTorrent Password</label>
|
||||||
|
<input type="password" name="qbittorrent_password" value="${config['qbittorrent_password']}" size="30">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>QBitTorrent Label</label>
|
||||||
|
<input type="text" name="qbittorrent_label" value="${config['qbittorrent_label']}" size="30">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
<fieldset id="deluge_options">
|
<fieldset id="deluge_options">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Deluge WebUI Host and Port</label>
|
<label>Deluge WebUI Host and Port</label>
|
||||||
@@ -400,7 +427,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Deluge Password</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<small>Note: With Deluge, you can specify a different download directory for downloads sent from Headphones.
|
<small>Note: With Deluge, you can specify a different download directory for downloads sent from Headphones.
|
||||||
@@ -466,7 +493,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Password</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Don't have an account? Sign up!</a>
|
<a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Don't have an account? Sign up!</a>
|
||||||
@@ -574,23 +601,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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>
|
<fieldset>
|
||||||
<div class="row checkbox left">
|
<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>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Password</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Seed Ratio</label>
|
<label>Seed Ratio</label>
|
||||||
<input type="text" class="override-float" name="rutracker_ratio" value="${config['rutracker_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
|
<input type="text" class="override-float" name="rutracker_ratio" value="${config['rutracker_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
|
||||||
</div>
|
</div>
|
||||||
|
<div 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>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="row checkbox left">
|
<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>
|
||||||
<div class="config">
|
<div class="config">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Username</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Password</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Seed Ratio</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="row checkbox left">
|
<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>
|
||||||
<div class="config">
|
<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">
|
<div class="row">
|
||||||
<label>Seed Ratio</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -671,13 +701,17 @@
|
|||||||
<div class="config" id="torznab1">
|
<div class="config" id="torznab1">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Torznab Host</label>
|
<label>Torznab Host</label>
|
||||||
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="30">
|
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="36">
|
||||||
<small>e.g. http://localhost:9117/torznab/iptorrents</small>
|
<small>e.g. http://localhost:9117/api/v2.0/indexers/demonoid/results/torznab/</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Torznab API</label>
|
<label>Torznab API</label>
|
||||||
<input type="text" name="torznab_apikey" value="${config['torznab_apikey']}" size="36">
|
<input type="text" name="torznab_apikey" value="${config['torznab_apikey']}" size="36">
|
||||||
</div>
|
</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">
|
<div class="row checkbox">
|
||||||
<input id="torznab_enabled" type="checkbox" name="torznab_enabled" value="1" ${config['torznab_enabled']} /><label>Enabled</label>
|
<input id="torznab_enabled" type="checkbox" name="torznab_enabled" value="1" ${config['torznab_enabled']} /><label>Enabled</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -687,7 +721,7 @@
|
|||||||
%>
|
%>
|
||||||
%for torznab in config['extra_torznabs']:
|
%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"
|
torznab_enabled = "checked"
|
||||||
else:
|
else:
|
||||||
torznab_enabled = ""
|
torznab_enabled = ""
|
||||||
@@ -695,12 +729,16 @@
|
|||||||
<div class="config" id="torznab${torznab_number}">
|
<div class="config" id="torznab${torznab_number}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Torznab Host</label>
|
<label>Torznab Host</label>
|
||||||
<input type="text" name="torznab_host${torznab_number}" value="${torznab[0]}" size="30">
|
<input type="text" name="torznab_host${torznab_number}" value="${torznab[0]}" size="36">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Torznab API</label>
|
<label>Torznab API</label>
|
||||||
<input type="text" name="torznab_api${torznab_number}" value="${torznab[1]}" size="36">
|
<input type="text" name="torznab_api${torznab_number}" value="${torznab[1]}" size="36">
|
||||||
</div>
|
</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">
|
<div class="row checkbox">
|
||||||
<input id="torznab_enabled" type="checkbox" name="torznab_enabled${torznab_number}" value="1" ${torznab_enabled} /><label>Enabled</label>
|
<input id="torznab_enabled" type="checkbox" name="torznab_enabled${torznab_number}" value="1" ${torznab_enabled} /><label>Enabled</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -732,18 +770,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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>
|
</fieldset>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -884,30 +910,42 @@
|
|||||||
<input type="checkbox" name="keep_nfo" value="1" ${config['keep_nfo']} />
|
<input type="checkbox" name="keep_nfo" value="1" ${config['keep_nfo']} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row checkbox left clearfix nopad">
|
||||||
|
<label>
|
||||||
|
Embed lyrics
|
||||||
|
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row checkbox left clearfix nopad">
|
||||||
|
<label>
|
||||||
|
Embed album art in each file
|
||||||
|
<input type="checkbox" name="embed_album_art" id="embed_album_art" value="1" ${config['embed_album_art']}>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row checkbox left clearfix nopad">
|
<div class="row checkbox left clearfix nopad">
|
||||||
<label>
|
<label>
|
||||||
Add album art jpeg to album folder
|
Add album art jpeg to album folder
|
||||||
<input type="checkbox" name="add_album_art" id="add_album_art" value="1" ${config['add_album_art']}>
|
<input type="checkbox" name="add_album_art" id="add_album_art" value="1" ${config['add_album_art']}>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="album_art_options" style="padding-left: 20px">
|
<div id="album_art_options" style="padding-left: 20px">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg
|
as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg
|
||||||
</div>
|
</div>
|
||||||
<small>Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').</small>
|
<small>Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="row checkbox left clearfix nopad">
|
|
||||||
<label>
|
<div id="album_art_size_options" style="padding-left: 20px">
|
||||||
Embed album art in each file
|
<div class="row">
|
||||||
<input type="checkbox" name="embed_album_art" value="1" ${config['embed_album_art']}>
|
Album art min width <input type="text" class="override-float" name="album_art_min_width" value="${config['album_art_min_width']}" size="4" title="Only images with a pixel width greater or equal are considered as valid album art candidates. Default: 0.">\
|
||||||
</label>
|
Album art max width <input type="text" class="override-float" name="album_art_max_width" value="${config['album_art_max_width']}" size="4" title="A maximum image width to downscale fetched images if they are too big.">
|
||||||
</div>
|
</div>
|
||||||
<div class="row checkbox left clearfix nopad">
|
|
||||||
<label>
|
|
||||||
Embed lyrics
|
|
||||||
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>
|
<label>
|
||||||
Destination Directory:
|
Destination Directory:
|
||||||
@@ -964,7 +1002,7 @@
|
|||||||
<label>SMTP User</label><input type="text" name="email_smtp_user" value="${config['email_smtp_user']}" size="254">
|
<label>SMTP User</label><input type="text" name="email_smtp_user" value="${config['email_smtp_user']}" size="254">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<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>
|
||||||
<div class="row checkbox">
|
<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>
|
<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">
|
<label>Growl Host:Port</label><input type="text" name="growl_host" value="${config['growl_host']}" size="30">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<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>
|
||||||
<div class="row checkbox">
|
<div class="row checkbox">
|
||||||
<input type="checkbox" name="growl_onsnatch" value="1" ${config['growl_onsnatch']} /><label>Notify on snatch?</label>
|
<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">
|
<label>Username</label><input type="text" name="xbmc_username" value="${config['xbmc_username']}" size="30">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<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>
|
||||||
<div class="checkbox row">
|
<div class="checkbox row">
|
||||||
<input type="checkbox" name="xbmc_update" value="1" ${config['xbmc_update']} /><label>Update Library</label>
|
<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>
|
<small>Username of your Plex client API (blank for none)</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<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>
|
<small>Password of your Plex client API (blank for none)</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -1226,7 +1264,7 @@
|
|||||||
<label>Subsonic Username</label><input type="text" name="subsonic_username" value="${config['subsonic_username']}" size="30">
|
<label>Subsonic Username</label><input type="text" name="subsonic_username" value="${config['subsonic_username']}" size="30">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -1236,7 +1274,7 @@
|
|||||||
<input type="checkbox" name="synoindex_enabled" id="synoindex" value="1" ${config['synoindex_enabled']} /><label for="synoindex"><span class="option">Synology NAS</span></label>
|
<input type="checkbox" name="synoindex_enabled" id="synoindex" value="1" ${config['synoindex_enabled']} /><label for="synoindex"><span class="option">Synology NAS</span></label>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<!--
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="row checkbox left">
|
<div class="row checkbox left">
|
||||||
<input type="checkbox" class="bigcheck" name="twitter_enabled" id="twitter" value="1" ${config['twitter_enabled']} /><label for="twitter"><span class="option">Twitter</span></label>
|
<input type="checkbox" class="bigcheck" name="twitter_enabled" id="twitter" value="1" ${config['twitter_enabled']} /><label for="twitter"><span class="option">Twitter</span></label>
|
||||||
@@ -1257,6 +1295,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
-->
|
||||||
|
<fieldset>
|
||||||
|
<div class="row checkbox left">
|
||||||
|
<input type="checkbox" class="bigcheck" name="slack_enabled" id="slack" value="1" ${config['slack_enabled']} /><label for="slack"><span class="option">Slack</span></label>
|
||||||
|
</div>
|
||||||
|
<div id="slackoptions">
|
||||||
|
<div class="row">
|
||||||
|
<label>Slack Webhook</label><input type="text" name="slack_url" value="${config['slack_url']}" size="50">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Channel</label><input type="text" name="slack_channel" value="${config['slack_channel']}" size="50">
|
||||||
|
</div>
|
||||||
|
<div class="row checkbox">
|
||||||
|
<label>Emoji</label><input type="text" name="slack_emoji" value="${config['slack_emoji']}" size="50">
|
||||||
|
</div>
|
||||||
|
<div class="row checkbox">
|
||||||
|
<input type="checkbox" name="slack_onsnatch" value="1" ${config['slack_onsnatch']} /><label>Notify on snatch?</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="row checkbox left">
|
<div class="row checkbox left">
|
||||||
@@ -1275,6 +1333,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<div class="row checkbox left">
|
||||||
|
<input type="checkbox" class="bigcheck" name="join_enabled" id="join" value="1" ${config['join_enabled']} /><label for="join"><span class="option">Join</span></label>
|
||||||
|
</div>
|
||||||
|
<div id="joinoptions">
|
||||||
|
<div class="row">
|
||||||
|
<label>Join API Key</label><input type="text" name="join_apikey" value="${config['join_apikey']}" size="50">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Device ID(s)</label><input type="text" name="join_deviceid" value="${config['join_deviceid']}" size="50"><small>Comma separated list. Leave blank to send to all devices</small>
|
||||||
|
</div>
|
||||||
|
<div class="row checkbox">
|
||||||
|
<input type="checkbox" name="join_onsnatch" value="1" ${config['join_onsnatch']} /><label>Notify on snatch?</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -1570,6 +1645,10 @@
|
|||||||
<label>Cache Directory</label>
|
<label>Cache Directory</label>
|
||||||
<input type="text" name="cache_dir" value="${config['cache_dir']}" size="50">
|
<input type="text" name="cache_dir" value="${config['cache_dir']}" size="50">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Post Processing Temporary Directory</label>
|
||||||
|
<input type="text" name="keep_torrent_files_dir" value="${config['keep_torrent_files_dir']}" size="50" title="Enter the directory to preserve files for seeding, default is system temp directory">
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -1626,7 +1705,7 @@
|
|||||||
<label>Username</label><input type="text" class="customuser" name="customuser" value="${config['customuser']}" size="20">
|
<label>Username</label><input type="text" class="customuser" name="customuser" value="${config['customuser']}" size="20">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<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>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -1639,7 +1718,7 @@
|
|||||||
<label>Username</label><input type="text" class="hpuser" name="hpuser" value="${config['hpuser']}" size="20">
|
<label>Username</label><input type="text" class="hpuser" name="hpuser" value="${config['hpuser']}" size="20">
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<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>
|
<a href="https://headphones.codeshy.com/vip" id="vipserver" target="_blank">Get an Account!</a>
|
||||||
</div>
|
</div>
|
||||||
</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"))
|
if ($("#add_album_art").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#album_art_options").show();
|
$("#album_art_options").show();
|
||||||
|
$("#album_art_size_options").show();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1827,10 +1933,15 @@
|
|||||||
if ($("#add_album_art").is(":checked"))
|
if ($("#add_album_art").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#album_art_options").slideDown();
|
$("#album_art_options").slideDown();
|
||||||
|
$("#album_art_size_options").slideDown();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$("#album_art_options").slideUp();
|
$("#album_art_options").slideUp();
|
||||||
|
if (!$("#embed_album_art").is(":checked"))
|
||||||
|
{
|
||||||
|
$("#album_art_size_options").slideUp();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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"))
|
if ($("#twitter").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#twitteroptions").show();
|
$("#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"))
|
if ($("#telegram").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#telegramoptions").show();
|
$("#telegramoptions").show();
|
||||||
@@ -2231,26 +2384,30 @@
|
|||||||
|
|
||||||
if ($("#torrent_downloader_blackhole").is(":checked"))
|
if ($("#torrent_downloader_blackhole").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#transmission_options,#utorrent_options,#deluge_options").hide();
|
$("#transmission_options,#utorrent_options,#deluge_options,#qbittorrent_options").hide();
|
||||||
$("#torrent_blackhole_options").show();
|
$("#torrent_blackhole_options").show();
|
||||||
}
|
}
|
||||||
if ($("#torrent_downloader_transmission").is(":checked"))
|
if ($("#torrent_downloader_transmission").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").hide();
|
$("#torrent_blackhole_options,#utorrent_options,#deluge_options,#qbittorrent_options").hide();
|
||||||
$("#transmission_options").show();
|
$("#transmission_options").show();
|
||||||
}
|
}
|
||||||
if ($("#torrent_downloader_utorrent").is(":checked"))
|
if ($("#torrent_downloader_utorrent").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options").hide();
|
$("#torrent_blackhole_options,#transmission_options,#deluge_options,#qbittorrent_options").hide();
|
||||||
$("#utorrent_options").show();
|
$("#utorrent_options").show();
|
||||||
}
|
}
|
||||||
|
if ($("#torrent_downloader_qbittorrent").is(":checked"))
|
||||||
|
{
|
||||||
|
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#deluge_options").hide();
|
||||||
|
$("#qbittorrent_options").show();
|
||||||
|
}
|
||||||
if ($("#torrent_downloader_deluge").is(":checked"))
|
if ($("#torrent_downloader_deluge").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options").hide();
|
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#qbittorrent_options").hide();
|
||||||
$("#deluge_options").show();
|
$("#deluge_options").show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$('input[type=radio]').change(function(){
|
$('input[type=radio]').change(function(){
|
||||||
if ($("#preferred_bitrate").is(":checked"))
|
if ($("#preferred_bitrate").is(":checked"))
|
||||||
{
|
{
|
||||||
@@ -2286,19 +2443,23 @@
|
|||||||
}
|
}
|
||||||
if ($("#torrent_downloader_blackhole").is(":checked"))
|
if ($("#torrent_downloader_blackhole").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
|
$("#transmission_options,#utorrent_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
|
||||||
}
|
}
|
||||||
if ($("#torrent_downloader_transmission").is(":checked"))
|
if ($("#torrent_downloader_transmission").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
|
$("#torrent_blackhole_options,#utorrent_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
|
||||||
}
|
}
|
||||||
if ($("#torrent_downloader_utorrent").is(":checked"))
|
if ($("#torrent_downloader_utorrent").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
|
$("#torrent_blackhole_options,#transmission_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
|
||||||
}
|
}
|
||||||
|
if ($("#torrent_downloader_qbittorrent").is(":checked"))
|
||||||
|
{
|
||||||
|
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#qbittorrent_options").fadeIn() });
|
||||||
|
}
|
||||||
if ($("#torrent_downloader_deluge").is(":checked"))
|
if ($("#torrent_downloader_deluge").is(":checked"))
|
||||||
{
|
{
|
||||||
$("#torrent_blackhole_options,#utorrent_options,#transmission_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
|
$("#torrent_blackhole_options,#utorrent_options,#transmission_options,#qbittorrent_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2360,7 +2521,7 @@
|
|||||||
|
|
||||||
$("#add_torznab").click(function() {
|
$("#add_torznab").click(function() {
|
||||||
var intId = $("#torznab_providers > div").size() + deletedTorznabs + 1;
|
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>");
|
var removeTorznabButton = $("<div class=\"row\"><input type=\"button\" class=\"remove_torznab\" value=\"Remove\" /></div>");
|
||||||
removeTorznabButton.click(function() {
|
removeTorznabButton.click(function() {
|
||||||
$(this).parent().remove();
|
$(this).parent().remove();
|
||||||
@@ -2389,18 +2550,16 @@
|
|||||||
initConfigCheckbox("#use_nzbsorg");
|
initConfigCheckbox("#use_nzbsorg");
|
||||||
initConfigCheckbox("#use_omgwtfnzbs");
|
initConfigCheckbox("#use_omgwtfnzbs");
|
||||||
initConfigCheckbox("#use_torznab");
|
initConfigCheckbox("#use_torznab");
|
||||||
initConfigCheckbox("#use_kat");
|
|
||||||
initConfigCheckbox("#use_piratebay");
|
initConfigCheckbox("#use_piratebay");
|
||||||
initConfigCheckbox("#use_oldpiratebay");
|
initConfigCheckbox("#use_oldpiratebay");
|
||||||
initConfigCheckbox("#use_mininova");
|
|
||||||
initConfigCheckbox("#use_waffles");
|
initConfigCheckbox("#use_waffles");
|
||||||
initConfigCheckbox("#use_rutracker");
|
initConfigCheckbox("#use_rutracker");
|
||||||
initConfigCheckbox("#use_whatcd");
|
initConfigCheckbox("#use_orpheus");
|
||||||
initConfigCheckbox("#use_strike");
|
initConfigCheckbox("#use_redacted");
|
||||||
initConfigCheckbox("#api_enabled");
|
initConfigCheckbox("#api_enabled");
|
||||||
initConfigCheckbox("#enable_https");
|
initConfigCheckbox("#enable_https");
|
||||||
initConfigCheckbox("#customauth");
|
initConfigCheckbox("#customauth");
|
||||||
|
initConfigCheckbox("#mb_ignore_age_missing");
|
||||||
|
|
||||||
$('#twitterStep1').click(function () {
|
$('#twitterStep1').click(function () {
|
||||||
$.get("/twitterStep1", function (data) {window.open(data); })
|
$.get("/twitterStep1", function (data) {window.open(data); })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<%inherit file="base.html"/>
|
<%inherit file="base.html"/>
|
||||||
<%!
|
<%!
|
||||||
from headphones import helpers
|
from headphones import helpers
|
||||||
import cgi
|
from html import escape as html_escape
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
@@ -62,11 +62,11 @@
|
|||||||
%>
|
%>
|
||||||
<tr class="grade${grade}">
|
<tr class="grade${grade}">
|
||||||
<td id="dateadded">${item['DateAdded']}</td>
|
<td id="dateadded">${item['DateAdded']}</td>
|
||||||
<td id="filename">${cgi.escape(item['Title'], quote=True)} [<a href="${item['URL']}">${fileid}</a>]<a href="albumPage?AlbumID=${item['AlbumID']}">[album page]</a></td>
|
<td id="filename">${html_escape(item['Title'], quote=True)} [<a href="${item['URL']}">${fileid}</a>]<a href="albumPage?AlbumID=${item['AlbumID']}">[album page]</a></td>
|
||||||
<td id="size">${helpers.bytes_to_mb(item['Size'])}</td>
|
<td id="size">${helpers.bytes_to_mb(item['Size'])}</td>
|
||||||
<td title="${folder}" id="status">${item['Status']}</td>
|
<td title="${folder}" id="status">${item['Status']}</td>
|
||||||
<td id="action">[<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&redirect=history', $(this),'table')" data-success="Retrying download of '${cgi.escape(item['Title'], quote=True)}'">retry</a>][<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&new=True&redirect=history',$(this),'table')" data-success="Looking for a new version of '${cgi.escape(item['Title'], quote=True)}'">new</a>]</td>
|
<td id="action">[<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&redirect=history', $(this),'table')" data-success="Retrying download of '${html_escape(item['Title'], quote=True)}'">retry</a>][<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&new=True&redirect=history',$(this),'table')" data-success="Looking for a new version of '${html_escape(item['Title'], quote=True)}'">new</a>]</td>
|
||||||
<td id="delete"><a href="javascript:void(0)" onclick="doAjaxCall('clearhistory?date_added=${item['DateAdded']}&title=${cgi.escape(item['Title'], quote=True)}',$(this),'table')" data-success="${cgi.escape(item['Title'], quote=True)} cleared from history"><img src="interfaces/default/images/trashcan.png" height="18" width="18" id="trashcan" title="Clear this item from the history"></a>
|
<td id="delete"><a href="javascript:void(0)" onclick="doAjaxCall('clearhistory?date_added=${item['DateAdded']}&title=${html_escape(item['Title'], quote=True)}',$(this),'table')" data-success="${html_escape(item['Title'], quote=True)} cleared from history"><img src="interfaces/default/images/trashcan.png" height="18" width="18" id="trashcan" title="Clear this item from the history"></a>
|
||||||
</tr>
|
</tr>
|
||||||
%endfor
|
%endfor
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
|
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],
|
"aTargets": [0],
|
||||||
"mData":"ArtistID",
|
"mData":"ArtistID",
|
||||||
"mRender": function ( data, type, full ) {
|
"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({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
data: dataString,
|
data: dataString,
|
||||||
|
type: 'POST',
|
||||||
beforeSend: function(jqXHR, settings) {
|
beforeSend: function(jqXHR, settings) {
|
||||||
// Start loader etc.
|
// Start loader etc.
|
||||||
feedback.prepend(loader);
|
feedback.prepend(loader);
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ CURRENT_VERSION = None
|
|||||||
LATEST_VERSION = None
|
LATEST_VERSION = None
|
||||||
COMMITS_BEHIND = None
|
COMMITS_BEHIND = None
|
||||||
|
|
||||||
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma"]
|
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma", "opus"]
|
||||||
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff"]
|
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff", "aif"]
|
||||||
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
|
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
|
||||||
|
|
||||||
MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
|
MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
|
||||||
@@ -218,7 +218,7 @@ def daemonize():
|
|||||||
pid = os.fork() # @UndefinedVariable - only available in UNIX
|
pid = os.fork() # @UndefinedVariable - only available in UNIX
|
||||||
if pid != 0:
|
if pid != 0:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
raise RuntimeError("1st fork failed: %s [%d]", e.strerror, e.errno)
|
raise RuntimeError("1st fork failed: %s [%d]", e.strerror, e.errno)
|
||||||
|
|
||||||
os.setsid()
|
os.setsid()
|
||||||
@@ -232,10 +232,10 @@ def daemonize():
|
|||||||
pid = os.fork() # @UndefinedVariable - only available in UNIX
|
pid = os.fork() # @UndefinedVariable - only available in UNIX
|
||||||
if pid != 0:
|
if pid != 0:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
except OSError, e:
|
except OSError as e:
|
||||||
raise RuntimeError("2nd fork failed: %s [%d]", e.strerror, e.errno)
|
raise RuntimeError("2nd fork failed: %s [%d]", e.strerror, e.errno)
|
||||||
|
|
||||||
dev_null = file('/dev/null', 'r')
|
dev_null = open('/dev/null', 'r')
|
||||||
os.dup2(dev_null.fileno(), sys.stdin.fileno())
|
os.dup2(dev_null.fileno(), sys.stdin.fileno())
|
||||||
|
|
||||||
si = open('/dev/null', "r")
|
si = open('/dev/null', "r")
|
||||||
@@ -251,7 +251,7 @@ def daemonize():
|
|||||||
|
|
||||||
if CREATEPID:
|
if CREATEPID:
|
||||||
logger.info("Writing PID %d to %s", pid, PIDFILE)
|
logger.info("Writing PID %d to %s", pid, PIDFILE)
|
||||||
with file(PIDFILE, 'w') as fp:
|
with open(PIDFILE, 'w') as fp:
|
||||||
fp.write("%s\n" % pid)
|
fp.write("%s\n" % pid)
|
||||||
|
|
||||||
|
|
||||||
@@ -306,9 +306,10 @@ def initialize_scheduler():
|
|||||||
minutes=minutes)
|
minutes=minutes)
|
||||||
|
|
||||||
# Remove Torrent + data if Post Processed and finished Seeding
|
# Remove Torrent + data if Post Processed and finished Seeding
|
||||||
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
if headphones.CONFIG.TORRENT_DOWNLOADER != 0:
|
||||||
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
||||||
minutes=minutes)
|
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
||||||
|
minutes=minutes)
|
||||||
|
|
||||||
# Start scheduler
|
# Start scheduler
|
||||||
if start_jobs and len(SCHED.get_jobs()):
|
if start_jobs and len(SCHED.get_jobs()):
|
||||||
@@ -359,6 +360,8 @@ def sig_handler(signum=None, frame=None):
|
|||||||
|
|
||||||
|
|
||||||
def dbcheck():
|
def dbcheck():
|
||||||
|
logger.debug("SQLite Version: %s", sqlite3.sqlite_version)
|
||||||
|
logger.debug("DB-API Version: %s", sqlite3.version)
|
||||||
conn = sqlite3.connect(DB_FILE)
|
conn = sqlite3.connect(DB_FILE)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -376,7 +379,7 @@ def dbcheck():
|
|||||||
c.execute(
|
c.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS alltracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)')
|
'CREATE TABLE IF NOT EXISTS alltracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)')
|
||||||
c.execute(
|
c.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT, Kind TEXT)')
|
'CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT, Kind TEXT, TorrentHash TEXT)')
|
||||||
# Matched is a temporary value used to see if there was a match found in
|
# Matched is a temporary value used to see if there was a match found in
|
||||||
# alltracks
|
# alltracks
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -416,6 +419,33 @@ def dbcheck():
|
|||||||
'CREATE INDEX IF NOT EXISTS tracks_Location ON tracks(Location ASC)')
|
'CREATE INDEX IF NOT EXISTS tracks_Location ON tracks(Location ASC)')
|
||||||
c.execute(
|
c.execute(
|
||||||
'CREATE INDEX IF NOT EXISTS alltracks_Location ON alltracks(Location ASC)')
|
'CREATE INDEX IF NOT EXISTS alltracks_Location ON alltracks(Location ASC)')
|
||||||
|
c.execute(
|
||||||
|
'CREATE INDEX IF NOT EXISTS tracks_artistid ON tracks(ArtistID ASC)')
|
||||||
|
|
||||||
|
# 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:
|
try:
|
||||||
c.execute('SELECT IncludeExtras from artists')
|
c.execute('SELECT IncludeExtras from artists')
|
||||||
@@ -613,6 +643,20 @@ def dbcheck():
|
|||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
c.execute('ALTER TABLE artists ADD COLUMN MetaCritic TEXT DEFAULT NULL')
|
c.execute('ALTER TABLE artists ADD COLUMN MetaCritic TEXT DEFAULT NULL')
|
||||||
|
|
||||||
|
try:
|
||||||
|
c.execute('SELECT TorrentHash from snatched')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
c.execute('ALTER TABLE snatched ADD COLUMN TorrentHash TEXT')
|
||||||
|
c.execute('UPDATE snatched SET TorrentHash = FolderName WHERE Status LIKE "Seed_%"')
|
||||||
|
|
||||||
|
# One off script to set CleanName to lower case
|
||||||
|
clean_name_mixed = c.execute('SELECT CleanName FROM have ORDER BY Date Desc').fetchone()
|
||||||
|
if clean_name_mixed and clean_name_mixed[0] != clean_name_mixed[0].lower():
|
||||||
|
logger.info("Updating track clean name, this could take some time...")
|
||||||
|
c.execute('UPDATE tracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||||
|
c.execute('UPDATE alltracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||||
|
c.execute('UPDATE have SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
c.close()
|
c.close()
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,171 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from headphones import request, db, logger
|
import struct
|
||||||
|
from six.moves.urllib.parse import urlencode
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import headphones
|
||||||
|
from headphones import db, request, logger
|
||||||
|
|
||||||
|
|
||||||
def getAlbumArt(albumid):
|
def getAlbumArt(albumid):
|
||||||
myDB = db.DBConnection()
|
|
||||||
asin = myDB.action(
|
|
||||||
'SELECT AlbumASIN from albums WHERE AlbumID=?', [albumid]).fetchone()[0]
|
|
||||||
|
|
||||||
if asin:
|
artwork_path = None
|
||||||
return 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % asin
|
artwork = None
|
||||||
|
|
||||||
|
# CAA
|
||||||
|
logger.info("Searching for artwork at CAA")
|
||||||
|
artwork_path = 'https://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 = 'https://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 = bytes()
|
||||||
|
url = "https://images.weserv.nl"
|
||||||
|
params = {
|
||||||
|
"url": artwork_path,
|
||||||
|
"w": maxwidth
|
||||||
|
}
|
||||||
|
r = request.request_response(
|
||||||
|
url,
|
||||||
|
params=params,
|
||||||
|
timeout=20,
|
||||||
|
stream=True,
|
||||||
|
whitelist_status_code=404
|
||||||
|
)
|
||||||
|
if r:
|
||||||
|
for chunk in r.iter_content(chunk_size=1024):
|
||||||
|
artwork += chunk
|
||||||
|
r.close()
|
||||||
|
logger.info("Artwork is greater than the maximum width, downsized using proxy service")
|
||||||
|
break
|
||||||
|
resp.close()
|
||||||
|
|
||||||
|
return artwork
|
||||||
|
|
||||||
|
|
||||||
def getCachedArt(albumid):
|
def getCachedArt(albumid):
|
||||||
@@ -34,7 +189,7 @@ def getCachedArt(albumid):
|
|||||||
if not artwork_path:
|
if not artwork_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
if artwork_path.startswith('http://'):
|
if artwork_path.startswith("http"):
|
||||||
artwork = request.request_content(artwork_path, timeout=20)
|
artwork = request.request_content(artwork_path, timeout=20)
|
||||||
|
|
||||||
if not artwork:
|
if not artwork:
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ def switch(AlbumID, ReleaseID):
|
|||||||
|
|
||||||
# Update have track counts on index
|
# Update have track counts on index
|
||||||
totaltracks = len(myDB.select(
|
totaltracks = len(myDB.select(
|
||||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")',
|
'SELECT TrackTitle from tracks AS tr INNER JOIN albums AS al ON al.AlbumID = tr.AlbumID WHERE al.ArtistID=? '
|
||||||
[newalbumdata['ArtistID']]))
|
'AND al.Status != "Ignored"', [newalbumdata['ArtistID']]))
|
||||||
havetracks = len(myDB.select(
|
havetracks = len(myDB.select(
|
||||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
||||||
[newalbumdata['ArtistID']]))
|
[newalbumdata['ArtistID']]))
|
||||||
|
|||||||
@@ -82,11 +82,11 @@ class Api(object):
|
|||||||
def fetchData(self):
|
def fetchData(self):
|
||||||
|
|
||||||
if self.data == 'OK':
|
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 = getattr(self, "_" + self.cmd)
|
||||||
methodToCall(**self.kwargs)
|
methodToCall(**self.kwargs)
|
||||||
if 'callback' not in self.kwargs:
|
if 'callback' not in self.kwargs:
|
||||||
if isinstance(self.data, basestring):
|
if isinstance(self.data, str):
|
||||||
return self.data
|
return self.data
|
||||||
else:
|
else:
|
||||||
return json.dumps(self.data)
|
return json.dumps(self.data)
|
||||||
@@ -106,7 +106,7 @@ class Api(object):
|
|||||||
rows_as_dic = []
|
rows_as_dic = []
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
row_as_dic = dict(zip(row.keys(), row))
|
row_as_dic = dict(list(zip(list(row.keys()), row)))
|
||||||
rows_as_dic.append(row_as_dic)
|
rows_as_dic.append(row_as_dic)
|
||||||
|
|
||||||
return rows_as_dic
|
return rows_as_dic
|
||||||
@@ -474,17 +474,17 @@ class Api(object):
|
|||||||
# Handle situations where the torrent url contains arguments that are
|
# Handle situations where the torrent url contains arguments that are
|
||||||
# parsed
|
# parsed
|
||||||
if kwargs:
|
if kwargs:
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
import urllib2
|
import urllib.request, urllib.error, urllib.parse
|
||||||
url = urllib2.quote(
|
url = urllib.parse.quote(
|
||||||
url, safe=":?/=&") + '&' + urllib.urlencode(kwargs)
|
url, safe=":?/=&") + '&' + urllib.parse.urlencode(kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = [(title, int(size), url, provider, kind)]
|
result = [(title, int(size), url, provider, kind)]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
result = [(title, float(size), url, provider, kind)]
|
result = [(title, float(size), url, provider, kind)]
|
||||||
|
|
||||||
logger.info(u"Making sure we can download the chosen result")
|
logger.info("Making sure we can download the chosen result")
|
||||||
(data, bestqual) = searcher.preprocess(result)
|
(data, bestqual) = searcher.preprocess(result)
|
||||||
|
|
||||||
if data and bestqual:
|
if data and bestqual:
|
||||||
|
|||||||
@@ -14,11 +14,16 @@
|
|||||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from six.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
import headphones
|
import headphones
|
||||||
from headphones import db, helpers, logger, lastfm, request
|
from headphones import db, helpers, logger, lastfm, request, mb
|
||||||
|
|
||||||
LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
|
LASTFM_API_KEY = "8d983789c771afaeb7412ac358d4bad0"
|
||||||
|
|
||||||
|
FANART_URL = 'https://webservice.fanart.tv/v3/music/'
|
||||||
|
FANART_PROJECT_KEY = '22b73c9603eba09d0c855f2d2bdba31c'
|
||||||
|
FANART_CLIENT_KEY = '919b389a18a3f0b2c916090022ab3c7a'
|
||||||
|
|
||||||
|
|
||||||
class Cache(object):
|
class Cache(object):
|
||||||
@@ -96,6 +101,11 @@ class Cache(object):
|
|||||||
def _is_current(self, filename=None, date=None):
|
def _is_current(self, filename=None, date=None):
|
||||||
|
|
||||||
if filename:
|
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)
|
base_filename = os.path.basename(filename)
|
||||||
date = base_filename.split('.')[1]
|
date = base_filename.split('.')[1]
|
||||||
|
|
||||||
@@ -211,20 +221,34 @@ class Cache(object):
|
|||||||
if ArtistID:
|
if ArtistID:
|
||||||
|
|
||||||
self.id_type = 'artist'
|
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:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
if data.get('artistthumb'):
|
||||||
image_url = data['artist']['image'][-1]['#text']
|
image_url = data['artistthumb'][0]['url']
|
||||||
except (KeyError, IndexError):
|
elif data.get('artistbackground'):
|
||||||
logger.debug('No artist image found')
|
image_url = data['artistbackground'][0]['url']
|
||||||
image_url = None
|
# elif data.get('hdmusiclogo'):
|
||||||
|
# image_url = data['hdmusiclogo'][0]['url']
|
||||||
|
|
||||||
thumb_url = self._get_thumb_url(data)
|
# fallback to 1st album cover if none of the above
|
||||||
if not thumb_url:
|
elif 'albums' in data:
|
||||||
logger.debug('No artist thumbnail image found')
|
for mbid, art in list(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:
|
else:
|
||||||
|
|
||||||
@@ -283,6 +307,7 @@ class Cache(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
myDB = db.DBConnection()
|
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
|
# 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
|
# 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)
|
data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=LASTFM_API_KEY)
|
||||||
|
|
||||||
|
# Try with name if not found
|
||||||
|
if not data:
|
||||||
|
dbartist = myDB.action('SELECT ArtistName, Type FROM artists WHERE ArtistID=?', [self.id]).fetchone()
|
||||||
|
if dbartist:
|
||||||
|
data = lastfm.request_lastfm("artist.getinfo",
|
||||||
|
artist=helpers.clean_musicbrainz_name(dbartist['ArtistName']),
|
||||||
|
api_key=LASTFM_API_KEY)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -303,30 +336,70 @@ class Cache(object):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
logger.debug('No artist bio found')
|
logger.debug('No artist bio found')
|
||||||
self.info_content = None
|
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)
|
# 2019 last.fm no longer allows access to artist images, try fanart.tv instead
|
||||||
if not thumb_url:
|
image_url = None
|
||||||
logger.debug('No artist thumbnail image found')
|
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 list(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:
|
else:
|
||||||
dbalbum = myDB.action(
|
dbalbum = myDB.action(
|
||||||
'SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?',
|
'SELECT ArtistName, AlbumTitle, ReleaseID, Type FROM albums WHERE AlbumID=?',
|
||||||
[self.id]).fetchone()
|
[self.id]).fetchone()
|
||||||
if dbalbum['ReleaseID'] != self.id:
|
if dbalbum['ReleaseID'] != self.id:
|
||||||
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'],
|
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'],
|
||||||
api_key=LASTFM_API_KEY)
|
api_key=LASTFM_API_KEY)
|
||||||
if not data:
|
if not data:
|
||||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
data = lastfm.request_lastfm("album.getinfo",
|
||||||
album=dbalbum['AlbumTitle'],
|
artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
|
||||||
|
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||||
api_key=LASTFM_API_KEY)
|
api_key=LASTFM_API_KEY)
|
||||||
else:
|
else:
|
||||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
if dbalbum['Type'] != "part of":
|
||||||
album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
|
data = lastfm.request_lastfm("album.getinfo",
|
||||||
|
artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
|
||||||
|
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||||
|
api_key=LASTFM_API_KEY)
|
||||||
|
else:
|
||||||
|
|
||||||
|
# Series, use actual artist for the release-group
|
||||||
|
artist = mb.getArtistForReleaseGroup(self.id)
|
||||||
|
if artist:
|
||||||
|
data = lastfm.request_lastfm("album.getinfo",
|
||||||
|
artist=helpers.clean_musicbrainz_name(artist),
|
||||||
|
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||||
|
api_key=LASTFM_API_KEY)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
@@ -406,7 +479,11 @@ class Cache(object):
|
|||||||
|
|
||||||
ext = os.path.splitext(image_url)[1]
|
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)
|
self.id + '.' + helpers.today() + ext)
|
||||||
try:
|
try:
|
||||||
with open(artwork_path, 'wb') as f:
|
with open(artwork_path, 'wb') as f:
|
||||||
@@ -422,7 +499,9 @@ class Cache(object):
|
|||||||
# as it's missing/outdated.
|
# as it's missing/outdated.
|
||||||
if thumb_url and self.query_type in ['thumb', 'artwork'] and not (
|
if thumb_url and self.query_type in ['thumb', 'artwork'] and not (
|
||||||
self.thumb_files and self._is_current(self.thumb_files[0])):
|
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:
|
if artwork:
|
||||||
# Make sure the artwork dir exists:
|
# Make sure the artwork dir exists:
|
||||||
@@ -445,11 +524,39 @@ class Cache(object):
|
|||||||
|
|
||||||
ext = os.path.splitext(image_url)[1]
|
ext = os.path.splitext(image_url)[1]
|
||||||
|
|
||||||
thumb_path = os.path.join(self.path_to_art_cache,
|
if fanart:
|
||||||
'T_' + self.id + '.' + helpers.today() + ext)
|
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:
|
try:
|
||||||
with open(thumb_path, 'wb') as f:
|
if self.id_type != 'artist':
|
||||||
f.write(artwork)
|
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
|
||||||
|
url = "https://images.weserv.nl"
|
||||||
|
params = {
|
||||||
|
"url": thumb_url,
|
||||||
|
"w": 300
|
||||||
|
}
|
||||||
|
artwork_thumb = request.request_content(
|
||||||
|
url,
|
||||||
|
params=params,
|
||||||
|
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))
|
os.chmod(thumb_path, int(headphones.CONFIG.FILE_PERMISSIONS, 8))
|
||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e:
|
||||||
@@ -465,7 +572,7 @@ def getArtwork(ArtistID=None, AlbumID=None):
|
|||||||
if not artwork_path:
|
if not artwork_path:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if artwork_path.startswith('http://'):
|
if artwork_path.startswith(('http://', 'https://')):
|
||||||
return artwork_path
|
return artwork_path
|
||||||
else:
|
else:
|
||||||
artwork_file = os.path.basename(artwork_path)
|
artwork_file = os.path.basename(artwork_path)
|
||||||
@@ -479,7 +586,7 @@ def getThumb(ArtistID=None, AlbumID=None):
|
|||||||
if not artwork_path:
|
if not artwork_path:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if artwork_path.startswith('http://'):
|
if artwork_path.startswith(('http://', 'https://')):
|
||||||
return artwork_path
|
return artwork_path
|
||||||
else:
|
else:
|
||||||
thumbnail_file = os.path.basename(artwork_path)
|
thumbnail_file = os.path.basename(artwork_path)
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
#######################################
|
#######################################
|
||||||
|
|
||||||
|
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
|
|
||||||
from common import USER_AGENT
|
from .common import USER_AGENT
|
||||||
|
|
||||||
|
|
||||||
class HeadphonesURLopener(urllib.FancyURLopener):
|
class HeadphonesURLopener(urllib.request.FancyURLopener):
|
||||||
version = USER_AGENT
|
version = USER_AGENT
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ class AuthURLOpener(HeadphonesURLopener):
|
|||||||
self.numTries = 0
|
self.numTries = 0
|
||||||
|
|
||||||
# call the base class
|
# call the base class
|
||||||
urllib.FancyURLopener.__init__(self)
|
urllib.request.FancyURLopener.__init__(self)
|
||||||
|
|
||||||
def prompt_user_passwd(self, host, realm):
|
def prompt_user_passwd(self, host, realm):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import operator
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from headphones import version
|
from headphones import version
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
|
||||||
# Identify Our Application
|
# Identify Our Application
|
||||||
@@ -74,7 +75,7 @@ class Quality:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _getStatusStrings(status):
|
def _getStatusStrings(status):
|
||||||
toReturn = {}
|
toReturn = {}
|
||||||
for x in Quality.qualityStrings.keys():
|
for x in list(Quality.qualityStrings.keys()):
|
||||||
toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status] + " (" + \
|
toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status] + " (" + \
|
||||||
Quality.qualityStrings[x] + ")"
|
Quality.qualityStrings[x] + ")"
|
||||||
return toReturn
|
return toReturn
|
||||||
@@ -93,7 +94,7 @@ class Quality:
|
|||||||
def splitQuality(quality):
|
def splitQuality(quality):
|
||||||
anyQualities = []
|
anyQualities = []
|
||||||
bestQualities = []
|
bestQualities = []
|
||||||
for curQual in Quality.qualityStrings.keys():
|
for curQual in list(Quality.qualityStrings.keys()):
|
||||||
if curQual & quality:
|
if curQual & quality:
|
||||||
anyQualities.append(curQual)
|
anyQualities.append(curQual)
|
||||||
if curQual << 16 & quality:
|
if curQual << 16 & quality:
|
||||||
@@ -151,7 +152,7 @@ class Quality:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def splitCompositeStatus(status):
|
def splitCompositeStatus(status):
|
||||||
"""Returns a tuple containing (status, quality)"""
|
"""Returns a tuple containing (status, quality)"""
|
||||||
for x in sorted(Quality.qualityStrings.keys(), reverse=True):
|
for x in sorted(list(Quality.qualityStrings.keys()), reverse=True):
|
||||||
if status > x * 100:
|
if status > x * 100:
|
||||||
return (status - x * 100, x)
|
return (status - x * 100, x)
|
||||||
|
|
||||||
@@ -169,10 +170,10 @@ class Quality:
|
|||||||
SNATCHED_PROPER = None
|
SNATCHED_PROPER = None
|
||||||
|
|
||||||
|
|
||||||
Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()]
|
Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in list(Quality.qualityStrings.keys())]
|
||||||
Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()]
|
Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in list(Quality.qualityStrings.keys())]
|
||||||
Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in
|
Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in
|
||||||
Quality.qualityStrings.keys()]
|
list(Quality.qualityStrings.keys())]
|
||||||
|
|
||||||
MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], [])
|
MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], [])
|
||||||
LOSSLESS = Quality.combineQualities([Quality.FLAC], [])
|
LOSSLESS = Quality.combineQualities([Quality.FLAC], [])
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ import itertools
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import ast
|
||||||
|
from configparser import ConfigParser
|
||||||
import headphones.logger
|
import headphones.logger
|
||||||
from configobj import ConfigObj
|
|
||||||
|
|
||||||
|
|
||||||
def bool_int(value):
|
def bool_int(value):
|
||||||
"""
|
"""
|
||||||
Casts a config value into a 0 or 1
|
Casts a config value into a 0 or 1
|
||||||
"""
|
"""
|
||||||
if isinstance(value, basestring):
|
if isinstance(value, str):
|
||||||
if value.lower() in ('', '0', 'false', 'f', 'no', 'n', 'off'):
|
if value.lower() in ('', '0', 'false', 'f', 'no', 'n', 'off'):
|
||||||
value = 0
|
value = 0
|
||||||
return int(bool(value))
|
return int(bool(value))
|
||||||
@@ -34,11 +35,18 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'ADD_ALBUM_ART': (int, 'General', 0),
|
'ADD_ALBUM_ART': (int, 'General', 0),
|
||||||
'ADVANCEDENCODER': (str, 'General', ''),
|
'ADVANCEDENCODER': (str, 'General', ''),
|
||||||
'ALBUM_ART_FORMAT': (str, 'General', 'folder'),
|
'ALBUM_ART_FORMAT': (str, 'General', 'folder'),
|
||||||
|
'ALBUM_ART_MIN_WIDTH': (str, 'General', ''),
|
||||||
|
'ALBUM_ART_MAX_WIDTH': (str, 'General', ''),
|
||||||
# This is used in importer.py to determine how complete an album needs to
|
# This is used in importer.py to determine how complete an album needs to
|
||||||
# be - to be considered "downloaded". Percentage from 0-100
|
# be - to be considered "downloaded". Percentage from 0-100
|
||||||
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
|
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
|
||||||
'API_ENABLED': (int, 'General', 0),
|
'API_ENABLED': (int, 'General', 0),
|
||||||
'API_KEY': (str, 'General', ''),
|
'API_KEY': (str, 'General', ''),
|
||||||
|
'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_ALL': (int, 'General', 0),
|
||||||
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
|
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
|
||||||
'AUTOWANT_UPCOMING': (int, 'General', 1),
|
'AUTOWANT_UPCOMING': (int, 'General', 1),
|
||||||
@@ -138,12 +146,14 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'IGNORED_FILES': (list, 'Advanced', []), # path
|
'IGNORED_FILES': (list, 'Advanced', []), # path
|
||||||
'INCLUDE_EXTRAS': (int, 'General', 0),
|
'INCLUDE_EXTRAS': (int, 'General', 0),
|
||||||
'INTERFACE': (str, 'General', 'default'),
|
'INTERFACE': (str, 'General', 'default'),
|
||||||
|
'JOIN_APIKEY': (str, 'Join', ''),
|
||||||
|
'JOIN_DEVICEID': (str, 'Join', ''),
|
||||||
|
'JOIN_ENABLED': (int, 'Join', 0),
|
||||||
|
'JOIN_ONSNATCH': (int, 'Join', 0),
|
||||||
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
||||||
'KAT': (int, 'Kat', 0),
|
|
||||||
'KAT_PROXY_URL': (str, 'Kat', ''),
|
|
||||||
'KAT_RATIO': (str, 'Kat', ''),
|
|
||||||
'KEEP_NFO': (int, 'General', 0),
|
'KEEP_NFO': (int, 'General', 0),
|
||||||
'KEEP_TORRENT_FILES': (int, 'General', 0),
|
'KEEP_TORRENT_FILES': (int, 'General', 0),
|
||||||
|
'KEEP_TORRENT_FILES_DIR': (path, 'General', ''),
|
||||||
'LASTFM_USERNAME': (str, 'General', ''),
|
'LASTFM_USERNAME': (str, 'General', ''),
|
||||||
'LAUNCH_BROWSER': (int, 'General', 1),
|
'LAUNCH_BROWSER': (int, 'General', 1),
|
||||||
'LIBRARYSCAN': (int, 'General', 1),
|
'LIBRARYSCAN': (int, 'General', 1),
|
||||||
@@ -155,8 +165,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'LOSSLESS_BITRATE_TO': (int, 'General', 0),
|
'LOSSLESS_BITRATE_TO': (int, 'General', 0),
|
||||||
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
|
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
|
||||||
'MB_IGNORE_AGE': (int, 'General', 365),
|
'MB_IGNORE_AGE': (int, 'General', 365),
|
||||||
'MININOVA': (int, 'Mininova', 0),
|
'MB_IGNORE_AGE_MISSING': (int, 'General', 0),
|
||||||
'MININOVA_RATIO': (str, 'Mininova', ''),
|
|
||||||
'MIRROR': (str, 'General', 'musicbrainz.org'),
|
'MIRROR': (str, 'General', 'musicbrainz.org'),
|
||||||
'MOVE_FILES': (int, 'General', 0),
|
'MOVE_FILES': (int, 'General', 0),
|
||||||
'MPC_ENABLED': (bool_int, 'MPC', False),
|
'MPC_ENABLED': (bool_int, 'MPC', False),
|
||||||
@@ -226,6 +235,10 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
||||||
'PUSHOVER_ONSNATCH': (int, 'Pushover', 0),
|
'PUSHOVER_ONSNATCH': (int, 'Pushover', 0),
|
||||||
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
||||||
|
'QBITTORRENT_HOST': (str, 'QBitTorrent', ''),
|
||||||
|
'QBITTORRENT_LABEL': (str, 'QBitTorrent', ''),
|
||||||
|
'QBITTORRENT_PASSWORD': (str, 'QBitTorrent', ''),
|
||||||
|
'QBITTORRENT_USERNAME': (str, 'QBitTorrent', ''),
|
||||||
'RENAME_FILES': (int, 'General', 0),
|
'RENAME_FILES': (int, 'General', 0),
|
||||||
'RENAME_UNPROCESSED': (bool_int, 'General', 1),
|
'RENAME_UNPROCESSED': (bool_int, 'General', 1),
|
||||||
'RENAME_FROZEN': (bool_int, 'General', 1),
|
'RENAME_FROZEN': (bool_int, 'General', 1),
|
||||||
@@ -236,6 +249,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
|
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
|
||||||
'RUTRACKER_RATIO': (str, 'Rutracker', ''),
|
'RUTRACKER_RATIO': (str, 'Rutracker', ''),
|
||||||
'RUTRACKER_USER': (str, 'Rutracker', ''),
|
'RUTRACKER_USER': (str, 'Rutracker', ''),
|
||||||
|
'RUTRACKER_COOKIE': (str, 'Rutracker', ''),
|
||||||
'SAB_APIKEY': (str, 'SABnzbd', ''),
|
'SAB_APIKEY': (str, 'SABnzbd', ''),
|
||||||
'SAB_CATEGORY': (str, 'SABnzbd', ''),
|
'SAB_CATEGORY': (str, 'SABnzbd', ''),
|
||||||
'SAB_HOST': (str, 'SABnzbd', ''),
|
'SAB_HOST': (str, 'SABnzbd', ''),
|
||||||
@@ -243,13 +257,16 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'SAB_USERNAME': (str, 'SABnzbd', ''),
|
'SAB_USERNAME': (str, 'SABnzbd', ''),
|
||||||
'SAMPLINGFREQUENCY': (int, 'General', 44100),
|
'SAMPLINGFREQUENCY': (int, 'General', 44100),
|
||||||
'SEARCH_INTERVAL': (int, 'General', 1440),
|
'SEARCH_INTERVAL': (int, 'General', 1440),
|
||||||
|
'SLACK_ENABLED': (int, 'Slack', 0),
|
||||||
|
'SLACK_URL': (str, 'Slack', ''),
|
||||||
|
'SLACK_CHANNEL': (str, 'Slack', ''),
|
||||||
|
'SLACK_EMOJI': (str, 'Slack', ''),
|
||||||
|
'SLACK_ONSNATCH': (int, 'Slack', 0),
|
||||||
'SOFT_CHROOT': (path, 'General', ''),
|
'SOFT_CHROOT': (path, 'General', ''),
|
||||||
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
|
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
|
||||||
'SONGKICK_ENABLED': (int, 'Songkick', 1),
|
'SONGKICK_ENABLED': (int, 'Songkick', 1),
|
||||||
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
|
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
|
||||||
'SONGKICK_LOCATION': (str, 'Songkick', ''),
|
'SONGKICK_LOCATION': (str, 'Songkick', ''),
|
||||||
'STRIKE': (int, 'Strike', 0),
|
|
||||||
'STRIKE_RATIO': (str, 'Strike', ''),
|
|
||||||
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
|
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
|
||||||
'SUBSONIC_HOST': (str, 'Subsonic', ''),
|
'SUBSONIC_HOST': (str, 'Subsonic', ''),
|
||||||
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
|
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
|
||||||
@@ -266,6 +283,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'TORZNAB_APIKEY': (str, 'Torznab', ''),
|
'TORZNAB_APIKEY': (str, 'Torznab', ''),
|
||||||
'TORZNAB_ENABLED': (int, 'Torznab', 1),
|
'TORZNAB_ENABLED': (int, 'Torznab', 1),
|
||||||
'TORZNAB_HOST': (str, 'Torznab', ''),
|
'TORZNAB_HOST': (str, 'Torznab', ''),
|
||||||
|
'TORZNAB_RATIO': (str, 'Torznab', ''),
|
||||||
'TRANSMISSION_HOST': (str, 'Transmission', ''),
|
'TRANSMISSION_HOST': (str, 'Transmission', ''),
|
||||||
'TRANSMISSION_PASSWORD': (str, 'Transmission', ''),
|
'TRANSMISSION_PASSWORD': (str, 'Transmission', ''),
|
||||||
'TRANSMISSION_USERNAME': (str, 'Transmission', ''),
|
'TRANSMISSION_USERNAME': (str, 'Transmission', ''),
|
||||||
@@ -286,10 +304,11 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'WAFFLES_PASSKEY': (str, 'Waffles', ''),
|
'WAFFLES_PASSKEY': (str, 'Waffles', ''),
|
||||||
'WAFFLES_RATIO': (str, 'Waffles', ''),
|
'WAFFLES_RATIO': (str, 'Waffles', ''),
|
||||||
'WAFFLES_UID': (str, 'Waffles', ''),
|
'WAFFLES_UID': (str, 'Waffles', ''),
|
||||||
'WHATCD': (int, 'What.cd', 0),
|
'REDACTED': (int, 'Redacted', 0),
|
||||||
'WHATCD_PASSWORD': (str, 'What.cd', ''),
|
'REDACTED_USERNAME': (str, 'Redacted', ''),
|
||||||
'WHATCD_RATIO': (str, 'What.cd', ''),
|
'REDACTED_PASSWORD': (str, 'Redacted', ''),
|
||||||
'WHATCD_USERNAME': (str, 'What.cd', ''),
|
'REDACTED_RATIO': (str, 'Redacted', ''),
|
||||||
|
'REDACTED_USE_FLTOKEN': (int, 'Redacted', 0),
|
||||||
'XBMC_ENABLED': (int, 'XBMC', 0),
|
'XBMC_ENABLED': (int, 'XBMC', 0),
|
||||||
'XBMC_HOST': (str, 'XBMC', ''),
|
'XBMC_HOST': (str, 'XBMC', ''),
|
||||||
'XBMC_NOTIFY': (int, 'XBMC', 0),
|
'XBMC_NOTIFY': (int, 'XBMC', 0),
|
||||||
@@ -308,8 +327,9 @@ class Config(object):
|
|||||||
def __init__(self, config_file):
|
def __init__(self, config_file):
|
||||||
""" Initialize the config with values from a file """
|
""" Initialize the config with values from a file """
|
||||||
self._config_file = config_file
|
self._config_file = config_file
|
||||||
self._config = ConfigObj(self._config_file, encoding='utf-8')
|
self._config = ConfigParser()
|
||||||
for key in _CONFIG_DEFINITIONS.keys():
|
self._config.read(self._config_file)
|
||||||
|
for key in list(_CONFIG_DEFINITIONS.keys()):
|
||||||
self.check_setting(key)
|
self.check_setting(key)
|
||||||
self.ENCODER_MULTICORE_COUNT = max(0, self.ENCODER_MULTICORE_COUNT)
|
self.ENCODER_MULTICORE_COUNT = max(0, self.ENCODER_MULTICORE_COUNT)
|
||||||
self._upgrade()
|
self._upgrade()
|
||||||
@@ -326,7 +346,7 @@ class Config(object):
|
|||||||
|
|
||||||
def check_section(self, section):
|
def check_section(self, section):
|
||||||
""" Check if INI section exists, if not create it """
|
""" Check if INI section exists, if not create it """
|
||||||
if section not in self._config:
|
if not self._config.has_section(section):
|
||||||
self._config[section] = {}
|
self._config[section] = {}
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -336,28 +356,38 @@ class Config(object):
|
|||||||
""" Cast any value in the config to the right type or use the default """
|
""" Cast any value in the config to the right type or use the default """
|
||||||
key, definition_type, section, ini_key, default = self._define(key)
|
key, definition_type, section, ini_key, default = self._define(key)
|
||||||
self.check_section(section)
|
self.check_section(section)
|
||||||
|
|
||||||
|
# ConfigParser values are strings, so need to convert to actual list
|
||||||
|
if definition_type == list:
|
||||||
|
definition_type = ast.literal_eval
|
||||||
|
|
||||||
try:
|
try:
|
||||||
my_val = definition_type(self._config[section][ini_key])
|
my_val = definition_type(self._config[section][ini_key])
|
||||||
|
# ConfigParser interprets empty strings in the config
|
||||||
|
# literally, so we need to sanitize it. It's not really
|
||||||
|
# a config upgrade, since a user can at any time put
|
||||||
|
# some_key = ''
|
||||||
|
if my_val == '""' or my_val == "''":
|
||||||
|
my_val = ''
|
||||||
except Exception:
|
except Exception:
|
||||||
my_val = definition_type(default)
|
my_val = default
|
||||||
self._config[section][ini_key] = my_val
|
self._config[section][ini_key] = str(my_val)
|
||||||
return my_val
|
return my_val
|
||||||
|
|
||||||
def write(self):
|
def write(self):
|
||||||
""" Make a copy of the stored config and write it to the configured file """
|
""" Make a copy of the stored config and write it to the configured file """
|
||||||
new_config = ConfigObj(encoding="UTF-8")
|
new_config = ConfigParser()
|
||||||
new_config.filename = self._config_file
|
|
||||||
|
|
||||||
# first copy over everything from the old config, even if it is not
|
# first copy over everything from the old config, even if it is not
|
||||||
# correctly defined to keep from losing data
|
# correctly defined to keep from losing data
|
||||||
for key, subkeys in self._config.items():
|
for key, subkeys in list(self._config.items()):
|
||||||
if key not in new_config:
|
if key not in new_config:
|
||||||
new_config[key] = {}
|
new_config[key] = {}
|
||||||
for subkey, value in subkeys.items():
|
for subkey, value in list(subkeys.items()):
|
||||||
new_config[key][subkey] = value
|
new_config[key][subkey] = value
|
||||||
|
|
||||||
# next make sure that everything we expect to have defined is so
|
# next make sure that everything we expect to have defined is so
|
||||||
for key in _CONFIG_DEFINITIONS.keys():
|
for key in list(_CONFIG_DEFINITIONS.keys()):
|
||||||
key, definition_type, section, ini_key, default = self._define(key)
|
key, definition_type, section, ini_key, default = self._define(key)
|
||||||
self.check_setting(key)
|
self.check_setting(key)
|
||||||
if section not in new_config:
|
if section not in new_config:
|
||||||
@@ -368,14 +398,15 @@ class Config(object):
|
|||||||
headphones.logger.info("Writing configuration to file")
|
headphones.logger.info("Writing configuration to file")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_config.write()
|
with open(self._config_file, 'w') as configfile:
|
||||||
|
new_config.write(configfile)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
headphones.logger.error("Error writing configuration file: %s", e)
|
headphones.logger.error("Error writing configuration file: %s", e)
|
||||||
|
|
||||||
def get_extra_newznabs(self):
|
def get_extra_newznabs(self):
|
||||||
""" Return the extra newznab tuples """
|
""" Return the extra newznab tuples """
|
||||||
extra_newznabs = list(
|
extra_newznabs = list(
|
||||||
itertools.izip(*[itertools.islice(self.EXTRA_NEWZNABS, i, None, 3)
|
zip(*[itertools.islice(self.EXTRA_NEWZNABS, i, None, 3)
|
||||||
for i in range(3)])
|
for i in range(3)])
|
||||||
)
|
)
|
||||||
return extra_newznabs
|
return extra_newznabs
|
||||||
@@ -394,8 +425,8 @@ class Config(object):
|
|||||||
def get_extra_torznabs(self):
|
def get_extra_torznabs(self):
|
||||||
""" Return the extra torznab tuples """
|
""" Return the extra torznab tuples """
|
||||||
extra_torznabs = list(
|
extra_torznabs = list(
|
||||||
itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 3)
|
zip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 4)
|
||||||
for i in range(3)])
|
for i in range(4)])
|
||||||
)
|
)
|
||||||
return extra_torznabs
|
return extra_torznabs
|
||||||
|
|
||||||
@@ -430,20 +461,21 @@ class Config(object):
|
|||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
key, definition_type, section, ini_key, default = self._define(name)
|
key, definition_type, section, ini_key, default = self._define(name)
|
||||||
self._config[section][ini_key] = definition_type(value)
|
self._config[section][ini_key] = str(value)
|
||||||
return self._config[section][ini_key]
|
return self._config[section][ini_key]
|
||||||
|
|
||||||
def process_kwargs(self, kwargs):
|
def process_kwargs(self, kwargs):
|
||||||
"""
|
"""
|
||||||
Given a big bunch of key value pairs, apply them to the ini.
|
Given a big bunch of key value pairs, apply them to the ini.
|
||||||
"""
|
"""
|
||||||
for name, value in kwargs.items():
|
for name, value in list(kwargs.items()):
|
||||||
key, definition_type, section, ini_key, default = self._define(name)
|
key, definition_type, section, ini_key, default = self._define(name)
|
||||||
self._config[section][ini_key] = definition_type(value)
|
self._config[section][ini_key] = str(value)
|
||||||
|
|
||||||
def _upgrade(self):
|
def _upgrade(self):
|
||||||
"""
|
"""
|
||||||
Bring old configs up to date
|
Bring old configs up to date. Although this is kind of a dumb
|
||||||
|
way to do it because it doesn't handle multi-step upgrades
|
||||||
"""
|
"""
|
||||||
if self.CONFIG_VERSION == '2':
|
if self.CONFIG_VERSION == '2':
|
||||||
# Update the config to use direct path to the encoder rather than the encoder folder
|
# Update the config to use direct path to the encoder rather than the encoder folder
|
||||||
@@ -466,4 +498,17 @@ class Config(object):
|
|||||||
if self.CONFIG_VERSION == '5':
|
if self.CONFIG_VERSION == '5':
|
||||||
if self.OPEN_MAGNET_LINKS:
|
if self.OPEN_MAGNET_LINKS:
|
||||||
self.MAGNET_LINKS = 2
|
self.MAGNET_LINKS = 2
|
||||||
self.CONFIG_VERSION = '5'
|
|
||||||
|
# Add Seed Ratio to Torznabs
|
||||||
|
if self.EXTRA_TORZNABS:
|
||||||
|
extra_torznabs = list(
|
||||||
|
zip(*[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], '', torznab[2]])
|
||||||
|
if new_torznabs:
|
||||||
|
self.EXTRA_TORZNABS = new_torznabs
|
||||||
|
|
||||||
|
self.CONFIG_VERSION = '6'
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import mock
|
|||||||
from mock import MagicMock
|
from mock import MagicMock
|
||||||
import headphones.config
|
import headphones.config
|
||||||
import re
|
import re
|
||||||
import unittestcompat
|
from . import unittestcompat
|
||||||
from unittestcompat import TestCase, TestArgs
|
from .unittestcompat import TestCase, TestArgs
|
||||||
|
|
||||||
|
|
||||||
class ConfigApiTest(TestCase):
|
class ConfigApiTest(TestCase):
|
||||||
@@ -101,7 +101,7 @@ class ConfigApiTest(TestCase):
|
|||||||
# call methods
|
# call methods
|
||||||
c = headphones.config.Config(path)
|
c = headphones.config.Config(path)
|
||||||
# assertions:
|
# assertions:
|
||||||
with self.assertRaisesRegexp(KeyError, exc_regex):
|
with self.assertRaisesRegex(KeyError, exc_regex):
|
||||||
c.check_setting(setting_name)
|
c.check_setting(setting_name)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -355,86 +355,89 @@ class ConfigApiTest(TestCase):
|
|||||||
# TORZNABS
|
# TORZNABS
|
||||||
# TODO : here is copypaste from of NEZNABS tests. Make tests better, plz refactor them
|
# TODO : here is copypaste from of NEZNABS tests. Make tests better, plz refactor them
|
||||||
#
|
#
|
||||||
@TestArgs(
|
# TODO: Fix tests for following:
|
||||||
('', []),
|
# CONFIG_VERSION == '5' each entry = 'host, api, enabled'
|
||||||
('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
|
# CONFIG_VERSION > '5' each entry = 'host, api, seed ratio, enabled'
|
||||||
(['ABC', 'DEF'], []),
|
# @TestArgs(
|
||||||
([1], []),
|
# ('', []),
|
||||||
([1, 2], []),
|
# ('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
|
||||||
([1, 2, 3], [(1, 2, 3)]),
|
# (['ABC', 'DEF'], []),
|
||||||
|
# ([1], []),
|
||||||
([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
|
# ([1, 2], []),
|
||||||
([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
|
# ([1, 2, 3], [(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')]),
|
# ([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
|
||||||
)
|
# ([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
|
||||||
def test_get_extra_torznabs(self, conf_value, expected):
|
# ([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||||
""" Config: get_extra_torznabs """
|
# ([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc', 'Ddddda'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||||
path = '/tmp/notexist'
|
# )
|
||||||
|
# def test_get_extra_torznabs(self, conf_value, expected):
|
||||||
# itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
|
# """ Config: get_extra_torznabs """
|
||||||
# set up mocks:
|
# path = '/tmp/notexist'
|
||||||
# 'EXTRA_TORZNABS': (list, '', ''),
|
#
|
||||||
self.config_mock["Torznab"] = {"extra_torznabs": conf_value}
|
# # itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
|
||||||
|
# # set up mocks:
|
||||||
# call methods
|
# # 'EXTRA_TORZNABS': (list, '', ''),
|
||||||
c = headphones.config.Config(path)
|
# self.config_mock["Torznab"] = {"extra_torznabs": conf_value}
|
||||||
res = c.get_extra_torznabs()
|
#
|
||||||
|
# # call methods
|
||||||
# assertions:
|
# c = headphones.config.Config(path)
|
||||||
self.assertEqual(res, expected)
|
# res = c.get_extra_torznabs()
|
||||||
|
#
|
||||||
def test_clear_extra_torznabs(self):
|
# # assertions:
|
||||||
""" Config: clear_extra_torznabs """
|
# self.assertEqual(res, expected)
|
||||||
path = '/tmp/notexist'
|
#
|
||||||
|
# def test_clear_extra_torznabs(self):
|
||||||
random_value = -1292721
|
# """ Config: clear_extra_torznabs """
|
||||||
self.config_mock["Torznab"] = {"extra_torznabs": [1, 2, 3]}
|
# path = '/tmp/notexist'
|
||||||
self.config_mock["Torznab"] = {"do_not_touch": random_value}
|
#
|
||||||
|
# random_value = -1292721
|
||||||
# call methods
|
# self.config_mock["Torznab"] = {"extra_torznabs": [1, 2, 3]}
|
||||||
c = headphones.config.Config(path)
|
# self.config_mock["Torznab"] = {"do_not_touch": random_value}
|
||||||
res = c.clear_extra_torznabs()
|
#
|
||||||
|
# # call methods
|
||||||
# assertions:
|
# c = headphones.config.Config(path)
|
||||||
self.assertIsNone(res)
|
# res = c.clear_extra_torznabs()
|
||||||
self.assertEqual(self.config_mock["Torznab"]["extra_torznabs"], [])
|
#
|
||||||
self.assertEqual(self.config_mock["Torznab"]["do_not_touch"], random_value)
|
# # assertions:
|
||||||
|
# self.assertIsNone(res)
|
||||||
@TestArgs(
|
# self.assertEqual(self.config_mock["Torznab"]["extra_torznabs"], [])
|
||||||
([], [''], ['']),
|
# self.assertEqual(self.config_mock["Torznab"]["do_not_touch"], random_value)
|
||||||
([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
|
#
|
||||||
|
# @TestArgs(
|
||||||
([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
|
# ([], [''], ['']),
|
||||||
)
|
# ([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
|
||||||
def test_add_extra_torznab(self, initial, added, expected):
|
#
|
||||||
""" Config: add_extra_torznab """
|
# ([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
|
||||||
path = '/tmp/notexist'
|
# )
|
||||||
|
# def test_add_extra_torznab(self, initial, added, expected):
|
||||||
self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
# """ Config: add_extra_torznab """
|
||||||
|
# path = '/tmp/notexist'
|
||||||
# call methods
|
#
|
||||||
c = headphones.config.Config(path)
|
# self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
||||||
c.add_extra_torznab(added)
|
#
|
||||||
act = self.config_mock["Torznab"]["extra_torznabs"]
|
# # call methods
|
||||||
|
# c = headphones.config.Config(path)
|
||||||
# assertions:
|
# c.add_extra_torznab(added)
|
||||||
self.assertEqual(act, expected)
|
# act = self.config_mock["Torznab"]["extra_torznabs"]
|
||||||
|
#
|
||||||
@TestArgs(
|
# # assertions:
|
||||||
(None),
|
# self.assertEqual(act, expected)
|
||||||
([]),
|
#
|
||||||
([1, 2, 3]),
|
# @TestArgs(
|
||||||
([True]),
|
# (None),
|
||||||
)
|
# ([]),
|
||||||
def test_add_extra_torznab_raise_on_none(self, initial):
|
# ([1, 2, 3]),
|
||||||
""" Config: add_extra_torznab should raise on None adding"""
|
# ([True]),
|
||||||
path = '/tmp/notexist'
|
# )
|
||||||
|
# def test_add_extra_torznab_raise_on_none(self, initial):
|
||||||
self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
# """ Config: add_extra_torznab should raise on None adding"""
|
||||||
|
# path = '/tmp/notexist'
|
||||||
# call methods
|
#
|
||||||
c = headphones.config.Config(path)
|
# self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
||||||
with self.assertRaises(TypeError):
|
#
|
||||||
c.add_extra_torznab(None)
|
# # call methods
|
||||||
pass
|
# 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().items():
|
||||||
|
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))
|
||||||
@@ -87,7 +87,7 @@ def check_splitter(command):
|
|||||||
|
|
||||||
def split_baby(split_file, split_cmd):
|
def split_baby(split_file, split_cmd):
|
||||||
'''Let's split baby'''
|
'''Let's split baby'''
|
||||||
logger.info('Splitting %s...', split_file.decode(headphones.SYS_ENCODING, 'replace'))
|
logger.info(f"Splitting {split_file}...")
|
||||||
logger.debug(subprocess.list2cmdline(split_cmd))
|
logger.debug(subprocess.list2cmdline(split_cmd))
|
||||||
|
|
||||||
# Prevent Windows from opening a terminal window
|
# Prevent Windows from opening a terminal window
|
||||||
@@ -108,16 +108,16 @@ def split_baby(split_file, split_cmd):
|
|||||||
|
|
||||||
process = subprocess.Popen(split_cmd, startupinfo=startupinfo,
|
process = subprocess.Popen(split_cmd, startupinfo=startupinfo,
|
||||||
stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE,
|
stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE, env=env)
|
stderr=subprocess.PIPE, env=env, text=True)
|
||||||
stdout, stderr = process.communicate()
|
stdout, stderr = process.communicate()
|
||||||
|
|
||||||
if process.returncode:
|
if process.returncode:
|
||||||
logger.error('Split failed for %s', split_file.decode(headphones.SYS_ENCODING, 'replace'))
|
logger.error(f"Split failed for {split_file}")
|
||||||
out = stdout if stdout else stderr
|
out = stdout or stderr
|
||||||
logger.error('Error details: %s', out.decode(headphones.SYS_ENCODING, 'replace'))
|
logger.error(f"Error details: {out}")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logger.info('Split success %s', split_file.decode(headphones.SYS_ENCODING, 'replace'))
|
logger.info(f"Split succeeded for {split_file}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ class Directory:
|
|||||||
for i in list_dir:
|
for i in list_dir:
|
||||||
if not check_match(i):
|
if not check_match(i):
|
||||||
# music file
|
# music file
|
||||||
if os.path.splitext(i)[-1] in WAVE_FILE_TYPE_BY_EXTENSION.keys():
|
if os.path.splitext(i)[-1] in list(WAVE_FILE_TYPE_BY_EXTENSION.keys()):
|
||||||
track_nr = identify_track_number(i)
|
track_nr = identify_track_number(i)
|
||||||
if track_nr:
|
if track_nr:
|
||||||
self.content.append(WaveFile(self.path + os.sep + i, track_nr=track_nr))
|
self.content.append(WaveFile(self.path + os.sep + i, track_nr=track_nr))
|
||||||
@@ -378,7 +378,7 @@ class CueFile(File):
|
|||||||
except:
|
except:
|
||||||
raise ValueError('Cant encode CUE Sheet.')
|
raise ValueError('Cant encode CUE Sheet.')
|
||||||
|
|
||||||
if self.content[0] == u'\ufeff':
|
if self.content[0] == '\ufeff':
|
||||||
self.content = self.content[1:]
|
self.content = self.content[1:]
|
||||||
|
|
||||||
header = header_parser()
|
header = header_parser()
|
||||||
@@ -581,7 +581,7 @@ def split(albumpath):
|
|||||||
|
|
||||||
# use xld profile to split cue
|
# use xld profile to split cue
|
||||||
if headphones.CONFIG.ENCODER == 'xld' and headphones.CONFIG.MUSIC_ENCODER and headphones.CONFIG.XLDPROFILE:
|
if headphones.CONFIG.ENCODER == 'xld' and headphones.CONFIG.MUSIC_ENCODER and headphones.CONFIG.XLDPROFILE:
|
||||||
import getXldProfile
|
from . import getXldProfile
|
||||||
xldprofile, xldformat, _ = getXldProfile.getXldProfile(headphones.CONFIG.XLDPROFILE)
|
xldprofile, xldformat, _ = getXldProfile.getXldProfile(headphones.CONFIG.XLDPROFILE)
|
||||||
if not xldformat:
|
if not xldformat:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -601,7 +601,7 @@ def split(albumpath):
|
|||||||
raise ValueError('Command not found, ensure shntool or xld installed')
|
raise ValueError('Command not found, ensure shntool or xld installed')
|
||||||
|
|
||||||
# Determine if file can be split
|
# Determine if file can be split
|
||||||
if wave.name_ext not in WAVE_FILE_TYPE_BY_EXTENSION.keys():
|
if wave.name_ext not in list(WAVE_FILE_TYPE_BY_EXTENSION.keys()):
|
||||||
raise ValueError('Cannot split, audio file has unsupported extension')
|
raise ValueError('Cannot split, audio file has unsupported extension')
|
||||||
|
|
||||||
# Split with xld
|
# Split with xld
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
# Stolen from Sick-Beard's db.py #
|
# Stolen from Sick-Beard's db.py #
|
||||||
###################################
|
###################################
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
@@ -45,36 +47,97 @@ class DBConnection:
|
|||||||
self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
|
self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
|
||||||
# don't wait for the disk to finish writing
|
# don't wait for the disk to finish writing
|
||||||
self.connection.execute("PRAGMA synchronous = OFF")
|
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)
|
self.connection.execute("PRAGMA journal_mode = %s" % headphones.CONFIG.JOURNAL_MODE)
|
||||||
# 64mb of cache memory,probably need to make it user configurable
|
# 64mb of cache memory,probably need to make it user configurable
|
||||||
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize() * 1024))
|
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize() * 1024))
|
||||||
self.connection.row_factory = sqlite3.Row
|
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:
|
if query is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
sqlResult = None
|
sqlResult = None
|
||||||
|
attempts = 0
|
||||||
|
dberror = None
|
||||||
|
|
||||||
try:
|
while attempts < 10:
|
||||||
with self.connection as c:
|
try:
|
||||||
if args is None:
|
with self.connection as c:
|
||||||
sqlResult = c.execute(query)
|
|
||||||
|
# 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 as 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:
|
else:
|
||||||
sqlResult = c.execute(query, args)
|
logger.error('Database error: %s', e)
|
||||||
|
raise
|
||||||
except sqlite3.OperationalError, e:
|
except sqlite3.DatabaseError as e:
|
||||||
if "unable to open database file" in e.message or "database is locked" in e.message:
|
logger.error('Fatal Error executing %s :: %s', query, e)
|
||||||
logger.warn('Database Error: %s', e)
|
|
||||||
else:
|
|
||||||
logger.error('Database error: %s', e)
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except sqlite3.DatabaseError, e:
|
# log if no results returned due to lock
|
||||||
logger.error('Fatal Error executing %s :: %s', query, e)
|
if not sqlResult and attempts:
|
||||||
raise
|
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
|
return sqlResult
|
||||||
|
|
||||||
@@ -88,24 +151,19 @@ class DBConnection:
|
|||||||
return sqlResults
|
return sqlResults
|
||||||
|
|
||||||
def upsert(self, tableName, valueDict, keyDict):
|
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):
|
def genParams(myDict):
|
||||||
return [x + " = ?" for x in myDict.keys()]
|
return [x + " = ?" for x in list(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(
|
insert_query = ("INSERT INTO " + tableName + " (" + ", ".join(list(valueDict.keys()) + list(keyDict.keys())) + ")" + " SELECT " + ", ".join(
|
||||||
genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
|
["?"] * len(list(valueDict.keys()) + list(keyDict.keys()))) + " WHERE changes()=0")
|
||||||
|
|
||||||
self.action(update_query, valueDict.values() + keyDict.values())
|
try:
|
||||||
|
self.action(update_query, list(valueDict.values()) + list(keyDict.values()), upsert_insert_qry=insert_query)
|
||||||
if self.connection.total_changes == changesBefore:
|
except sqlite3.IntegrityError:
|
||||||
insert_query = (
|
logger.info('Queries failed: %s and %s', update_query, 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)
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of Headphones.
|
# This file is part of Headphones.
|
||||||
#
|
#
|
||||||
# Headphones is free software: you can redistribute it and/or modify
|
# Headphones is free software: you can redistribute it and/or modify
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with SickRage. If not, see <http://www.gnu.org/licenses/>.
|
# along with SickRage. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from headphones import logger
|
from headphones import logger
|
||||||
|
|
||||||
@@ -49,6 +51,7 @@ delugeweb_auth = {}
|
|||||||
delugeweb_url = ''
|
delugeweb_url = ''
|
||||||
deluge_verify_cert = False
|
deluge_verify_cert = False
|
||||||
scrub_logs = True
|
scrub_logs = True
|
||||||
|
headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
|
||||||
|
|
||||||
|
|
||||||
def _scrubber(text):
|
def _scrubber(text):
|
||||||
@@ -80,7 +83,7 @@ def addTorrent(link, data=None, name=None):
|
|||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
retid = False
|
retid = False
|
||||||
url_what = ['https://what.cd/', 'http://what.cd/']
|
url_orpheus = ['https://orpheus.network/', 'http://orpheus.network/']
|
||||||
url_waffles = ['https://waffles.ch/', 'http://waffles.ch/']
|
url_waffles = ['https://waffles.ch/', 'http://waffles.ch/']
|
||||||
|
|
||||||
if link.lower().startswith('magnet:'):
|
if link.lower().startswith('magnet:'):
|
||||||
@@ -94,7 +97,7 @@ def addTorrent(link, data=None, name=None):
|
|||||||
if link.lower().startswith(tuple(url_waffles)):
|
if link.lower().startswith(tuple(url_waffles)):
|
||||||
if 'rss=' not in link:
|
if 'rss=' not in link:
|
||||||
link = link + '&rss=1'
|
link = link + '&rss=1'
|
||||||
if link.lower().startswith(tuple(url_what)):
|
if link.lower().startswith(tuple(url_orpheus)):
|
||||||
logger.debug('Deluge: Using different User-Agent for this site')
|
logger.debug('Deluge: Using different User-Agent for this site')
|
||||||
user_agent = 'Headphones'
|
user_agent = 'Headphones'
|
||||||
# This method will make Deluge download the file
|
# This method will make Deluge download the file
|
||||||
@@ -104,11 +107,11 @@ def addTorrent(link, data=None, name=None):
|
|||||||
# return addTorrent(local_torrent_path)
|
# return addTorrent(local_torrent_path)
|
||||||
else:
|
else:
|
||||||
user_agent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
|
user_agent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
|
||||||
headers = {'User-Agent': user_agent}
|
get_headers = {'User-Agent': user_agent}
|
||||||
torrentfile = ''
|
torrentfile = ''
|
||||||
logger.debug('Deluge: Trying to download (GET)')
|
logger.debug('Deluge: Trying to download (GET)')
|
||||||
try:
|
try:
|
||||||
r = requests.get(link, headers=headers)
|
r = requests.get(link, headers=get_headers)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
logger.debug('Deluge: 200 OK')
|
logger.debug('Deluge: 200 OK')
|
||||||
# .text will ruin the encoding for some torrents
|
# .text will ruin the encoding for some torrents
|
||||||
@@ -135,7 +138,10 @@ def addTorrent(link, data=None, name=None):
|
|||||||
# remove '.torrent' suffix
|
# remove '.torrent' suffix
|
||||||
if name[-len('.torrent'):] == '.torrent':
|
if name[-len('.torrent'):] == '.torrent':
|
||||||
name = name[:-len('.torrent')]
|
name = name[:-len('.torrent')]
|
||||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
try:
|
||||||
|
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||||
|
except:
|
||||||
|
logger.debug('Deluge: Sending Deluge torrent with problematic name and some content')
|
||||||
result = {'type': 'torrent',
|
result = {'type': 'torrent',
|
||||||
'name': name,
|
'name': name,
|
||||||
'content': torrentfile}
|
'content': torrentfile}
|
||||||
@@ -164,7 +170,10 @@ def addTorrent(link, data=None, name=None):
|
|||||||
# remove '.torrent' suffix
|
# remove '.torrent' suffix
|
||||||
if name[-len('.torrent'):] == '.torrent':
|
if name[-len('.torrent'):] == '.torrent':
|
||||||
name = name[:-len('.torrent')]
|
name = name[:-len('.torrent')]
|
||||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
try:
|
||||||
|
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name.decode('utf-8'), str(torrentfile)[:40]))
|
||||||
result = {'type': 'torrent',
|
result = {'type': 'torrent',
|
||||||
'name': name,
|
'name': name,
|
||||||
'content': torrentfile}
|
'content': torrentfile}
|
||||||
@@ -199,7 +208,7 @@ def getTorrentFolder(result):
|
|||||||
],
|
],
|
||||||
"id": 21})
|
"id": 21})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
result['total_done'] = json.loads(response.text)['result']['total_done']
|
result['total_done'] = json.loads(response.text)['result']['total_done']
|
||||||
|
|
||||||
tries = 0
|
tries = 0
|
||||||
@@ -207,7 +216,7 @@ def getTorrentFolder(result):
|
|||||||
tries += 1
|
tries += 1
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
result['total_done'] = json.loads(response.text)['result']['total_done']
|
result['total_done'] = json.loads(response.text)['result']['total_done']
|
||||||
|
|
||||||
post_data = json.dumps({"method": "web.get_torrent_status",
|
post_data = json.dumps({"method": "web.get_torrent_status",
|
||||||
@@ -226,7 +235,7 @@ def getTorrentFolder(result):
|
|||||||
"id": 23})
|
"id": 23})
|
||||||
|
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
|
|
||||||
result['save_path'] = json.loads(response.text)['result']['save_path']
|
result['save_path'] = json.loads(response.text)['result']['save_path']
|
||||||
result['name'] = json.loads(response.text)['result']['name']
|
result['name'] = json.loads(response.text)['result']['name']
|
||||||
@@ -242,18 +251,45 @@ def removeTorrent(torrentid, remove_data=False):
|
|||||||
_get_auth()
|
_get_auth()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = False
|
logger.debug('Deluge: Checking if torrent %s finished seeding' % str(torrentid))
|
||||||
post_data = json.dumps({"method": "core.remove_torrent",
|
post_data = json.dumps({"method": "web.get_torrent_status",
|
||||||
"params": [
|
"params": [
|
||||||
torrentid,
|
torrentid,
|
||||||
remove_data
|
[
|
||||||
],
|
"name",
|
||||||
"id": 25})
|
"ratio",
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
"state"
|
||||||
verify=deluge_verify_cert)
|
]
|
||||||
result = json.loads(response.text)['result']
|
],
|
||||||
|
"id": 26})
|
||||||
|
|
||||||
return result
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
|
verify=deluge_verify_cert, headers=headers)
|
||||||
|
|
||||||
|
try:
|
||||||
|
state = json.loads(response.text)['result']['state']
|
||||||
|
except KeyError as e:
|
||||||
|
logger.debug('Deluge: "state" KeyError when trying to remove torrent %s' % str(torrentid))
|
||||||
|
return False
|
||||||
|
|
||||||
|
not_finished = ["queued", "seeding", "downloading", "checking", "error"]
|
||||||
|
result = False
|
||||||
|
if state.lower() in not_finished:
|
||||||
|
logger.debug('Deluge: Torrent %s is either queued or seeding, not removing yet' % str(torrentid))
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.debug('Deluge: Removing torrent %s' % str(torrentid))
|
||||||
|
post_data = json.dumps({"method": "core.remove_torrent",
|
||||||
|
"params": [
|
||||||
|
torrentid,
|
||||||
|
remove_data
|
||||||
|
],
|
||||||
|
"id": 25})
|
||||||
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
|
verify=deluge_verify_cert, headers=headers)
|
||||||
|
result = json.loads(response.text)['result']
|
||||||
|
|
||||||
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Deluge: Removing torrent failed: %s' % str(e))
|
logger.error('Deluge: Removing torrent failed: %s' % str(e))
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
@@ -269,7 +305,8 @@ def _get_auth():
|
|||||||
delugeweb_host = headphones.CONFIG.DELUGE_HOST
|
delugeweb_host = headphones.CONFIG.DELUGE_HOST
|
||||||
delugeweb_cert = headphones.CONFIG.DELUGE_CERT
|
delugeweb_cert = headphones.CONFIG.DELUGE_CERT
|
||||||
delugeweb_password = headphones.CONFIG.DELUGE_PASSWORD
|
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'):
|
if not delugeweb_host.startswith('http'):
|
||||||
delugeweb_host = 'http://%s' % delugeweb_host
|
delugeweb_host = 'http://%s' % delugeweb_host
|
||||||
@@ -292,12 +329,12 @@ def _get_auth():
|
|||||||
"id": 1})
|
"id": 1})
|
||||||
try:
|
try:
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
except requests.ConnectionError:
|
except requests.ConnectionError:
|
||||||
try:
|
try:
|
||||||
logger.debug('Deluge: Connection failed, let\'s try HTTPS just in case')
|
logger.debug('Deluge: Connection failed, let\'s try HTTPS just in case')
|
||||||
response = requests.post(delugeweb_url.replace('http:', 'https:'), data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url.replace('http:', 'https:'), data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
# If the previous line didn't fail, change delugeweb_url for the rest of this session
|
# If the previous line didn't fail, change delugeweb_url for the rest of this session
|
||||||
logger.error('Deluge: Switching to HTTPS, but certificate won\'t be verified because NO CERTIFICATE WAS CONFIGURED!')
|
logger.error('Deluge: Switching to HTTPS, but certificate won\'t be verified because NO CERTIFICATE WAS CONFIGURED!')
|
||||||
delugeweb_url = delugeweb_url.replace('http:', 'https:')
|
delugeweb_url = delugeweb_url.replace('http:', 'https:')
|
||||||
@@ -322,7 +359,7 @@ def _get_auth():
|
|||||||
"id": 10})
|
"id": 10})
|
||||||
try:
|
try:
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
@@ -339,7 +376,7 @@ def _get_auth():
|
|||||||
"id": 11})
|
"id": 11})
|
||||||
try:
|
try:
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
@@ -347,7 +384,8 @@ def _get_auth():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
delugeweb_hosts = json.loads(response.text)['result']
|
delugeweb_hosts = json.loads(response.text)['result']
|
||||||
if len(delugeweb_hosts) == 0:
|
# Check if delugeweb_hosts is None before checking its length
|
||||||
|
if not delugeweb_hosts or len(delugeweb_hosts) == 0:
|
||||||
logger.error('Deluge: WebUI does not contain daemons')
|
logger.error('Deluge: WebUI does not contain daemons')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -357,7 +395,7 @@ def _get_auth():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
@@ -370,7 +408,7 @@ def _get_auth():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
@@ -395,7 +433,7 @@ def _add_torrent_magnet(result):
|
|||||||
"params": [result['url'], {}],
|
"params": [result['url'], {}],
|
||||||
"id": 2})
|
"id": 2})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
result['hash'] = json.loads(response.text)['result']
|
result['hash'] = json.loads(response.text)['result']
|
||||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||||
return json.loads(response.text)['result']
|
return json.loads(response.text)['result']
|
||||||
@@ -415,7 +453,7 @@ def _add_torrent_url(result):
|
|||||||
"params": [result['url'], {}],
|
"params": [result['url'], {}],
|
||||||
"id": 32})
|
"id": 32})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
result['location'] = json.loads(response.text)['result']
|
result['location'] = json.loads(response.text)['result']
|
||||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||||
return json.loads(response.text)['result']
|
return json.loads(response.text)['result']
|
||||||
@@ -433,30 +471,14 @@ def _add_torrent_file(result):
|
|||||||
try:
|
try:
|
||||||
# content is torrent file contents that needs to be encoded to base64
|
# content is torrent file contents that needs to be encoded to base64
|
||||||
post_data = json.dumps({"method": "core.add_torrent_file",
|
post_data = json.dumps({"method": "core.add_torrent_file",
|
||||||
"params": [result['name'] + '.torrent', b64encode(result['content'].encode('utf8')), {}],
|
"params": [result['name'] + '.torrent',
|
||||||
|
b64encode(result['content']).decode(), {}],
|
||||||
"id": 2})
|
"id": 2})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
result['hash'] = json.loads(response.text)['result']
|
result['hash'] = json.loads(response.text)['result']
|
||||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||||
return json.loads(response.text)['result']
|
return json.loads(response.text)['result']
|
||||||
except UnicodeDecodeError:
|
|
||||||
try:
|
|
||||||
# content is torrent file contents that needs to be encoded to base64
|
|
||||||
# this time let's try leaving the encoding as is
|
|
||||||
post_data = json.dumps({"method": "core.add_torrent_file",
|
|
||||||
"params": [result['name'] + '.torrent', b64encode(result['content']), {}],
|
|
||||||
"id": 22})
|
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
|
||||||
verify=deluge_verify_cert)
|
|
||||||
result['hash'] = json.loads(response.text)['result']
|
|
||||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
|
||||||
return json.loads(response.text)['result']
|
|
||||||
except Exception as e:
|
|
||||||
logger.error('Deluge: Adding torrent file failed after decode: %s' % str(e))
|
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
|
||||||
logger.error('; '.join(formatted_lines))
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Deluge: Adding torrent file failed: %s' % str(e))
|
logger.error('Deluge: Adding torrent file failed: %s' % str(e))
|
||||||
formatted_lines = traceback.format_exc().splitlines()
|
formatted_lines = traceback.format_exc().splitlines()
|
||||||
@@ -480,7 +502,7 @@ def setTorrentLabel(result):
|
|||||||
"params": [],
|
"params": [],
|
||||||
"id": 3})
|
"id": 3})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
labels = json.loads(response.text)['result']
|
labels = json.loads(response.text)['result']
|
||||||
|
|
||||||
if labels is not None:
|
if labels is not None:
|
||||||
@@ -491,7 +513,7 @@ def setTorrentLabel(result):
|
|||||||
"params": [label],
|
"params": [label],
|
||||||
"id": 4})
|
"id": 4})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
logger.debug('Deluge: %s label added to Deluge' % label)
|
logger.debug('Deluge: %s label added to Deluge' % label)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Deluge: Setting label failed: %s' % str(e))
|
logger.error('Deluge: Setting label failed: %s' % str(e))
|
||||||
@@ -503,7 +525,7 @@ def setTorrentLabel(result):
|
|||||||
"params": [result['hash'], label],
|
"params": [result['hash'], label],
|
||||||
"id": 5})
|
"id": 5})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
logger.debug('Deluge: %s label added to torrent' % label)
|
logger.debug('Deluge: %s label added to torrent' % label)
|
||||||
else:
|
else:
|
||||||
logger.debug('Deluge: Label plugin not detected')
|
logger.debug('Deluge: Label plugin not detected')
|
||||||
@@ -527,12 +549,12 @@ def setSeedRatio(result):
|
|||||||
"params": [result['hash'], True],
|
"params": [result['hash'], True],
|
||||||
"id": 5})
|
"id": 5})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
post_data = json.dumps({"method": "core.set_torrent_stop_ratio",
|
post_data = json.dumps({"method": "core.set_torrent_stop_ratio",
|
||||||
"params": [result['hash'], float(ratio)],
|
"params": [result['hash'], float(ratio)],
|
||||||
"id": 6})
|
"id": 6})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
|
|
||||||
return not json.loads(response.text)['error']
|
return not json.loads(response.text)['error']
|
||||||
|
|
||||||
@@ -555,7 +577,7 @@ def setTorrentPath(result):
|
|||||||
"params": [result['hash'], True],
|
"params": [result['hash'], True],
|
||||||
"id": 7})
|
"id": 7})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
|
|
||||||
if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
|
if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
|
||||||
move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
|
move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
|
||||||
@@ -569,7 +591,7 @@ def setTorrentPath(result):
|
|||||||
"params": [result['hash'], move_to],
|
"params": [result['hash'], move_to],
|
||||||
"id": 8})
|
"id": 8})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
|
|
||||||
return not json.loads(response.text)['error']
|
return not json.loads(response.text)['error']
|
||||||
|
|
||||||
@@ -592,7 +614,7 @@ def setTorrentPause(result):
|
|||||||
"params": [[result['hash']]],
|
"params": [[result['hash']]],
|
||||||
"id": 9})
|
"id": 9})
|
||||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||||
verify=deluge_verify_cert)
|
verify=deluge_verify_cert, headers=headers)
|
||||||
|
|
||||||
return not json.loads(response.text)['error']
|
return not json.loads(response.text)['error']
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def getXldProfile(xldProfile):
|
|||||||
# Get xld preferences plist
|
# Get xld preferences plist
|
||||||
try:
|
try:
|
||||||
preferences = biplist.readPlist(expanded)
|
preferences = biplist.readPlist(expanded)
|
||||||
except (biplist.InvalidPlistException, biplist.NotBinaryPlistException), e:
|
except (biplist.InvalidPlistException, biplist.NotBinaryPlistException) as e:
|
||||||
logger.error("Error reading xld preferences plist: %s", e)
|
logger.error("Error reading xld preferences plist: %s", e)
|
||||||
return (xldProfileNotFound, None, None)
|
return (xldProfileNotFound, None, None)
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,19 @@ import datetime
|
|||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import glob
|
||||||
|
|
||||||
|
from beets import logging as beetslogging
|
||||||
|
import six
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
import functools
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
from mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||||
|
from unidecode import unidecode
|
||||||
import headphones
|
import headphones
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +42,17 @@ RE_FEATURING = re.compile(r"[fF]t\.|[fF]eaturing|[fF]eat\.|\b[wW]ith\b|&|vs\.")
|
|||||||
RE_CD_ALBUM = re.compile(r"\(?((CD|disc)\s*[0-9]+)\)?", re.I)
|
RE_CD_ALBUM = re.compile(r"\(?((CD|disc)\s*[0-9]+)\)?", re.I)
|
||||||
RE_CD = re.compile(r"^(CD|dics)\s*[0-9]+$", re.I)
|
RE_CD = re.compile(r"^(CD|dics)\s*[0-9]+$", re.I)
|
||||||
|
|
||||||
|
def cmp(x, y):
|
||||||
|
"""
|
||||||
|
Replacement for built-in function cmp that was removed in Python 3
|
||||||
|
|
||||||
|
Compare the two objects x and y and return an integer according to
|
||||||
|
the outcome. The return value is negative if x < y, zero if x == y
|
||||||
|
and strictly positive if x > y.
|
||||||
|
|
||||||
|
https://portingguide.readthedocs.io/en/latest/comparisons.html#the-cmp-function
|
||||||
|
"""
|
||||||
|
return (x > y) - (x < y)
|
||||||
|
|
||||||
def multikeysort(items, columns):
|
def multikeysort(items, columns):
|
||||||
comparers = [
|
comparers = [
|
||||||
@@ -48,7 +67,7 @@ def multikeysort(items, columns):
|
|||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
return sorted(items, cmp=comparer)
|
return sorted(items, key=functools.cmp_to_key(comparer))
|
||||||
|
|
||||||
|
|
||||||
def checked(variable):
|
def checked(variable):
|
||||||
@@ -191,14 +210,20 @@ def piratesize(size):
|
|||||||
return 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
|
from headphones import pathrender
|
||||||
if not text:
|
if not pattern:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
if normalize:
|
if normalize:
|
||||||
new_dic = {}
|
new_dic = {}
|
||||||
for i, j in dic.iteritems():
|
for i, j in dic.items():
|
||||||
if j is not None:
|
if j is not None:
|
||||||
try:
|
try:
|
||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
@@ -210,20 +235,28 @@ def replace_all(text, dic, normalize=False):
|
|||||||
j.decode(headphones.SYS_ENCODING, 'replace'))
|
j.decode(headphones.SYS_ENCODING, 'replace'))
|
||||||
new_dic[i] = j
|
new_dic[i] = j
|
||||||
dic = new_dic
|
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.items():
|
||||||
|
text = text.replace(i, j)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def replace_illegal_chars(string, type="file"):
|
def replace_illegal_chars(string, type="file"):
|
||||||
if type == "file":
|
if type == "file":
|
||||||
string = re.sub('[\?"*:|<>/]', '_', string)
|
string = re.sub('[\?"*:|<>/]', '_', string)
|
||||||
if type == "folder":
|
if type == "folder":
|
||||||
string = re.sub('[:\?<>"|]', '_', string)
|
string = re.sub('[:\?<>"|*]', '_', string)
|
||||||
|
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
_CN_RE1 = re.compile(ur'[^\w]+', re.UNICODE)
|
_CN_RE1 = re.compile(r'[^\w]+', re.UNICODE)
|
||||||
_CN_RE2 = re.compile(ur'[\s_]+', re.UNICODE)
|
_CN_RE2 = re.compile(r'[\s_]+', re.UNICODE)
|
||||||
|
|
||||||
|
|
||||||
_XLATE_GRAPHICAL_AND_DIACRITICAL = {
|
_XLATE_GRAPHICAL_AND_DIACRITICAL = {
|
||||||
@@ -233,25 +266,33 @@ _XLATE_GRAPHICAL_AND_DIACRITICAL = {
|
|||||||
# ©ª«®²³¹»¼½¾ÆÐØÞßæðøþĐđĦħıIJijĸĿŀŁłŒœŦŧDŽDždžLJLjljNJNjnjǤǥDZDzdzȤȥ. This
|
# ©ª«®²³¹»¼½¾ÆÐØÞßæðøþĐđĦħıIJijĸĿŀŁłŒœŦŧDŽDždžLJLjljNJNjnjǤǥDZDzdzȤȥ. This
|
||||||
# includes also some graphical symbols which can be easily replaced and
|
# includes also some graphical symbols which can be easily replaced and
|
||||||
# usually are written by people who don't have appropriate keyboard layout.
|
# usually are written by people who don't have appropriate keyboard layout.
|
||||||
u'©': '(C)', u'ª': 'a.', u'«': '<<', u'®': '(R)', u'²': '2', u'³': '3',
|
'©': '(C)', 'ª': 'a.', '«': '<<', '®': '(R)', '²': '2', '³': '3',
|
||||||
u'¹': '1', u'»': '>>', u'¼': ' 1/4 ', u'½': ' 1/2 ', u'¾': ' 3/4 ',
|
'¹': '1', '»': '>>', '¼': ' 1/4 ', '½': ' 1/2 ', '¾': ' 3/4 ',
|
||||||
u'Æ': 'AE', u'Ð': 'D', u'Ø': 'O', u'Þ': 'Th', u'ß': 'ss', u'æ': 'ae',
|
'Æ': 'AE', 'Ð': 'D', 'Ø': 'O', 'Þ': 'Th', 'ß': 'ss', 'æ': 'ae',
|
||||||
u'ð': 'd', u'ø': 'o', u'þ': 'th', u'Đ': 'D', u'đ': 'd', u'Ħ': 'H',
|
'ð': 'd', 'ø': 'o', 'þ': 'th', 'Đ': 'D', 'đ': 'd', 'Ħ': 'H',
|
||||||
u'ħ': 'h', u'ı': 'i', u'IJ': 'IJ', u'ij': 'ij', u'ĸ': 'q', u'Ŀ': 'L',
|
'ħ': 'h', 'ı': 'i', 'IJ': 'IJ', 'ij': 'ij', 'ĸ': 'q', 'Ŀ': 'L',
|
||||||
u'ŀ': 'l', u'Ł': 'L', u'ł': 'l', u'Œ': 'OE', u'œ': 'oe', u'Ŧ': 'T',
|
'ŀ': 'l', 'Ł': 'L', 'ł': 'l', 'Œ': 'OE', 'œ': 'oe', 'Ŧ': 'T',
|
||||||
u'ŧ': 't', u'DŽ': 'DZ', u'Dž': 'Dz', u'LJ': 'LJ', u'Lj': 'Lj',
|
'ŧ': 't', 'DŽ': 'DZ', 'Dž': 'Dz', 'LJ': 'LJ', 'Lj': 'Lj',
|
||||||
u'lj': 'lj', u'NJ': 'NJ', u'Nj': 'Nj', u'nj': 'nj',
|
'lj': 'lj', 'NJ': 'NJ', 'Nj': 'Nj', 'nj': 'nj',
|
||||||
u'Ǥ': 'G', u'ǥ': 'g', u'DZ': 'DZ', u'Dz': 'Dz', u'dz': 'dz',
|
'Ǥ': 'G', 'ǥ': 'g', 'DZ': 'DZ', 'Dz': 'Dz', 'dz': 'dz',
|
||||||
u'Ȥ': 'Z', u'ȥ': 'z', u'№': 'No.',
|
'Ȥ': 'Z', 'ȥ': 'z', '№': 'No.',
|
||||||
u'º': 'o.', # normalize Nº abbrev (popular w/ classical music),
|
'º': 'o.', # normalize Nº abbrev (popular w/ classical music),
|
||||||
# this is 'masculine ordering indicator', not degree
|
# this is 'masculine ordering indicator', not degree
|
||||||
}
|
}
|
||||||
|
|
||||||
_XLATE_SPECIAL = {
|
_XLATE_SPECIAL = {
|
||||||
# Translation table.
|
# Translation table.
|
||||||
# Cover additional special characters processing normalization.
|
# Cover additional special characters processing normalization.
|
||||||
u"'": '', # replace apostrophe with nothing
|
"'": '', # replace apostrophe with nothing
|
||||||
u'&': ' and ', # expand & to ' and '
|
"’": '', # replace musicbrainz style apostrophe with nothing
|
||||||
|
'&': ' and ', # expand & to ' and '
|
||||||
|
}
|
||||||
|
|
||||||
|
_XLATE_MUSICBRAINZ = {
|
||||||
|
# Translation table for Musicbrainz.
|
||||||
|
"…": '...', # HORIZONTAL ELLIPSIS (U+2026)
|
||||||
|
"’": "'", # APOSTROPHE (U+0027)
|
||||||
|
"‐": "-", # EN DASH (U+2013)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -286,10 +327,10 @@ def _transliterate(u, xlate):
|
|||||||
Perform transliteration using the specified dictionary
|
Perform transliteration using the specified dictionary
|
||||||
"""
|
"""
|
||||||
u = unicodedata.normalize('NFD', u)
|
u = unicodedata.normalize('NFD', u)
|
||||||
u = u''.join([u'' if _is_unicode_combining(x) else x for x in u])
|
u = ''.join(['' if _is_unicode_combining(x) else x for x in u])
|
||||||
u = _translate(u, xlate)
|
u = _translate(u, xlate)
|
||||||
# at this point output is either unicode, or plain ascii
|
# at this point output is either unicode, or plain ascii
|
||||||
return unicode(u)
|
return str(u)
|
||||||
|
|
||||||
|
|
||||||
def clean_name(s):
|
def clean_name(s):
|
||||||
@@ -299,10 +340,10 @@ def clean_name(s):
|
|||||||
:param s: string to clean up, possibly unicode one.
|
:param s: string to clean up, possibly unicode one.
|
||||||
:return: cleaned-up version of input string.
|
:return: cleaned-up version of input string.
|
||||||
"""
|
"""
|
||||||
if not isinstance(s, unicode):
|
if not isinstance(s, str):
|
||||||
# ignore extended chars if someone was dumb enough to pass non-ascii
|
# ignore extended chars if someone was dumb enough to pass non-ascii
|
||||||
# narrow string here, use only unicode for meaningful texts
|
# narrow string here, use only unicode for meaningful texts
|
||||||
u = unicode(s, 'ascii', 'replace')
|
u = str(s, 'ascii', 'replace')
|
||||||
else:
|
else:
|
||||||
u = s
|
u = s
|
||||||
# 1. don't bother doing normalization NFKC, rather transliterate
|
# 1. don't bother doing normalization NFKC, rather transliterate
|
||||||
@@ -313,15 +354,33 @@ def clean_name(s):
|
|||||||
# 3. translate spacials
|
# 3. translate spacials
|
||||||
u = _translate(u, _XLATE_SPECIAL)
|
u = _translate(u, _XLATE_SPECIAL)
|
||||||
# 4. replace any non-alphanumeric character sequences by spaces
|
# 4. replace any non-alphanumeric character sequences by spaces
|
||||||
u = _CN_RE1.sub(u' ', u)
|
u = _CN_RE1.sub(' ', u)
|
||||||
# 5. coalesce interleaved space/underscore sequences
|
# 5. coalesce interleaved space/underscore sequences
|
||||||
u = _CN_RE2.sub(u' ', u)
|
u = _CN_RE2.sub(' ', u)
|
||||||
# 6. trim
|
# 6. trim
|
||||||
u = u.strip()
|
u = u.strip()
|
||||||
# 7. lowercase
|
# 7. lowercase
|
||||||
|
u = u.lower()
|
||||||
return u
|
return u
|
||||||
|
|
||||||
|
|
||||||
|
def clean_musicbrainz_name(s, return_as_string=True):
|
||||||
|
# type: (basestring)->unicode
|
||||||
|
"""Substitute special Musicbrainz characters.
|
||||||
|
:param s: string to clean up, probably unicode.
|
||||||
|
:return: cleaned-up version of input string.
|
||||||
|
"""
|
||||||
|
if not isinstance(s, str):
|
||||||
|
u = str(s, 'ascii', 'replace')
|
||||||
|
else:
|
||||||
|
u = s
|
||||||
|
u = _translate(u, _XLATE_MUSICBRAINZ)
|
||||||
|
if return_as_string:
|
||||||
|
return u.encode('utf-8')
|
||||||
|
else:
|
||||||
|
return u
|
||||||
|
|
||||||
|
|
||||||
def cleanTitle(title):
|
def cleanTitle(title):
|
||||||
title = re.sub('[\.\-\/\_]', ' ', title).lower()
|
title = re.sub('[\.\-\/\_]', ' ', title).lower()
|
||||||
|
|
||||||
@@ -406,8 +465,7 @@ def expand_subfolders(f):
|
|||||||
|
|
||||||
if difference > 0:
|
if difference > 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder.",
|
f"Found {len(media_folders)} media folders, but depth difference between lowest and deepest media folder is {difference} (expected zero). If this is a discography or a collection of albums, make sure albums are per folder.")
|
||||||
len(media_folders), difference)
|
|
||||||
|
|
||||||
# While already failed, advice the user what he could try. We assume the
|
# While already failed, advice the user what he could try. We assume the
|
||||||
# directory may contain separate CD's and maybe some extra's. The
|
# directory may contain separate CD's and maybe some extra's. The
|
||||||
@@ -419,8 +477,7 @@ def expand_subfolders(f):
|
|||||||
set([os.path.join(*media_folder) for media_folder in extra_media_folders]))
|
set([os.path.join(*media_folder) for media_folder in extra_media_folders]))
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Please look at the following folder(s), since they cause the depth difference: %s",
|
f"Please look at the following folder(s), since they cause the depth difference: {extra_media_folders}")
|
||||||
extra_media_folders)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Convert back to paths and remove duplicates, which may be there after
|
# Convert back to paths and remove duplicates, which may be there after
|
||||||
@@ -434,7 +491,7 @@ def expand_subfolders(f):
|
|||||||
logger.debug("Did not expand subfolder, as it resulted in one folder.")
|
logger.debug("Did not expand subfolder, as it resulted in one folder.")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug("Expanded subfolders in folder: %s", media_folders)
|
logger.debug(f"Expanded subfolders in folder: {media_folders}")
|
||||||
return media_folders
|
return media_folders
|
||||||
|
|
||||||
|
|
||||||
@@ -452,7 +509,7 @@ def path_match_patterns(path, patterns):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def path_filter_patterns(paths, patterns, root=None):
|
def path_filter_patterns(paths, patterns, root=''):
|
||||||
"""
|
"""
|
||||||
Scan for ignored paths based on glob patterns. Note that the whole path
|
Scan for ignored paths based on glob patterns. Note that the whole path
|
||||||
will be matched, therefore paths should only contain the relative paths.
|
will be matched, therefore paths should only contain the relative paths.
|
||||||
@@ -466,8 +523,7 @@ def path_filter_patterns(paths, patterns, root=None):
|
|||||||
|
|
||||||
for path in paths[:]:
|
for path in paths[:]:
|
||||||
if path_match_patterns(path, patterns):
|
if path_match_patterns(path, patterns):
|
||||||
logger.debug("Path ignored by pattern: %s",
|
logger.debug(f"Path ignored by pattern: {os.path.join(root, path)}")
|
||||||
os.path.join(root or "", path))
|
|
||||||
|
|
||||||
ignored += 1
|
ignored += 1
|
||||||
paths.remove(path)
|
paths.remove(path)
|
||||||
@@ -549,7 +605,7 @@ def extract_metadata(f):
|
|||||||
count_ratio = 0.75
|
count_ratio = 0.75
|
||||||
|
|
||||||
if count < (count_ratio * len(results)):
|
if count < (count_ratio * len(results)):
|
||||||
logger.info("Counted %d media files, but only %d have tags, ignoring.", count, len(results))
|
logger.info(f"Counted {count} media files, but only {len(results)} have tags, ignoring.")
|
||||||
return (None, None, None)
|
return (None, None, None)
|
||||||
|
|
||||||
# Count distinct values
|
# Count distinct values
|
||||||
@@ -567,8 +623,7 @@ def extract_metadata(f):
|
|||||||
old_album = new_albums[index]
|
old_album = new_albums[index]
|
||||||
new_albums[index] = RE_CD_ALBUM.sub("", album).strip()
|
new_albums[index] = RE_CD_ALBUM.sub("", album).strip()
|
||||||
|
|
||||||
logger.debug("Stripped albumd number identifier: %s -> %s", old_album,
|
logger.debug(f"Stripped album number identifier: {old_album} -> {new_albums[index]}")
|
||||||
new_albums[index])
|
|
||||||
|
|
||||||
# Remove duplicates
|
# Remove duplicates
|
||||||
new_albums = list(set(new_albums))
|
new_albums = list(set(new_albums))
|
||||||
@@ -586,7 +641,7 @@ def extract_metadata(f):
|
|||||||
if len(artists) > 1 and len(albums) == 1:
|
if len(artists) > 1 and len(albums) == 1:
|
||||||
split_artists = [RE_FEATURING.split(x) for x in artists]
|
split_artists = [RE_FEATURING.split(x) for x in artists]
|
||||||
featurings = [len(split_artist) - 1 for split_artist in split_artists]
|
featurings = [len(split_artist) - 1 for split_artist in split_artists]
|
||||||
logger.info("Album seem to feature %d different artists", sum(featurings))
|
logger.info("Album seem to feature {sum(featurings)} different artists")
|
||||||
|
|
||||||
if sum(featurings) > 0:
|
if sum(featurings) > 0:
|
||||||
# Find the artist of which the least splits have been generated.
|
# Find the artist of which the least splits have been generated.
|
||||||
@@ -598,9 +653,11 @@ def extract_metadata(f):
|
|||||||
return (artist, albums[0], years[0])
|
return (artist, albums[0], years[0])
|
||||||
|
|
||||||
# Not sure what to do here.
|
# Not sure what to do here.
|
||||||
logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring", len(artists),
|
logger.info(
|
||||||
len(albums), len(years))
|
f"Found {len(artists)} artists, {len(albums)} albums and "
|
||||||
logger.debug("Artists: %s, Albums: %s, Years: %s", artists, albums, years)
|
f"{len(years)} years in metadata, so ignoring"
|
||||||
|
)
|
||||||
|
logger.debug("Artists: {artists}, Albums: {albums}, Years: {years}")
|
||||||
|
|
||||||
return (None, None, None)
|
return (None, None, None)
|
||||||
|
|
||||||
@@ -620,26 +677,71 @@ def get_downloaded_track_list(albumpath):
|
|||||||
return downloaded_track_list
|
return downloaded_track_list
|
||||||
|
|
||||||
|
|
||||||
def preserve_torrent_directory(albumpath):
|
def preserve_torrent_directory(albumpath, forced=False, single=False):
|
||||||
"""
|
"""
|
||||||
Copy torrent directory to headphones-modified to keep files for seeding.
|
Copy torrent directory to temp headphones_ directory to keep files for seeding.
|
||||||
"""
|
"""
|
||||||
from headphones import logger
|
from headphones import logger
|
||||||
new_folder = os.path.join(albumpath,
|
|
||||||
'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
|
# Create temp dir
|
||||||
logger.info(
|
if headphones.CONFIG.KEEP_TORRENT_FILES_DIR:
|
||||||
"Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
|
tempdir = headphones.CONFIG.KEEP_TORRENT_FILES_DIR
|
||||||
|
else:
|
||||||
|
tempdir = tempfile.gettempdir()
|
||||||
|
|
||||||
|
logger.info(f"Preparing to copy to a temporary directory for post processing: {albumpath}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
shutil.copytree(albumpath, new_folder)
|
file_name = os.path.basename(os.path.normpath(albumpath))
|
||||||
return new_folder
|
if not single:
|
||||||
|
prefix = "headphones_" + file_name + "_@hp@_"
|
||||||
|
else:
|
||||||
|
prefix = "headphones_" + os.path.splitext(file_name)[0] + "_@hp@_"
|
||||||
|
new_folder = tempfile.mkdtemp(prefix=prefix, dir=tempdir)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn("Cannot copy/move files to temp folder: " +
|
logger.error(f"Cannot create temp directory: {tempdir}. Error: {e}")
|
||||||
new_folder.decode(headphones.SYS_ENCODING, 'replace') +
|
return None
|
||||||
". Not continuing. Error: " + str(e))
|
|
||||||
|
# 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 "
|
||||||
|
)
|
||||||
|
shutil.rmtree(new_folder)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(
|
||||||
|
"Cannot determine if already copied/processed, will copy anyway. "
|
||||||
|
f"Warning: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy to temp dir
|
||||||
|
try:
|
||||||
|
subdir = os.path.join(new_folder, "headphones")
|
||||||
|
logger.info(f"Copying files to {subdir}")
|
||||||
|
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(
|
||||||
|
f"Cannot copy/move files to temp directory: {new_folder}. "
|
||||||
|
f"Not continuing. Error: {e}"
|
||||||
|
)
|
||||||
|
shutil.rmtree(new_folder)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def cue_split(albumpath):
|
def cue_split(albumpath, keep_original_folder=False):
|
||||||
"""
|
"""
|
||||||
Attempts to check and split audio files by a cue for the given directory.
|
Attempts to check and split audio files by a cue for the given directory.
|
||||||
"""
|
"""
|
||||||
@@ -661,6 +763,15 @@ def cue_split(albumpath):
|
|||||||
# Split cue
|
# Split cue
|
||||||
if cue_count and cue_count >= count and cue_dirs:
|
if cue_count and cue_count >= count and cue_dirs:
|
||||||
|
|
||||||
|
# Copy to temp directory
|
||||||
|
if keep_original_folder:
|
||||||
|
temppath = preserve_torrent_directory(albumpath)
|
||||||
|
if temppath:
|
||||||
|
cue_dirs = [cue_dir.replace(albumpath, temppath) for cue_dir in cue_dirs]
|
||||||
|
albumpath = temppath
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
from headphones import logger, cuesplit
|
from headphones import logger, cuesplit
|
||||||
logger.info("Attempting to split audio files by cue")
|
logger.info("Attempting to split audio files by cue")
|
||||||
|
|
||||||
@@ -670,13 +781,13 @@ def cue_split(albumpath):
|
|||||||
cuesplit.split(cue_dir)
|
cuesplit.split(cue_dir)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
os.chdir(cwd)
|
os.chdir(cwd)
|
||||||
logger.warn("Cue not split: " + str(e))
|
logger.warn(f"Cue not split. Error: {e}")
|
||||||
return False
|
return None
|
||||||
|
|
||||||
os.chdir(cwd)
|
os.chdir(cwd)
|
||||||
return True
|
return albumpath
|
||||||
|
|
||||||
return False
|
return None
|
||||||
|
|
||||||
|
|
||||||
def extract_logline(s):
|
def extract_logline(s):
|
||||||
@@ -708,7 +819,7 @@ def extract_song_data(s):
|
|||||||
year = match.group("year")
|
year = match.group("year")
|
||||||
return (name, album, year)
|
return (name, album, year)
|
||||||
else:
|
else:
|
||||||
logger.info("Couldn't parse %s into a valid default format", s)
|
logger.info(f"Couldn't parse {s} into a valid default format")
|
||||||
|
|
||||||
# newzbin default format
|
# newzbin default format
|
||||||
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s\((?P<year>\d+?\))', re.VERBOSE)
|
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s\((?P<year>\d+?\))', re.VERBOSE)
|
||||||
@@ -719,7 +830,7 @@ def extract_song_data(s):
|
|||||||
year = match.group("year")
|
year = match.group("year")
|
||||||
return (name, album, year)
|
return (name, album, year)
|
||||||
else:
|
else:
|
||||||
logger.info("Couldn't parse %s into a valid Newbin format", s)
|
logger.info(f"Couldn't parse {s} into a valid Newbin format")
|
||||||
return (name, album, year)
|
return (name, album, year)
|
||||||
|
|
||||||
|
|
||||||
@@ -728,9 +839,11 @@ def smartMove(src, dest, delete=True):
|
|||||||
|
|
||||||
source_dir = os.path.dirname(src)
|
source_dir = os.path.dirname(src)
|
||||||
filename = os.path.basename(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)):
|
if os.path.isfile(dest_path):
|
||||||
logger.info('Destination file exists: %s', os.path.join(dest, filename))
|
logger.info(f"Destination file exists: {dest_path}")
|
||||||
title = os.path.splitext(filename)[0]
|
title = os.path.splitext(filename)[0]
|
||||||
ext = os.path.splitext(filename)[1]
|
ext = os.path.splitext(filename)[1]
|
||||||
i = 1
|
i = 1
|
||||||
@@ -739,24 +852,37 @@ def smartMove(src, dest, delete=True):
|
|||||||
if os.path.isfile(os.path.join(dest, newfile)):
|
if os.path.isfile(os.path.join(dest, newfile)):
|
||||||
i += 1
|
i += 1
|
||||||
else:
|
else:
|
||||||
logger.info('Renaming to %s', newfile)
|
logger.info(f"Renaming to {newfile}")
|
||||||
try:
|
try:
|
||||||
os.rename(src, os.path.join(source_dir, newfile))
|
os.rename(src, os.path.join(source_dir, newfile))
|
||||||
filename = newfile
|
filename = newfile
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn('Error renaming %s: %s',
|
logger.warn(f"Error renaming {src}: {e}")
|
||||||
src.decode(headphones.SYS_ENCODING, 'replace'), e)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
if delete:
|
||||||
if delete:
|
try:
|
||||||
shutil.move(os.path.join(source_dir, filename), os.path.join(dest, filename))
|
logger.info('Moving "%s" to "%s"', source_path, dest_path)
|
||||||
else:
|
shutil.move(source_path, dest_path)
|
||||||
shutil.copy(os.path.join(source_dir, filename), os.path.join(dest, filename))
|
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(
|
||||||
|
f"Successfully moved {filename}, but something went wrong: {e}"
|
||||||
|
)
|
||||||
|
os.unlink(source_path)
|
||||||
|
else:
|
||||||
|
# remove faultly copied file
|
||||||
|
if exists:
|
||||||
|
os.unlink(dest_path)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
logger.info(f"Copying {source_path} to {dest_path}")
|
||||||
|
shutil.copy(source_path, dest_path)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'),
|
logger.warn(f"Error copying {filename}: {e}")
|
||||||
e)
|
|
||||||
|
|
||||||
|
|
||||||
def walk_directory(basedir, followlinks=True):
|
def walk_directory(basedir, followlinks=True):
|
||||||
@@ -765,7 +891,7 @@ def walk_directory(basedir, followlinks=True):
|
|||||||
with care. In case a folder is already processed, don't traverse it again.
|
with care. In case a folder is already processed, don't traverse it again.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logger
|
from . import logger
|
||||||
|
|
||||||
# Add the base path, because symlinks poiting to the basedir should not be
|
# Add the base path, because symlinks poiting to the basedir should not be
|
||||||
# traversed again.
|
# traversed again.
|
||||||
@@ -779,8 +905,10 @@ def walk_directory(basedir, followlinks=True):
|
|||||||
real_path = os.path.abspath(os.readlink(path))
|
real_path = os.path.abspath(os.readlink(path))
|
||||||
|
|
||||||
if real_path in traversed:
|
if real_path in traversed:
|
||||||
logger.debug("Skipping '%s' since it is a symlink to "
|
logger.debug(
|
||||||
"'%s', which is already visited.", path, real_path)
|
f"Skipping {path} since it is a symlink to "
|
||||||
|
f"{real_path}, which is already visited."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
traversed.append(real_path)
|
traversed.append(real_path)
|
||||||
|
|
||||||
@@ -822,22 +950,15 @@ def sab_sanitize_foldername(name):
|
|||||||
FL_ILLEGAL = CH_ILLEGAL + ':\x92"'
|
FL_ILLEGAL = CH_ILLEGAL + ':\x92"'
|
||||||
FL_LEGAL = CH_LEGAL + "-''"
|
FL_LEGAL = CH_LEGAL + "-''"
|
||||||
|
|
||||||
uFL_ILLEGAL = FL_ILLEGAL.decode('latin-1')
|
|
||||||
uFL_LEGAL = FL_LEGAL.decode('latin-1')
|
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
return name
|
return
|
||||||
if isinstance(name, unicode):
|
|
||||||
illegal = uFL_ILLEGAL
|
name = unidecode(name)
|
||||||
legal = uFL_LEGAL
|
|
||||||
else:
|
|
||||||
illegal = FL_ILLEGAL
|
|
||||||
legal = FL_LEGAL
|
|
||||||
|
|
||||||
lst = []
|
lst = []
|
||||||
for ch in name.strip():
|
for ch in name.strip():
|
||||||
if ch in illegal:
|
if ch in FL_ILLEGAL:
|
||||||
ch = legal[illegal.find(ch)]
|
ch = FL_LEGAL[FL_ILLEGAL.find(ch)]
|
||||||
lst.append(ch)
|
lst.append(ch)
|
||||||
else:
|
else:
|
||||||
lst.append(ch)
|
lst.append(ch)
|
||||||
@@ -893,7 +1014,28 @@ def create_https_certificates(ssl_cert, ssl_key):
|
|||||||
with open(ssl_cert, "w") as fp:
|
with open(ssl_cert, "w") as fp:
|
||||||
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error("Error creating SSL key and certificate: %s", e)
|
logger.error(f"Error creating SSL key and certificate: e")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class BeetsLogCapture(beetslogging.Handler):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
beetslogging.Handler.__init__(self)
|
||||||
|
self.messages = []
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
self.messages.append(six.text_type(record.msg))
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def capture_beets_log(logger='beets'):
|
||||||
|
capture = BeetsLogCapture()
|
||||||
|
log = beetslogging.getLogger(logger)
|
||||||
|
log.addHandler(capture)
|
||||||
|
try:
|
||||||
|
yield capture.messages
|
||||||
|
finally:
|
||||||
|
log.removeHandler(capture)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from unittestcompat import TestCase
|
from .unittestcompat import TestCase
|
||||||
from headphones.helpers import clean_name
|
from headphones.helpers import clean_name
|
||||||
|
|
||||||
|
|
||||||
@@ -8,28 +8,28 @@ class HelpersTest(TestCase):
|
|||||||
def test_clean_name(self):
|
def test_clean_name(self):
|
||||||
"""helpers: check correctness of clean_name() function"""
|
"""helpers: check correctness of clean_name() function"""
|
||||||
cases = {
|
cases = {
|
||||||
u' Weiße & rose ': 'Weisse and rose',
|
' Weiße & rose ': 'Weisse and rose',
|
||||||
u'Multiple / spaces': 'Multiple spaces',
|
'Multiple / spaces': 'Multiple spaces',
|
||||||
u'Kevin\'s m²': 'Kevins m2',
|
'Kevin\'s m²': 'Kevins m2',
|
||||||
u'Symphonęy Nº9': 'Symphoney No.9',
|
'Symphonęy Nº9': 'Symphoney No.9',
|
||||||
u'ÆæßðÞIJij': u'AeaessdThIJıj',
|
'ÆæßðÞIJij': 'AeaessdThIJıj',
|
||||||
u'Obsessió (Cerebral Apoplexy remix)': 'obsessio cerebral '
|
'Obsessió (Cerebral Apoplexy remix)': 'obsessio cerebral '
|
||||||
'apoplexy remix',
|
'apoplexy remix',
|
||||||
u'Doktór Hałabała i siedmiu zbojów': 'doktor halabala i siedmiu '
|
'Doktór Hałabała i siedmiu zbojów': 'doktor halabala i siedmiu '
|
||||||
'zbojow',
|
'zbojow',
|
||||||
u'Arbetets Söner och Döttrar': 'arbetets soner och dottrar',
|
'Arbetets Söner och Döttrar': 'arbetets soner och dottrar',
|
||||||
u'Björk Guðmundsdóttir': 'bjork gudmundsdottir',
|
'Björk Guðmundsdóttir': 'bjork gudmundsdottir',
|
||||||
u'L\'Arc~en~Ciel': 'larc en ciel',
|
'L\'Arc~en~Ciel': 'larc en ciel',
|
||||||
u'Orquesta de la Luz (オルケスタ・デ・ラ・ルス)':
|
'Orquesta de la Luz (オルケスタ・デ・ラ・ルス)':
|
||||||
u'Orquesta de la Luz オルケスタ デ ラ ルス'
|
'Orquesta de la Luz オルケスタ デ ラ ルス'
|
||||||
|
|
||||||
}
|
}
|
||||||
for first, second in cases.iteritems():
|
for first, second in cases.items():
|
||||||
nf = clean_name(first).lower()
|
nf = clean_name(first).lower()
|
||||||
ns = clean_name(second).lower()
|
ns = clean_name(second).lower()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
nf, ns, u"check cleaning of case (%s,"
|
nf, ns, "check cleaning of case (%s,"
|
||||||
u"%s)" % (nf, ns)
|
"%s)" % (nf, ns)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_clean_name_nonunicode(self):
|
def test_clean_name_nonunicode(self):
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from headphones import logger, helpers, db, mb, lastfm, metacritic
|
from headphones import logger, helpers, db, mb, lastfm, metacritic
|
||||||
from beets.mediafile import MediaFile
|
from mediafile import MediaFile
|
||||||
import headphones
|
import headphones
|
||||||
|
|
||||||
blacklisted_special_artist_names = ['[anonymous]', '[data]', '[no artist]',
|
blacklisted_special_artist_names = ['[anonymous]', '[data]', '[no artist]',
|
||||||
@@ -39,7 +39,7 @@ def is_exists(artistid):
|
|||||||
|
|
||||||
if any(artistid in x for x in artistlist):
|
if any(artistid in x for x in artistlist):
|
||||||
logger.info(artistlist[0][
|
logger.info(artistlist[0][
|
||||||
1] + u" is already in the database. Updating 'have tracks', but not artist information")
|
1] + " is already in the database. Updating 'have tracks', but not artist information")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -53,7 +53,7 @@ def artistlist_to_mbids(artistlist, forced=False):
|
|||||||
|
|
||||||
# If adding artists through Manage New Artists, they're coming through as non-unicode (utf-8?)
|
# If adding artists through Manage New Artists, they're coming through as non-unicode (utf-8?)
|
||||||
# and screwing everything up
|
# and screwing everything up
|
||||||
if not isinstance(artist, unicode):
|
if not isinstance(artist, str):
|
||||||
try:
|
try:
|
||||||
artist = artist.decode('utf-8', 'replace')
|
artist = artist.decode('utf-8', 'replace')
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -89,11 +89,13 @@ def artistlist_to_mbids(artistlist, forced=False):
|
|||||||
addArtisttoDB(artistid)
|
addArtisttoDB(artistid)
|
||||||
|
|
||||||
# Just update the tracks if it does
|
# Just update the tracks if it does
|
||||||
else:
|
|
||||||
havetracks = len(
|
# not sure this is correct and we're updating during scanning in librarysync
|
||||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len(
|
# else:
|
||||||
myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist]))
|
# havetracks = len(
|
||||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, artistid])
|
# 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
|
# Delete it from the New Artists if the request came from there
|
||||||
if forced:
|
if forced:
|
||||||
@@ -105,7 +107,7 @@ def artistlist_to_mbids(artistlist, forced=False):
|
|||||||
try:
|
try:
|
||||||
lastfm.getSimilar()
|
lastfm.getSimilar()
|
||||||
except Exception as e:
|
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):
|
def addArtistIDListToDB(artistidlist):
|
||||||
@@ -182,7 +184,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
|||||||
else:
|
else:
|
||||||
sortname = artist['artist_name']
|
sortname = artist['artist_name']
|
||||||
|
|
||||||
logger.info(u"Now adding/updating: " + artist['artist_name'])
|
logger.info("Now adding/updating: " + artist['artist_name'])
|
||||||
controlValueDict = {"ArtistID": artistid}
|
controlValueDict = {"ArtistID": artistid}
|
||||||
newValueDict = {"ArtistName": artist['artist_name'],
|
newValueDict = {"ArtistName": artist['artist_name'],
|
||||||
"ArtistSortName": sortname,
|
"ArtistSortName": sortname,
|
||||||
@@ -261,9 +263,13 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
|||||||
new_releases = mb.get_new_releases(rgid, includeExtras)
|
new_releases = mb.get_new_releases(rgid, includeExtras)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if check_release_date is None or check_release_date == u"None":
|
if check_release_date is None or check_release_date == "None":
|
||||||
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
|
if not headphones.CONFIG.MB_IGNORE_AGE_MISSING:
|
||||||
new_releases = mb.get_new_releases(rgid, includeExtras, True)
|
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
|
||||||
|
new_releases = mb.get_new_releases(rgid, includeExtras, True)
|
||||||
|
else:
|
||||||
|
logger.info("[%s] Skipping update of: %s (No Release Date)" % (artist['artist_name'], rg['title']))
|
||||||
|
new_releases = 0
|
||||||
else:
|
else:
|
||||||
if len(check_release_date) == 10:
|
if len(check_release_date) == 10:
|
||||||
release_date = check_release_date
|
release_date = check_release_date
|
||||||
@@ -305,7 +311,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
|||||||
# This will be used later to build a hybrid release
|
# This will be used later to build a hybrid release
|
||||||
fullreleaselist = []
|
fullreleaselist = []
|
||||||
# Search for releases within a release group
|
# 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']])
|
[rg['id']])
|
||||||
|
|
||||||
# Build the dictionary for the fullreleaselist
|
# Build the dictionary for the fullreleaselist
|
||||||
@@ -511,8 +517,11 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
|||||||
marked_as_downloaded = True
|
marked_as_downloaded = True
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
u"[%s] Seeing if we need album art for %s" % (artist['artist_name'], rg['title']))
|
"[%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
|
# 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,
|
# downloaded and autowant_all is selected. This search is deferred,
|
||||||
@@ -521,17 +530,23 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
|||||||
album_searches.append(rg['id'])
|
album_searches.append(rg['id'])
|
||||||
else:
|
else:
|
||||||
if skip_log == 0:
|
if skip_log == 0:
|
||||||
logger.info(u"[%s] No new releases, so no changes made to %s" % (
|
logger.info("[%s] No new releases, so no changes made to %s" % (
|
||||||
artist['artist_name'], rg['title']))
|
artist['artist_name'], rg['title']))
|
||||||
|
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
finalize_update(artistid, artist['artist_name'], errors)
|
finalize_update(artistid, artist['artist_name'], errors)
|
||||||
|
|
||||||
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
|
logger.info("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'])
|
logger.info("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:
|
if errors:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -539,7 +554,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
|||||||
artist['artist_name'], artist['artist_name']))
|
artist['artist_name'], artist['artist_name']))
|
||||||
else:
|
else:
|
||||||
myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']])
|
myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']])
|
||||||
logger.info(u"Updating complete for: %s" % artist['artist_name'])
|
logger.info("Updating complete for: %s" % artist['artist_name'])
|
||||||
|
|
||||||
# Start searching for newly added albums
|
# Start searching for newly added albums
|
||||||
if album_searches:
|
if album_searches:
|
||||||
@@ -648,7 +663,7 @@ def addReleaseById(rid, rgid=None):
|
|||||||
sortname = release_dict['artist_name']
|
sortname = release_dict['artist_name']
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
u"Now manually adding: " + release_dict['artist_name'] + " - with status Paused")
|
"Now manually adding: " + release_dict['artist_name'] + " - with status Paused")
|
||||||
controlValueDict = {"ArtistID": release_dict['artist_id']}
|
controlValueDict = {"ArtistID": release_dict['artist_id']}
|
||||||
newValueDict = {"ArtistName": release_dict['artist_name'],
|
newValueDict = {"ArtistName": release_dict['artist_name'],
|
||||||
"ArtistSortName": sortname,
|
"ArtistSortName": sortname,
|
||||||
@@ -681,7 +696,7 @@ def addReleaseById(rid, rgid=None):
|
|||||||
|
|
||||||
if not rg_exists and release_dict or status == 'Loading' and release_dict: # it should never be the case that we have an rg and not the artist
|
if not rg_exists and release_dict or status == 'Loading' and release_dict: # it should never be the case that we have an rg and not the artist
|
||||||
# but if it is this will fail
|
# but if it is this will fail
|
||||||
logger.info(u"Now adding-by-id album (" + release_dict['title'] + ") from id: " + rgid)
|
logger.info("Now adding-by-id album (" + release_dict['title'] + ") from id: " + rgid)
|
||||||
controlValueDict = {"AlbumID": rgid}
|
controlValueDict = {"AlbumID": rgid}
|
||||||
if status != 'Loading':
|
if status != 'Loading':
|
||||||
status = 'Wanted'
|
status = 'Wanted'
|
||||||
@@ -757,7 +772,7 @@ def addReleaseById(rid, rgid=None):
|
|||||||
|
|
||||||
# Start a search for the album
|
# Start a search for the album
|
||||||
if headphones.CONFIG.AUTOWANT_MANUALLY_ADDED:
|
if headphones.CONFIG.AUTOWANT_MANUALLY_ADDED:
|
||||||
import searcher
|
from . import searcher
|
||||||
searcher.searchforalbum(rgid, False)
|
searcher.searchforalbum(rgid, False)
|
||||||
|
|
||||||
elif not rg_exists and not release_dict:
|
elif not rg_exists and not release_dict:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from headphones import db, logger, request
|
|||||||
|
|
||||||
TIMEOUT = 60.0 # seconds
|
TIMEOUT = 60.0 # seconds
|
||||||
REQUEST_LIMIT = 1.0 / 5 # seconds
|
REQUEST_LIMIT = 1.0 / 5 # seconds
|
||||||
ENTRY_POINT = "http://ws.audioscrobbler.com/2.0/"
|
ENTRY_POINT = "https://ws.audioscrobbler.com/2.0/"
|
||||||
API_KEY = "395e6ec6bb557382fc41fde867bce66f"
|
API_KEY = "395e6ec6bb557382fc41fde867bce66f"
|
||||||
|
|
||||||
# Required for API request limit
|
# Required for API request limit
|
||||||
@@ -55,7 +55,7 @@ def request_lastfm(method, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if "error" in data:
|
if "error" in data:
|
||||||
logger.error("Last.FM returned an error: %s", data["message"])
|
logger.debug("Last.FM returned an error: %s", data["message"])
|
||||||
return
|
return
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -91,7 +91,7 @@ def getSimilar():
|
|||||||
for artist, mbid in artistlist:
|
for artist, mbid in artistlist:
|
||||||
count[artist, mbid] += 1
|
count[artist, mbid] += 1
|
||||||
|
|
||||||
items = count.items()
|
items = list(count.items())
|
||||||
top_list = sorted(items, key=lambda x: x[1], reverse=True)[:25]
|
top_list = sorted(items, key=lambda x: x[1], reverse=True)[:25]
|
||||||
|
|
||||||
random.shuffle(top_list)
|
random.shuffle(top_list)
|
||||||
@@ -149,7 +149,10 @@ def getTagTopArtists(tag, limit=50):
|
|||||||
logger.debug("Fetched %d artists from Last.FM", len(artists))
|
logger.debug("Fetched %d artists from Last.FM", len(artists))
|
||||||
|
|
||||||
for artist in artists:
|
for artist in artists:
|
||||||
artist_mbid = artist["mbid"]
|
try:
|
||||||
|
artist_mbid = artist["mbid"]
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
if not any(artist_mbid in x for x in results):
|
if not any(artist_mbid in x for x in results):
|
||||||
artistlist.append(artist_mbid)
|
artistlist.append(artist_mbid)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import os
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
import headphones
|
import headphones
|
||||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
from mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||||
from headphones import db, logger, helpers, importer, lastfm
|
from headphones import db, logger, helpers, importer, lastfm
|
||||||
|
|
||||||
|
|
||||||
@@ -30,56 +30,64 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
|||||||
|
|
||||||
if not dir:
|
if not dir:
|
||||||
if not headphones.CONFIG.MUSIC_DIR:
|
if not headphones.CONFIG.MUSIC_DIR:
|
||||||
|
logger.info(
|
||||||
|
"No music directory configured. Add it under "
|
||||||
|
"Manage -> Scan Music Library"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
dir = headphones.CONFIG.MUSIC_DIR
|
dir = headphones.CONFIG.MUSIC_DIR
|
||||||
|
|
||||||
# If we're appending a dir, it's coming from the post processor which is
|
|
||||||
# already bytestring
|
|
||||||
if not append or artistScan:
|
|
||||||
dir = dir.encode(headphones.SYS_ENCODING)
|
|
||||||
|
|
||||||
if not os.path.isdir(dir):
|
if not os.path.isdir(dir):
|
||||||
logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING,
|
logger.warn(f"Cannot find music directory: {dir}")
|
||||||
'replace'))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
myDB = db.DBConnection()
|
myDB = db.DBConnection()
|
||||||
new_artists = []
|
new_artists = []
|
||||||
|
|
||||||
logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
|
logger.info(f"Scanning music directory: {dir}")
|
||||||
|
|
||||||
if not append:
|
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')
|
|
||||||
|
|
||||||
for track in tracks:
|
# Clean up bad filepaths. Queries can take some time, ensure all results are loaded before processing
|
||||||
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace')
|
if ArtistID:
|
||||||
if not os.path.isfile(encoded_track_string):
|
dbtracks = 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:
|
||||||
|
dbtracks = myDB.action(
|
||||||
|
'SELECT Location FROM alltracks WHERE Location IS NOT NULL UNION SELECT Location FROM tracks WHERE Location IS NOT NULL')
|
||||||
|
|
||||||
|
for track in dbtracks:
|
||||||
|
track_location = track['Location']
|
||||||
|
if not os.path.isfile(track_location):
|
||||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||||
[None, None, None, track['Location']])
|
[None, None, None, track_location])
|
||||||
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||||
[None, None, None, track['Location']])
|
[None, None, None, track_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')
|
||||||
|
|
||||||
for track in del_have_tracks:
|
for track in del_have_tracks:
|
||||||
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace')
|
if not os.path.isfile(track['Location']):
|
||||||
if not os.path.isfile(encoded_track_string):
|
|
||||||
if track['ArtistName']:
|
if track['ArtistName']:
|
||||||
# Make sure deleted files get accounted for when updating artist track counts
|
# Make sure deleted files get accounted for when updating artist track counts
|
||||||
new_artists.append(track['ArtistName'])
|
new_artists.append(track['ArtistName'])
|
||||||
myDB.action('DELETE FROM have WHERE Location=?', [track['Location']])
|
myDB.action('DELETE FROM have WHERE Location=?', [Track['Location']])
|
||||||
logger.info(
|
logger.info(
|
||||||
'File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(
|
f"{Track['Location']} removed from Headphones, as it "
|
||||||
headphones.SYS_ENCODING, 'replace'))
|
f"is no longer on disk"
|
||||||
|
)
|
||||||
|
|
||||||
bitrates = []
|
bitrates = []
|
||||||
song_list = []
|
track_list = []
|
||||||
latest_subdirectory = []
|
latest_subdirectory = []
|
||||||
|
|
||||||
new_song_count = 0
|
new_track_count = 0
|
||||||
file_count = 0
|
file_count = 0
|
||||||
|
|
||||||
for r, d, f in helpers.walk_directory(dir):
|
for r, d, f in helpers.walk_directory(dir):
|
||||||
@@ -94,32 +102,16 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
|||||||
subdirectory = r.replace(dir, '')
|
subdirectory = r.replace(dir, '')
|
||||||
latest_subdirectory.append(subdirectory)
|
latest_subdirectory.append(subdirectory)
|
||||||
|
|
||||||
if file_count == 0 and r.replace(dir, '') != '':
|
track_path = os.path.join(r, files)
|
||||||
logger.info("[%s] Now scanning subdirectory %s" % (
|
|
||||||
dir.decode(headphones.SYS_ENCODING, 'replace'),
|
|
||||||
subdirectory.decode(headphones.SYS_ENCODING, 'replace')))
|
|
||||||
elif latest_subdirectory[file_count] != latest_subdirectory[
|
|
||||||
file_count - 1] and file_count != 0:
|
|
||||||
logger.info("[%s] Now scanning subdirectory %s" % (
|
|
||||||
dir.decode(headphones.SYS_ENCODING, 'replace'),
|
|
||||||
subdirectory.decode(headphones.SYS_ENCODING, 'replace')))
|
|
||||||
|
|
||||||
song = os.path.join(r, files)
|
|
||||||
|
|
||||||
# We need the unicode path to use for logging, inserting into database
|
|
||||||
unicode_song_path = song.decode(headphones.SYS_ENCODING, 'replace')
|
|
||||||
|
|
||||||
# Try to read the metadata
|
# Try to read the metadata
|
||||||
try:
|
try:
|
||||||
f = MediaFile(song)
|
f = MediaFile(track_path)
|
||||||
except (FileTypeError, UnreadableFileError):
|
except (FileTypeError, UnreadableFileError):
|
||||||
logger.warning(
|
logger.warning(f"Cannot read `{track_path}`. It may be corrupted or not a media file.")
|
||||||
"Cannot read media file '%s', skipping. It may be corrupted or not a media file.",
|
|
||||||
unicode_song_path)
|
|
||||||
continue
|
continue
|
||||||
except IOError:
|
except IOError:
|
||||||
logger.warning("Cannnot read media file '%s', skipping. Does the file exists?",
|
logger.warning(f"Cannnot read `{track_path}`. Does the file exists?")
|
||||||
unicode_song_path)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Grab the bitrates for the auto detect bit rate option
|
# Grab the bitrates for the auto detect bit rate option
|
||||||
@@ -134,15 +126,15 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
|||||||
else:
|
else:
|
||||||
f_artist = None
|
f_artist = None
|
||||||
|
|
||||||
# Add the song to our song list -
|
# Add the track to our track list -
|
||||||
# TODO: skip adding songs without the minimum requisite information (just a matter of putting together the right if statements)
|
# TODO: skip adding tracks without the minimum requisite information (just a matter of putting together the right if statements)
|
||||||
|
|
||||||
if f_artist and f.album and f.title:
|
if f_artist and f.album and f.title:
|
||||||
CleanName = helpers.clean_name(f_artist + ' ' + f.album + ' ' + f.title)
|
CleanName = helpers.clean_name(f_artist + ' ' + f.album + ' ' + f.title)
|
||||||
else:
|
else:
|
||||||
CleanName = None
|
CleanName = None
|
||||||
|
|
||||||
controlValueDict = {'Location': unicode_song_path}
|
controlValueDict = {'Location': track_path}
|
||||||
|
|
||||||
newValueDict = {'TrackID': f.mb_trackid,
|
newValueDict = {'TrackID': f.mb_trackid,
|
||||||
# 'ReleaseID' : f.mb_albumid,
|
# 'ReleaseID' : f.mb_albumid,
|
||||||
@@ -158,24 +150,24 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
|||||||
'CleanName': CleanName
|
'CleanName': CleanName
|
||||||
}
|
}
|
||||||
|
|
||||||
# song_list.append(song_dict)
|
# track_list.append(track_dict)
|
||||||
check_exist_song = myDB.action("SELECT * FROM have WHERE Location=?",
|
check_exist_track = myDB.action("SELECT * FROM have WHERE Location=?",
|
||||||
[unicode_song_path]).fetchone()
|
[track_path]).fetchone()
|
||||||
# Only attempt to match songs that are new, haven't yet been matched, or metadata has changed.
|
# Only attempt to match tracks that are new, haven't yet been matched, or metadata has changed.
|
||||||
if not check_exist_song:
|
if not check_exist_track:
|
||||||
# This is a new track
|
# This is a new track
|
||||||
if f_artist:
|
if f_artist:
|
||||||
new_artists.append(f_artist)
|
new_artists.append(f_artist)
|
||||||
myDB.upsert("have", newValueDict, controlValueDict)
|
myDB.upsert("have", newValueDict, controlValueDict)
|
||||||
new_song_count += 1
|
new_track_count += 1
|
||||||
else:
|
else:
|
||||||
if check_exist_song['ArtistName'] != f_artist or check_exist_song[
|
if check_exist_track['ArtistName'] != f_artist or check_exist_track[
|
||||||
'AlbumTitle'] != f.album or check_exist_song['TrackTitle'] != f.title:
|
'AlbumTitle'] != f.album or check_exist_track['TrackTitle'] != f.title:
|
||||||
# Important track metadata has been modified, need to run matcher again
|
# Important track metadata has been modified, need to run matcher again
|
||||||
if f_artist and f_artist != check_exist_song['ArtistName']:
|
if f_artist and f_artist != check_exist_track['ArtistName']:
|
||||||
new_artists.append(f_artist)
|
new_artists.append(f_artist)
|
||||||
elif f_artist and f_artist == check_exist_song['ArtistName'] and \
|
elif f_artist and f_artist == check_exist_track['ArtistName'] and \
|
||||||
check_exist_song['Matched'] != "Ignored":
|
check_exist_track['Matched'] != "Ignored":
|
||||||
new_artists.append(f_artist)
|
new_artists.append(f_artist)
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
@@ -184,49 +176,59 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
|||||||
myDB.upsert("have", newValueDict, controlValueDict)
|
myDB.upsert("have", newValueDict, controlValueDict)
|
||||||
myDB.action(
|
myDB.action(
|
||||||
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||||
[None, None, None, unicode_song_path])
|
[None, None, None, track_path])
|
||||||
myDB.action(
|
myDB.action(
|
||||||
'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||||
[None, None, None, unicode_song_path])
|
[None, None, None, track_path])
|
||||||
new_song_count += 1
|
new_track_count += 1
|
||||||
else:
|
else:
|
||||||
# This track information hasn't changed
|
# This track information hasn't changed
|
||||||
if f_artist and check_exist_song['Matched'] != "Ignored":
|
if f_artist and check_exist_track['Matched'] != "Ignored":
|
||||||
new_artists.append(f_artist)
|
new_artists.append(f_artist)
|
||||||
|
|
||||||
file_count += 1
|
file_count += 1
|
||||||
|
|
||||||
# Now we start track matching
|
# Now we start track matching
|
||||||
logger.info("%s new/modified songs found and added to the database" % new_song_count)
|
logger.info(f"{new_track_count} new/modified tracks found and added to the database")
|
||||||
song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?",
|
dbtracks = myDB.action(
|
||||||
[dir.decode(headphones.SYS_ENCODING, 'replace') + "%"])
|
"SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?",
|
||||||
total_number_of_songs = \
|
[f"{dir}%"]
|
||||||
myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?",
|
)
|
||||||
[dir.decode(headphones.SYS_ENCODING, 'replace') + "%"]).fetchone()[0]
|
dbtracks_count = myDB.action(
|
||||||
logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(
|
"SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?",
|
||||||
headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....")
|
[f"{dir}%"]
|
||||||
|
).fetchone()[0]
|
||||||
|
logger.info(f"Found {dbtracks_count} new/modified tracks in `{dir}`")
|
||||||
|
logger.info("Matching tracks to the appropriate releases....")
|
||||||
|
|
||||||
# Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid)
|
|
||||||
# When we insert into the database, the tracks with the most specific information will overwrite the more general matches
|
|
||||||
|
|
||||||
# song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID'])
|
|
||||||
song_list = helpers.multikeysort(song_list, ['ArtistName', 'AlbumTitle'])
|
|
||||||
|
|
||||||
# We'll use this to give a % completion, just because the track matching might take a while
|
# Sort the track_list by most vague (e.g. no trackid or releaseid)
|
||||||
song_count = 0
|
# to most specific (both trackid & releaseid)
|
||||||
latest_artist = []
|
# When we insert into the database, the tracks with the most
|
||||||
|
# specific information will overwrite the more general matches
|
||||||
|
|
||||||
|
sorted_dbtracks = helpers.multikeysort(dbtracks, ['ArtistName', 'AlbumTitle'])
|
||||||
|
|
||||||
|
|
||||||
|
# We'll use this to give a % completion, just because the
|
||||||
|
# track matching might take a while
|
||||||
|
tracks_completed = 0
|
||||||
|
latest_artist = None
|
||||||
last_completion_percentage = 0
|
last_completion_percentage = 0
|
||||||
|
prev_artist_name = None
|
||||||
|
artistid = None
|
||||||
|
|
||||||
for song in song_list:
|
for track in sorted_dbtracks:
|
||||||
|
|
||||||
latest_artist.append(song['ArtistName'])
|
if latest_artist != track['ArtistName']:
|
||||||
if song_count == 0:
|
logger.info(f"Now matching tracks by {track['ArtistName']}")
|
||||||
logger.info("Now matching songs by %s" % song['ArtistName'])
|
latest_artist = track['ArtistName']
|
||||||
elif latest_artist[song_count] != latest_artist[song_count - 1] and song_count != 0:
|
|
||||||
logger.info("Now matching songs by %s" % song['ArtistName'])
|
|
||||||
|
|
||||||
song_count += 1
|
tracks_completed += 1
|
||||||
completion_percentage = math.floor(float(song_count) / total_number_of_songs * 1000) / 10
|
completion_percentage = math.floor(
|
||||||
|
float(tracks_completed) / dbtracks_count * 1000
|
||||||
|
) / 10
|
||||||
|
|
||||||
if completion_percentage >= (last_completion_percentage + 10):
|
if completion_percentage >= (last_completion_percentage + 10):
|
||||||
logger.info("Track matching is " + str(completion_percentage) + "% complete")
|
logger.info("Track matching is " + str(completion_percentage) + "% complete")
|
||||||
@@ -237,139 +239,184 @@ 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
|
# 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.
|
# (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(
|
if track['ArtistName'] and track['CleanName']:
|
||||||
'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
artist_name = track['ArtistName']
|
||||||
[song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
clean_name = track['CleanName']
|
||||||
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)
|
|
||||||
|
|
||||||
controlValueDict2 = {'Location': song['Location']}
|
# Only update if artist is in the db
|
||||||
newValueDict2 = {'Matched': track['AlbumID']}
|
if artist_name != prev_artist_name:
|
||||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
prev_artist_name = artist_name
|
||||||
have_updated = True
|
artistid = None
|
||||||
else:
|
|
||||||
track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?',
|
|
||||||
[song['CleanName']]).fetchone()
|
|
||||||
if track:
|
|
||||||
controlValueDict = {'CleanName': track['CleanName']}
|
|
||||||
newValueDict = {'Location': song['Location'],
|
|
||||||
'BitRate': song['BitRate'],
|
|
||||||
'Format': song['Format']}
|
|
||||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
|
||||||
|
|
||||||
controlValueDict2 = {'Location': song['Location']}
|
artist_lookup = "\"" + artist_name.replace("\"", "\"\"") + "\""
|
||||||
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(
|
try:
|
||||||
'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM artists WHERE ArtistName LIKE ' + artist_lookup + '')
|
||||||
[song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
except:
|
||||||
if alltrack:
|
dbartist = None
|
||||||
controlValueDict = {'ArtistName': alltrack['ArtistName'],
|
if not dbartist:
|
||||||
'AlbumTitle': alltrack['AlbumTitle'],
|
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM tracks WHERE CleanName = ?', [clean_name])
|
||||||
'TrackTitle': alltrack['TrackTitle']}
|
if not dbartist:
|
||||||
newValueDict = {'Location': song['Location'],
|
dbartist = myDB.select('SELECT DISTINCT ArtistID, ArtistName FROM alltracks WHERE CleanName = ?', [clean_name])
|
||||||
'BitRate': song['BitRate'],
|
if not dbartist:
|
||||||
'Format': song['Format']}
|
clean_artist = helpers.clean_name(artist_name)
|
||||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
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 + '{'])
|
||||||
|
|
||||||
controlValueDict2 = {'Location': song['Location']}
|
if dbartist:
|
||||||
newValueDict2 = {'Matched': alltrack['AlbumID']}
|
artistid = dbartist[0][0]
|
||||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
|
||||||
else:
|
if artistid:
|
||||||
alltrack = myDB.action(
|
|
||||||
'SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?',
|
# This was previously using Artist, Album, Title with a SELECT LIKE ? and was not using an index
|
||||||
[song['CleanName']]).fetchone()
|
# (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
|
||||||
|
dbtrack = myDB.action('SELECT AlbumID, ArtistName FROM tracks WHERE CleanName = ? AND ArtistID = ?', [clean_name, artistid]).fetchone()
|
||||||
|
if dbtrack:
|
||||||
|
albumid = dbtrack['AlbumID']
|
||||||
|
myDB.action(
|
||||||
|
'UPDATE tracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ? AND ArtistID = ?',
|
||||||
|
[track['Location'], track['BitRate'], track['Format'], clean_name, artistid])
|
||||||
|
|
||||||
|
# Update alltracks
|
||||||
|
alltrack = myDB.action('SELECT AlbumID, ArtistName FROM alltracks WHERE CleanName = ? AND ArtistID = ?', [clean_name, artistid]).fetchone()
|
||||||
if alltrack:
|
if alltrack:
|
||||||
controlValueDict = {'CleanName': alltrack['CleanName']}
|
albumid = alltrack['AlbumID']
|
||||||
newValueDict = {'Location': song['Location'],
|
myDB.action(
|
||||||
'BitRate': song['BitRate'],
|
'UPDATE alltracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ? AND ArtistID = ?',
|
||||||
'Format': song['Format']}
|
[track['Location'], track['BitRate'], track['Format'], clean_name, artistid])
|
||||||
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)
|
|
||||||
|
|
||||||
|
# Update have
|
||||||
|
controlValueDict2 = {'Location': track['Location']}
|
||||||
|
if albumid:
|
||||||
|
newValueDict2 = {'Matched': albumid}
|
||||||
else:
|
else:
|
||||||
controlValueDict2 = {'Location': song['Location']}
|
|
||||||
newValueDict2 = {'Matched': "Failed"}
|
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( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [track['ArtistName'], track['AlbumTitle'], track['TrackNumber'], track['TrackTitle'], track['TrackLength'], track['BitRate'], track['Genre'], track['Date'], track['TrackID'], track['Location'], CleanName, track['Format']])
|
||||||
|
|
||||||
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING,
|
logger.info(f"Completed matching tracks from `{dir}`")
|
||||||
'replace'))
|
|
||||||
|
|
||||||
if not append or artistScan:
|
if not append or artistScan:
|
||||||
logger.info('Updating scanned artist track counts')
|
logger.info('Updating scanned artist track counts')
|
||||||
|
|
||||||
# Clean up the new artist list
|
# Clean up the new artist list
|
||||||
unique_artists = {}.fromkeys(new_artists).keys()
|
unique_artists = list({}.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.
|
# # Don't think we need to do this, check the db instead below
|
||||||
artist_list = [
|
#
|
||||||
x for x in unique_artists
|
# # artist scan
|
||||||
if helpers.clean_name(x).lower() not in [
|
# if ArtistName:
|
||||||
helpers.clean_name(y[0]).lower()
|
# current_artists = [[ArtistName]]
|
||||||
for y in current_artists
|
# # directory scan
|
||||||
]
|
# else:
|
||||||
]
|
# current_artists = myDB.select('SELECT ArtistName, ArtistID FROM artists WHERE ArtistName IS NOT NULL')
|
||||||
artists_checked = [
|
#
|
||||||
x for x in unique_artists
|
# # There was a bug where artists with special characters (-,') would show up in new artists.
|
||||||
if helpers.clean_name(x).lower() in [
|
#
|
||||||
helpers.clean_name(y[0]).lower()
|
# # artist_list = scanned artists not in the db
|
||||||
for y in current_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 = 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
|
new_artist_list = []
|
||||||
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])
|
|
||||||
|
|
||||||
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:
|
if headphones.CONFIG.AUTO_ADD_ARTISTS:
|
||||||
logger.info('Importing %i new artists' % len(artist_list))
|
logger.info('Importing %i new artists' % len(new_artist_list))
|
||||||
importer.artistlist_to_mbids(artist_list)
|
importer.artistlist_to_mbids(new_artist_list)
|
||||||
else:
|
else:
|
||||||
logger.info('To add these artists, go to Manage->Manage New Artists')
|
logger.info('To add these artists, go to Manage->Manage New Artists')
|
||||||
# myDB.action('DELETE from newartists')
|
# 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])
|
myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist])
|
||||||
|
|
||||||
if headphones.CONFIG.DETECT_BITRATE and bitrates:
|
if headphones.CONFIG.DETECT_BITRATE and bitrates:
|
||||||
@@ -379,50 +426,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
|
# If we're appending a new album to the database, update the artists total track counts
|
||||||
logger.info('Updating artist track counts')
|
logger.info('Updating artist track counts')
|
||||||
|
|
||||||
havetracks = len(
|
artist_lookup = "\"" + ArtistName.replace("\"", "\"\"") + "\""
|
||||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
try:
|
||||||
[ArtistID])) + len(myDB.select(
|
havetracks = len(
|
||||||
'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
myDB.select('SELECT ArtistID FROM tracks WHERE ArtistID = ? AND Location IS NOT NULL',
|
||||||
[ArtistName]))
|
[ArtistID])) + len(myDB.select(
|
||||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
|
'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:
|
if havetracks:
|
||||||
update_album_status()
|
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:
|
if not append and not artistScan:
|
||||||
lastfm.getSimilar()
|
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
|
# 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()
|
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:
|
if AlbumID:
|
||||||
album_status_updater = myDB.action(
|
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:
|
else:
|
||||||
album_status_updater = myDB.action('SELECT AlbumID, AlbumTitle, Status from albums')
|
album_status_updater = myDB.action(
|
||||||
for album in album_status_updater:
|
'SELECT'
|
||||||
track_counter = myDB.action('SELECT Location from tracks where AlbumID=?',
|
' a.AlbumID, a.ArtistID, a.ArtistName, a.AlbumTitle, a.Status, AVG(t.Location IS NOT NULL) * 100 AS album_completion '
|
||||||
[album['AlbumID']])
|
'FROM'
|
||||||
total_tracks = 0
|
' albums AS a '
|
||||||
have_tracks = 0
|
'JOIN tracks AS t ON t.AlbumID = a.AlbumID '
|
||||||
for track in track_counter:
|
'WHERE'
|
||||||
total_tracks += 1
|
' a.ArtistID = ? AND a.Status != "Downloaded" '
|
||||||
if track['Location']:
|
'GROUP BY'
|
||||||
have_tracks += 1
|
' a.AlbumID '
|
||||||
if total_tracks != 0:
|
'HAVING'
|
||||||
album_completion = float(float(have_tracks) / float(total_tracks)) * 100
|
' AVG(t.Location IS NOT NULL) * 100 >= ?',
|
||||||
else:
|
[ArtistID, headphones.CONFIG.ALBUM_COMPLETION_PCT]
|
||||||
album_completion = 0
|
)
|
||||||
logger.info('Album %s does not have any tracks in database' % album['AlbumTitle'])
|
|
||||||
|
|
||||||
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 don't think we want to change Downloaded->Skipped.....
|
||||||
# I think we can only automatically change Skipped->Downloaded when updating
|
# I think we can only automatically change Skipped->Downloaded when updating
|
||||||
@@ -433,10 +508,13 @@ def update_album_status(AlbumID=None):
|
|||||||
# new_album_status = "Skipped"
|
# new_album_status = "Skipped"
|
||||||
# else:
|
# else:
|
||||||
# new_album_status = album['Status']
|
# new_album_status = album['Status']
|
||||||
else:
|
# else:
|
||||||
new_album_status = album['Status']
|
# new_album_status = album['Status']
|
||||||
|
#
|
||||||
|
# myDB.upsert("albums", {'Status': new_album_status}, {'AlbumID': album['AlbumID']})
|
||||||
|
# if new_album_status != album['Status']:
|
||||||
|
# logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status))
|
||||||
|
# logger.info('Album status update complete')
|
||||||
|
|
||||||
myDB.upsert("albums", {'Status': new_album_status}, {'AlbumID': album['AlbumID']})
|
myDB.action('UPDATE albums SET Status = ? WHERE AlbumID = ?', [new_album_status, album[0]])
|
||||||
if new_album_status != album['Status']:
|
logger.info('Album: %s - %s. Status updated to %s' % (album[1], album[2], new_album_status))
|
||||||
logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status))
|
|
||||||
logger.info('Album status update complete')
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Locking-related classes
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import Queue
|
import queue
|
||||||
|
|
||||||
import headphones.logger
|
import headphones.logger
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class TimedLock(object):
|
|||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.last_used = 0
|
self.last_used = 0
|
||||||
self.minimum_delta = minimum_delta
|
self.minimum_delta = minimum_delta
|
||||||
self.queue = Queue.Queue()
|
self.queue = queue.Queue()
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""
|
"""
|
||||||
@@ -47,7 +47,7 @@ class TimedLock(object):
|
|||||||
seconds = self.queue.get(False)
|
seconds = self.queue.get(False)
|
||||||
headphones.logger.debug('Sleeping %s (queued)', seconds)
|
headphones.logger.debug('Sleeping %s (queued)', seconds)
|
||||||
time.sleep(seconds)
|
time.sleep(seconds)
|
||||||
except Queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,8 @@ def initLogger(console=False, log_dir=False, verbose=False):
|
|||||||
file_formatter = logging.Formatter(
|
file_formatter = logging.Formatter(
|
||||||
'%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
'%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||||
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE,
|
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE,
|
||||||
backupCount=MAX_FILES)
|
backupCount=MAX_FILES,
|
||||||
|
encoding='utf8')
|
||||||
file_handler.setLevel(logging.DEBUG)
|
file_handler.setLevel(logging.DEBUG)
|
||||||
file_handler.setFormatter(file_formatter)
|
file_handler.setFormatter(file_formatter)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import htmlentitydefs
|
import html.entities
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from headphones import logger, request
|
from headphones import logger, request
|
||||||
@@ -25,7 +25,7 @@ def getLyrics(artist, song):
|
|||||||
"fmt": 'xml'
|
"fmt": 'xml'
|
||||||
}
|
}
|
||||||
|
|
||||||
url = 'http://lyrics.wikia.com/api.php'
|
url = 'https://lyrics.wikia.com/api.php'
|
||||||
data = request.request_minidom(url, params=params)
|
data = request.request_minidom(url, params=params)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
@@ -53,7 +53,7 @@ def getLyrics(artist, song):
|
|||||||
'''<div class='lyricbox'><span style="padding:1em"><a href="/Category:Instrumental" title="Instrumental">''').search(
|
'''<div class='lyricbox'><span style="padding:1em"><a href="/Category:Instrumental" title="Instrumental">''').search(
|
||||||
lyricspage)
|
lyricspage)
|
||||||
if m:
|
if m:
|
||||||
return u'(Instrumental)'
|
return '(Instrumental)'
|
||||||
else:
|
else:
|
||||||
logger.warn('Cannot find lyrics on: %s' % lyricsurl)
|
logger.warn('Cannot find lyrics on: %s' % lyricsurl)
|
||||||
return
|
return
|
||||||
@@ -72,7 +72,7 @@ def convert_html_entities(s):
|
|||||||
name = hit[2:-1]
|
name = hit[2:-1]
|
||||||
try:
|
try:
|
||||||
entnum = int(name)
|
entnum = int(name)
|
||||||
s = s.replace(hit, unichr(entnum))
|
s = s.replace(hit, chr(entnum))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ def convert_html_entities(s):
|
|||||||
hits.remove(amp)
|
hits.remove(amp)
|
||||||
for hit in hits:
|
for hit in hits:
|
||||||
name = hit[1:-1]
|
name = hit[1:-1]
|
||||||
if name in htmlentitydefs.name2codepoint:
|
if name in html.entities.name2codepoint:
|
||||||
s = s.replace(hit, unichr(htmlentitydefs.name2codepoint[name]))
|
s = s.replace(hit, chr(html.entities.name2codepoint[name]))
|
||||||
s = s.replace(amp, "&")
|
s = s.replace(amp, "&")
|
||||||
return s
|
return s
|
||||||
|
|||||||
@@ -91,10 +91,6 @@ def findArtist(name, limit=1):
|
|||||||
artistlist = []
|
artistlist = []
|
||||||
artistResults = None
|
artistResults = None
|
||||||
|
|
||||||
chars = set('!?*-')
|
|
||||||
if any((c in chars) for c in name):
|
|
||||||
name = '"' + name + '"'
|
|
||||||
|
|
||||||
criteria = {'artist': name.lower()}
|
criteria = {'artist': name.lower()}
|
||||||
|
|
||||||
with mb_lock:
|
with mb_lock:
|
||||||
@@ -116,9 +112,9 @@ def findArtist(name, limit=1):
|
|||||||
return False
|
return False
|
||||||
for result in artistResults:
|
for result in artistResults:
|
||||||
if 'disambiguation' in result:
|
if 'disambiguation' in result:
|
||||||
uniquename = unicode(result['sort-name'] + " (" + result['disambiguation'] + ")")
|
uniquename = str(result['sort-name'] + " (" + result['disambiguation'] + ")")
|
||||||
else:
|
else:
|
||||||
uniquename = unicode(result['sort-name'])
|
uniquename = str(result['sort-name'])
|
||||||
if result['name'] != uniquename and limit == 1:
|
if result['name'] != uniquename and limit == 1:
|
||||||
logger.info(
|
logger.info(
|
||||||
'Found an artist with a disambiguation: %s - doing an album based search' % name)
|
'Found an artist with a disambiguation: %s - doing an album based search' % name)
|
||||||
@@ -128,20 +124,16 @@ def findArtist(name, limit=1):
|
|||||||
'Cannot determine the best match from an artist/album search. Using top match instead')
|
'Cannot determine the best match from an artist/album search. Using top match instead')
|
||||||
artistlist.append({
|
artistlist.append({
|
||||||
# Just need the artist id if the limit is 1
|
# Just need the artist id if the limit is 1
|
||||||
# 'name': unicode(result['sort-name']),
|
'id': str(result['id']),
|
||||||
# 'uniquename': uniquename,
|
|
||||||
'id': unicode(result['id']),
|
|
||||||
# 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
|
|
||||||
# 'score': int(result['ext:score'])
|
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
artistlist.append(artistdict)
|
artistlist.append(artistdict)
|
||||||
else:
|
else:
|
||||||
artistlist.append({
|
artistlist.append({
|
||||||
'name': unicode(result['sort-name']),
|
'name': str(result['sort-name']),
|
||||||
'uniquename': uniquename,
|
'uniquename': uniquename,
|
||||||
'id': unicode(result['id']),
|
'id': str(result['id']),
|
||||||
'url': unicode("http://musicbrainz.org/artist/" + result['id']),
|
'url': str("https://musicbrainz.org/artist/" + result['id']),
|
||||||
# probably needs to be changed
|
# probably needs to be changed
|
||||||
'score': int(result['ext:score'])
|
'score': int(result['ext:score'])
|
||||||
})
|
})
|
||||||
@@ -156,16 +148,13 @@ def findRelease(name, limit=1, artist=None):
|
|||||||
if not artist and ':' in name:
|
if not artist and ':' in name:
|
||||||
name, artist = name.rsplit(":", 1)
|
name, artist = name.rsplit(":", 1)
|
||||||
|
|
||||||
chars = set('!?*-')
|
criteria = {'release': name.lower()}
|
||||||
if any((c in chars) for c in name):
|
if artist:
|
||||||
name = '"' + name + '"'
|
criteria['artist'] = artist.lower()
|
||||||
if artist and any((c in chars) for c in artist):
|
|
||||||
artist = '"' + artist + '"'
|
|
||||||
|
|
||||||
with mb_lock:
|
with mb_lock:
|
||||||
try:
|
try:
|
||||||
releaseResults = musicbrainzngs.search_releases(query=name, limit=limit, artist=artist)[
|
releaseResults = musicbrainzngs.search_releases(limit=limit, **criteria)['release-list']
|
||||||
'release-list']
|
|
||||||
except musicbrainzngs.WebServiceError as e: # need to update exceptions
|
except musicbrainzngs.WebServiceError as e: # need to update exceptions
|
||||||
logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e)))
|
logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e)))
|
||||||
mb_lock.snooze(5)
|
mb_lock.snooze(5)
|
||||||
@@ -194,7 +183,7 @@ def findRelease(name, limit=1, artist=None):
|
|||||||
if tracks:
|
if tracks:
|
||||||
tracks += ' + '
|
tracks += ' + '
|
||||||
tracks += str(medium['track-count'])
|
tracks += str(medium['track-count'])
|
||||||
for format, count in format_dict.items():
|
for format, count in list(format_dict.items()):
|
||||||
if formats:
|
if formats:
|
||||||
formats += ' + '
|
formats += ' + '
|
||||||
if count > 1:
|
if count > 1:
|
||||||
@@ -210,22 +199,22 @@ def findRelease(name, limit=1, artist=None):
|
|||||||
rg_type = secondary_type
|
rg_type = secondary_type
|
||||||
|
|
||||||
releaselist.append({
|
releaselist.append({
|
||||||
'uniquename': unicode(result['artist-credit'][0]['artist']['name']),
|
'uniquename': str(result['artist-credit'][0]['artist']['name']),
|
||||||
'title': unicode(title),
|
'title': str(title),
|
||||||
'id': unicode(result['artist-credit'][0]['artist']['id']),
|
'id': str(result['artist-credit'][0]['artist']['id']),
|
||||||
'albumid': unicode(result['id']),
|
'albumid': str(result['id']),
|
||||||
'url': unicode(
|
'url': str(
|
||||||
"http://musicbrainz.org/artist/" + result['artist-credit'][0]['artist']['id']),
|
"https://musicbrainz.org/artist/" + result['artist-credit'][0]['artist']['id']),
|
||||||
# probably needs to be changed
|
# probably needs to be changed
|
||||||
'albumurl': unicode("http://musicbrainz.org/release/" + result['id']),
|
'albumurl': str("https://musicbrainz.org/release/" + result['id']),
|
||||||
# probably needs to be changed
|
# probably needs to be changed
|
||||||
'score': int(result['ext:score']),
|
'score': int(result['ext:score']),
|
||||||
'date': unicode(result['date']) if 'date' in result else '',
|
'date': str(result['date']) if 'date' in result else '',
|
||||||
'country': unicode(result['country']) if 'country' in result else '',
|
'country': str(result['country']) if 'country' in result else '',
|
||||||
'formats': unicode(formats),
|
'formats': str(formats),
|
||||||
'tracks': unicode(tracks),
|
'tracks': str(tracks),
|
||||||
'rgid': unicode(result['release-group']['id']),
|
'rgid': str(result['release-group']['id']),
|
||||||
'rgtype': unicode(rg_type)
|
'rgtype': str(rg_type)
|
||||||
})
|
})
|
||||||
return releaselist
|
return releaselist
|
||||||
|
|
||||||
@@ -234,10 +223,6 @@ def findSeries(name, limit=1):
|
|||||||
serieslist = []
|
serieslist = []
|
||||||
seriesResults = None
|
seriesResults = None
|
||||||
|
|
||||||
chars = set('!?*-')
|
|
||||||
if any((c in chars) for c in name):
|
|
||||||
name = '"' + name + '"'
|
|
||||||
|
|
||||||
criteria = {'series': name.lower()}
|
criteria = {'series': name.lower()}
|
||||||
|
|
||||||
with mb_lock:
|
with mb_lock:
|
||||||
@@ -251,15 +236,15 @@ def findSeries(name, limit=1):
|
|||||||
return False
|
return False
|
||||||
for result in seriesResults:
|
for result in seriesResults:
|
||||||
if 'disambiguation' in result:
|
if 'disambiguation' in result:
|
||||||
uniquename = unicode(result['name'] + " (" + result['disambiguation'] + ")")
|
uniquename = str(result['name'] + " (" + result['disambiguation'] + ")")
|
||||||
else:
|
else:
|
||||||
uniquename = unicode(result['name'])
|
uniquename = str(result['name'])
|
||||||
serieslist.append({
|
serieslist.append({
|
||||||
'uniquename': uniquename,
|
'uniquename': uniquename,
|
||||||
'name': unicode(result['name']),
|
'name': str(result['name']),
|
||||||
'type': unicode(result['type']),
|
'type': str(result['type']),
|
||||||
'id': unicode(result['id']),
|
'id': str(result['id']),
|
||||||
'url': unicode("http://musicbrainz.org/series/" + result['id']),
|
'url': str("https://musicbrainz.org/series/" + result['id']),
|
||||||
# probably needs to be changed
|
# probably needs to be changed
|
||||||
'score': int(result['ext:score'])
|
'score': int(result['ext:score'])
|
||||||
})
|
})
|
||||||
@@ -295,19 +280,19 @@ def getArtist(artistid, extrasonly=False):
|
|||||||
if not artist:
|
if not artist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
artist_dict['artist_name'] = unicode(artist['name'])
|
artist_dict['artist_name'] = str(artist['name'])
|
||||||
|
|
||||||
releasegroups = []
|
releasegroups = []
|
||||||
|
|
||||||
if not extrasonly:
|
if not extrasonly:
|
||||||
for rg in artist['release-group-list']:
|
for rg in artist['release-group-list']:
|
||||||
if "secondary-type-list" in rg.keys(): # only add releases without a secondary type
|
if "secondary-type-list" in list(rg.keys()): # only add releases without a secondary type
|
||||||
continue
|
continue
|
||||||
releasegroups.append({
|
releasegroups.append({
|
||||||
'title': unicode(rg['title']),
|
'title': str(rg['title']),
|
||||||
'id': unicode(rg['id']),
|
'id': str(rg['id']),
|
||||||
'url': u"http://musicbrainz.org/release-group/" + rg['id'],
|
'url': "https://musicbrainz.org/release-group/" + rg['id'],
|
||||||
'type': unicode(rg['type'])
|
'type': str(rg['type'])
|
||||||
})
|
})
|
||||||
|
|
||||||
# See if we need to grab extras. Artist specific extras take precedence over global option
|
# See if we need to grab extras. Artist specific extras take precedence over global option
|
||||||
@@ -325,7 +310,7 @@ def getArtist(artistid, extrasonly=False):
|
|||||||
|
|
||||||
# Need to convert extras string from something like '2,5.6' to ['ep','live','remix'] (append new extras to end)
|
# Need to convert extras string from something like '2,5.6' to ['ep','live','remix'] (append new extras to end)
|
||||||
if db_artist['Extras']:
|
if db_artist['Extras']:
|
||||||
extras = map(int, db_artist['Extras'].split(','))
|
extras = list(map(int, db_artist['Extras'].split(',')))
|
||||||
else:
|
else:
|
||||||
extras = []
|
extras = []
|
||||||
extras_list = headphones.POSSIBLE_EXTRAS
|
extras_list = headphones.POSSIBLE_EXTRAS
|
||||||
@@ -365,10 +350,10 @@ def getArtist(artistid, extrasonly=False):
|
|||||||
rg_type = secondary_type
|
rg_type = secondary_type
|
||||||
|
|
||||||
releasegroups.append({
|
releasegroups.append({
|
||||||
'title': unicode(rg['title']),
|
'title': str(rg['title']),
|
||||||
'id': unicode(rg['id']),
|
'id': str(rg['id']),
|
||||||
'url': u"http://musicbrainz.org/release-group/" + rg['id'],
|
'url': "https://musicbrainz.org/release-group/" + rg['id'],
|
||||||
'type': unicode(rg_type)
|
'type': str(rg_type)
|
||||||
})
|
})
|
||||||
artist_dict['releasegroups'] = releasegroups
|
artist_dict['releasegroups'] = releasegroups
|
||||||
return artist_dict
|
return artist_dict
|
||||||
@@ -393,10 +378,10 @@ def getSeries(seriesid):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if 'disambiguation' in series:
|
if 'disambiguation' in series:
|
||||||
series_dict['artist_name'] = unicode(
|
series_dict['artist_name'] = str(
|
||||||
series['name'] + " (" + unicode(series['disambiguation']) + ")")
|
series['name'] + " (" + str(series['disambiguation']) + ")")
|
||||||
else:
|
else:
|
||||||
series_dict['artist_name'] = unicode(series['name'])
|
series_dict['artist_name'] = str(series['name'])
|
||||||
|
|
||||||
releasegroups = []
|
releasegroups = []
|
||||||
|
|
||||||
@@ -459,42 +444,42 @@ def getRelease(releaseid, include_artist_info=True):
|
|||||||
if not results:
|
if not results:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
release['title'] = unicode(results['title'])
|
release['title'] = str(results['title'])
|
||||||
release['id'] = unicode(results['id'])
|
release['id'] = str(results['id'])
|
||||||
release['asin'] = unicode(results['asin']) if 'asin' in results else None
|
release['asin'] = str(results['asin']) if 'asin' in results else None
|
||||||
release['date'] = unicode(results['date']) if 'date' in results else None
|
release['date'] = str(results['date']) if 'date' in results else None
|
||||||
try:
|
try:
|
||||||
release['format'] = unicode(results['medium-list'][0]['format'])
|
release['format'] = str(results['medium-list'][0]['format'])
|
||||||
except:
|
except:
|
||||||
release['format'] = u'Unknown'
|
release['format'] = 'Unknown'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
release['country'] = unicode(results['country'])
|
release['country'] = str(results['country'])
|
||||||
except:
|
except:
|
||||||
release['country'] = u'Unknown'
|
release['country'] = 'Unknown'
|
||||||
|
|
||||||
if include_artist_info:
|
if include_artist_info:
|
||||||
|
|
||||||
if 'release-group' in results:
|
if 'release-group' in results:
|
||||||
release['rgid'] = unicode(results['release-group']['id'])
|
release['rgid'] = str(results['release-group']['id'])
|
||||||
release['rg_title'] = unicode(results['release-group']['title'])
|
release['rg_title'] = str(results['release-group']['title'])
|
||||||
try:
|
try:
|
||||||
release['rg_type'] = unicode(results['release-group']['type'])
|
release['rg_type'] = str(results['release-group']['type'])
|
||||||
|
|
||||||
if release['rg_type'] == 'Album' and 'secondary-type-list' in results[
|
if release['rg_type'] == 'Album' and 'secondary-type-list' in results[
|
||||||
'release-group']:
|
'release-group']:
|
||||||
secondary_type = unicode(results['release-group']['secondary-type-list'][0])
|
secondary_type = str(results['release-group']['secondary-type-list'][0])
|
||||||
if secondary_type != release['rg_type']:
|
if secondary_type != release['rg_type']:
|
||||||
release['rg_type'] = secondary_type
|
release['rg_type'] = secondary_type
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
release['rg_type'] = u'Unknown'
|
release['rg_type'] = 'Unknown'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.warn("Release " + releaseid + "had no ReleaseGroup associated")
|
logger.warn("Release " + releaseid + "had no ReleaseGroup associated")
|
||||||
|
|
||||||
release['artist_name'] = unicode(results['artist-credit'][0]['artist']['name'])
|
release['artist_name'] = str(results['artist-credit'][0]['artist']['name'])
|
||||||
release['artist_id'] = unicode(results['artist-credit'][0]['artist']['id'])
|
release['artist_id'] = str(results['artist-credit'][0]['artist']['id'])
|
||||||
|
|
||||||
release['tracks'] = getTracksFromRelease(results)
|
release['tracks'] = getTracksFromRelease(results)
|
||||||
|
|
||||||
@@ -540,7 +525,7 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
|||||||
force_repackage1 = 0
|
force_repackage1 = 0
|
||||||
if len(results) != 0:
|
if len(results) != 0:
|
||||||
for release_mark in results:
|
for release_mark in results:
|
||||||
release_list.append(unicode(release_mark['id']))
|
release_list.append(str(release_mark['id']))
|
||||||
release_title = release_mark['title']
|
release_title = release_mark['title']
|
||||||
remove_missing_releases = myDB.action("SELECT ReleaseID FROM allalbums WHERE AlbumID=?",
|
remove_missing_releases = myDB.action("SELECT ReleaseID FROM allalbums WHERE AlbumID=?",
|
||||||
[rgid])
|
[rgid])
|
||||||
@@ -572,31 +557,31 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
|||||||
# DELETE all references to this release since we're updating it anyway.
|
# DELETE all references to this release since we're updating it anyway.
|
||||||
myDB.action('DELETE from allalbums WHERE ReleaseID=?', [rel_id_check])
|
myDB.action('DELETE from allalbums WHERE ReleaseID=?', [rel_id_check])
|
||||||
myDB.action('DELETE from alltracks WHERE ReleaseID=?', [rel_id_check])
|
myDB.action('DELETE from alltracks WHERE ReleaseID=?', [rel_id_check])
|
||||||
release['AlbumTitle'] = unicode(releasedata['title'])
|
release['AlbumTitle'] = str(releasedata['title'])
|
||||||
release['AlbumID'] = unicode(rgid)
|
release['AlbumID'] = str(rgid)
|
||||||
release['AlbumASIN'] = unicode(releasedata['asin']) if 'asin' in releasedata else None
|
release['AlbumASIN'] = str(releasedata['asin']) if 'asin' in releasedata else None
|
||||||
release['ReleaseDate'] = unicode(releasedata['date']) if 'date' in releasedata else None
|
release['ReleaseDate'] = str(releasedata['date']) if 'date' in releasedata else None
|
||||||
release['ReleaseID'] = releasedata['id']
|
release['ReleaseID'] = releasedata['id']
|
||||||
if 'release-group' not in releasedata:
|
if 'release-group' not in releasedata:
|
||||||
raise Exception('No release group associated with release id ' + releasedata[
|
raise Exception('No release group associated with release id ' + releasedata[
|
||||||
'id'] + ' album id' + rgid)
|
'id'] + ' album id' + rgid)
|
||||||
release['Type'] = unicode(releasedata['release-group']['type'])
|
release['Type'] = str(releasedata['release-group']['type'])
|
||||||
|
|
||||||
if release['Type'] == 'Album' and 'secondary-type-list' in releasedata['release-group']:
|
if release['Type'] == 'Album' and 'secondary-type-list' in releasedata['release-group']:
|
||||||
secondary_type = unicode(releasedata['release-group']['secondary-type-list'][0])
|
secondary_type = str(releasedata['release-group']['secondary-type-list'][0])
|
||||||
if secondary_type != release['Type']:
|
if secondary_type != release['Type']:
|
||||||
release['Type'] = secondary_type
|
release['Type'] = secondary_type
|
||||||
|
|
||||||
# making the assumption that the most important artist will be first in the list
|
# making the assumption that the most important artist will be first in the list
|
||||||
if 'artist-credit' in releasedata:
|
if 'artist-credit' in releasedata:
|
||||||
release['ArtistID'] = unicode(releasedata['artist-credit'][0]['artist']['id'])
|
release['ArtistID'] = str(releasedata['artist-credit'][0]['artist']['id'])
|
||||||
release['ArtistName'] = unicode(releasedata['artist-credit-phrase'])
|
release['ArtistName'] = str(releasedata['artist-credit-phrase'])
|
||||||
else:
|
else:
|
||||||
logger.warn('Release ' + releasedata['id'] + ' has no Artists associated.')
|
logger.warn('Release ' + releasedata['id'] + ' has no Artists associated.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
release['ReleaseCountry'] = unicode(
|
release['ReleaseCountry'] = str(
|
||||||
releasedata['country']) if 'country' in releasedata else u'Unknown'
|
releasedata['country']) if 'country' in releasedata else 'Unknown'
|
||||||
# assuming that the list will contain media and that the format will be consistent
|
# assuming that the list will contain media and that the format will be consistent
|
||||||
try:
|
try:
|
||||||
additional_medium = ''
|
additional_medium = ''
|
||||||
@@ -611,9 +596,9 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
|||||||
disc_number = str(medium_count) + 'x'
|
disc_number = str(medium_count) + 'x'
|
||||||
packaged_medium = disc_number + releasedata['medium-list'][0][
|
packaged_medium = disc_number + releasedata['medium-list'][0][
|
||||||
'format'] + additional_medium
|
'format'] + additional_medium
|
||||||
release['ReleaseFormat'] = unicode(packaged_medium)
|
release['ReleaseFormat'] = str(packaged_medium)
|
||||||
except:
|
except:
|
||||||
release['ReleaseFormat'] = u'Unknown'
|
release['ReleaseFormat'] = 'Unknown'
|
||||||
|
|
||||||
release['Tracks'] = getTracksFromRelease(releasedata)
|
release['Tracks'] = getTracksFromRelease(releasedata)
|
||||||
|
|
||||||
@@ -695,14 +680,14 @@ def getTracksFromRelease(release):
|
|||||||
for medium in release['medium-list']:
|
for medium in release['medium-list']:
|
||||||
for track in medium['track-list']:
|
for track in medium['track-list']:
|
||||||
try:
|
try:
|
||||||
track_title = unicode(track['title'])
|
track_title = str(track['title'])
|
||||||
except:
|
except:
|
||||||
track_title = unicode(track['recording']['title'])
|
track_title = str(track['recording']['title'])
|
||||||
tracks.append({
|
tracks.append({
|
||||||
'number': totalTracks,
|
'number': totalTracks,
|
||||||
'title': track_title,
|
'title': track_title,
|
||||||
'id': unicode(track['recording']['id']),
|
'id': str(track['recording']['id']),
|
||||||
'url': u"http://musicbrainz.org/track/" + track['recording']['id'],
|
'url': "https://musicbrainz.org/track/" + track['recording']['id'],
|
||||||
'duration': int(track['length']) if 'length' in track else 0
|
'duration': int(track['length']) if 'length' in track else 0
|
||||||
})
|
})
|
||||||
totalTracks += 1
|
totalTracks += 1
|
||||||
@@ -744,34 +729,19 @@ def findArtistbyAlbum(name):
|
|||||||
for releaseGroup in results:
|
for releaseGroup in results:
|
||||||
newArtist = releaseGroup['artist-credit'][0]['artist']
|
newArtist = releaseGroup['artist-credit'][0]['artist']
|
||||||
# Only need the artist ID if we're doing an artist+album lookup
|
# Only need the artist ID if we're doing an artist+album lookup
|
||||||
# if 'disambiguation' in newArtist:
|
artist_dict['id'] = str(newArtist['id'])
|
||||||
# uniquename = unicode(newArtist['sort-name'] + " (" + newArtist['disambiguation'] + ")")
|
|
||||||
# else:
|
|
||||||
# uniquename = unicode(newArtist['sort-name'])
|
|
||||||
# artist_dict['name'] = unicode(newArtist['sort-name'])
|
|
||||||
# artist_dict['uniquename'] = uniquename
|
|
||||||
artist_dict['id'] = unicode(newArtist['id'])
|
|
||||||
# artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id']
|
|
||||||
# artist_dict['score'] = int(releaseGroup['ext:score'])
|
|
||||||
|
|
||||||
return artist_dict
|
return artist_dict
|
||||||
|
|
||||||
|
|
||||||
def findAlbumID(artist=None, album=None):
|
def findAlbumID(artist=None, album=None):
|
||||||
results = None
|
results = None
|
||||||
chars = set('!?*-')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if album and artist:
|
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 = {'release': album.lower()}
|
||||||
criteria['artist'] = artist.lower()
|
criteria['artist'] = artist.lower()
|
||||||
else:
|
else:
|
||||||
if any((c in chars) for c in album):
|
|
||||||
album = '"' + album + '"'
|
|
||||||
criteria = {'release': album.lower()}
|
criteria = {'release': album.lower()}
|
||||||
with mb_lock:
|
with mb_lock:
|
||||||
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get(
|
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get(
|
||||||
@@ -786,5 +756,52 @@ def findAlbumID(artist=None, album=None):
|
|||||||
|
|
||||||
if len(results) < 1:
|
if len(results) < 1:
|
||||||
return False
|
return False
|
||||||
rgid = unicode(results[0]['id'])
|
rgid = str(results[0]['id'])
|
||||||
return rgid
|
return rgid
|
||||||
|
|
||||||
|
|
||||||
|
def getArtistForReleaseGroup(rgid):
|
||||||
|
"""
|
||||||
|
Returns artist name for a release group
|
||||||
|
Used for series where we store the series instead of the artist
|
||||||
|
"""
|
||||||
|
releaseGroup = None
|
||||||
|
try:
|
||||||
|
with mb_lock:
|
||||||
|
releaseGroup = musicbrainzngs.get_release_group_by_id(
|
||||||
|
rgid, ["artists"])
|
||||||
|
releaseGroup = releaseGroup['release-group']
|
||||||
|
except musicbrainzngs.WebServiceError as e:
|
||||||
|
logger.warn(
|
||||||
|
'Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (
|
||||||
|
rgid, str(e)))
|
||||||
|
mb_lock.snooze(5)
|
||||||
|
|
||||||
|
if not releaseGroup:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return releaseGroup['artist-credit'][0]['artist']['name']
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||