Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2be2e109b | ||
|
|
1b68edd85f | ||
|
|
ca4101bde5 | ||
|
|
fb6664303f | ||
|
|
c3de5872ee | ||
|
|
81954d8641 | ||
|
|
2dbd70f1d7 | ||
|
|
b3ac8d016d | ||
|
|
2f01f7b493 | ||
|
|
6bd9185a8d | ||
|
|
8501708591 | ||
|
|
7b7acf9a42 | ||
|
|
87dd0b156b | ||
|
|
2cc1d5fd19 | ||
|
|
eed48189ff | ||
|
|
14ec8b081d | ||
|
|
ff2afa8bff | ||
|
|
a6a37d744c | ||
|
|
9bbc1302ac | ||
|
|
672c79cd79 | ||
|
|
c3812ea5d0 | ||
|
|
e2fa5a85ea | ||
|
|
728887d881 | ||
|
|
6f65f700d5 | ||
|
|
a5f7ea4600 | ||
|
|
f290248d3f | ||
|
|
4c6adf9550 | ||
|
|
9f081ca545 | ||
|
|
c835664014 | ||
|
|
40b71cf6ed | ||
|
|
9fecbe8e56 | ||
|
|
5b15d1139a | ||
|
|
bd48b17d7b | ||
|
|
ab70020574 | ||
|
|
a23b72cf84 | ||
|
|
c8401efcaa | ||
|
|
62757c0fcf | ||
|
|
63c992a4e5 | ||
|
|
63549b2471 | ||
|
|
3ff431da4b | ||
|
|
8ccc3cb146 | ||
|
|
6764783aa7 | ||
|
|
828fc0ccda | ||
|
|
68a495127e | ||
|
|
26a6095851 | ||
|
|
95bd0a57ff | ||
|
|
5efbd561d6 | ||
|
|
b690850019 | ||
|
|
a09e91ff8a | ||
|
|
94d62430a0 | ||
|
|
2d5102d29c | ||
|
|
046d4d82b4 | ||
|
|
a63098a919 | ||
|
|
38d8e13e4e | ||
|
|
b3edfa0d87 | ||
|
|
aeebc67365 | ||
|
|
d6f22a2326 | ||
|
|
5b1d9c0750 | ||
|
|
ba666f68b8 | ||
|
|
000509353a | ||
|
|
c264c75282 | ||
|
|
4c15b33c58 | ||
|
|
167b5817e7 | ||
|
|
46c7582fb1 | ||
|
|
7b9e32f525 | ||
|
|
37c4e19663 | ||
|
|
e5beb5291d | ||
|
|
ebe8a60ca5 | ||
|
|
040de81d3c | ||
|
|
1bf8c35154 | ||
|
|
dae4d80490 | ||
|
|
acf73368c9 | ||
|
|
abac9b5a15 | ||
|
|
ace2b4f26a | ||
|
|
ebba115443 | ||
|
|
b54218abbd | ||
|
|
3e354ff128 | ||
|
|
b8168ec8eb | ||
|
|
c0c636d545 | ||
|
|
33d1d17c97 | ||
|
|
8fd13621f5 | ||
|
|
56674adfea | ||
|
|
489c6cbe71 | ||
|
|
6afe31bffc | ||
|
|
742529a92d | ||
|
|
944d066903 | ||
|
|
87819a3c74 | ||
|
|
c9fbe29c90 | ||
|
|
d78cb7d14e | ||
|
|
e8c392824f | ||
|
|
9811df2779 | ||
|
|
1a4865ed38 | ||
|
|
a06fb40f50 | ||
|
|
ad6a4f570e | ||
|
|
3685d32a7d | ||
|
|
152f5daa8c | ||
|
|
39054a04df | ||
|
|
1c4b9c10f0 | ||
|
|
73ca787cf1 | ||
|
|
c7bc852868 | ||
|
|
391b0cc465 | ||
|
|
4aaeaa704f | ||
|
|
4d14b028ff | ||
|
|
a78f38c174 | ||
|
|
14f2a6d22c | ||
|
|
2e4299efa7 | ||
|
|
0610c2fa93 | ||
|
|
9add571886 | ||
|
|
fcf59a9b38 | ||
|
|
74f9e91afc | ||
|
|
83398cb102 | ||
|
|
61c2e1f821 | ||
|
|
3e3047aef2 | ||
|
|
fff44e4631 | ||
|
|
0964371de8 | ||
|
|
654f923a8d | ||
|
|
b91206c64a | ||
|
|
c9ba59ee9a | ||
|
|
b7e35d5ff0 | ||
|
|
9d82143abe | ||
|
|
eaf2db6c59 | ||
|
|
586b9ed3c8 | ||
|
|
d89f4171da | ||
|
|
9f7be5348b | ||
|
|
9c254ff222 | ||
|
|
ba969fd3b8 | ||
|
|
c851d5ed1a | ||
|
|
2223928958 | ||
|
|
164c3cacbc | ||
|
|
16d4ac8895 | ||
|
|
f4d60226b3 | ||
|
|
9ca87e23b2 | ||
|
|
d934c865c6 | ||
|
|
de74cd2502 | ||
|
|
f41db714a9 | ||
|
|
f03b82e5f6 | ||
|
|
e2db680b9e | ||
|
|
a3db89c11d | ||
|
|
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 | ||
|
|
138d01db4a | ||
|
|
dc22bb006d | ||
|
|
b49cd8a813 | ||
|
|
d7275e5a99 | ||
|
|
aa1867cf06 | ||
|
|
19b766a7ce | ||
|
|
b45f3565da | ||
|
|
22525668fd | ||
|
|
af79bc47e8 | ||
|
|
6208fe0e69 | ||
|
|
f66a37abbd | ||
|
|
4b52ae445d | ||
|
|
8a2c3169e2 | ||
|
|
805eb7dd13 | ||
|
|
81486d2cd2 | ||
|
|
d1237b052a | ||
|
|
a4ef757d12 | ||
|
|
bb8eb19dbf | ||
|
|
602e115a29 | ||
|
|
e2c335db2d | ||
|
|
44583d42dd | ||
|
|
43d465ca22 | ||
|
|
bd60935493 | ||
|
|
0264b1fb0d | ||
|
|
24f22834ff | ||
|
|
aed2e63984 | ||
|
|
694c17a40b | ||
|
|
6c34bc46e9 | ||
|
|
0b2600b783 | ||
|
|
693e5b1624 | ||
|
|
287278c049 | ||
|
|
496560909f | ||
|
|
22b1bf36ab | ||
|
|
b506a1797d | ||
|
|
ff8417b410 | ||
|
|
741cb466ef | ||
|
|
a99a6abde2 | ||
|
|
4631b3cbbf | ||
|
|
7230428a31 | ||
|
|
2b5b2bd506 | ||
|
|
99c3e8498d | ||
|
|
472f657c88 | ||
|
|
368e30067b | ||
|
|
053fd40e76 | ||
|
|
7af7f6be1e | ||
|
|
5fd20e50fd | ||
|
|
43df2956fb | ||
|
|
0c96997005 | ||
|
|
81651b119a | ||
|
|
ecad4d39eb | ||
|
|
0d3be4866f | ||
|
|
eeed89811b | ||
|
|
d9bff5dffe | ||
|
|
3b48f22bd9 | ||
|
|
79d508fb07 | ||
|
|
ae8fadd587 | ||
|
|
5d7fdc7a82 | ||
|
|
1ded166765 | ||
|
|
22eb21a4ef | ||
|
|
4283b381b6 | ||
|
|
2cdc6669da | ||
|
|
e3983d615d | ||
|
|
de8fac347d | ||
|
|
da9287d7cf | ||
|
|
f9d73da31b | ||
|
|
3cc280666e | ||
|
|
0828643c09 | ||
|
|
9b3d7f3bbb | ||
|
|
68c551c828 | ||
|
|
b7af246340 | ||
|
|
4c3b60be68 | ||
|
|
539db608e2 | ||
|
|
0b6f263985 | ||
|
|
a10ec8dce4 | ||
|
|
867f75a681 | ||
|
|
c8057676db | ||
|
|
9197f27a50 | ||
|
|
e7062a6710 | ||
|
|
9c2bfcf7d8 | ||
|
|
4d575787cd | ||
|
|
bdf55f8662 | ||
|
|
cd57668b92 | ||
|
|
f20520541d | ||
|
|
48dd832d4e | ||
|
|
af10088dc8 | ||
|
|
cf9737e804 | ||
|
|
4f118eaaef | ||
|
|
79277ddc33 | ||
|
|
3ee2598f4c | ||
|
|
1e1b8a9351 | ||
|
|
0cc2dd7b31 | ||
|
|
80fe81e345 | ||
|
|
0c98085018 | ||
|
|
e96a6fc7c8 | ||
|
|
ee7254ddbe | ||
|
|
3e44baaded | ||
|
|
914ab65577 | ||
|
|
4d674689b1 | ||
|
|
ae328a15aa | ||
|
|
c612a228b6 | ||
|
|
334493d81e | ||
|
|
50de38ce82 | ||
|
|
36434b57ee | ||
|
|
3110659633 | ||
|
|
ecadf8cdab | ||
|
|
6c0b451882 | ||
|
|
755944f15c | ||
|
|
0acb5a413a | ||
|
|
e3c7790039 | ||
|
|
d802948a35 | ||
|
|
b2f5919b26 | ||
|
|
74d5333bdf | ||
|
|
54edacdfe7 | ||
|
|
cd2860f4e3 | ||
|
|
bc0ce99adf | ||
|
|
3bfdc9f069 | ||
|
|
39e589bc08 | ||
|
|
cef1563b4b | ||
|
|
04e8767ea9 | ||
|
|
2df13ad823 | ||
|
|
18d85d12dd | ||
|
|
1e3cbb64ca | ||
|
|
d6e1a16286 | ||
|
|
979b06ae55 | ||
|
|
7fec54289b | ||
|
|
49d632a156 | ||
|
|
a2a14e185a | ||
|
|
50851a4953 | ||
|
|
61a3abbf20 | ||
|
|
e8f436a5db | ||
|
|
34b8a11145 | ||
|
|
f0fe23899a | ||
|
|
4eabc1c614 | ||
|
|
b9d41ad779 | ||
|
|
8e1cd3155f | ||
|
|
624a3d6cc1 | ||
|
|
a042fd44a2 | ||
|
|
2e5ce520eb | ||
|
|
e9407f5dbc | ||
|
|
8eb79701a6 | ||
|
|
4c95f99d80 | ||
|
|
e1e361a46e | ||
|
|
437d33e7a8 | ||
|
|
2832166842 | ||
|
|
2841f36d8d | ||
|
|
de15f6b632 | ||
|
|
5bb6ec04a9 | ||
|
|
1da1f5124a | ||
|
|
f8b82337a2 | ||
|
|
3efd3931f5 | ||
|
|
95402144d1 | ||
|
|
220a8931d1 | ||
|
|
a96b186825 | ||
|
|
de3b73dfa5 | ||
|
|
263564f15f | ||
|
|
a5ca14e6d6 | ||
|
|
861747c235 | ||
|
|
4095bf213e | ||
|
|
f3020207c0 | ||
|
|
1cee19be74 | ||
|
|
f877caff06 | ||
|
|
9c0297326e | ||
|
|
1ed12770c2 | ||
|
|
519fdc3015 | ||
|
|
91ed781acb | ||
|
|
bf5d07efec | ||
|
|
33fdb3d7bd | ||
|
|
3f67396a55 | ||
|
|
3680c2b349 | ||
|
|
ff7ff79f85 | ||
|
|
debc65db5d | ||
|
|
a02cc4d7b9 | ||
|
|
578ae2c2d8 | ||
|
|
85749c4e94 | ||
|
|
e415de7698 | ||
|
|
a9bbd34a52 | ||
|
|
7797cd0681 | ||
|
|
9ccb0f85dc | ||
|
|
50185ab3d3 | ||
|
|
697e695f7d | ||
|
|
36ad37f005 | ||
|
|
a45894b802 | ||
|
|
dd1e4bfe3b | ||
|
|
88366443fd | ||
|
|
6a1aca66b1 | ||
|
|
ef6a203cda | ||
|
|
83c50ccd4f | ||
|
|
3e4c5cb5e2 | ||
|
|
8c1621f604 | ||
|
|
bc465edd91 | ||
|
|
3d1a9e8947 | ||
|
|
9d6a7d1d36 | ||
|
|
3cfc434e8a | ||
|
|
e7017305d8 | ||
|
|
a0fa658956 | ||
|
|
709073358b | ||
|
|
e6b74be1f6 | ||
|
|
c29dda3af6 | ||
|
|
335586893c | ||
|
|
44bb1bea08 | ||
|
|
9f378ec79c | ||
|
|
5501d84611 | ||
|
|
34746400e6 | ||
|
|
cf502aa880 | ||
|
|
2ca498d74f | ||
|
|
432018a2a4 | ||
|
|
784ec2927e | ||
|
|
fa1cb27e04 | ||
|
|
7e915c5902 | ||
|
|
7f48b021ec | ||
|
|
e8009c00d2 | ||
|
|
772fb524c7 | ||
|
|
b538e5b321 | ||
|
|
decc548086 | ||
|
|
82695311c4 | ||
|
|
6d40178ce2 | ||
|
|
4d1107ee54 | ||
|
|
8fd9958d70 | ||
|
|
f8900795da | ||
|
|
b8e8e752f0 | ||
|
|
d04cdcd8fa | ||
|
|
edc243a245 | ||
|
|
b71c0d8b93 | ||
|
|
0ecfe499fc | ||
|
|
68d85841ea | ||
|
|
1ffcc177a4 | ||
|
|
2230c5bc0c | ||
|
|
352b009e91 | ||
|
|
b6b33e1b1e | ||
|
|
dff1ac2e41 | ||
|
|
36f75f44e9 | ||
|
|
92601e0eb8 | ||
|
|
6e82423947 | ||
|
|
0a805a01f3 | ||
|
|
579b5a2f2f | ||
|
|
c77f43be5e | ||
|
|
f10d1834dd | ||
|
|
b4c7ed2227 | ||
|
|
0086980b1b | ||
|
|
89e1793978 | ||
|
|
5a598fe611 | ||
|
|
b58f670694 | ||
|
|
5103436d17 | ||
|
|
c9a1796e56 | ||
|
|
7428b56dd5 | ||
|
|
42294d261d | ||
|
|
31a99a908a | ||
|
|
aa503f0fe8 | ||
|
|
d3d48d27df | ||
|
|
bd8eb74662 | ||
|
|
99680e9a0f | ||
|
|
0cc58bdb46 | ||
|
|
09435b8786 | ||
|
|
5d2753a34e | ||
|
|
8eb3ece2f9 | ||
|
|
ed923a2a94 | ||
|
|
95cc0670e2 | ||
|
|
a62b55c40f | ||
|
|
7440b77377 | ||
|
|
372c6ffd3b | ||
|
|
14d1524971 | ||
|
|
9cddcabad4 | ||
|
|
656e7161cb | ||
|
|
7c12761066 | ||
|
|
d3584a5ae9 | ||
|
|
c0c64621e6 | ||
|
|
323d364572 | ||
|
|
9a681a9422 | ||
|
|
a7a709df51 | ||
|
|
3f23b5553a | ||
|
|
5a2082ede5 | ||
|
|
e29f3a0a9a | ||
|
|
f4bebf6a41 | ||
|
|
564df8136c |
@@ -11,7 +11,6 @@ cache:
|
||||
- lib
|
||||
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
|
||||
install:
|
||||
|
||||
@@ -1,5 +1,97 @@
|
||||
# Changelog
|
||||
|
||||
## v0.6.3
|
||||
Released 26 May 2024
|
||||
|
||||
Highlights:
|
||||
* Hotfix for searcher not returning results
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.6.2...v0.6.3).
|
||||
|
||||
## v0.6.2
|
||||
Released 26 May 2024
|
||||
|
||||
Highlights:
|
||||
* Added soulseek support
|
||||
* Added bandcamp support
|
||||
* Changes and dependency updates to work with Python >= 3.12
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.6.1...v0.6.2).
|
||||
|
||||
## v0.6.1
|
||||
R eleased 26 November 2023
|
||||
|
||||
Highlights:
|
||||
* Dependency updates to work with > Python 3.11
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.6.0...v0.6.1).
|
||||
|
||||
## v0.6.0
|
||||
Released 13 November 2022
|
||||
|
||||
Highlights:
|
||||
* Updated to python 3
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.20...v0.6.0).
|
||||
|
||||
## v0.5.20
|
||||
Released 15 October 2021
|
||||
|
||||
Highlights:
|
||||
* Improved: Last.fm & metacrtic api fixes
|
||||
* Improved: Various database fixes
|
||||
* Added: qBitTorrent
|
||||
* Added: Telegram notifications
|
||||
* Improved: Many more since the last release. Check the list
|
||||
|
||||
Many thanks to @AdeHub for keeping the ship afloat
|
||||
and to everyone else who contributed.
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.19...v0.5.20).
|
||||
|
||||
## v0.5.19
|
||||
Released 27 March 2018
|
||||
|
||||
Highlights:
|
||||
* Improved: Windows encoding fixes
|
||||
* Improved: Performance enhancements
|
||||
* Improved: Many more since the last release. Check the list
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.18...v0.5.19).
|
||||
|
||||
## v0.5.18
|
||||
Released 01 December 2016
|
||||
|
||||
Highlights:
|
||||
* Added: PassTheHeadphones support
|
||||
* Fixed: Special characters in password fields breaking on config page
|
||||
* Improved: Updated t411 url
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.17...v0.5.18).
|
||||
|
||||
|
||||
## v0.5.17
|
||||
Released 10 November 2016
|
||||
|
||||
Highlights:
|
||||
* Added: t411 support
|
||||
* Fixed: Rutracker login
|
||||
* Fixed: Deluge empty password
|
||||
* Fixed: FreeBSD init script
|
||||
* Improved: Musicbrainz searching
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.16...v0.5.17).
|
||||
|
||||
## v0.5.16
|
||||
Released 10 June 2016
|
||||
|
||||
Hotfix update
|
||||
|
||||
Highlights:
|
||||
* Fixed: Waffles url
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.15...v0.5.16).
|
||||
|
||||
## v0.5.15
|
||||
Released 07 June 2016
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
if sys.version_info <= (3, 6):
|
||||
sys.stdout.write("Headphones requires Python >= 3.7\n")
|
||||
sys.exit(1)
|
||||
|
||||
# Ensure lib added to path, before any other imports
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
|
||||
|
||||
@@ -54,7 +58,10 @@ def main():
|
||||
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
headphones.SYS_ENCODING = locale.getpreferredencoding()
|
||||
if headphones.SYS_PLATFORM == 'win32':
|
||||
headphones.SYS_ENCODING = sys.getdefaultencoding().upper()
|
||||
else:
|
||||
headphones.SYS_ENCODING = locale.getpreferredencoding()
|
||||
except (locale.Error, IOError):
|
||||
pass
|
||||
|
||||
@@ -81,6 +88,8 @@ def main():
|
||||
help='Prevent browser from launching on startup')
|
||||
parser.add_argument(
|
||||
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
|
||||
parser.add_argument(
|
||||
'--host', help='Specify a host (default - localhost)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -170,6 +179,13 @@ def main():
|
||||
else:
|
||||
http_port = int(headphones.CONFIG.HTTP_PORT)
|
||||
|
||||
# Force the http host if neccessary
|
||||
if args.host:
|
||||
http_host = args.host
|
||||
logger.info('Using forced web server host: %s', http_host)
|
||||
else:
|
||||
http_host = headphones.CONFIG.HTTP_HOST
|
||||
|
||||
# Check if pyOpenSSL is installed. It is required for certificate generation
|
||||
# and for CherryPy.
|
||||
if headphones.CONFIG.ENABLE_HTTPS:
|
||||
@@ -183,7 +199,7 @@ def main():
|
||||
# Try to start the server. Will exit here is address is already in use.
|
||||
web_config = {
|
||||
'http_port': http_port,
|
||||
'http_host': headphones.CONFIG.HTTP_HOST,
|
||||
'http_host': http_host,
|
||||
'http_root': headphones.CONFIG.HTTP_ROOT,
|
||||
'http_proxy': headphones.CONFIG.HTTP_PROXY,
|
||||
'enable_https': headphones.CONFIG.ENABLE_HTTPS,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## Headphones
|
||||
##  Headphones
|
||||
|
||||
[](https://travis-ci.org/rembo10/headphones)
|
||||
[](https://travis-ci.org/rembo10/headphones)
|
||||
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
# Ensure that we use the Headphones provided libraries.
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../lib"))
|
||||
|
||||
import urlparse
|
||||
import urllib.parse
|
||||
|
||||
|
||||
def can_import(module):
|
||||
@@ -89,7 +89,7 @@ def main():
|
||||
url = sys.argv[1]
|
||||
|
||||
# Check if it is a HTTPS website.
|
||||
parts = urlparse.urlparse(url)
|
||||
parts = urllib.parse.urlparse(url)
|
||||
|
||||
if parts.scheme.lower() != "https":
|
||||
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 |
@@ -5,7 +5,6 @@
|
||||
%>
|
||||
|
||||
<%def name="headerIncludes()">
|
||||
|
||||
<div id="subhead_container">
|
||||
<div id="back_to_previous_link">
|
||||
<a href="artistPage?ArtistID=${album['ArtistID']}" class="back">« Back to ${album['ArtistName']}</a>
|
||||
@@ -13,44 +12,47 @@
|
||||
<div id="subhead_menu">
|
||||
|
||||
<a id="menu_link_delete" href="deleteAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Album</a>
|
||||
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
|
||||
<a id="menu_link_wanted" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=False', $(this),true)" data-success="'${album['AlbumTitle']}' added to queue"><i class="fa fa-heart"></i> Mark Album as Wanted</a>
|
||||
%elif album['Status'] == 'Wanted':
|
||||
<a id="menu_link_check" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this));" data-success="Forced checking successful"><i class="fa fa-search"></i> Force Check</a>
|
||||
<a id="menu_link_skipped" href="javascript:void(0)" onclick="doAjaxCall('unqueueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}', $(this),true);" data-success="'${album['AlbumTitle']}' marked as Skipped"><i class="fa fa-step-forward"></i> Mark Album as Skipped</a>
|
||||
%else:
|
||||
<a id="menu_link_retry" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=False', $(this),true);" data-success="Retrying the same version of '${album['AlbumTitle']}'"><i class="fa fa-refresh"></i> Retry Download</a>
|
||||
<a id="menu_link_new" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this),true);" data-success="Looking for a new version of '${album['AlbumTitle']}'"><i class="fa fa-download"></i> Try New Version</a>
|
||||
%endif
|
||||
<a class="menu_link_edit" id="album_chooser" href="javascript:void(0)"><i class="fa fa-pencil"></i> Choose Alternate Release</a>
|
||||
<div id="dialog" title="Choose an Alternate Release" style="display:none" class="configtable">
|
||||
<div class="links">
|
||||
<%
|
||||
alternate_albums = myDB.select("SELECT * from allalbums WHERE AlbumID=? ORDER BY ReleaseDate ASC", [album['AlbumID']])
|
||||
%>
|
||||
%if not alternate_albums:
|
||||
<p>No alternate releases found. Try refreshing the artist (if the artist is being refreshed, please wait until it's finished)</p>
|
||||
<h2><a id="refresh_artist" onclick="doAjaxCall('refreshArtist?ArtistID=${album['ArtistID']}', $(this)), true" href="javascript:void(0)" data-success="'${album['ArtistName']}' is being refreshed">Refresh Artist</a></h2>
|
||||
%else:
|
||||
%for alternate_album in alternate_albums:
|
||||
<%
|
||||
track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=?", [alternate_album['ReleaseID']]))
|
||||
mb_link = "http://musicbrainz.org/release/" + alternate_album['ReleaseID']
|
||||
have_track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=? AND Location IS NOT NULL", [alternate_album['ReleaseID']]))
|
||||
if alternate_album['AlbumID'] == alternate_album['ReleaseID']:
|
||||
alternate_album_name = "Headphones Default Release (" + str(alternate_album['ReleaseDate']) + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
|
||||
else:
|
||||
alternate_album_name = alternate_album['AlbumTitle'] + " (" + alternate_album['ReleaseCountry'] + ", " + str(alternate_album['ReleaseDate']) + ", " + alternate_album['ReleaseFormat'] + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
|
||||
|
||||
%>
|
||||
<a href="javascript:void(0)" onclick="doAjaxCall('switchAlbum?AlbumID=${album['AlbumID']}&ReleaseID=${alternate_album['ReleaseID']}', $(this), 'table');" data-success="Switched release to: ${alternate_album_name}">${alternate_album_name}</a><a href="${mb_link}" target="_blank">MB</a><br>
|
||||
%endfor
|
||||
%endif
|
||||
</div>
|
||||
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
|
||||
<a id="menu_link_wanted" href="#" class="album-action" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-new="False" data-success-msg="'${album['AlbumTitle']}' added to queue"><i class="fa fa-heart"></i> Mark Album as Wanted</a>
|
||||
%elif album['Status'] == 'Wanted':
|
||||
<a id="menu_link_check" href="#" class="album-action" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-new="True" data-success-msg="Forced checking successful"><i class="fa fa-search"></i> Force Check</a>
|
||||
<a id="menu_link_skipped" href="#" class="album-action" data-action="unqueueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-success-msg="'${album['AlbumTitle']}' marked as Skipped"><i class="fa fa-step-forward"></i> Mark Album as Skipped</a>
|
||||
%else:
|
||||
<a id="menu_link_retry" href="#" class="album-action" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-new="False" data-success-msg="Retrying the same version of '${album['AlbumTitle']}'"><i class="fa fa-refresh"></i> Retry Download</a>
|
||||
<a id="menu_link_new" href="#" class="album-action" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-new="True" data-success-msg="Looking for a new version of '${album['AlbumTitle']}'"><i class="fa fa-download"></i> Try New Version</a>
|
||||
%endif
|
||||
|
||||
<a class="menu_link_edit dialog-trigger" id="album_chooser" href="#" data-dialog-id="dialog"><i class="fa fa-pencil"></i> Choose Alternate Release</a>
|
||||
<div id="dialog" title="Choose an Alternate Release" style="display:none" class="configtable">
|
||||
<div class="links">
|
||||
<%
|
||||
alternate_albums = myDB.select("SELECT * from allalbums WHERE AlbumID=? ORDER BY ReleaseDate ASC", [album['AlbumID']])
|
||||
%>
|
||||
%if not alternate_albums:
|
||||
<p>No alternate releases found. Try refreshing the artist (if the artist is being refreshed, please wait until it's finished)</p>
|
||||
<h2><a id="refresh_artist_btn" href="#" class="album-action" data-action="refreshArtist" data-artist-id="${album['ArtistID']}" data-success-msg="'${album['ArtistName']}' is being refreshed">Refresh Artist</a></h2>
|
||||
%else:
|
||||
%for alternate_album in alternate_albums:
|
||||
<%
|
||||
track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=?", [alternate_album['ReleaseID']]))
|
||||
mb_link = "http://musicbrainz.org/release/" + alternate_album['ReleaseID']
|
||||
have_track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=? AND Location IS NOT NULL", [alternate_album['ReleaseID']]))
|
||||
if alternate_album['AlbumID'] == alternate_album['ReleaseID']:
|
||||
alternate_album_name = "Headphones Default Release (" + str(alternate_album['ReleaseDate']) + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
|
||||
else:
|
||||
alternate_album_name = alternate_album['AlbumTitle'] + " (" + alternate_album['ReleaseCountry'] + ", " + str(alternate_album['ReleaseDate']) + ", " + alternate_album['ReleaseFormat'] + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
|
||||
|
||||
%>
|
||||
<a href="#" class="album-action alternate-release-switch" data-action="switchAlbum" data-album-id="${album['AlbumID']}" data-release-id="${alternate_album['ReleaseID']}" data-success-msg="Switched release to: ${alternate_album_name}">${alternate_album_name}</a><a href="${mb_link}" target="_blank" class="external-link">MB</a><br>
|
||||
%endfor
|
||||
%endif
|
||||
</div>
|
||||
<a class="menu_link_edit" id="edit_search_term" href="javascript:void(0)"><i class="fa fa-pencil"></i> Edit Search Term</a>
|
||||
</div>
|
||||
|
||||
<a class="menu_link_edit dialog-trigger" id="edit_search_term" href="#" data-dialog-id="dialog2"><i class="fa fa-pencil"></i> Edit Search Term</a>
|
||||
<div id="dialog2" title="Enter your own search term for this album" style="display:none" class="configtable">
|
||||
<form action="editSearchTerm" method="GET" id="editSearchTerm">
|
||||
<form action="editSearchTerm" method="GET" id="editSearchTermForm">
|
||||
<input type="hidden" name="AlbumID" value="${album['AlbumID']}">
|
||||
<div class="row">
|
||||
<%
|
||||
@@ -61,10 +63,11 @@
|
||||
%>
|
||||
<input type="text" value="${search_term}" name="SearchTerm" size="40" />
|
||||
</div>
|
||||
<input type="button" value="Save changes" onclick="doAjaxCall('editSearchTerm',$(this),'tabs',true);return false;" data-success="Search term updated"/>
|
||||
<button type="submit" class="album-action-submit" data-action="editSearchTerm" data-success-msg="Search term updated">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
<a class="menu_link_edit" id="choose_specific_download" href="javascript:void(0)" onclick="getAvailableDownloads()"><i class="fa fa-search"></i> Choose Specific Download</a>
|
||||
|
||||
<a class="menu_link_edit dialog-trigger" id="choose_specific_download" href="#" data-dialog-id="choose_specific_download_dialog" data-action="getAvailableDownloads"><i class="fa fa-search"></i> Choose Specific Download</a>
|
||||
<div id="choose_specific_download_dialog" title="Choose a specific download for this album" style="display:none" class="configtable">
|
||||
<table class="display" id="downloads_table">
|
||||
<thead>
|
||||
@@ -88,7 +91,7 @@
|
||||
<div class="table_wrapper">
|
||||
<div id="albumheader" class="clearfix">
|
||||
<div id="albumImg">
|
||||
<img height="200" alt="" class="albumArt" src="artwork/album/${album['AlbumID']}">
|
||||
<img alt="${album['AlbumTitle']} album art" class="albumArt" src="artwork/album/${album['AlbumID']}">
|
||||
</div>
|
||||
|
||||
<h1 id="albumname">
|
||||
@@ -106,7 +109,6 @@
|
||||
albumduration = helpers.convert_milliseconds(totalduration)
|
||||
except:
|
||||
albumduration = 'n/a'
|
||||
|
||||
%>
|
||||
<div class="albuminfo">
|
||||
<div id="albumInfo"></div>
|
||||
@@ -187,178 +189,290 @@
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
<script>
|
||||
function getAlbumInfo() {
|
||||
// Define a global object for page-specific functions to avoid polluting global scope directly
|
||||
var AlbumPage = AlbumPage || {};
|
||||
|
||||
AlbumPage.search_results = []; // Moved to page-specific scope
|
||||
|
||||
AlbumPage.getAlbumInfo = function() {
|
||||
var id = "${album['AlbumID']}";
|
||||
var elem = $("#albumInfo");
|
||||
getInfo(elem,id,'album');
|
||||
}
|
||||
// Assuming getInfo is defined in common.js
|
||||
if (typeof getInfo === 'function') {
|
||||
getInfo(elem,id,'album');
|
||||
} else {
|
||||
console.warn("getInfo function not found. Album info might not be loaded.");
|
||||
}
|
||||
};
|
||||
|
||||
function initThisPage() {
|
||||
$('#album_chooser').click(function() {
|
||||
$('#dialog').dialog({
|
||||
width: 500,
|
||||
maxHeight: 500
|
||||
});
|
||||
return false;
|
||||
});
|
||||
$('#edit_search_term').click(function() {
|
||||
$('#dialog2').dialog({
|
||||
width: 500,
|
||||
maxHeight: 500
|
||||
});
|
||||
return false;
|
||||
});
|
||||
$('#refresh_artist').click(function() {
|
||||
$('#dialog').dialog("close");
|
||||
});
|
||||
initActions();
|
||||
setTimeout(function(){
|
||||
initFancybox();
|
||||
}, 1000);
|
||||
AlbumPage.initDialogs = function() {
|
||||
// General handler for opening dialogs based on data-dialog-id
|
||||
$(document).on('click', '.dialog-trigger', function(e) {
|
||||
e.preventDefault();
|
||||
var dialogId = $(this).data('dialog-id');
|
||||
var $dialog = $('#' + dialogId);
|
||||
|
||||
$('#track_table').dataTable({
|
||||
"aaSorting": [],
|
||||
"bFilter": false,
|
||||
"bInfo": false,
|
||||
"bPaginate": false,
|
||||
"bDestroy": true
|
||||
if ($dialog.length) {
|
||||
// Specific logic for choose_specific_download_dialog before opening
|
||||
if (dialogId === 'choose_specific_download_dialog') {
|
||||
AlbumPage.getAvailableDownloads();
|
||||
} else {
|
||||
$dialog.dialog({
|
||||
width: 500,
|
||||
maxHeight: 500,
|
||||
// Add close event to clear dynamic content if any
|
||||
close: function() {
|
||||
if (dialogId === 'downloads_table_body') {
|
||||
$('#downloads_table_body').empty(); // Clear table content on close
|
||||
// Re-initialize DataTable if needed on next open, or destroy it here
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Specific handler for refreshing artist from alternate releases dialog
|
||||
$(document).on('click', '#refresh_artist_btn', function(e) {
|
||||
e.preventDefault();
|
||||
var $this = $(this);
|
||||
// Assuming doAjaxCall is defined in common.js
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall($this.data('action') + '?ArtistID=' + $this.data('artist-id'), $this, true, $this.data('success-msg'));
|
||||
}
|
||||
$('#dialog').dialog("close"); // Close dialog after action
|
||||
});
|
||||
};
|
||||
|
||||
function getAvailableDownloads() {
|
||||
ShowSpinner();
|
||||
$.getJSON("choose_specific_download?AlbumID=${album['AlbumID']}", function(data) {
|
||||
loader.remove();
|
||||
feedback.fadeOut();
|
||||
search_results = data
|
||||
AlbumPage.initDataTables = function() {
|
||||
// Initialize track_table
|
||||
$('#track_table').DataTable({ // Use .DataTable() for new versions of DataTables
|
||||
"ordering": false, // aaSorting -> ordering
|
||||
"searching": false, // bFilter -> searching
|
||||
"info": false, // bInfo -> info
|
||||
"paging": false, // bPaginate -> paging
|
||||
"destroy": true // bDestroy -> destroy
|
||||
});
|
||||
};
|
||||
|
||||
AlbumPage.getAvailableDownloads = function() {
|
||||
AlbumPage.ShowSpinner();
|
||||
var albumId = "${album['AlbumID']}";
|
||||
$.getJSON("choose_specific_download?AlbumID=" + albumId, function(data) {
|
||||
AlbumPage.loader.remove(); // Assuming loader is attached to AlbumPage scope now
|
||||
AlbumPage.feedback.fadeOut(); // Assuming feedback is attached to AlbumPage scope now
|
||||
AlbumPage.search_results = data; // Store results
|
||||
|
||||
// Clear previous content
|
||||
$('#downloads_table_body').empty();
|
||||
|
||||
for( var i = 0, len = data.length; i < len; i++ ) {
|
||||
$('#downloads_table_body').append('<tr><td id="title"><a href="javascript:void(0)" onclick="downloadSpecificRelease('+i+')">'+data[i].title+'</a></td><td id="size"><span title='+data[i].size+'></span>'+(data[i].size / (1024*1024)).toFixed(2)+' MB</td><td id="provider">'+data[i].provider+'</td><td id="kind">'+data[i].kind+'</td><td id="matches">'+data[i].matches+'</td></tr>');
|
||||
$('#downloads_table_body').append(
|
||||
'<tr>' +
|
||||
'<td id="title"><a href="#" class="download-specific-release-link" data-index="' + i + '">' + data[i].title + '</a></td>' +
|
||||
'<td id="size"><span title="'+data[i].size+'"></span>' + (data[i].size / (1024*1024)).toFixed(2) + ' MB</td>' +
|
||||
'<td id="provider">' + data[i].provider + '</td>' +
|
||||
'<td id="kind">' + data[i].kind + '</td>' +
|
||||
'<td id="matches">' + data[i].matches + '</td>' +
|
||||
'</tr>'
|
||||
);
|
||||
}
|
||||
$('#downloads_table').dataTable({
|
||||
"aoColumns": [
|
||||
null,
|
||||
{"sType": "title-numeric"},
|
||||
null,
|
||||
null,
|
||||
{"sType": "string"}
|
||||
|
||||
// Destroy and re-initialize the DataTable for downloads
|
||||
if ($.fn.DataTable.isDataTable('#downloads_table')) {
|
||||
$('#downloads_table').DataTable().destroy();
|
||||
}
|
||||
$('#downloads_table').DataTable({
|
||||
"columnDefs": [ // aoColumns -> columnDefs
|
||||
{ "orderable": false, "targets": [0, 2, 3] }, // Disable ordering for Title, Provider, Kind
|
||||
{ "type": "title-numeric", "targets": 1 }, // sType -> type
|
||||
{ "type": "string", "targets": 4 }
|
||||
],
|
||||
"aaSorting": [[ 4, 'desc']],
|
||||
"bFilter": false,
|
||||
"bInfo": false,
|
||||
"bPaginate": false,
|
||||
"bDestroy": true
|
||||
"order": [[ 4, 'desc']], // aaSorting -> order
|
||||
"searching": false,
|
||||
"info": false,
|
||||
"paging": false,
|
||||
"destroy": true // Important for re-initialization
|
||||
});
|
||||
|
||||
$("#choose_specific_download_dialog").dialog({
|
||||
width: "80%",
|
||||
maxHeight: 500
|
||||
maxHeight: 500,
|
||||
modal: true // Added modal to make it more common practice
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function downloadSpecificRelease(i){
|
||||
AlbumPage.downloadSpecificRelease = function(i){
|
||||
var release = AlbumPage.search_results[i]; // Get from stored results
|
||||
|
||||
title = search_results[i].title
|
||||
size = search_results[i].size
|
||||
url = search_results[i].url
|
||||
provider = search_results[i].provider
|
||||
kind = search_results[i].kind
|
||||
var url = "download_specific_release?AlbumID=${album['AlbumID']}" +
|
||||
"&title=" + encodeURIComponent(release.title) +
|
||||
"&size=" + release.size +
|
||||
"&url=" + encodeURIComponent(release.url) +
|
||||
"&provider=" + encodeURIComponent(release.provider) +
|
||||
"&kind=" + encodeURIComponent(release.kind);
|
||||
|
||||
ShowSpinner();
|
||||
$.getJSON("download_specific_release?AlbumID=${album['AlbumID']}&title="+title+"&size="+size+"&url="+url+"&provider="+provider+"&kind=" + kind, function(data) {
|
||||
loader.remove();
|
||||
feedback.fadeOut();
|
||||
refreshSubmenu();
|
||||
AlbumPage.ShowSpinner();
|
||||
$.getJSON(url, function(data) {
|
||||
AlbumPage.loader.remove();
|
||||
AlbumPage.feedback.fadeOut();
|
||||
// Assuming refreshSubmenu is defined globally or in common.js
|
||||
if (typeof refreshSubmenu === 'function') {
|
||||
refreshSubmenu();
|
||||
}
|
||||
$("#choose_specific_download_dialog").dialog("close");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function ShowSpinner() {
|
||||
feedback = $("#ajaxMsg");
|
||||
update = $("#updatebar");
|
||||
AlbumPage.ShowSpinner = function() {
|
||||
AlbumPage.feedback = $("#ajaxMsg"); // Assign to AlbumPage scope
|
||||
var update = $("#updatebar");
|
||||
if ( update.is(":visible") ) {
|
||||
var height = update.height() + 35;
|
||||
feedback.css("bottom",height + "px");
|
||||
AlbumPage.feedback.css("bottom",height + "px");
|
||||
} else {
|
||||
feedback.removeAttr("style");
|
||||
AlbumPage.feedback.removeAttr("style");
|
||||
}
|
||||
loader = $("<i class='fa fa-refresh fa-spin'></i>");
|
||||
feedback.prepend(loader);
|
||||
feedback.fadeIn();
|
||||
}
|
||||
AlbumPage.loader = $("<i class='fa fa-refresh fa-spin'></i>"); // Assign to AlbumPage scope
|
||||
AlbumPage.feedback.prepend(AlbumPage.loader);
|
||||
AlbumPage.feedback.fadeIn();
|
||||
};
|
||||
|
||||
var loadingMessage = false;
|
||||
var spinner_active = false;
|
||||
var loadingtext_active = false;
|
||||
var refreshInterval;
|
||||
var wasLoading = false;
|
||||
var x = 0;
|
||||
AlbumPage.loadingMessage = false;
|
||||
AlbumPage.spinner_active = false;
|
||||
AlbumPage.loadingtext_active = false;
|
||||
AlbumPage.refreshInterval = null; // Initialize as null
|
||||
AlbumPage.wasLoading = false;
|
||||
AlbumPage.x = 0;
|
||||
|
||||
function checkAlbumStatus() {
|
||||
AlbumPage.checkAlbumStatus = function() {
|
||||
$.getJSON("getAlbumjson?AlbumID=${album['AlbumID']}", function(data) {
|
||||
if (data['Status'] == "Loading"){
|
||||
wasLoading = true;
|
||||
if (data['Status'] === "Loading"){
|
||||
AlbumPage.wasLoading = true;
|
||||
$('#albumnamelink').text(data["AlbumTitle"]);
|
||||
$('#artistnamelink').text(data["ArtistName"]);
|
||||
if (loadingMessage == false){
|
||||
if (AlbumPage.loadingMessage === false){
|
||||
$("#ajaxMsg").after( "<div id='ajaxMsg2' class='ajaxMsg'></div>" );
|
||||
showArtistMsg("Getting album information");
|
||||
loadingMessage = true;
|
||||
// Assuming showArtistMsg is defined globally or in common.js
|
||||
if (typeof showArtistMsg === 'function') {
|
||||
showArtistMsg("Getting album information");
|
||||
}
|
||||
AlbumPage.loadingMessage = true;
|
||||
}
|
||||
if (spinner_active == false){
|
||||
$('#albumname').prepend('<i class="fa fa-refresh fa-spin" id="albumnamespinner"></i>')
|
||||
spinner_active = true;
|
||||
if (AlbumPage.spinner_active === false){
|
||||
$('#albumname').prepend('<i class="fa fa-refresh fa-spin" id="albumnamespinner"></i>');
|
||||
AlbumPage.spinner_active = true;
|
||||
}
|
||||
if (loadingtext_active == false){
|
||||
$('#albumname').append('<h3 id="loadingtext"><i>(Album information is currently being loaded)</i></h3>')
|
||||
loadingtext_active = true;
|
||||
if (AlbumPage.loadingtext_active === false){
|
||||
$('#albumname').append('<h3 id="loadingtext"><i>(Album information is currently being loaded)</i></h3>');
|
||||
AlbumPage.loadingtext_active = true;
|
||||
}
|
||||
}
|
||||
else{
|
||||
if (++x === 10) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
var sts = $("#artistname").text();
|
||||
if (wasLoading == true || sts == "Loading"){
|
||||
location.reload();
|
||||
$('#albumnamespinner').remove()
|
||||
$('#loadingtext').remove()
|
||||
$('#ajaxMsg2').remove()
|
||||
spinner_active = false
|
||||
loadingtext_active = false
|
||||
loadingMessage = false
|
||||
} else {
|
||||
AlbumPage.x++;
|
||||
if (AlbumPage.x === 10 || AlbumPage.wasLoading || $("#artistname").text().trim() === "Loading") { // Combined conditions
|
||||
if (AlbumPage.refreshInterval) { // Clear only if interval is set
|
||||
clearInterval(AlbumPage.refreshInterval);
|
||||
}
|
||||
location.reload(); // Reload the page to show updated status
|
||||
}
|
||||
$('#albumnamespinner').remove();
|
||||
$('#loadingtext').remove();
|
||||
$('#ajaxMsg2').remove();
|
||||
AlbumPage.spinner_active = false;
|
||||
AlbumPage.loadingtext_active = false;
|
||||
AlbumPage.loadingMessage = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// jQuery DataTables custom sorting
|
||||
jQuery.extend( jQuery.fn.dataTableExt.oSort, {
|
||||
"title-numeric-pre": function ( a ) {
|
||||
var x = a.match(/title="*(-?[0-9\.]+)/)[1];
|
||||
return parseFloat( x );
|
||||
// Ensure it handles cases where title attribute might be missing or different
|
||||
var match = a.match(/title="*(-?[0-9\.]+)/);
|
||||
return match ? parseFloat( match[1] ) : -Infinity; // Return a safe default
|
||||
},
|
||||
|
||||
"title-numeric-asc": function ( a, b ) {
|
||||
return ((a < b) ? -1 : ((a > b) ? 1 : 0));
|
||||
},
|
||||
|
||||
"title-numeric-desc": function ( a, b ) {
|
||||
return ((a < b) ? 1 : ((a > b) ? -1 : 0));
|
||||
}
|
||||
} );
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
getAlbumInfo();
|
||||
initThisPage();
|
||||
checkAlbumStatus();
|
||||
refreshInterval = setInterval(function(){
|
||||
checkAlbumStatus();
|
||||
AlbumPage.getAlbumInfo();
|
||||
AlbumPage.initDialogs(); // Initialize dialog triggers
|
||||
AlbumPage.initDataTables(); // Initialize DataTables
|
||||
// Do not use initActions() here unless it's strictly needed and modernized itself.
|
||||
// setTimeout for fancybox is likely not needed if elements are ready or use delegated events.
|
||||
// If fancybox is for dynamic content, initialize it after content is loaded.
|
||||
// initFancybox(); // Re-evaluate if this is still needed or how it's used.
|
||||
|
||||
// Event handler for album actions
|
||||
// Delegated to #subhead_menu as these links are within it
|
||||
$('#subhead_menu').on('click', '.album-action', function(e) {
|
||||
e.preventDefault();
|
||||
var $this = $(this);
|
||||
var action = $this.data('action');
|
||||
var albumId = $this.data('album-id');
|
||||
var artistId = $this.data('artist-id');
|
||||
var newStatus = $this.data('new'); // Boolean 'true' or 'false'
|
||||
var releaseId = $this.data('release-id');
|
||||
var successMsg = $this.data('success-msg');
|
||||
|
||||
var url = action + '?AlbumID=' + albumId;
|
||||
if (artistId) url += '&ArtistID=' + artistId;
|
||||
if (newStatus !== undefined) url += '&new=' + newStatus;
|
||||
if (releaseId) url += '&ReleaseID=' + releaseId;
|
||||
|
||||
// Assuming doAjaxCall is defined globally or in common.js
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(url, $this, true, successMsg);
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined!");
|
||||
}
|
||||
});
|
||||
|
||||
// Event handler for specific download links within the dialog
|
||||
// Delegated to #downloads_table_body as these links are dynamically added
|
||||
$('#downloads_table_body').on('click', '.download-specific-release-link', function(e) {
|
||||
e.preventDefault();
|
||||
var index = $(this).data('index');
|
||||
AlbumPage.downloadSpecificRelease(index);
|
||||
});
|
||||
|
||||
// Event handler for form submission within dialogs
|
||||
// Delegated to the specific form's ID
|
||||
$('#editSearchTermForm').on('submit', function(e) {
|
||||
e.preventDefault(); // Prevent default form submission
|
||||
var $this = $(this);
|
||||
var action = $this.attr('action'); // Get action from form
|
||||
var formData = $this.serialize(); // Serialize form data
|
||||
var successMsg = $this.find('.album-action-submit').data('success-msg');
|
||||
|
||||
// Assuming doAjaxCall is defined globally or in common.js
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
// Pass form data instead of element, specify 'form' type if common.js handles it
|
||||
doAjaxCall(action + '?' + formData, $this.find('.album-action-submit'), true, successMsg);
|
||||
}
|
||||
$('#dialog2').dialog("close"); // Close dialog after submission
|
||||
});
|
||||
|
||||
|
||||
// Start checking album status periodically
|
||||
AlbumPage.checkAlbumStatus();
|
||||
AlbumPage.refreshInterval = setInterval(function(){
|
||||
AlbumPage.checkAlbumStatus();
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,31 +8,35 @@
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a id="menu_link_refresh" onclick="doSimpleAjaxCall('refreshArtist?ArtistID=${artist['ArtistID']}')" href="javascript:void(0)"><i class="fa fa-refresh"></i> Refresh Artist</a>
|
||||
<a id="menu_link_refresh" href="#" class="artist-action" data-action="refreshArtist" data-artist-id="${artist['ArtistID']}" data-success-msg="${artist['ArtistName']} refreshed"><i class="fa fa-refresh"></i> Refresh Artist</a>
|
||||
<a id="menu_link_delete" href="deleteArtist?ArtistID=${artist['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Artist</a>
|
||||
<a id="menu_link_scan" onclick="doAjaxCall('scanArtist?ArtistID=${artist['ArtistID']}', $(this)),'table'" href="javascript:void(0)" data-success="'${artist['ArtistName']}' was scanned"><i class="fa fa-refresh"></i> Scan Artist</a>
|
||||
<a id="menu_link_scan" href="#" class="artist-action" data-action="scanArtist" data-artist-id="${artist['ArtistID']}" data-success-msg="'${artist['ArtistName']}' was scanned"><i class="fa fa-refresh"></i> Scan Artist</a>
|
||||
|
||||
%if artist['Status'] == 'Paused':
|
||||
<a id="menu_link_resume" href="javascript:void(0)" onclick="doAjaxCall('resumeArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} resumed"><i class="fa fa-play"></i> Resume Artist</a>
|
||||
<a id="menu_link_resume" href="#" class="artist-action" data-action="resumeArtist" data-artist-id="${artist['ArtistID']}" data-success-msg="${artist['ArtistName']} resumed"><i class="fa fa-play"></i> Resume Artist</a>
|
||||
%else:
|
||||
<a id="menu_link_pauze" href="javascript:void(0)" onclick="doAjaxCall('pauseArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} paused"><i class="fa fa-pause"></i> Pause Artist</a>
|
||||
<a id="menu_link_pauze" href="#" class="artist-action" data-action="pauseArtist" data-artist-id="${artist['ArtistID']}" data-success-msg="${artist['ArtistName']} paused"><i class="fa fa-pause"></i> Pause Artist</a>
|
||||
%endif
|
||||
|
||||
%if artist['IncludeExtras']:
|
||||
<a id="menu_link_removeextra" href="javascript:void(0)" onclick="doAjaxCall('removeExtras?ArtistID=${artist['ArtistID']}&ArtistName=${artist['ArtistName']}',$(this),'submenu&table')" data-success="Extras removed for ${artist['ArtistName']}"><i class="fa fa-minus"></i> Remove Extras</a>
|
||||
<a class="menu_link_edit" id="menu_link_modifyextra" href="javascript:void(0)"><i class="fa fa-pencil"></i> Modify Extras</a>
|
||||
<a id="menu_link_removeextra" href="#" class="artist-action" data-action="removeExtras" data-artist-id="${artist['ArtistID']}" data-artist-name="${artist['ArtistName']}" data-success-msg="Extras removed for ${artist['ArtistName']}"><i class="fa fa-minus"></i> Remove Extras</a>
|
||||
<a class="menu_link_edit dialog-trigger" id="menu_link_modifyextra" href="#" data-dialog-id="dialog"><i class="fa fa-pencil"></i> Modify Extras</a>
|
||||
%else:
|
||||
<a id="menu_link_getextra" href="javascript:void(0)"><i class="fa fa-plus"></i> Get Extras</a>
|
||||
<a id="menu_link_getextra" href="#" class="dialog-trigger" data-dialog-id="dialog"><i class="fa fa-plus"></i> Get Extras</a>
|
||||
%endif
|
||||
<div id="dialog" title="Choose Which Extras to Fetch" style="display:none" class="configtable">
|
||||
<form action="getExtras" method="get" class="form">
|
||||
<input type="hidden" name="ArtistID" value="${artist['ArtistID']}">
|
||||
<input type="hidden" name="newstyle" value="true">
|
||||
%for extra in extras:
|
||||
<input type="checkbox" id="${extra}" name="${extra}" value="1" ${extras[extra]} />${string.capwords(extra)}<br>
|
||||
%endfor
|
||||
<br>
|
||||
<input id="submit" type="submit" value="Fetch Extras">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="dialog" title="Choose Which Extras to Fetch" style="display:none" class="configtable">
|
||||
<form action="getExtras" method="get" id="getExtrasForm">
|
||||
<input type="hidden" name="ArtistID" value="${artist['ArtistID']}">
|
||||
<input type="hidden" name="newstyle" value="true">
|
||||
%for extra in extras:
|
||||
<input type="checkbox" id="extra_${extra}" name="${extra}" value="1" ${extras[extra]} />
|
||||
<label for="extra_${extra}">${string.capwords(extra)}</label><br>
|
||||
%endfor
|
||||
<br>
|
||||
<button type="submit">Fetch Extras</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="home" class="back">« Back to overview</a>
|
||||
@@ -41,7 +45,7 @@
|
||||
<%def name="body()">
|
||||
<div id="artistheader" class="clearfix">
|
||||
<div id="artistImg">
|
||||
<img id="artistImage" class="albumArt" alt="" src="artwork/artist/${artist['ArtistID']}"/>
|
||||
<img id="artistImage" class="albumArt" alt="${artist['ArtistName']} artist image" src="artwork/artist/${artist['ArtistID']}"/>
|
||||
</div>
|
||||
<h1 id="artistname">
|
||||
<a href="http://musicbrainz.org/artist/${artist['ArtistID']}" id="artistnamelink">${artist['ArtistName']}</a>
|
||||
@@ -49,10 +53,10 @@
|
||||
<div id="artistBio"></div>
|
||||
</div>
|
||||
<ul id="artistCalendar" style="display:none;"></ul>
|
||||
<form action="markAlbums" method="get" id="markAlbums">
|
||||
<form action="markAlbums" method="get" id="markAlbumsForm">
|
||||
<input type="hidden" name="ArtistID" value=${artist['ArtistID']}>
|
||||
<div id="markalbum">Mark selected albums as
|
||||
<select name="action" onChange="doAjaxCall('markAlbums',$(this),'table',true);" data-error="You didn't select any albums">
|
||||
<select name="action" id="markAlbumsActionSelect">
|
||||
<option disabled="disabled" selected="selected">Choose...</option>
|
||||
<option value="Wanted">Wanted</option>
|
||||
<option value="WantedNew">Wanted (new only)</option>
|
||||
@@ -60,12 +64,12 @@
|
||||
<option value="Ignored">Ignored</option>
|
||||
<option value="Downloaded">Downloaded</option>
|
||||
</select>
|
||||
<input type="hidden" value="Go">
|
||||
<button type="submit" style="display:none;"></button> <%-- Hidden submit to allow form submission via JS --%>
|
||||
</div>
|
||||
<table class="display" id="album_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="select"><input type="checkbox" onClick="toggle(this)" /></th>
|
||||
<th id="select"><input type="checkbox" class="select-all-checkbox" aria-label="Select all albums" /></th>
|
||||
<th id="albumart"></th>
|
||||
<th id="albumname">Name</th>
|
||||
<th id="reldate">Date</th>
|
||||
@@ -121,24 +125,24 @@
|
||||
|
||||
%>
|
||||
<tr class="grade${grade}">
|
||||
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></td>
|
||||
<td id="albumart"><img class="albumArtnostretch" id="${album['AlbumID']}" src="artwork/thumbs/album/${album['AlbumID']}" height="64" width="64"></td>
|
||||
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox album-checkbox" /></td>
|
||||
<td id="albumart"><img class="albumArtnostretch" id="thumb_${album['AlbumID']}" alt="${album['AlbumTitle']} thumbnail" src="artwork/thumbs/album/${album['AlbumID']}"></td>
|
||||
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||
<td id="reldate">${album['ReleaseDate']}</td>
|
||||
<td id="type">${album['Type']}</td>
|
||||
<td id="score">${album['CriticScore']}/${album['UserScore']}</td>
|
||||
<td id="status">${album['Status']}
|
||||
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
|
||||
[<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' added to Wanted list">want</a>]
|
||||
[<a href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-success-msg="'${album['AlbumTitle']}' added to Wanted list">want</a>]
|
||||
%elif (album['Status'] == 'Wanted' or album['Status'] == 'Wanted Lossless'):
|
||||
[<a href="javascript:void(0)" onclick="doAjaxCall('unqueueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' skipped">skip</a>] [<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}', $(this),'table')" data-success="Trying to download'${album['AlbumTitle']}'" title="Search if available for download">search</a>]
|
||||
[<a href="#" class="album-action-inline" data-action="unqueueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-success-msg="'${album['AlbumTitle']}' skipped">skip</a>] [<a href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-success-msg="Trying to download'${album['AlbumTitle']}'" title="Search if available for download">search</a>]
|
||||
%else:
|
||||
[<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}', $(this),'table')" data-success="Retrying the same version of '${album['AlbumTitle']}'" title="Retry the same download again">retry</a>][<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this),'table')" title="Try a new download, skipping all previously tried nzbs" data-success="Downloading new version for '${album['AlbumTitle']}'" data-success="Looking for a new version of '${album['AlbumTitle']}'">new</a>]
|
||||
[<a href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-success-msg="Retrying the same version of '${album['AlbumTitle']}'" title="Retry the same download again">retry</a>][<a href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-new="True" title="Try a new download, skipping all previously tried nzbs" data-success-msg="Looking for a new version of '${album['AlbumTitle']}'">new</a>]
|
||||
%endif
|
||||
%if albumformat in lossy_formats and album['Status'] == 'Skipped':
|
||||
[<a id="wantlossless" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&lossless=True', $(this),'table')" data-success="Lossless version of '${album['AlbumTitle']}' added to queue">want lossless</a>]
|
||||
[<a id="wantlossless" href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-lossless="True" data-success-msg="Lossless version of '${album['AlbumTitle']}' added to queue">want lossless</a>]
|
||||
%elif albumformat in lossy_formats and (album['Status'] == 'Snatched' or album['Status'] == 'Downloaded'):
|
||||
[<a id="wantlossless" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&lossless=True', $(this),'table')" data-success="Retrying the same lossless version of '${album['AlbumTitle']}'">retry lossless</a>]
|
||||
[<a id="wantlossless" href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-lossless="True" data-success-msg="Retrying the same lossless version of '${album['AlbumTitle']}'">retry lossless</a>]
|
||||
%endif
|
||||
</td>
|
||||
<td id="have"><span title="${percent}"><span><div class="progress-container"><div style="width:${percent}%"><div class="havetracks">${havetracks}/${totaltracks}</div></div></div></td>
|
||||
@@ -152,177 +156,313 @@
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
|
||||
<script>
|
||||
function getArtistBio() {
|
||||
// Define a global object for page-specific functions to avoid polluting global scope directly
|
||||
var ArtistPage = ArtistPage || {};
|
||||
|
||||
ArtistPage.getArtistBio = function() {
|
||||
var id = "${artist['ArtistID']}";
|
||||
var elem = $("#artistBio");
|
||||
getInfo(elem,id,'artist');
|
||||
}
|
||||
// Assuming getInfo is defined in common.js
|
||||
if (typeof getInfo === 'function') {
|
||||
getInfo(elem,id,'artist');
|
||||
} else {
|
||||
console.warn("getInfo function not found. Artist bio might not be loaded.");
|
||||
}
|
||||
};
|
||||
|
||||
<%
|
||||
if headphones.CONFIG.SONGKICK_FILTER_ENABLED:
|
||||
songkick_filter_enabled = "true"
|
||||
else:
|
||||
songkick_filter_enabled = "false"
|
||||
ArtistPage.initDialogs = function() {
|
||||
// General handler for opening dialogs based on data-dialog-id
|
||||
$(document).on('click', '.dialog-trigger', function(e) {
|
||||
e.preventDefault();
|
||||
var dialogId = $(this).data('dialog-id');
|
||||
var $dialog = $('#' + dialogId);
|
||||
|
||||
if not headphones.CONFIG.SONGKICK_LOCATION:
|
||||
songkick_location = "none"
|
||||
else:
|
||||
songkick_location = headphones.CONFIG.SONGKICK_LOCATION
|
||||
if ($dialog.length) {
|
||||
$dialog.dialog({
|
||||
width: 500,
|
||||
maxHeight: 500,
|
||||
modal: true // Added modal for better UX
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if headphones.CONFIG.SONGKICK_ENABLED:
|
||||
songkick_enabled = "true"
|
||||
else:
|
||||
songkick_enabled = "false"
|
||||
// Submit handler for getExtrasForm
|
||||
$('#getExtrasForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var $this = $(this);
|
||||
var action = $this.attr('action');
|
||||
var formData = $this.serialize();
|
||||
var artistID = $this.find('input[name="ArtistID"]').val(); // Get ArtistID
|
||||
var successMsg = "Extras fetch initiated for " + "${artist['ArtistName']}"; // Dynamic success message
|
||||
|
||||
%>
|
||||
function getArtistsCalendar() {
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(action + '?' + formData, $this, true, successMsg);
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined!");
|
||||
}
|
||||
$('#dialog').dialog("close");
|
||||
});
|
||||
};
|
||||
|
||||
ArtistPage.getArtistsCalendar = function() {
|
||||
var template, calendarDomNode;
|
||||
|
||||
calendarDomNode = $("#artistCalendar");
|
||||
|
||||
template = '<li><a target="_blank" href="URI"><span class="sk-name">NAME</span><span class="sk-location">LOC</span></a></li>';
|
||||
|
||||
$.getJSON("https://api.songkick.com/api/3.0/artists/mbid:${artist['ArtistID']}/calendar.json?apikey=${headphones.CONFIG.SONGKICK_APIKEY}&jsoncallback=?",
|
||||
// Python variables are injected directly into this JS block
|
||||
var songkick_filter_enabled = ${'true' if headphones.CONFIG.SONGKICK_FILTER_ENABLED else 'false'};
|
||||
var songkick_location = ${'"{}"'.format(headphones.CONFIG.SONGKICK_LOCATION) if headphones.CONFIG.SONGKICK_LOCATION else 'null'};
|
||||
var songkick_enabled = ${'true' if headphones.CONFIG.SONGKICK_ENABLED else 'false'};
|
||||
var songkick_apikey = "${headphones.CONFIG.SONGKICK_APIKEY}";
|
||||
|
||||
if (!songkick_enabled || !songkick_apikey) {
|
||||
console.log("Songkick is not enabled or API key is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
$.getJSON("https://api.songkick.com/api/3.0/artists/mbid:${artist['ArtistID']}/calendar.json?apikey=" + songkick_apikey + "&jsoncallback=?",
|
||||
function(data){
|
||||
if (data['resultsPage'].totalEntries >= 1) {
|
||||
if (${songkick_filter_enabled}) {
|
||||
data.resultsPage.results.event = $.grep(data.resultsPage.results.event, function(element,index){
|
||||
return element.venue.metroArea.id == ${songkick_location};
|
||||
if (data['resultsPage'] && data['resultsPage'].totalEntries >= 1 && data['resultsPage'].results && data['resultsPage'].results.event) {
|
||||
var events = data.resultsPage.results.event;
|
||||
if (songkick_filter_enabled && songkick_location) {
|
||||
events = $.grep(events, function(element,index){
|
||||
return element.venue && element.venue.metroArea && element.venue.metroArea.id == songkick_location;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.resultsPage.results.event.length > 0) {
|
||||
var tourDate;
|
||||
|
||||
if (events.length > 0) {
|
||||
calendarDomNode.show();
|
||||
$("#artistImg").addClass('on-tour');
|
||||
|
||||
jQuery.each(data.resultsPage.results.event, function(i, event) {
|
||||
tourDate = template;
|
||||
tourDate = tourDate.replace('URI',event.uri);
|
||||
tourDate = tourDate.replace('NAME',event.displayName);
|
||||
tourDate = tourDate.replace('LOC',event.location.city);
|
||||
|
||||
$.each(events, function(i, event) {
|
||||
var tourDate = template;
|
||||
tourDate = tourDate.replace('URI', event.uri || '#');
|
||||
tourDate = tourDate.replace('NAME', event.displayName || 'N/A');
|
||||
tourDate = tourDate.replace('LOC', (event.location && event.location.city) ? event.location.city : 'N/A');
|
||||
calendarDomNode.append(tourDate);
|
||||
});
|
||||
|
||||
calendarDomNode.append('<li><img src="interfaces/default/images/songkick.png" alt="concerts by songkick" class="sk-logo" /></li>');
|
||||
|
||||
$(function() {
|
||||
$("#artistCalendar").each(function() {
|
||||
// Handle "More..." button logic for calendar
|
||||
calendarDomNode.each(function() {
|
||||
$("li:gt(4)", this).hide(); /* :gt() is zero-indexed */
|
||||
$("li:nth-child(5)", this).after("<br><li class='more'><a href='#'>More...</a></li>"); /* :nth-child() is one-indexed */
|
||||
});
|
||||
$("li.more").on("click", 'a', function() {
|
||||
});
|
||||
$("li.more").on("click", 'a', function(e) {
|
||||
e.preventDefault(); // Prevent default link behavior
|
||||
var li = $(this).parents("li:first");
|
||||
li.parent().children().show();
|
||||
li.remove();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||
console.error("Songkick API call failed: ", textStatus, errorThrown);
|
||||
});
|
||||
};
|
||||
|
||||
var loadingMessage = false;
|
||||
var spinner_active = false;
|
||||
var loadingtext_active = false;
|
||||
ArtistPage.loadingMessage = false;
|
||||
ArtistPage.spinner_active = false;
|
||||
ArtistPage.loadingtext_active = false;
|
||||
ArtistPage.refreshInterval = null; // Initialize as null
|
||||
|
||||
function checkArtistStatus() {
|
||||
ArtistPage.checkArtistStatus = function() {
|
||||
$.getJSON("getArtistjson?ArtistID=${artist['ArtistID']}", function(data) {
|
||||
if (data['Status'] == "Loading"){
|
||||
refreshTable();
|
||||
if (data['Status'] === "Loading"){
|
||||
// Assuming refreshTable() is defined globally or in common.js and it updates the album_table
|
||||
if (typeof refreshTable === 'function') {
|
||||
refreshTable(); // Refresh the album table to show loading states if implemented there
|
||||
}
|
||||
|
||||
$('#artistnamelink').text(data["ArtistName"]);
|
||||
if (loadingMessage == false){
|
||||
if (ArtistPage.loadingMessage === false){
|
||||
$("#ajaxMsg").after( "<div id='ajaxMsg2' class='ajaxMsg'></div>" );
|
||||
showArtistMsg("Getting artist information");
|
||||
loadingMessage = true;
|
||||
if (typeof showArtistMsg === 'function') {
|
||||
showArtistMsg("Getting artist information");
|
||||
}
|
||||
ArtistPage.loadingMessage = true;
|
||||
}
|
||||
if (spinner_active == false){
|
||||
$('#artistname').prepend('<i class="fa fa-refresh fa-spin" id="artistnamespinner"></i>')
|
||||
spinner_active = true;
|
||||
if (ArtistPage.spinner_active === false){
|
||||
$('#artistname').prepend('<i class="fa fa-refresh fa-spin" id="artistnamespinner"></i>');
|
||||
ArtistPage.spinner_active = true;
|
||||
}
|
||||
if (loadingtext_active == false){
|
||||
$('#artistname').append('<h3 id="loadingtext"><i>(Album information for this artist is currently being loaded)</i></h3>')
|
||||
loadingtext_active = true;
|
||||
if (ArtistPage.loadingtext_active === false){
|
||||
$('#artistname').append('<h3 id="loadingtext"><i>(Album information for this artist is currently being loaded)</i></h3>');
|
||||
ArtistPage.loadingtext_active = true;
|
||||
}
|
||||
} else {
|
||||
// Only reload if previously loading to avoid unnecessary refreshes
|
||||
if (ArtistPage.spinner_active || ArtistPage.loadingtext_active || ArtistPage.loadingMessage) {
|
||||
location.reload(); // Reload the page to show updated status once loading is complete
|
||||
}
|
||||
$('#artistnamespinner').remove();
|
||||
$('#loadingtext').remove();
|
||||
$('#ajaxMsg2').remove();
|
||||
ArtistPage.spinner_active = false;
|
||||
ArtistPage.loadingtext_active = false;
|
||||
ArtistPage.loadingMessage = false;
|
||||
}
|
||||
else{
|
||||
$('#artistnamespinner').remove()
|
||||
$('#loadingtext').remove()
|
||||
$('#ajaxMsg2').remove()
|
||||
spinner_active = false
|
||||
loadingtext_active = false
|
||||
loadingMessage = false
|
||||
}
|
||||
});
|
||||
}
|
||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||
console.error("Error checking artist status: ", textStatus, errorThrown);
|
||||
});
|
||||
};
|
||||
|
||||
function initThisPage() {
|
||||
$('#menu_link_getextra').click(function(event) {
|
||||
$('#dialog').dialog();
|
||||
event.preventDefault();
|
||||
});
|
||||
$('#menu_link_modifyextra').click(function(event) {
|
||||
$('#dialog').dialog();
|
||||
event.preventDefault();
|
||||
});
|
||||
$('#album_table').dataTable({
|
||||
"bDestroy": true,
|
||||
"aoColumns": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ "sType": "date" },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ "sType": "title-numeric"},
|
||||
null,
|
||||
null
|
||||
ArtistPage.initDataTables = function() {
|
||||
$('#album_table').DataTable({
|
||||
"destroy": true, // bDestroy -> destroy
|
||||
"columns": [ // aoColumns -> columns
|
||||
null, null, null,
|
||||
{ "type": "date" }, // sType -> type
|
||||
null, null, null,
|
||||
{ "type": "title-numeric"},
|
||||
null, null
|
||||
],
|
||||
"aoColumnDefs": [
|
||||
{ 'bSortable': false, 'aTargets': [ 0,1 ] }
|
||||
"columnDefs": [ // aoColumnDefs -> columnDefs
|
||||
{ 'orderable': false, 'targets': [ 0, 1 ] } // bSortable -> orderable, aTargets -> targets
|
||||
],
|
||||
"bStateSave": true,
|
||||
"oLanguage": {
|
||||
"sLengthMenu":"Show _MENU_ albums per page",
|
||||
"sEmptyTable": "No album information available",
|
||||
"sInfo":"Showing _TOTAL_ albums",
|
||||
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
|
||||
"sInfoFiltered":"(filtered from _MAX_ total albums)",
|
||||
"sSearch": ""},
|
||||
"bPaginate": false,
|
||||
"aaSorting": [[4, 'asc'],[3,'desc']]
|
||||
|
||||
"stateSave": true, // bStateSave -> stateSave
|
||||
"language": { // oLanguage -> language
|
||||
"lengthMenu":"Show _MENU_ albums per page",
|
||||
"emptyTable": "No album information available",
|
||||
"info":"Showing _TOTAL_ albums",
|
||||
"infoEmpty":"Showing 0 to 0 of 0 albums",
|
||||
"infoFiltered":"(filtered from _MAX_ total albums)",
|
||||
"search": "" // No need for "Search: " label if using custom icon
|
||||
},
|
||||
"paging": false, // bPaginate -> paging
|
||||
"order": [[4, 'asc'],[3,'desc']] // aaSorting -> order
|
||||
});
|
||||
resetFilters("albums");
|
||||
setTimeout(function(){
|
||||
initFancybox();
|
||||
},1500);
|
||||
}
|
||||
// Assuming resetFilters is defined globally or in common.js
|
||||
if (typeof resetFilters === 'function') {
|
||||
resetFilters("albums");
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
initActions();
|
||||
initThisPage();
|
||||
getArtistBio();
|
||||
if( ${songkick_enabled} ){
|
||||
getArtistsCalendar();
|
||||
}
|
||||
checkArtistStatus();
|
||||
setInterval(function(){
|
||||
checkArtistStatus();
|
||||
}, 1500);
|
||||
});
|
||||
// Init common actions if they are not already handled in base.html document.ready
|
||||
// if (typeof initActions === 'function') {
|
||||
// initActions();
|
||||
// }
|
||||
|
||||
ArtistPage.getArtistBio();
|
||||
ArtistPage.initDialogs();
|
||||
ArtistPage.initDataTables();
|
||||
|
||||
// Songkick Calendar
|
||||
// The python config values are injected above, so they are available here.
|
||||
if( ${'true' if headphones.CONFIG.SONGKICK_ENABLED else 'false'} ){
|
||||
ArtistPage.getArtistsCalendar();
|
||||
}
|
||||
|
||||
// Artist Status Polling
|
||||
ArtistPage.checkArtistStatus(); // Initial check
|
||||
ArtistPage.refreshInterval = setInterval(function(){
|
||||
ArtistPage.checkArtistStatus();
|
||||
}, 3000); // Increased interval to 3 seconds, was 1.5s which is very frequent
|
||||
|
||||
// Event handler for artist actions in subhead_menu
|
||||
$('#subhead_menu').on('click', '.artist-action', function(e) {
|
||||
e.preventDefault();
|
||||
var $this = $(this);
|
||||
var action = $this.data('action');
|
||||
var artistId = $this.data('artist-id');
|
||||
var artistName = $this.data('artist-name'); // For removeExtras
|
||||
var successMsg = $this.data('success-msg');
|
||||
|
||||
var url = action + '?ArtistID=' + artistId;
|
||||
if (artistName) url += '&ArtistName=' + encodeURIComponent(artistName);
|
||||
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(url, $this, true, successMsg); // 'true' for refreshing content, if common.js supports it
|
||||
} else if (typeof doSimpleAjaxCall === 'function' && action === 'refreshArtist') {
|
||||
// Fallback for refreshArtist if doAjaxCall isn't what's expected for it
|
||||
doSimpleAjaxCall(url);
|
||||
} else {
|
||||
console.error("doAjaxCall or doSimpleAjaxCall function is not defined!");
|
||||
}
|
||||
});
|
||||
|
||||
// Event handler for album actions within the table
|
||||
// Delegated to #album_table body as these links are dynamically added
|
||||
$('#album_table tbody').on('click', '.album-action-inline', function(e) {
|
||||
e.preventDefault();
|
||||
var $this = $(this);
|
||||
var action = $this.data('action');
|
||||
var albumId = $this.data('album-id');
|
||||
var artistId = $this.data('artist-id');
|
||||
var newStatus = $this.data('new'); // Boolean 'true' or 'false'
|
||||
var lossless = $this.data('lossless'); // Boolean 'true' or 'false'
|
||||
var successMsg = $this.data('success-msg');
|
||||
|
||||
var url = action + '?AlbumID=' + albumId + '&ArtistID=' + artistId;
|
||||
if (newStatus !== undefined) url += '&new=' + newStatus;
|
||||
if (lossless !== undefined) url += '&lossless=' + lossless;
|
||||
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(url, $this, 'table', successMsg); // 'table' for refreshing content, if common.js supports it
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined!");
|
||||
}
|
||||
});
|
||||
|
||||
// "Mark selected albums as" dropdown change
|
||||
$('#markAlbumsActionSelect').on('change', function() {
|
||||
var selectedAction = $(this).val();
|
||||
if (selectedAction) {
|
||||
// Manually trigger form submission with AJAX
|
||||
var $form = $('#markAlbumsForm');
|
||||
var formData = $form.serialize(); // Includes ArtistID and action
|
||||
|
||||
// Get selected album IDs
|
||||
var selectedAlbumIDs = $('.album-checkbox:checked').map(function() {
|
||||
return $(this).attr('name'); // AlbumID is the name attribute
|
||||
}).get();
|
||||
|
||||
if (selectedAlbumIDs.length === 0) {
|
||||
// Show error message as per original data-error
|
||||
if (typeof doAjaxCall === 'function') { // Assuming doAjaxCall can show errors
|
||||
doAjaxCall(null, $(this), null, null, 'You didn\'t select any albums');
|
||||
} else {
|
||||
alert('You didn\'t select any albums');
|
||||
}
|
||||
$(this).val('Choose...'); // Reset dropdown
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct the URL for markAlbums action
|
||||
var url = $form.attr('action') + '?' + formData;
|
||||
// Add selected album IDs as a comma-separated list or multiple parameters
|
||||
$.each(selectedAlbumIDs, function(index, id) {
|
||||
url += '&AlbumID=' + id; // Append each selected AlbumID
|
||||
});
|
||||
|
||||
// Assuming doAjaxCall is defined in common.js
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
// Using null for the element as it's a general form submission
|
||||
doAjaxCall(url, null, 'table', 'Albums marked successfully'); // 'table' for refreshing, success message
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined!");
|
||||
}
|
||||
$(this).val('Choose...'); // Reset dropdown after action
|
||||
}
|
||||
});
|
||||
|
||||
// "Select all" checkbox functionality
|
||||
$('.select-all-checkbox').on('change', function() {
|
||||
$('.album-checkbox').prop('checked', $(this).prop('checked'));
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
@@ -2,142 +2,182 @@
|
||||
import headphones
|
||||
from headphones import version
|
||||
%>
|
||||
<!doctype html>
|
||||
<!--[if IE 7 ]> <html lang="en" class="no-js ie7"> <![endif]-->
|
||||
<!--[if IE 8 ]> <html lang="en" class="no-js ie8"> <![endif]-->
|
||||
<!--[if IE 9 ]> <html lang="en" class="no-js ie9"> <![endif]-->
|
||||
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"> <!--<![endif]-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<title>Headphones - ${title}</title>
|
||||
<meta name="description" content="Headphones 'default' interface - made by Elmar Kouwenhoven">
|
||||
<meta name="author" content="Elmar Kouwenhoven">
|
||||
<title>Headphones - ${title}</title>
|
||||
<meta name="description" content="Headphones 'default' interface - made by Elmar Kouwenhoven">
|
||||
<meta name="author" content="Elmar Kouwenhoven">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="images/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="images/headphoneslogo.png">
|
||||
|
||||
<link rel="shortcut icon" href="images/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="images/headphoneslogo.png">
|
||||
|
||||
<link rel="stylesheet" href="css/jquery-ui.min.css">
|
||||
|
||||
<link rel="stylesheet" href="interfaces/default/css/style.css">
|
||||
<link rel="stylesheet" href="interfaces/default/css/font-awesome.min.css">
|
||||
${next.headIncludes()}
|
||||
|
||||
<script src="js/libs/modernizr-2.8.3.min.js"></script>
|
||||
<link rel="stylesheet" href="css/jquery-ui.min.css">
|
||||
<link rel="stylesheet" href="interfaces/default/css/style.css">
|
||||
<link rel="stylesheet" href="interfaces/default/css/font-awesome.min.css">
|
||||
${next.headIncludes()}
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="ajaxMsg" class="ajaxMsg"></div>
|
||||
% if headphones.CONFIG.CHECK_GITHUB and not headphones.CURRENT_VERSION:
|
||||
<div id="updatebar">
|
||||
You're running an unknown version of Headphones. <a href="update">Update</a> or
|
||||
<a href="javascript:void(0)" onclick="$('#updatebar').slideUp('slow');">Close</a>
|
||||
</div>
|
||||
% elif headphones.CONFIG.CHECK_GITHUB and headphones.CURRENT_VERSION != headphones.LATEST_VERSION and headphones.COMMITS_BEHIND > 0 and headphones.INSTALL_TYPE != 'win':
|
||||
<div id="updatebar">
|
||||
A <a href="https://github.com/${headphones.CONFIG.GIT_USER}/headphones/compare/${headphones.CURRENT_VERSION}...${headphones.LATEST_VERSION}"> newer version</a> is available. You're ${headphones.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or <a href="javascript:void(0)" onclick="$('#updatebar').slideUp('slow');">Close</a>
|
||||
</div>
|
||||
% endif
|
||||
<div id="container">
|
||||
<div id="ajaxMsg" class="ajaxMsg"></div>
|
||||
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<div id="logo">
|
||||
<a href="home"><img src="images/headphoneslogo.png" alt="headphones" width="64"></a>
|
||||
</div>
|
||||
<ul id="nav">
|
||||
<li><a href="upcoming">wanted</a></li>
|
||||
<li><a href="extras">extras</a></li>
|
||||
<li><a href="manage">manage</a></li>
|
||||
<li><a href="history">history</a></li>
|
||||
<li><a href="logs">logs</a></li>
|
||||
<li><a href="config" class="config"><i class="fa fa-gear fa-lg"></i></a></li>
|
||||
</ul>
|
||||
<div id="searchbar">
|
||||
<form action="search" method="get">
|
||||
<input type="text" value="" placeholder="Search" onfocus="if(this.value==this.defaultValue) this.value='';" name="name" />
|
||||
<i class='fa fa-search mini-icon'></i>
|
||||
<select name="type" id="search_type">
|
||||
<option value="artist">Artist</option>
|
||||
<option value="album">Album</option>
|
||||
<option value="series">Series</option>
|
||||
</select>
|
||||
<input type="submit" value="Search"/>
|
||||
</form>
|
||||
</div>
|
||||
% if headphones.CONFIG.CHECK_GITHUB and not headphones.CURRENT_VERSION:
|
||||
<div id="updatebar">
|
||||
You're running an unknown version of Headphones. <a href="update">Update</a> or
|
||||
<a href="#" class="close-updatebar">Close</a>
|
||||
</div>
|
||||
% elif headphones.CONFIG.CHECK_GITHUB and headphones.CURRENT_VERSION != headphones.LATEST_VERSION and headphones.COMMITS_BEHIND > 0 and headphones.INSTALL_TYPE != 'win':
|
||||
<div id="updatebar">
|
||||
A <a href="https://github.com/${headphones.CONFIG.GIT_USER}/headphones/compare/${headphones.CURRENT_VERSION}...${headphones.LATEST_VERSION}"> newer version</a> is available. You're ${headphones.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or <a href="#" class="close-updatebar">Close</a>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
</div>
|
||||
</header>
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<div id="logo">
|
||||
<a href="home"><img src="images/headphoneslogo.png" alt="headphones" width="64"></a>
|
||||
</div>
|
||||
<nav id="nav">
|
||||
<ul>
|
||||
<li><a href="upcoming">wanted</a></li>
|
||||
<li><a href="extras">extras</a></li>
|
||||
<li><a href="manage">manage</a></li>
|
||||
<li><a href="history">history</a></li>
|
||||
<li><a href="logs">logs</a></li>
|
||||
<li><a href="config" class="config" aria-label="Configuration"><i class="fa fa-gear fa-lg"></i></a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div id="searchbar">
|
||||
<form action="search" method="get">
|
||||
<input type="text" value="" placeholder="Search" name="name" id="search-input" />
|
||||
<i class='fa fa-search mini-icon'></i>
|
||||
<select name="type" id="search_type">
|
||||
<option value="artist">Artist</option>
|
||||
<option value="album">Album</option>
|
||||
<option value="series">Series</option>
|
||||
</select>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="main" class="main">
|
||||
<div id="subhead">
|
||||
${next.headerIncludes()}
|
||||
</div>
|
||||
${next.body()}
|
||||
</div>
|
||||
<main id="main" class="main">
|
||||
<div id="subhead">
|
||||
${next.headerIncludes()}
|
||||
</div>
|
||||
${next.body()}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div id="info">
|
||||
<small>
|
||||
<a href="https://github.com/rembo10/headphones"><i class="fa fa-headphones"></i> Website</a> |
|
||||
%if headphones.CONFIG.GIT_USER != 'rembo10':
|
||||
<a href="https://github.com/${headphones.CONFIG.GIT_USER}/headphones" title="Open this fork on github"><i class="fa fa-github"></i> GitHub</a> |
|
||||
%endif
|
||||
<a href="https://github.com/rembo10/headphones/wiki/TroubleShooting"><i class="fa fa-ambulance"></i> Help</a>
|
||||
</small>
|
||||
</div>
|
||||
<div id="actions">
|
||||
<small>
|
||||
<a href="shutdown"><i class="fa fa-power-off"></i> Shutdown</a> |
|
||||
<a href="restart"><i class="fa fa-power-off"></i> Restart</a> |
|
||||
<a href="javascript:void(0)" onclick="doAjaxCall('checkGithub',$(this))" data-success="Checking for update successful" data-error="Error checking for update"><i class="fa fa-refresh"></i> Check for new version</a>
|
||||
</small>
|
||||
</div>
|
||||
<div id="version">
|
||||
Version: <em>${headphones.CURRENT_VERSION}</em>
|
||||
%if version.HEADPHONES_VERSION != 'master':
|
||||
(${version.HEADPHONES_VERSION})
|
||||
%endif
|
||||
%if headphones.CONFIG.GIT_BRANCH != 'master':
|
||||
(${headphones.CONFIG.GIT_BRANCH})
|
||||
%endif
|
||||
</div>
|
||||
</footer>
|
||||
<a href="#main" id="toTop"><i class="fa fa-angle-double-up"></i> <span>Back to top</span></a>
|
||||
</div>
|
||||
<footer>
|
||||
<div id="info">
|
||||
<small>
|
||||
<a href="https://github.com/rembo10/headphones"><i class="fa fa-headphones"></i> Website</a> |
|
||||
%if headphones.CONFIG.GIT_USER != 'rembo10':
|
||||
<a href="https://github.com/${headphones.CONFIG.GIT_USER}/headphones" title="Open this fork on github"><i class="fa fa-github"></i> GitHub</a> |
|
||||
%endif
|
||||
<a href="https://github.com/rembo10/headphones/wiki/TroubleShooting"><i class="fa fa-ambulance"></i> Help</a>
|
||||
</small>
|
||||
</div>
|
||||
<div id="actions">
|
||||
<small>
|
||||
<a href="shutdown"><i class="fa fa-power-off"></i> Shutdown</a> |
|
||||
<a href="restart"><i class="fa fa-power-off"></i> Restart</a> |
|
||||
<a href="#" class="check-update-btn" data-success="Checking for update successful" data-error="Error checking for update"><i class="fa fa-refresh"></i> Check for new version</a>
|
||||
</small>
|
||||
</div>
|
||||
<div id="version">
|
||||
Version: <em>${headphones.CURRENT_VERSION}</em>
|
||||
%if version.HEADPHONES_VERSION != 'master':
|
||||
(${version.HEADPHONES_VERSION})
|
||||
%endif
|
||||
%if headphones.CONFIG.GIT_BRANCH != 'master':
|
||||
(${headphones.CONFIG.GIT_BRANCH})
|
||||
%endif
|
||||
</div>
|
||||
</footer>
|
||||
<a href="#main" id="toTop" aria-label="Back to top"><i class="fa fa-angle-double-up"></i> <span>Back to top</span></a>
|
||||
</div>
|
||||
|
||||
<script src="js/libs/jquery-1.11.1.min.js"></script>
|
||||
<script src="js/libs/jquery-ui.min.js"></script>
|
||||
<script src="js/common.js"></script>
|
||||
<script src="js/libs/jquery-3.7.1.min.js"></script>
|
||||
<script src="js/libs/jquery-ui.min.js"></script>
|
||||
<script src="js/common.js"></script>
|
||||
|
||||
${next.javascriptIncludes()}
|
||||
${next.javascriptIncludes()}
|
||||
|
||||
<script src="interfaces/default/js/script.js"></script>
|
||||
<!-- This template is made by Elmar Kouwenhoven -->
|
||||
<script src="interfaces/default/js/script.js"></script>
|
||||
|
||||
<!-- Persist search type using local storage -->
|
||||
<script type="text/javascript">
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
// Focus on the first form input that's not hidden
|
||||
$('form:first *:input[type!=hidden]:first').focus();
|
||||
|
||||
$(document).ready(function() {
|
||||
$('form:first *:input[type!=hidden]:first').focus();
|
||||
try{
|
||||
var type = window.localStorage.getItem('search_type') || "artist";
|
||||
$("#search_type").val(type);
|
||||
} catch(e) {
|
||||
}
|
||||
});
|
||||
// Persist search type using local storage
|
||||
try {
|
||||
var type = window.localStorage.getItem('search_type') || "artist";
|
||||
$("#search_type").val(type);
|
||||
} catch (e) {
|
||||
console.error("Local Storage not available or error accessing it:", e);
|
||||
}
|
||||
|
||||
$('select[id=search_type]').change(function() {
|
||||
var type = $(this).val()
|
||||
try{
|
||||
window.localStorage.setItem('search_type', type);
|
||||
} catch(e) {
|
||||
}
|
||||
});
|
||||
// Modernized event listener for closing update bar (delegated)
|
||||
// Uses event delegation for robustness: attaches handler to parent (#container)
|
||||
// which listens for clicks on elements with class .close-updatebar
|
||||
$('#container').on('click', '.close-updatebar', function(e) {
|
||||
e.preventDefault(); // Prevent default link behavior
|
||||
$(this).closest('#updatebar').slideUp('slow');
|
||||
});
|
||||
|
||||
</script>
|
||||
// Modernized event listener for "Check for new version" (delegated)
|
||||
$('#actions').on('click', '.check-update-btn', function(e) {
|
||||
e.preventDefault(); // Prevent default link behavior
|
||||
// Assuming doAjaxCall is defined in common.js or script.js
|
||||
doAjaxCall('checkGithub', $(this));
|
||||
});
|
||||
|
||||
// Event listener for search type change
|
||||
$('select[id=search_type]').on('change', function() {
|
||||
var type = $(this).val();
|
||||
try {
|
||||
window.localStorage.setItem('search_type', type);
|
||||
} catch (e) {
|
||||
console.error("Local Storage not available or error accessing it:", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle placeholder text for search input (no longer needs onfocus in HTML)
|
||||
$('#search-input').on('focus', function() {
|
||||
if (this.value === this.defaultValue) {
|
||||
this.value = '';
|
||||
}
|
||||
}).on('blur', function() {
|
||||
if (this.value === '') {
|
||||
this.value = this.defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
// "Back to top" functionality
|
||||
// Show/hide #toTop button based on scroll position
|
||||
$(window).scroll(function() {
|
||||
if ($(this).scrollTop() > 100) { // Show after scrolling 100px
|
||||
$('#toTop').fadeIn();
|
||||
} else {
|
||||
$('#toTop').fadeOut();
|
||||
}
|
||||
});
|
||||
|
||||
// Smooth scroll to top when button is clicked
|
||||
$('#toTop').click(function(e) {
|
||||
e.preventDefault();
|
||||
$('html, body').animate({scrollTop : 0}, 800);
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<%inherit file="base.html" />
|
||||
|
||||
<%def name="body()">
|
||||
<div class="title">
|
||||
<h1 class="clearfix"><i class="fa fa-users"></i> Artists You Might Like</h1>
|
||||
@@ -7,9 +8,54 @@
|
||||
<div class="cloudtag">
|
||||
<ul id="cloud">
|
||||
%for artist in cloudlist:
|
||||
<li><a href="addArtist?artistid=${artist['ArtistID']}" class="tag${artist['Count']}">${artist['ArtistName']}</a></li>
|
||||
<%--
|
||||
Modified the anchor tag to use data attributes for AJAX
|
||||
and href="#" to prevent default navigation.
|
||||
--%>
|
||||
<li>
|
||||
<a href="#" class="tag${artist['Count']} add-artist-link"
|
||||
data-artist-id="${artist['ArtistID']}"
|
||||
data-artist-name="${artist['ArtistName']}">
|
||||
${artist['ArtistName']}
|
||||
</a>
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Delegated event handler for 'add-artist-link' class
|
||||
$('#cloud').on('click', '.add-artist-link', function(e) {
|
||||
e.preventDefault(); // Prevent the default link behavior (page navigation)
|
||||
|
||||
var $this = $(this);
|
||||
var artistId = $this.data('artist-id');
|
||||
var artistName = $this.data('artist-name'); // Get artist name for feedback
|
||||
|
||||
// Assuming 'doAjaxCall' is available globally (from common.js or similar)
|
||||
// and handles displaying success/error messages.
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall('addArtist?artistid=' + artistId, $this, 'simple',
|
||||
'Artist "' + artistName + '" added successfully!');
|
||||
|
||||
// Optionally, fade out or remove the artist from the list after adding
|
||||
$this.closest('li').fadeOut(500, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot add artist asynchronously.");
|
||||
// Fallback: allow the default link behavior if AJAX is not possible
|
||||
// window.location.href = 'addArtist?artistid=' + artistId;
|
||||
// Or just log an error and do nothing if page reload is not desired.
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%inherit file="base.html"/>
|
||||
<%!
|
||||
from headphones import helpers
|
||||
import cgi
|
||||
from html import escape as html_escape
|
||||
%>
|
||||
|
||||
<%def name="headerIncludes()">
|
||||
@@ -56,17 +56,19 @@
|
||||
fileid = 'torrent'
|
||||
if item['URL'].find('codeshy') != -1:
|
||||
fileid = 'nzb'
|
||||
if item['URL'].find('bandcamp') != -1:
|
||||
fileid = 'bandcamp'
|
||||
|
||||
folder = 'Folder: ' + item['FolderName']
|
||||
|
||||
%>
|
||||
<tr class="grade${grade}">
|
||||
<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 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="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="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=${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>
|
||||
%endfor
|
||||
</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 |
@@ -7,11 +7,11 @@
|
||||
<table class="display" id="artist_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="albumart"></th>
|
||||
<th id="name">Artist Name</th>
|
||||
<th id="status">Status</th>
|
||||
<th id="album">Latest Release</th>
|
||||
<th id="have">Have</th>
|
||||
<th class="column-albumart"></th> <%-- Changed id to class --%>
|
||||
<th class="column-name">Artist Name</th>
|
||||
<th class="column-status">Status</th>
|
||||
<th class="column-album">Latest Release</th>
|
||||
<th class="column-have">Have</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -20,14 +20,52 @@
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
<style>
|
||||
/* Basic CSS for album art thumbnail */
|
||||
.albumArt-thumb {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
object-fit: cover; /* Ensures image covers the area without distortion */
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Styling for the progress bar */
|
||||
.progress-container {
|
||||
background-color: #eee;
|
||||
border-radius: 5px;
|
||||
height: 15px; /* Adjust height as needed */
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%; /* Or a fixed width if preferred */
|
||||
}
|
||||
|
||||
.progress-container > div {
|
||||
background-color: #4CAF50; /* Green color for progress */
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 10px; /* Smaller font for percentage */
|
||||
line-height: 15px; /* Vertically align text */
|
||||
}
|
||||
|
||||
.havetracks {
|
||||
padding: 0 5px; /* Add some padding around the text */
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
<script src="js/libs/jquery.unveil.min.js"></script>
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script src="js/libs/jquery.unveil.min.js"></script> <%-- Keep if still using for lazy loading --%>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
<script>
|
||||
function initThisPage() {
|
||||
// Encapsulate page-specific logic
|
||||
var IndexPage = IndexPage || {};
|
||||
|
||||
IndexPage.initDataTable = function() {
|
||||
$('#artist_table').dataTable({
|
||||
"bDestroy": true,
|
||||
"aoColumnDefs": [
|
||||
@@ -36,14 +74,17 @@
|
||||
"aTargets": [0],
|
||||
"mData":"ArtistID",
|
||||
"mRender": function ( data, type, full ) {
|
||||
return '<div id="artistImg"><img class="albumArt" alt="" id="'+ data + '" data-src="artwork/thumbs/artist/' + data + '"/></div>';
|
||||
// Using a class for the image, and data-src for lazy loading
|
||||
// Added loading="lazy" for native lazy loading support where available
|
||||
return '<div class="artistImg-container"><img class="albumArt-thumb" alt="Album art for ' + full['ArtistName'] + '" data-src="artwork/thumbs/artist/' + data + '" loading="lazy" /></div>';
|
||||
}
|
||||
},
|
||||
{
|
||||
"aTargets":[1],
|
||||
"mDataProp":"ArtistSortName",
|
||||
"mRender":function (data,type,full) {
|
||||
return '<span title="' + full['ArtistID'] + '"></span><a href="artistPage?ArtistID=' + full['ArtistID'] + '">' + full['ArtistName'] + '</a>'
|
||||
// Using a class for the span, not an ID
|
||||
return '<span class="artist-sort-name" title="' + full['ArtistID'] + '"></span><a href="artistPage?ArtistID=' + full['ArtistID'] + '">' + full['ArtistName'] + '</a>'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -53,52 +94,52 @@
|
||||
"aTargets":[3],
|
||||
"mDataProp":"LatestAlbum",
|
||||
"mRender":function(data,type,full){
|
||||
artist = full;
|
||||
if (artist['ReleaseDate'] && artist['LatestAlbum'])
|
||||
{
|
||||
var artist = full; // Renamed to avoid confusion with outer scope
|
||||
var releasedate = '';
|
||||
var albumdisplay = '<i>None</i>';
|
||||
var grade = 'gradeZ'; // Default grade
|
||||
|
||||
if (artist['ReleaseDate'] && artist['LatestAlbum']) {
|
||||
releasedate = artist['ReleaseDate'];
|
||||
albumdisplay = '<i>' + artist['LatestAlbum'] + '</i> (' + artist['ReleaseDate'] + ')';
|
||||
}
|
||||
else if(artist['LatestAlbum'])
|
||||
{
|
||||
releasedate = '';
|
||||
} else if (artist['LatestAlbum']) {
|
||||
albumdisplay = '<i>' + artist['LatestAlbum'] + '</i>';
|
||||
}
|
||||
else
|
||||
{
|
||||
releasedate = '';
|
||||
albumdisplay = '<i>None</i>';
|
||||
}
|
||||
if (artist['ReleaseInFuture'] === 'True')
|
||||
{
|
||||
|
||||
if (artist['ReleaseInFuture'] === 'True') {
|
||||
grade = 'gradeA';
|
||||
}
|
||||
else
|
||||
{
|
||||
grade = 'gradeZ';
|
||||
}
|
||||
artist['Grade'] = grade;
|
||||
return '<span title="' + releasedate + '"></span><a href="albumPage?AlbumID=' + full['AlbumID'] + '">' + albumdisplay + '</a>'
|
||||
// artist['Grade'] is used in fnRowCallback, ensure it's set in the data.
|
||||
// If this 'Grade' is only for client-side sorting/filtering, it's fine.
|
||||
// If it affects server-side logic, it should be handled there.
|
||||
full['Grade'] = grade; // Ensure grade is part of the row data for fnRowCallback
|
||||
|
||||
// Using a class for the span, not an ID
|
||||
return '<span class="release-date-sort" title="' + releasedate + '"></span><a href="albumPage?AlbumID=' + full['AlbumID'] + '">' + albumdisplay + '</a>'
|
||||
}
|
||||
},
|
||||
{
|
||||
"aTargets":[4],
|
||||
"mDataProp":"HaveTracks",
|
||||
"mRender":function(data,type,full){
|
||||
if(full['TotalTracks'] > 0)
|
||||
{
|
||||
percent = (full['HaveTracks']*100.0)/full['TotalTracks']
|
||||
if(percent > 100){
|
||||
var percent = 0;
|
||||
var totalTracksDisplay = '?';
|
||||
|
||||
if (full['TotalTracks'] > 0) {
|
||||
percent = (full['HaveTracks']*100.0)/full['TotalTracks'];
|
||||
if (percent > 100) {
|
||||
percent = 100;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
full['TotalTracks'] = '?';
|
||||
percent = 0;
|
||||
totalTracksDisplay = full['TotalTracks'];
|
||||
}
|
||||
|
||||
return '<span title="' + percent + '"></span><div class="progress-container"><div style="width:' + percent + '%"><div class="havetracks">' + full['HaveTracks'] + '/' + full['TotalTracks'] + '</div></div></div>';
|
||||
// Added ARIA attributes for accessibility
|
||||
return '<span class="have-tracks-sort" title="' + percent + '"></span>' +
|
||||
'<div class="progress-container" role="progressbar" aria-valuenow="' + Math.round(percent) + '" aria-valuemin="0" aria-valuemax="100">' +
|
||||
'<div style="width:' + percent + '%">' +
|
||||
'<div class="havetracks">' + full['HaveTracks'] + '/' + totalTracksDisplay + '</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
},
|
||||
],
|
||||
@@ -110,46 +151,73 @@
|
||||
"sInfoFiltered":"(filtered from _MAX_ total artists)",
|
||||
"sEmptyTable": " ",
|
||||
},
|
||||
"bStateSave": true,
|
||||
"bStateSave": true, // Retain table state across page loads
|
||||
"iDisplayLength": 50,
|
||||
"sPaginationType": "full_numbers",
|
||||
"bProcessing": true,
|
||||
"bServerSide": true,
|
||||
"sAjaxSource": 'getArtists.json',
|
||||
"bProcessing": true, // Show processing indicator
|
||||
"bServerSide": true, // Enable server-side processing
|
||||
"sAjaxSource": 'getArtists.json', // API endpoint for data
|
||||
"fnRowCallback": function(nRow, aData, iDisplayIndex, iDisplayIndexFull) {
|
||||
$('td', nRow).closest('tr').addClass(aData['Grade'])
|
||||
nRow.children[0].id = 'albumart';
|
||||
nRow.children[1].id = 'name';
|
||||
nRow.children[2].id = 'status'
|
||||
nRow.children[3].id = 'album'
|
||||
nRow.children[4].id = 'have'
|
||||
// Apply the 'Grade' class from aData to the table row
|
||||
$(nRow).addClass(aData['Grade']);
|
||||
|
||||
// Removed setting duplicate IDs on child elements. Use classes if needed for styling.
|
||||
// For example:
|
||||
// $(nRow).find('td:eq(0)').addClass('albumart-cell');
|
||||
// $(nRow).find('td:eq(1)').addClass('name-cell');
|
||||
// etc.
|
||||
return nRow;
|
||||
},
|
||||
"fnServerData": function ( sSource, aoData, fnCallback ) {
|
||||
/* Add some extra data to the sender */
|
||||
$.getJSON( sSource, aoData, function (json) { fnCallback(json) } )
|
||||
},
|
||||
"fnInitComplete": function(oSettings, json)
|
||||
{
|
||||
// Custom function for fetching data, using $.getJSON
|
||||
$.getJSON( sSource, aoData, function (json) {
|
||||
fnCallback(json);
|
||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||
console.error("Error fetching artist data:", textStatus, errorThrown);
|
||||
// Provide user feedback if data loading fails
|
||||
// e.g., show an error message in the table or a global notification
|
||||
});
|
||||
},
|
||||
// Removed fnInitComplete as it was empty
|
||||
"fnDrawCallback": function (o) {
|
||||
// Jump to top of page
|
||||
$('html,body').scrollTop(0);
|
||||
// Re-unveil images after each draw for lazy loading
|
||||
$("img.albumArt-thumb").unveil();
|
||||
}
|
||||
});
|
||||
$('#artist_table').on("draw.dt", function () {
|
||||
$("img").unveil();
|
||||
});
|
||||
// jQuery Unveil listener for new images added to the DOM after DataTables draw
|
||||
// This is handled in fnDrawCallback now for better integration with DataTables.
|
||||
// $('#artist_table').on("draw.dt", function () {
|
||||
// $("img.albumArt-thumb").unveil();
|
||||
// });
|
||||
|
||||
resetFilters("artist or album");
|
||||
}
|
||||
// Assuming resetFilters is a global function from common.js
|
||||
if (typeof resetFilters === 'function') {
|
||||
resetFilters("artist or album");
|
||||
} else {
|
||||
console.warn("resetFilters function is not defined.");
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
initThisPage();
|
||||
IndexPage.initDataTable();
|
||||
});
|
||||
$(window).load(function(){
|
||||
initFancybox();
|
||||
refreshLoadArtist();
|
||||
|
||||
$(window).on('load', function(){
|
||||
// Ensure these functions exist and are necessary.
|
||||
// If initFancybox or refreshLoadArtist are global, call them here.
|
||||
// Otherwise, they should be encapsulated or refactored.
|
||||
if (typeof initFancybox === 'function') {
|
||||
initFancybox();
|
||||
} else {
|
||||
console.warn("initFancybox function is not defined.");
|
||||
}
|
||||
if (typeof refreshLoadArtist === 'function') {
|
||||
refreshLoadArtist();
|
||||
} else {
|
||||
console.warn("refreshLoadArtist function is not defined.");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
|
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 |
@@ -1,385 +1,539 @@
|
||||
function getThumb(imgElem,id,type) {
|
||||
/**
|
||||
* @file headphones.js
|
||||
* @brief Main JavaScript file for Headphones web interface.
|
||||
* Contains core UI, API, and utility functions.
|
||||
*/
|
||||
|
||||
if ( type == 'artist' ) {
|
||||
var thumbURL = "getThumb?ArtistID=" + id;
|
||||
// var imgURL = "getArtwork?ArtistID=" + id;
|
||||
} else {
|
||||
var thumbURL = "getThumb?AlbumID=" + id;
|
||||
// var imgURL = "getArtwork?AlbumID=" + id;
|
||||
}
|
||||
// Get Data from the cache by Artist ID
|
||||
$.ajax({
|
||||
url: thumbURL,
|
||||
cache: true,
|
||||
success: function(data){
|
||||
if ( data == "" ) {
|
||||
var imageUrl = "interfaces/default/images/no-cover-artist.png";
|
||||
}
|
||||
else {
|
||||
var imageUrl = data;
|
||||
}
|
||||
$(imgElem).attr("src",imageUrl).hide().fadeIn();
|
||||
// $(imgElem).wrap('<a href="'+ imgURL +'" rel="dialog" title="' + name + '"></a>');
|
||||
}
|
||||
});
|
||||
var Headphones = Headphones || {};
|
||||
|
||||
// --- Configuration ---
|
||||
Headphones.config = {
|
||||
fallbackImage: "interfaces/default/images/no-cover-art.png", // Generic fallback
|
||||
fallbackArtistImage: "interfaces/default/images/no-cover-artist.png", // Specific artist fallback
|
||||
messageTimeout: 3000 // Default timeout for messages in milliseconds
|
||||
};
|
||||
|
||||
// --- UI Messaging Module ---
|
||||
Headphones.UI = Headphones.UI || {};
|
||||
Headphones.UI.Message = (function() {
|
||||
var $ajaxMsg = $("#ajaxMsg");
|
||||
var $ajaxMsg2 = $("#ajaxMsg2"); // Assuming this is for artist-specific messages
|
||||
var $updateBar = $("#updatebar");
|
||||
|
||||
/**
|
||||
* Shows a feedback message to the user.
|
||||
* @param {string} msg - The message to display.
|
||||
* @param {string} type - Type of message ('success', 'error', 'info', 'loading').
|
||||
* @param {number} [timeout=Headphones.config.messageTimeout] - Duration to display the message (in ms). Set to 0 for no timeout.
|
||||
* @param {HTMLElement} [$targetElement=$ajaxMsg] - The jQuery element to display the message in.
|
||||
*/
|
||||
function showMessage(msg, type, timeout, $targetElement) {
|
||||
timeout = typeof timeout !== 'undefined' ? timeout : Headphones.config.messageTimeout;
|
||||
$targetElement = $targetElement || $ajaxMsg;
|
||||
|
||||
// Adjust position if update bar is visible
|
||||
if ($updateBar.is(":visible")) {
|
||||
var height = $updateBar.height() + 35;
|
||||
$targetElement.css("bottom", height + "px");
|
||||
} else {
|
||||
$targetElement.removeAttr("style");
|
||||
}
|
||||
|
||||
$targetElement.fadeIn();
|
||||
$targetElement.removeClass('success error info'); // Clear previous states
|
||||
|
||||
var iconClass = '';
|
||||
switch (type) {
|
||||
case 'success':
|
||||
iconClass = 'fa-check';
|
||||
$targetElement.addClass('success');
|
||||
break;
|
||||
case 'error':
|
||||
iconClass = 'fa-exclamation-triangle';
|
||||
$targetElement.addClass('error');
|
||||
break;
|
||||
case 'loading':
|
||||
iconClass = 'fa-refresh fa-spin';
|
||||
break;
|
||||
default: // info
|
||||
iconClass = 'fa-info-circle';
|
||||
$targetElement.addClass('info');
|
||||
}
|
||||
|
||||
var $message = $("<div class='msg'><i class='fa " + iconClass + "'></i> " + msg + "</div>");
|
||||
$targetElement.empty().append($message); // Clear and append new message
|
||||
|
||||
if (timeout > 0) {
|
||||
setTimeout(function() {
|
||||
$message.fadeOut(function() {
|
||||
$(this).remove();
|
||||
$targetElement.fadeOut();
|
||||
});
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific function for artist messages (if still needed, otherwise consolidate into showMessage)
|
||||
* @param {string} msg - The message to display.
|
||||
*/
|
||||
function showArtistMessage(msg) {
|
||||
showMessage(msg, 'loading', 0, $ajaxMsg2); // No timeout for loading message
|
||||
}
|
||||
|
||||
return {
|
||||
show: showMessage,
|
||||
showArtist: showArtistMessage // Consider removing if not distinct enough
|
||||
};
|
||||
})();
|
||||
|
||||
// --- API / AJAX Module ---
|
||||
Headphones.API = (function() {
|
||||
/**
|
||||
* Performs an AJAX call with standardized messaging and reload options.
|
||||
* @param {object} options - Configuration options for the AJAX call.
|
||||
* @param {string} options.url - The URL for the AJAX request.
|
||||
* @param {object|string} [options.data] - Data to send with the request.
|
||||
* @param {string} [options.type='POST'] - HTTP method ('GET' or 'POST').
|
||||
* @param {string} [options.successMessage='Success!'] - Message to display on success.
|
||||
* @param {string} [options.errorMessage='There was an error'] - Message to display on error.
|
||||
* @param {string} [options.reloadType] - Type of reload after success ('table', 'tabs', 'page', 'submenu', 'submenu&table').
|
||||
* @param {HTMLElement} [options.contextElement] - The DOM element that triggered the action (for data-attributes).
|
||||
* @param {function} [options.beforeSendCallback] - Custom function to call before sending.
|
||||
* @param {function} [options.successCallback] - Custom function to call on success.
|
||||
* @param {function} [options.errorCallback] - Custom function to call on error.
|
||||
* @param {function} [options.completeCallback] - Custom function to call on completion.
|
||||
*/
|
||||
function doAjaxCall(options) {
|
||||
var opts = $.extend(true, {
|
||||
url: '',
|
||||
data: {},
|
||||
type: 'POST',
|
||||
successMessage: 'Success!',
|
||||
errorMessage: 'There was an error',
|
||||
reloadType: null, // 'table', 'tabs', 'page', 'submenu', 'submenu&table'
|
||||
contextElement: null,
|
||||
beforeSendCallback: null,
|
||||
successCallback: null,
|
||||
errorCallback: null,
|
||||
completeCallback: null
|
||||
}, options);
|
||||
|
||||
// Get messages from data attributes if context element provided
|
||||
if (opts.contextElement) {
|
||||
var $elem = $(opts.contextElement);
|
||||
opts.successMessage = $elem.data('success') || opts.successMessage;
|
||||
opts.errorMessage = $elem.data('error') || opts.errorMessage;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: opts.url,
|
||||
data: opts.data,
|
||||
type: opts.type,
|
||||
beforeSend: function(jqXHR, settings) {
|
||||
Headphones.UI.Message.show('Loading...', 'loading', 0); // Show loading message indefinitely
|
||||
if (opts.beforeSendCallback) opts.beforeSendCallback(jqXHR, settings);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
Headphones.UI.Message.show(opts.errorMessage, 'error');
|
||||
if (opts.errorCallback) opts.errorCallback(jqXHR, textStatus, errorThrown);
|
||||
},
|
||||
success: function(data, textStatus, jqXHR) {
|
||||
Headphones.UI.Message.show(opts.successMessage, 'success');
|
||||
if (opts.successCallback) opts.successCallback(data, textStatus, jqXHR);
|
||||
|
||||
// Handle reloads based on reloadType
|
||||
setTimeout(function() { // Wait for message to fade out
|
||||
if (opts.reloadType === 'table') {
|
||||
Headphones.UI.Content.refreshTable();
|
||||
} else if (opts.reloadType === 'tabs') {
|
||||
Headphones.UI.Content.refreshTab();
|
||||
} else if (opts.reloadType === 'page') {
|
||||
location.reload();
|
||||
} else if (opts.reloadType === 'submenu') {
|
||||
Headphones.UI.Content.refreshSubmenu();
|
||||
} else if (opts.reloadType === 'submenu&table') {
|
||||
Headphones.UI.Content.refreshSubmenu();
|
||||
Headphones.UI.Content.refreshTable();
|
||||
}
|
||||
}, Headphones.config.messageTimeout + 100); // Wait for message fade out
|
||||
},
|
||||
complete: function(jqXHR, textStatus) {
|
||||
// The message will fade out automatically by showMessage
|
||||
if (opts.completeCallback) opts.completeCallback(jqXHR, textStatus);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a simple GET AJAX call without complex messaging or reload.
|
||||
* @param {string} url - The URL for the GET request.
|
||||
*/
|
||||
function doSimpleAjaxCall(url) {
|
||||
$.ajax({ url: url, type: 'GET' });
|
||||
}
|
||||
|
||||
return {
|
||||
call: doAjaxCall,
|
||||
simpleCall: doSimpleAjaxCall
|
||||
};
|
||||
})();
|
||||
|
||||
// --- Image Loading Module ---
|
||||
Headphones.Images = (function() {
|
||||
/**
|
||||
* Loads artwork/thumbnail for an element, handling lazy load and fallbacks.
|
||||
* @param {jQuery} $imgElem - The jQuery image element.
|
||||
* @param {string} id - The ID (AlbumID or ArtistID).
|
||||
* @param {string} type - 'album' or 'artist'.
|
||||
* @param {boolean} [unveil=false] - Whether to use data-src and unveil for lazy loading.
|
||||
*/
|
||||
function loadImage($imgElem, id, type, unveil) {
|
||||
var infoURL = "getImageLinks?" + (type === 'artist' ? "ArtistID=" : "AlbumID=") + id;
|
||||
|
||||
$.ajax({
|
||||
url: infoURL,
|
||||
cache: true,
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
var imageUrl = Headphones.config.fallbackImage;
|
||||
var artworkUrl = Headphones.config.fallbackImage;
|
||||
|
||||
if (data) {
|
||||
imageUrl = data.thumbnail || (type === 'artist' ? Headphones.config.fallbackArtistImage : Headphones.config.fallbackImage);
|
||||
artworkUrl = data.artwork || (type === 'artist' ? Headphones.config.fallbackArtistImage : Headphones.config.fallbackImage);
|
||||
}
|
||||
|
||||
if (unveil) {
|
||||
$imgElem.attr("data-src", imageUrl);
|
||||
$imgElem.unveil(); // Trigger unveil for lazy loading
|
||||
} else {
|
||||
$imgElem.attr("src", imageUrl).hide().fadeIn(); // Direct load with fade
|
||||
}
|
||||
|
||||
// If element has a rel="dialog" parent, update href for Fancybox
|
||||
var $wrapper = $imgElem.closest('a[rel="dialog"]');
|
||||
if ($wrapper.length) {
|
||||
$wrapper.attr('href', artworkUrl); // Link to full artwork
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
// On error, set to fallback image if not already set or handled by onerror on HTML
|
||||
if (unveil) {
|
||||
$imgElem.attr("data-src", Headphones.config.fallbackImage);
|
||||
$imgElem.unveil();
|
||||
} else {
|
||||
$imgElem.attr("src", Headphones.config.fallbackImage).hide().fadeIn();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and appends summary information.
|
||||
* @param {jQuery} $elem - The element to append summary to.
|
||||
* @param {string} id - The ID (AlbumID or ArtistID).
|
||||
* @param {string} type - 'album' or 'artist'.
|
||||
*/
|
||||
function getInfo($elem, id, type) {
|
||||
var infoURL = "getInfo?" + (type === 'artist' ? "ArtistID=" : "AlbumID=") + id;
|
||||
$.ajax({
|
||||
url: infoURL,
|
||||
cache: true,
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
if (data && data.Summary) {
|
||||
$elem.append(data.Summary);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
load: loadImage,
|
||||
getInfo: getInfo
|
||||
};
|
||||
})();
|
||||
|
||||
// --- UI Content Refresh Module ---
|
||||
Headphones.UI.Content = (function() {
|
||||
/**
|
||||
* Refreshes a specific part of the page content.
|
||||
* @param {string} targetSelector - jQuery selector for the element to load content into.
|
||||
* @param {string} sourceContentSelector - jQuery selector for the content to extract from the source URL.
|
||||
* @param {function} [initCallback] - Callback function to run after content is loaded.
|
||||
*/
|
||||
function refreshPart(targetSelector, sourceContentSelector, initCallback) {
|
||||
var url = window.location.href;
|
||||
$(targetSelector).load(url + " " + sourceContentSelector, function(response, status, xhr) {
|
||||
if (status === "error") {
|
||||
console.error("Failed to load content for " + targetSelector + ": " + xhr.status + " " + xhr.statusText);
|
||||
Headphones.UI.Message.show("Failed to refresh content.", 'error');
|
||||
} else if (initCallback) {
|
||||
// Ensure the callback is part of the Headphones object if it relies on it
|
||||
if (typeof initCallback === 'string' && Headphones.hasOwnProperty(initCallback)) {
|
||||
Headphones[initCallback](); // Call the global Headphones function
|
||||
} else if (typeof initCallback === 'function') {
|
||||
initCallback();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Refreshes the submenu by reloading its content. */
|
||||
function refreshSubmenu() {
|
||||
refreshPart("#subhead_container", "#subhead_menu", Headphones.UI.initActions); // Pass initActions to re-apply handlers
|
||||
}
|
||||
|
||||
/** Refreshes the main table content. */
|
||||
function refreshTable() {
|
||||
// This assumes there's one main .display table. If multiple, refine selector.
|
||||
// The original also reloaded tbody and thead separately, which is fine.
|
||||
refreshPart("table.display", "table.display tbody, table.display thead", function() {
|
||||
// Re-initialize DataTables after table refresh.
|
||||
// This might need specific DataTable re-initialization logic for each page.
|
||||
// A more robust solution would be to destroy and recreate the DataTable.
|
||||
// The template-specific `initThisPage()` usually contains this.
|
||||
// We need to call the appropriate initThisPage for the current page.
|
||||
// As `initThisPage` is defined per HTML file, we rely on the specific page's ready handler
|
||||
// to call its own initThisPage, or pass it as a parameter if this is called generically.
|
||||
// For now, let's assume `initThisPage` exists in the global scope where refreshTable is called.
|
||||
if (typeof initThisPage === 'function') {
|
||||
initThisPage();
|
||||
} else if (typeof SearchResultsPage !== 'undefined' && typeof SearchResultsPage.initDataTable === 'function') {
|
||||
SearchResultsPage.initDataTable();
|
||||
}
|
||||
// ... add more page-specific re-initializations as needed
|
||||
});
|
||||
}
|
||||
|
||||
/** Refreshes the currently active tab content. */
|
||||
function refreshTab() {
|
||||
var tabId = $('.ui-tabs-panel:visible').attr("id");
|
||||
if (tabId) {
|
||||
refreshPart('.ui-tabs-panel:visible', "#" + tabId, function() {
|
||||
// Similar to refreshTable, re-initialize specific tab content
|
||||
if (typeof initThisPage === 'function') { // assuming initThisPage handles tab content
|
||||
initThisPage();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls for status changes on rows with 'gradeL' class (loading state).
|
||||
* This is an older polling pattern; consider WebSockets for real-time updates if performance is critical.
|
||||
*/
|
||||
function refreshLoadArtist() {
|
||||
var $loadingRows = $("table.display tr.gradeL");
|
||||
if ($loadingRows.length > 0) {
|
||||
$loadingRows.each(function() {
|
||||
var $row = $(this);
|
||||
// Reload only the row's content to check status
|
||||
var rowIndex = $row.index() + 1; // 1-based index
|
||||
var url = window.location.href;
|
||||
$row.load(url + " table.display tbody tr:nth-child(" + rowIndex + ") td", function(response, status, xhr) {
|
||||
if (status === "error") {
|
||||
console.error("Failed to load row status: " + xhr.status + " " + xhr.statusText);
|
||||
// Optional: Show a message to the user
|
||||
} else {
|
||||
// Check status after loading the updated row content
|
||||
if ($row.find(".column-status").text() === 'Active') { // Assuming a status column with this text
|
||||
$row.removeClass('gradeL').addClass('gradeZ');
|
||||
// Re-initialize relevant JS if needed for the row (e.g., DataTables redrawing)
|
||||
if (typeof initThisPage === 'function') initThisPage(); // Re-initialize the table/page
|
||||
} else {
|
||||
// If still loading, set timeout for next check
|
||||
setTimeout(refreshLoadArtist, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
refreshSubmenu: refreshSubmenu,
|
||||
refreshTable: refreshTable,
|
||||
refreshTab: refreshTab,
|
||||
refreshLoadArtist: refreshLoadArtist
|
||||
};
|
||||
})();
|
||||
|
||||
// --- UI Element Initialization Module ---
|
||||
Headphones.UI.Elements = (function() {
|
||||
/** Initializes the header scroll-fade effect. */
|
||||
function initHeader() {
|
||||
var $header = $("#container header");
|
||||
var fadeSpeed = 100,
|
||||
fadeTo = 0.5,
|
||||
topDistance = 20;
|
||||
var inside = false;
|
||||
|
||||
$(window).on('scroll', function() {
|
||||
var position = $(window).scrollTop();
|
||||
if (position > topDistance && !inside) {
|
||||
//add events
|
||||
$header.fadeTo(fadeSpeed, fadeTo);
|
||||
$header.on('mouseenter', function() {
|
||||
$header.fadeTo(fadeSpeed, 1);
|
||||
});
|
||||
$header.on('mouseleave', function() {
|
||||
$header.fadeTo(fadeSpeed, fadeTo);
|
||||
});
|
||||
$("#toTop").fadeIn();
|
||||
inside = true;
|
||||
} else if (position < topDistance && inside) { // Added '&& inside' to prevent re-triggering
|
||||
$header.fadeTo(fadeSpeed, 1); // Ensure it's fully visible at top
|
||||
$header.off('mouseenter mouseleave'); // Remove events when at top
|
||||
$("#toTop").fadeOut();
|
||||
inside = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes config checkboxes to show/hide sibling content.
|
||||
* @param {HTMLElement} elem - The checkbox DOM element.
|
||||
*/
|
||||
function initConfigCheckbox(elem) {
|
||||
var $checkbox = $(elem);
|
||||
var $configContent = $checkbox.parent().next();
|
||||
|
||||
// Initial state
|
||||
if ($checkbox.is(":checked")) {
|
||||
$configContent.show();
|
||||
} else {
|
||||
$configContent.hide();
|
||||
}
|
||||
|
||||
// Click handler
|
||||
$checkbox.on('click', function() {
|
||||
if ($(this).is(":checked")) {
|
||||
$configContent.slideDown();
|
||||
} else {
|
||||
$configContent.slideUp();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Initializes various action buttons with jQuery UI button styling. */
|
||||
function initActions() {
|
||||
// Select all menu links under #subhead_menu and apply button()
|
||||
// This assumes jQuery UI Button widget is available and desired.
|
||||
$("#subhead_menu a[id^='menu_link_'], #subhead_menu .menu_link_edit").button();
|
||||
}
|
||||
|
||||
return {
|
||||
initHeader: initHeader,
|
||||
initConfigCheckbox: initConfigCheckbox,
|
||||
initActions: initActions
|
||||
};
|
||||
})();
|
||||
|
||||
// --- Utility Functions Module ---
|
||||
Headphones.Utils = (function() {
|
||||
/**
|
||||
* Sets placeholder text for DataTables search input.
|
||||
* @param {string} text - The placeholder text.
|
||||
*/
|
||||
function resetFilters(text) {
|
||||
// Ensure this targets the correct search input for DataTables
|
||||
if ($(".dataTables_filter input").length > 0) {
|
||||
$(".dataTables_filter input").attr("placeholder", "filter " + text + "");
|
||||
}
|
||||
}
|
||||
|
||||
/** Initializes Fancybox for dialog links. */
|
||||
function initFancybox() {
|
||||
// Check if Fancybox script is already loaded or needs to be loaded dynamically
|
||||
if ($.fn.fancybox) { // Check if fancybox function exists on jQuery object
|
||||
$("a[rel=dialog]").fancybox();
|
||||
} else if ($("a[rel=dialog]").length > 0) {
|
||||
// Dynamically load Fancybox script and CSS if elements with rel=dialog exist
|
||||
$.getScript('interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js', function() {
|
||||
$("head").append("<link rel='stylesheet' href='interfaces/default/js/fancybox/jquery.fancybox-1.3.4.css'>");
|
||||
$("a[rel=dialog]").fancybox();
|
||||
}).fail(function(jqxhr, settings, exception) {
|
||||
console.error("Failed to load Fancybox script: ", exception);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
resetFilters: resetFilters,
|
||||
initFancybox: initFancybox
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
// --- Global Functions (for backward compatibility if needed, but prefer to call from Headphones object) ---
|
||||
// These global functions would ideally be removed and replaced with direct calls to Headphones.API, Headphones.UI, etc.
|
||||
// For a gradual transition, they can act as wrappers.
|
||||
|
||||
// Original getThumb / getImageLinks replacement
|
||||
function getImageLinks(elem, id, type, unveil) {
|
||||
Headphones.Images.load($(elem), id, type, unveil);
|
||||
}
|
||||
|
||||
function getArtwork(imgElem,id,name,type) {
|
||||
|
||||
if ( type == 'artist' ) {
|
||||
var artworkURL = "getArtwork?ArtistID=" + id;
|
||||
} else {
|
||||
var artworkURL = "getArtwork?AlbumID=" + id;
|
||||
}
|
||||
// Get Data from the cache by Artist ID
|
||||
$.ajax({
|
||||
url: artworkURL,
|
||||
cache: true,
|
||||
success: function(data){
|
||||
if ( data == "" || data == undefined ) {
|
||||
var imageUrl = "interfaces/default/images/no-cover-artist.png";
|
||||
}
|
||||
else {
|
||||
var imageUrl = data;
|
||||
}
|
||||
$(imgElem).attr("src",imageUrl).hide().fadeIn();
|
||||
$(imgElem).wrap('<a href="'+ imageUrl +'" rel="dialog" title="' + name + '"></a>');
|
||||
}
|
||||
});
|
||||
// Original getInfo replacement
|
||||
function getInfo(elem, id, type) {
|
||||
Headphones.Images.getInfo($(elem), id, type);
|
||||
}
|
||||
|
||||
function getInfo(elem,id,type) {
|
||||
// Original doAjaxCall replacement (needs careful integration with new params)
|
||||
// The HTML templates are passing doAjaxCall(url,elem,reload,form).
|
||||
// This wrapper needs to convert those to the new options object.
|
||||
function doAjaxCall(url, elem, reloadType, isFormSubmission) {
|
||||
var options = {
|
||||
url: url,
|
||||
contextElement: elem,
|
||||
reloadType: reloadType
|
||||
};
|
||||
|
||||
if ( type == 'artist' ) {
|
||||
var infoURL = "getInfo?ArtistID=" + id;
|
||||
} else {
|
||||
var infoURL = "getInfo?AlbumID=" + id;
|
||||
}
|
||||
// Get Data from the cache by ID
|
||||
$.ajax({
|
||||
url: infoURL,
|
||||
cache: true,
|
||||
dataType: "json",
|
||||
success: function(data){
|
||||
var summary = data.Summary;
|
||||
$(elem).append(summary);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getImageLinks(elem,id,type,unveil) {
|
||||
if ( type == 'artist' ) {
|
||||
var infoURL = "getImageLinks?ArtistID=" + id;
|
||||
} else {
|
||||
var infoURL = "getImageLinks?AlbumID=" + id;
|
||||
}
|
||||
|
||||
// Get Data from the cache by ID
|
||||
$.ajax({
|
||||
url: infoURL,
|
||||
cache: true,
|
||||
dataType: "json",
|
||||
success: function(data){
|
||||
if (!data) {
|
||||
// Invalid response
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.thumbnail) {
|
||||
var thumbnail = "interfaces/default/images/no-cover-artist.png";
|
||||
}
|
||||
else {
|
||||
var thumbnail = data.thumbnail;
|
||||
}
|
||||
if (!data.artwork) {
|
||||
var artwork = "interfaces/default/images/no-cover-artist.png";
|
||||
}
|
||||
else {
|
||||
var artwork = data.artwork;
|
||||
}
|
||||
|
||||
if (unveil) {
|
||||
$(elem).attr("data-src", thumbnail);
|
||||
$(elem).unveil();
|
||||
}
|
||||
else {
|
||||
$(elem).attr("src", thumbnail);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initHeader() {
|
||||
//settings
|
||||
var header = $("#container header");
|
||||
var fadeSpeed = 100, fadeTo = 0.5, topDistance = 20;
|
||||
var topbarME = function() { $(header).fadeTo(fadeSpeed,1); }, topbarML = function() { $(header).fadeTo(fadeSpeed,fadeTo); };
|
||||
var inside = false;
|
||||
//do
|
||||
$(window).scroll(function() {
|
||||
position = $(window).scrollTop();
|
||||
if(position > topDistance && !inside) {
|
||||
//add events
|
||||
topbarML();
|
||||
$(header).bind('mouseenter',topbarME);
|
||||
$(header).bind('mouseleave',topbarML);
|
||||
$("#toTop").fadeIn();
|
||||
inside = true;
|
||||
}
|
||||
else if (position < topDistance){
|
||||
topbarME();
|
||||
$(header).unbind('mouseenter',topbarME);
|
||||
$(header).unbind('mouseleave',topbarML);
|
||||
$("#toTop").fadeOut();
|
||||
inside = false;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function initConfigCheckbox(elem) {
|
||||
var config = $(elem).parent().next();
|
||||
if ( $(elem).is(":checked") ) {
|
||||
config.show();
|
||||
} else {
|
||||
config.hide();
|
||||
}
|
||||
$(elem).click(function(){
|
||||
var config = $(this).parent().next();
|
||||
if ( $(this).is(":checked") ) {
|
||||
config.slideDown();
|
||||
} else {
|
||||
config.slideUp();
|
||||
}
|
||||
});
|
||||
}
|
||||
function initActions() {
|
||||
$("#subhead_menu #menu_link_refresh").button();
|
||||
$("#subhead_menu #menu_link_edit").button();
|
||||
$("#subhead_menu .menu_link_edit").button();
|
||||
$("#subhead_menu #menu_link_delete" ).button();
|
||||
$("#subhead_menu #menu_link_pauze").button();
|
||||
$("#subhead_menu #menu_link_resume").button();
|
||||
$("#subhead_menu #menu_link_getextra").button();
|
||||
$("#subhead_menu #menu_link_removeextra").button();
|
||||
$("#subhead_menu #menu_link_wanted" ).button();
|
||||
$("#subhead_menu #menu_link_check").button();
|
||||
$("#subhead_menu #menu_link_skipped").button();
|
||||
$("#subhead_menu #menu_link_retry").button();
|
||||
$("#subhead_menu #menu_link_new").button();
|
||||
$("#subhead_menu #menu_link_shutdown").button();
|
||||
$("#subhead_menu #menu_link_scan").button();
|
||||
}
|
||||
|
||||
function refreshSubmenu() {
|
||||
var url = $(location).attr('href');
|
||||
$("#subhead_container").load(url + " #subhead_menu",function(){
|
||||
initActions();
|
||||
});
|
||||
}
|
||||
function refreshTable() {
|
||||
var url = $(location).attr('href');
|
||||
$("table.display").load(url + " table.display tbody, table.display thead", function() {
|
||||
initThisPage();
|
||||
});
|
||||
}
|
||||
function refreshLoadArtist() {
|
||||
if ( $(".gradeL").length > 0 ) {
|
||||
var url = $(location).attr('href');
|
||||
var loadingRow = $("table.display tr.gradeL")
|
||||
loadingRow.each(function(){
|
||||
var row = $(this).index() + 1;
|
||||
var rowLoad = $("table.display tbody tr:nth-child("+row+")");
|
||||
$(rowLoad).load(url + " table.display tbody tr:nth-child("+ row +") td", function() {
|
||||
if ( $(rowLoad).children("#status").text() == 'Active' ) {
|
||||
// Active
|
||||
$(rowLoad).removeClass('gradeL').addClass('gradeZ');
|
||||
initThisPage();
|
||||
} else {
|
||||
// Still loading
|
||||
setTimeout(function(){
|
||||
refreshLoadArtist();
|
||||
},3000);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function refreshTab() {
|
||||
var url = $(location).attr('href');
|
||||
var tabId = $('.ui-tabs-panel:visible').attr("id");
|
||||
$('.ui-tabs-panel:visible').load(url + " #"+ tabId, function() {
|
||||
initThisPage();
|
||||
});
|
||||
}
|
||||
|
||||
function showMsg(msg,loader,timeout,ms) {
|
||||
var feedback = $("#ajaxMsg");
|
||||
update = $("#updatebar");
|
||||
if ( update.is(":visible") ) {
|
||||
var height = update.height() + 35;
|
||||
feedback.css("bottom",height + "px");
|
||||
} else {
|
||||
feedback.removeAttr("style");
|
||||
}
|
||||
feedback.fadeIn();
|
||||
var message = $("<div class='msg'>" + msg + "</div>");
|
||||
if (loader) {
|
||||
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||
feedback.css("padding","14px 10px")
|
||||
}
|
||||
$(feedback).prepend(message);
|
||||
if (timeout) {
|
||||
setTimeout(function(){
|
||||
message.fadeOut(function(){
|
||||
$(this).remove();
|
||||
feedback.fadeOut();
|
||||
});
|
||||
},ms);
|
||||
}
|
||||
}
|
||||
|
||||
function showArtistMsg(msg) {
|
||||
var feedback = $("#ajaxMsg2");
|
||||
update = $("#updatebar");
|
||||
if ( update.is(":visible") ) {
|
||||
var height = update.height() + 35;
|
||||
feedback.css("bottom",height + "px");
|
||||
} else {
|
||||
feedback.removeAttr("style");
|
||||
}
|
||||
feedback.fadeIn();
|
||||
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||
feedback.css("padding","14px 10px")
|
||||
$(feedback).prepend(message);
|
||||
}
|
||||
|
||||
function doAjaxCall(url,elem,reload,form) {
|
||||
// Set Message
|
||||
feedback = $("#ajaxMsg");
|
||||
update = $("#updatebar");
|
||||
if ( update.is(":visible") ) {
|
||||
var height = update.height() + 35;
|
||||
feedback.css("bottom",height + "px");
|
||||
} else {
|
||||
feedback.removeAttr("style");
|
||||
}
|
||||
|
||||
feedback.fadeIn();
|
||||
// Get Form data
|
||||
var formID = "#"+url;
|
||||
if ( form == true ) {
|
||||
var dataString = $(formID).serialize();
|
||||
}
|
||||
// Loader Image
|
||||
var loader = $("<i class='fa fa-refresh fa-spin'></i>");
|
||||
// Data Success Message
|
||||
var dataSucces = $(elem).data('success');
|
||||
if (typeof dataSucces === "undefined") {
|
||||
// Standard Message when variable is not set
|
||||
var dataSucces = "Success!";
|
||||
}
|
||||
// Data Errror Message
|
||||
var dataError = $(elem).data('error');
|
||||
if (typeof dataError === "undefined") {
|
||||
// Standard Message when variable is not set
|
||||
var dataError = "There was an error";
|
||||
}
|
||||
// Get Success & Error message from inline data, else use standard message
|
||||
var succesMsg = $("<div class='msg'><i class='fa fa-check'></i> " + dataSucces + "</div>");
|
||||
var errorMsg = $("<div class='msg'><i class='fa fa-exclamation-triangle'></i> " + dataError + "</div>");
|
||||
|
||||
// Check if checkbox is selected
|
||||
if ( form ) {
|
||||
if ( $('td#select input[type=checkbox]').length > 0 && !$('td#select input[type=checkbox]').is(':checked') || $('#importLastFM #username:visible').length > 0 && $("#importLastFM #username" ).val().length === 0 ) {
|
||||
feedback.addClass('error')
|
||||
$(feedback).prepend(errorMsg);
|
||||
setTimeout(function(){
|
||||
errorMsg.fadeOut(function(){
|
||||
$(this).remove();
|
||||
feedback.fadeOut(function(){
|
||||
feedback.removeClass('error');
|
||||
});
|
||||
})
|
||||
$(formID + " select").children('option[disabled=disabled]').attr('selected','selected');
|
||||
},2000);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ajax Call
|
||||
$.ajax({
|
||||
url: url,
|
||||
data: dataString,
|
||||
beforeSend: function(jqXHR, settings) {
|
||||
// Start loader etc.
|
||||
feedback.prepend(loader);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
feedback.addClass('error')
|
||||
feedback.prepend(errorMsg);
|
||||
setTimeout(function(){
|
||||
errorMsg.fadeOut(function(){
|
||||
$(this).remove();
|
||||
feedback.fadeOut(function(){
|
||||
feedback.removeClass('error')
|
||||
});
|
||||
})
|
||||
},2000);
|
||||
},
|
||||
success: function(data,jqXHR) {
|
||||
feedback.prepend(succesMsg);
|
||||
feedback.addClass('success')
|
||||
setTimeout(function(e){
|
||||
succesMsg.fadeOut(function(){
|
||||
$(this).remove();
|
||||
feedback.fadeOut(function(){
|
||||
feedback.removeClass('success');
|
||||
});
|
||||
if ( reload == true ) refreshSubmenu();
|
||||
if ( reload == "table") {
|
||||
console.log('refresh'); refreshTable();
|
||||
}
|
||||
if ( reload == "tabs") refreshTab();
|
||||
if ( reload == "page") location.reload();
|
||||
if ( reload == "submenu&table") {
|
||||
refreshSubmenu();
|
||||
refreshTable();
|
||||
}
|
||||
if ( form ) {
|
||||
// Change the option to 'choose...'
|
||||
$(formID + " select").children('option[disabled=disabled]').attr('selected','selected');
|
||||
}
|
||||
})
|
||||
},2000);
|
||||
},
|
||||
complete: function(jqXHR, textStatus) {
|
||||
// Remove loaders and stuff, ajax request is complete!
|
||||
loader.remove();
|
||||
}
|
||||
});
|
||||
if (isFormSubmission) {
|
||||
// This part needs careful migration. The original `doAjaxCall`
|
||||
// had logic like `var formID = "#"+url; var dataString = $(formID).serialize();`
|
||||
// and validation like `if ( $('td#select input[type=checkbox]').length > 0 && !$('td#select input[type=checkbox]').is(':checked') ... )`.
|
||||
// This validation MUST be done *before* calling doAjaxCall in the specific HTML template's JS.
|
||||
// Here, we assume `url` is the form ID if `isFormSubmission` is true.
|
||||
var $form = $('#' + url); // Assuming url is the form ID
|
||||
if ($form.length) {
|
||||
options.data = $form.serialize();
|
||||
} else {
|
||||
console.warn("doAjaxCall: form with ID '" + url + "' not found for submission.");
|
||||
// If the form cannot be found, it should likely be an error.
|
||||
Headphones.UI.Message.show("Form not found for submission.", 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Headphones.API.call(options);
|
||||
}
|
||||
|
||||
function doSimpleAjaxCall(url) {
|
||||
$.ajax(url);
|
||||
Headphones.API.simpleCall(url);
|
||||
}
|
||||
|
||||
function resetFilters(text){
|
||||
if ( $(".dataTables_filter").length > 0 ) {
|
||||
$(".dataTables_filter input").attr("placeholder","filter " + text + "");
|
||||
}
|
||||
function resetFilters(text) {
|
||||
Headphones.Utils.resetFilters(text);
|
||||
}
|
||||
|
||||
function initFancybox() {
|
||||
if ( $("a[rel=dialog]").length > 0 ) {
|
||||
$.getScript('interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js', function() {
|
||||
$("head").append("<link rel='stylesheet' href='interfaces/default/js/fancybox/jquery.fancybox-1.3.4.css'>");
|
||||
$("a[rel=dialog]").fancybox();
|
||||
});
|
||||
}
|
||||
Headphones.Utils.initFancybox();
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
initHeader();
|
||||
// Placeholder for `initThisPage` - this function is typically defined per HTML template.
|
||||
// It's called by `$(document).ready` in many templates.
|
||||
// This should be removed from `headphones.js` and defined in each template.
|
||||
// function initThisPage() { /* defined in each HTML template */ }
|
||||
|
||||
|
||||
// --- Document Ready ---
|
||||
$(document).ready(function() {
|
||||
Headphones.UI.Elements.initHeader(); // Initialize global header effects
|
||||
Headphones.UI.Elements.initActions(); // Initialize global action buttons (jQuery UI)
|
||||
|
||||
// Ensure `initThisPage()` is called from the individual HTML template's ready handler.
|
||||
// This `script.js` file should be included *before* the template's specific script block.
|
||||
});
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a class="menu_link_edit" href="clearLogs"><i class="fa fa-trash-o"></i> Clear log</a>
|
||||
<a class="menu_link_edit" href="toggleVerbose"><i class="fa fa-pencil"></i> Toggle Debug Log</a>
|
||||
<%-- Changed href to # and added data-action for JS handling --%>
|
||||
<a class="menu_link_action" href="#" data-action="clearLogs" data-success="Logs cleared successfully!"><i class="fa fa-trash-o"></i> Clear log</a>
|
||||
<a class="menu_link_action" href="#" data-action="toggleVerbose" data-success="Log verbosity toggled!"><i class="fa fa-pencil"></i> Toggle Debug Log</a>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
@@ -19,61 +20,81 @@
|
||||
<table class="display" id="log_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="timestamp">Timestamp</th>
|
||||
<th id="level">Level</th>
|
||||
<th id="message">Message</th>
|
||||
<th class="column-timestamp">Timestamp</th>
|
||||
<th class="column-level">Level</th>
|
||||
<th class="column-message">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<div align="center">Refresh rate:
|
||||
<select id="refreshrate" onchange="setRefresh()">
|
||||
<option value="0" selected="selected">No Refresh</option>
|
||||
<option value="5">5 Seconds</option>
|
||||
<option value="15">15 Seconds</option>
|
||||
<option value="30">30 Seconds</option>
|
||||
<option value="60">60 Seconds</option>
|
||||
<option value="300">5 Minutes</option>
|
||||
<option value="600">10 Minutes</option>
|
||||
</select></div>
|
||||
<div class="refresh-controls" align="center">
|
||||
Refresh rate:
|
||||
<select id="refreshrate"> <%-- Removed inline onchange --%>
|
||||
<option value="0" selected="selected">No Refresh</option>
|
||||
<option value="5">5 Seconds</option>
|
||||
<option value="15">15 Seconds</option>
|
||||
<option value="30">30 Seconds</option>
|
||||
<option value="60">60 Seconds</option>
|
||||
<option value="300">5 Minutes</option>
|
||||
<option value="600">10 Minutes</option>
|
||||
</select>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
<style>
|
||||
/* Example CSS for log levels */
|
||||
.gradeX { /* For ERROR */
|
||||
background-color: #fdd; /* Light red */
|
||||
}
|
||||
.gradeW { /* For WARNING */
|
||||
background-color: #ffc; /* Light yellow */
|
||||
}
|
||||
.gradeZ { /* For INFO/DEBUG */
|
||||
/* default background or light gray */
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
initActions();
|
||||
// Encapsulate page-specific logic
|
||||
var LogsPage = LogsPage || {};
|
||||
|
||||
$('#log_table').dataTable( {
|
||||
"bProcessing": true,
|
||||
LogsPage.timer = null; // To hold the interval timer ID
|
||||
|
||||
LogsPage.initDataTable = function() {
|
||||
$('#log_table').dataTable( {
|
||||
"bProcessing": true,
|
||||
"bServerSide": true,
|
||||
"sAjaxSource": 'getLog',
|
||||
"sAjaxSource": 'getLog',
|
||||
"sPaginationType": "full_numbers",
|
||||
"aaSorting": [[0, 'desc']],
|
||||
"aaSorting": [[0, 'desc']], // Sort by timestamp descending
|
||||
"iDisplayLength": 25,
|
||||
"bStateSave": true,
|
||||
"bStateSave": true, // Retain table state across page loads
|
||||
"oLanguage": {
|
||||
"sSearch":"Filter:",
|
||||
"sLengthMenu":"Show _MENU_ lines per page",
|
||||
"sEmptyTable": "No log information available",
|
||||
"sInfo":"Showing _START_ to _END_ of _TOTAL_ lines",
|
||||
"sInfoEmpty":"Showing 0 to 0 of 0 lines",
|
||||
"sInfoFiltered":"(filtered from _MAX_ total lines)"},
|
||||
"sSearch":"Filter:",
|
||||
"sLengthMenu":"Show _MENU_ lines per page",
|
||||
"sEmptyTable": "No log information available",
|
||||
"sInfo":"Showing _START_ to _END_ of _TOTAL_ lines",
|
||||
"sInfoEmpty":"Showing 0 to 0 of 0 lines",
|
||||
"sInfoFiltered":"(filtered from _MAX_ total lines)"
|
||||
},
|
||||
"fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
|
||||
// aData[1] contains the 'Level' from the server
|
||||
if (aData[1] === "ERROR") {
|
||||
$('td', nRow).closest('tr').addClass("gradeX");
|
||||
$(nRow).addClass("gradeX");
|
||||
} else if (aData[1] === "WARNING") {
|
||||
$('td', nRow).closest('tr').addClass("gradeW");
|
||||
$(nRow).addClass("gradeW");
|
||||
} else {
|
||||
$('td', nRow).closest('tr').addClass("gradeZ");
|
||||
$(nRow).addClass("gradeZ");
|
||||
}
|
||||
|
||||
return nRow;
|
||||
},
|
||||
"fnDrawCallback": function (o) {
|
||||
@@ -81,30 +102,76 @@
|
||||
$('html,body').scrollTop(0);
|
||||
},
|
||||
"fnServerData": function ( sSource, aoData, fnCallback ) {
|
||||
/* Add some extra data to the sender */
|
||||
$.getJSON(sSource, aoData, function (json) {
|
||||
fnCallback(json)
|
||||
});
|
||||
}
|
||||
});
|
||||
// Custom function for fetching data, using $.getJSON
|
||||
$.getJSON(sSource, aoData, function (json) {
|
||||
fnCallback(json);
|
||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||
console.error("Error fetching log data:", textStatus, errorThrown);
|
||||
// Optional: Display an error message to the user
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
LogsPage.setRefresh = function() {
|
||||
var refreshrateSelect = document.getElementById('refreshrate');
|
||||
if (refreshrateSelect != null) {
|
||||
// Clear any existing timer
|
||||
if (LogsPage.timer) {
|
||||
clearInterval(LogsPage.timer);
|
||||
}
|
||||
|
||||
var refreshValue = parseInt(refreshrateSelect.value, 10);
|
||||
if (refreshValue !== 0) {
|
||||
// Set a new interval to redraw the DataTable
|
||||
LogsPage.timer = setInterval(function() {
|
||||
$('#log_table').dataTable().fnDraw(false); // 'false' prevents resetting current page
|
||||
}, 1000 * refreshValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LogsPage.initActions = function() {
|
||||
// Event listener for the refresh rate dropdown
|
||||
$('#refreshrate').on('change', LogsPage.setRefresh);
|
||||
|
||||
// Event delegation for header action links
|
||||
$('#subhead_menu').on('click', '.menu_link_action', function(e) {
|
||||
e.preventDefault(); // Prevent default link behavior
|
||||
var $this = $(this);
|
||||
var action = $this.data('action'); // 'clearLogs' or 'toggleVerbose'
|
||||
var successMsg = $this.data('success');
|
||||
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
// Assuming doAjaxCall handles the AJAX request and success/error messages
|
||||
doAjaxCall('/' + action, $this, 'table', successMsg, function() {
|
||||
// Callback after successful AJAX call for specific actions
|
||||
if (action === 'clearLogs') {
|
||||
// Redraw table after clearing logs to show empty state or refreshed data
|
||||
$('#log_table').dataTable().fnDraw();
|
||||
}
|
||||
// No specific redraw needed for toggleVerbose unless it changes displayed logs
|
||||
});
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot perform log action.");
|
||||
// Fallback to direct navigation if AJAX utility is missing
|
||||
// window.location.href = '/' + action;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
$(document).ready(function() {
|
||||
LogsPage.initDataTable();
|
||||
LogsPage.initActions(); // Initialize new event handlers
|
||||
|
||||
// Initial call to set refresh based on default selected option
|
||||
LogsPage.setRefresh();
|
||||
|
||||
// Ensure global initActions from common.js is called if it handles other site-wide actions
|
||||
if (typeof initActions === 'function') {
|
||||
initActions();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
var timer;
|
||||
function setRefresh()
|
||||
{
|
||||
refreshrate = document.getElementById('refreshrate');
|
||||
if(refreshrate != null)
|
||||
{
|
||||
if(timer)
|
||||
{
|
||||
clearInterval(timer);
|
||||
}
|
||||
if(refreshrate.value != 0)
|
||||
{
|
||||
timer = setInterval("$('#log_table').dataTable().fnDraw()",1000*refreshrate.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a class="menu_link_edit" id="manage_albums" href="javascript:void(0)"><i class="fa fa-pencil"></i> Manage Albums</a>
|
||||
<div id="dialog" title="Choose Album Filter" style="display:none" class="configtable">
|
||||
<%-- Retained id="manage_albums" as it's a specific trigger for a dialog --%>
|
||||
<a class="menu_link_edit" id="manage_albums" href="#"><i class="fa fa-pencil"></i> Manage Albums</a>
|
||||
<div id="dialog-manage-albums" title="Choose Album Filter" style="display:none" class="configtable">
|
||||
<div class="links">
|
||||
<%-- Links within dialog changed to use data-status for filtering if AJAX is desired on manageAlbums page --%>
|
||||
<a href="manageAlbums?Status=Downloaded"><i class="fa fa-check fa-fw"></i> Manage Downloaded Albums</a><br>
|
||||
<a href="manageAlbums?Status=Skipped"><i class="fa fa-flag fa-fw"></i> Manage Skipped Albums</a><br>
|
||||
<a href="manageAlbums?Status=Snatched"><i class="fa fa-cloud-download fa-fw"></i> </span>Manage Snatched Albums</a><br>
|
||||
@@ -40,8 +42,9 @@
|
||||
<li><a href="#tabs-4">Force Legacy</a></li>
|
||||
</ul>
|
||||
<div id="tabs-1" class="configtable">
|
||||
<fieldset>
|
||||
<form action="musicScan" method="GET" id="musicScan">
|
||||
<%-- action="musicScan" method="GET" is fine for form submission if full page reload is intended --%>
|
||||
<form action="musicScan" method="GET" id="musicScan">
|
||||
<fieldset>
|
||||
<legend>Scan Music Library</legend>
|
||||
<p><strong>Where do you keep your music?</strong></p>
|
||||
<p>You can put in any directory, and it will scan for audio files in that folder
|
||||
@@ -52,25 +55,22 @@
|
||||
</p>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<label for="">Path to directory</label>
|
||||
%if headphones.CONFIG.MUSIC_DIR:
|
||||
<input type="text" value="${headphones.CONFIG.MUSIC_DIR}" name="path" size="70" />
|
||||
%else:
|
||||
<input type="text" value="Enter a Music Directory to scan" onfocus="if
|
||||
(this.value==this.defaultValue) this.value='';" name="path" size="70" />
|
||||
%endif
|
||||
<label for="music_dir_path">Path to directory</label>
|
||||
<%-- Using HTML5 placeholder attribute and proper ID --%>
|
||||
<input type="text" id="music_dir_path" value="${headphones.CONFIG.MUSIC_DIR or ''}" name="path" size="70" placeholder="Enter a Music Directory to scan" />
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="libraryscan" id="libraryscan" value="1" ${checked(headphones.CONFIG.LIBRARYSCAN)}><label>Automatically scan library</label>
|
||||
<input type="checkbox" name="libraryscan" id="libraryscan" value="1" ${checked(headphones.CONFIG.LIBRARYSCAN)}><label for="libraryscan">Automatically scan library</label>
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="autoadd" id="autoadd" value="1" ${checked(headphones.CONFIG.AUTO_ADD_ARTISTS)}><label>Auto-add new artists</label>
|
||||
<input type="checkbox" name="autoadd" id="autoadd" value="1" ${checked(headphones.CONFIG.AUTO_ADD_ARTISTS)}><label for="autoadd">Auto-add new artists</label>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
<br>
|
||||
<input type="button" value="Save Changes and Scan" onclick="addScanAction();doAjaxCall('musicScan',$(this),'tabs',true);return false;" data-success="Changes saved. Library will be scanned">
|
||||
<input type="button" value="Save Changes without Scanning Library" onclick="doAjaxCall('musicScan',$(this),'tabs',true);return false;" data-success="Changes Saved Successfully">
|
||||
<%-- Buttons use classes and data attributes for AJAX --%>
|
||||
<input type="button" class="ajax-button" data-action="musicScan" data-scan="1" data-success="Changes saved. Library will be scanned" value="Save Changes and Scan">
|
||||
<input type="button" class="ajax-button" data-action="musicScan" data-success="Changes Saved Successfully" value="Save Changes without Scanning Library">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -81,19 +81,15 @@
|
||||
<p>Enter the username whose artists you want to import:</p>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<label for="">Username</label>
|
||||
<%
|
||||
if headphones.CONFIG.LASTFM_USERNAME:
|
||||
lastfmvalue = headphones.CONFIG.LASTFM_USERNAME
|
||||
else:
|
||||
lastfmvalue = ''
|
||||
%>
|
||||
<input type="text" value="${lastfmvalue}" placeholder="Last.fm username" onfocus="if
|
||||
(this.value==this.defaultValue) this.value='';" name="username" id="username" size="18" />
|
||||
<a href="javascript:void(0)" onclick="doAjaxCall('importLastFM?username=',$(this),'tabs');return false;" data-success="Last.fm username has been reset"><i class="fa fa-reply"></i> Reset username</a>
|
||||
<label for="lastfm_username">Username</label>
|
||||
<%-- Using HTML5 placeholder attribute --%>
|
||||
<input type="text" id="lastfm_username" value="${headphones.CONFIG.LASTFM_USERNAME or ''}" placeholder="Last.fm username" name="username" size="18" />
|
||||
<%-- Changed to use class and data attributes for AJAX --%>
|
||||
<a href="#" class="ajax-link" data-action="importLastFM" data-reset="username" data-success="Last.fm username has been reset"><i class="fa fa-reply"></i> Reset username</a>
|
||||
</div>
|
||||
</fieldset>
|
||||
<input type="button" value="Save changes" onclick="doAjaxCall('importLastFM',$(this),'tabs',true);return false;" data-success="Last.fm artists will be imported" data-error="Fill in a last.fm username"/>
|
||||
<%-- Changed to use class and data attributes for AJAX --%>
|
||||
<input type="button" class="ajax-button" data-action="importLastFM" data-success="Last.fm artists will be imported" data-error="Fill in a last.fm username" value="Save changes"/>
|
||||
</form>
|
||||
<br/>
|
||||
<form action="importLastFMTag" method="GET" id="importLastFMTag">
|
||||
@@ -102,16 +98,15 @@
|
||||
<p>Enter tag from which you want import top artists:</p>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<label>Tag</label>
|
||||
<input type="text" value="" onfocus="if
|
||||
(this.value==this.defaultValue) this.value='';" name="tag" id="tag" size="18" />
|
||||
<label for="lastfm_tag">Tag</label>
|
||||
<input type="text" id="lastfm_tag" value="" name="tag" size="18" placeholder="Enter tag"/>
|
||||
<br/>
|
||||
<label>Limit</label>
|
||||
<input type="text" value="50" onfocus="if
|
||||
(this.value==this.defaultValue) this.value='';" name="limit" id="limit" size="18" />
|
||||
<label for="lastfm_limit">Limit</label>
|
||||
<input type="text" id="lastfm_limit" value="50" name="limit" size="18" placeholder="50"/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<input type="submit" />
|
||||
<%-- Standard submit button for this form --%>
|
||||
<input type="submit" value="Import Tag"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -121,40 +116,43 @@
|
||||
<fieldset>
|
||||
<legend>Force Search</legend>
|
||||
<div class="links">
|
||||
<a href="javascript:void(0)" onclick="doAjaxCall('forceSearch',$(this))" data-success="Checking for wanted albums successful" data-error="Error checking wanted albums"><i class="fa fa-search fa-fw"></i> Force Check for Wanted Albums</a>
|
||||
<a href="javascript:void(0)" onclick="doAjaxCall('forceUpdate',$(this))" data-success="Update active artists successful" data-error="Error forcing update artists"><i class="fa fa-heart fa-fw"></i> Force Update Active Artists [Fast]</a>
|
||||
<a href="javascript:void(0)" onclick="doAjaxCall('checkGithub',$(this))" data-success="Checking for update successful" data-error="Error checking for update"><i class="fa fa-refresh fa-fw"></i> Check for Headphones Updates</a>
|
||||
<a href="javascript:void(0)" id="delete_empty_artists"><i class="fa fa-trash-o fa-fw"></i> Delete empty Artists</a>
|
||||
<div id="emptyartistdialog" title="Confirm Artist Deletion" style="display:none" class="configtable">
|
||||
<%-- All links use classes and data attributes for AJAX --%>
|
||||
<a href="#" class="ajax-link" data-action="forceSearch" data-success="Checking for wanted albums successful" data-error="Error checking wanted albums"><i class="fa fa-search fa-fw"></i> Force Check for Wanted Albums</a>
|
||||
<a href="#" class="ajax-link" data-action="forceUpdate" data-success="Update active artists successful" data-error="Error forcing update artists"><i class="fa fa-heart fa-fw"></i> Force Update Active Artists [Fast]</a>
|
||||
<a href="#" class="ajax-link" data-action="checkGithub" data-success="Checking for update successful" data-error="Error checking for update"><i class="fa fa-refresh fa-fw"></i> Check for Headphones Updates</a>
|
||||
|
||||
<a href="#" class="open-dialog-trigger" data-dialog-id="dialog-empty-artists"><i class="fa fa-trash-o fa-fw"></i> Delete empty Artists</a>
|
||||
<div id="dialog-empty-artists" title="Confirm Artist Deletion" style="display:none" class="configtable">
|
||||
%if emptyArtists:
|
||||
<h3>The following artists will be deleted:</h3>
|
||||
|
||||
%for emptyArtist in emptyArtists:
|
||||
<p>${emptyArtist['ArtistName']}</p>
|
||||
%endfor
|
||||
<input type="button" value="Delete Empty Artists" onclick="doAjaxCall('deleteEmptyArtists',$(this))" data-success="Empty Artists deleted" data-error="Error deleting empty artists">
|
||||
<%-- Button uses class and data attributes for AJAX --%>
|
||||
<input type="button" class="ajax-button" data-action="deleteEmptyArtists" data-success="Empty Artists deleted" data-error="Error deleting empty artists" value="Delete Empty Artists">
|
||||
%else:
|
||||
No empty artists found.
|
||||
<p>No empty artists found.</p>
|
||||
%endif
|
||||
</div>
|
||||
<div id="post_process">
|
||||
<a href="javascript:void(0)" class="btnOpenDialog"><i class="fa fa-wrench fa-fw"></i> Force Post-Process Albums in Download Folder</a>
|
||||
<a href="#" class="open-dialog-trigger" data-dialog-id="dialog-post-process"><i class="fa fa-wrench fa-fw"></i> Force Post-Process Albums in Download Folder</a>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<div class="row" id="post_process_alternate">
|
||||
<label>Force Post-Process Albums in Alternate Folder</label>
|
||||
<input type="text" value="" name="dir" id="dir" size="50" />
|
||||
<input type="button" class="btnOpenDialog" value="Submit" />
|
||||
<label for="alt_dir_path">Force Post-Process Albums in Alternate Folder</label>
|
||||
<input type="text" value="" name="dir" id="alt_dir_path" size="50" placeholder="Enter alternate directory" />
|
||||
<input type="button" class="open-dialog-trigger ajax-dialog-submit" data-dialog-id="dialog-post-process" data-input-id="alt_dir_path" data-input-param="dir" value="Submit" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<div class="row" id="post_process_single">
|
||||
<label>Post-Process Single Folder</label>
|
||||
<input type="text" value="" name="album_dir" id="album_dir" size="50" />
|
||||
<input type="button" class="btnOpenDialog" value="Submit" />
|
||||
<label for="album_dir_path">Post-Process Single Folder</label>
|
||||
<input type="text" value="" name="album_dir" id="album_dir_path" size="50" placeholder="Enter album directory" />
|
||||
<input type="button" class="open-dialog-trigger ajax-dialog-submit" data-dialog-id="dialog-post-process" data-input-id="album_dir_path" data-input-param="album_dir" value="Submit" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -166,89 +164,200 @@
|
||||
<legend>Force Legacy</legend>
|
||||
<p>Please note that these functions will take a significant amount of time to complete.</p>
|
||||
<div class="links">
|
||||
<a href="javascript:void(0)" onclick="doAjaxCall('forceFullUpdate',$(this))" data-success="Update active artists successful" data-error="Error forcing update artists"><i class="fa fa-heart fa-fw"></i> Force Update Active Artists [Comprehensive]</a>
|
||||
<%-- All links use classes and data attributes for AJAX --%>
|
||||
<a href="#" class="ajax-link" data-action="forceFullUpdate" data-success="Update active artists successful" data-error="Error forcing update artists"><i class="fa fa-heart fa-fw"></i> Force Update Active Artists [Comprehensive]</a>
|
||||
<BR>
|
||||
<a href="javascript:void(0)" onclick="doAjaxCall('forceScan',$(this))" data-success="Library scan successful" data-error="Error forcing library scan"><i class="fa fa-refresh fa-fw"></i> Force Re-scan Library [Comprehensive]</a>
|
||||
<a href="#" class="ajax-link" data-action="forceScan" data-success="Library scan successful" data-error="Error forcing library scan"><i class="fa fa-refresh fa-fw"></i> Force Re-scan Library [Comprehensive]</a>
|
||||
<BR>
|
||||
<small>*Warning: If you choose [Force Re-scan Library], any manually ignored/matched artists/albums will be reset to "unmatched".</small>
|
||||
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</div>
|
||||
<div id="dialog-confirm"></div>
|
||||
|
||||
<%-- Shared dialog for post-process confirmation --%>
|
||||
<div id="dialog-post-process" title="Keep original folder(s)?" style="display:none">
|
||||
<p>Do you want to keep the original folder(s) after post-processing? If you click no, the folders still might be kept depending on your global settings</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</%def>
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script>
|
||||
function addScanAction() {
|
||||
$('#autoadd').append('<input type="hidden" name="scan" value=1 />');
|
||||
};
|
||||
// Encapsulate page-specific logic
|
||||
var ManagePage = ManagePage || {};
|
||||
|
||||
function fnOpenNormalDialog(id) {
|
||||
if (id === "post_process"){
|
||||
var url = "forcePostProcess?"
|
||||
}
|
||||
if (id === "post_process_alternate"){
|
||||
var dir = $('#dir').val();
|
||||
var url = "forcePostProcess?dir=" + dir + "&"
|
||||
}
|
||||
if (id === "post_process_single"){
|
||||
var dir = $('#album_dir').val();
|
||||
var url = "forcePostProcess?album_dir=" + dir + "&"
|
||||
}
|
||||
var t = $('<a data-success="Post-Processor is being loaded" data-error="Error during Post-Processing">');
|
||||
$("#dialog-confirm").html("Do you want to keep the original folder(s) after post-processing? If you click no, the folders still might be kept depending on your global settings");
|
||||
|
||||
// Define the Dialog and its properties.
|
||||
$("#dialog-confirm").dialog({
|
||||
resizable: false,
|
||||
modal: true,
|
||||
title: "Keep original folder(s)?",
|
||||
height: 170,
|
||||
width: 400,
|
||||
buttons: {
|
||||
"Yes": function () {
|
||||
$(this).dialog('close');
|
||||
doAjaxCall(url + "keep_original_folder=True", t);
|
||||
},
|
||||
"No": function () {
|
||||
$(this).dialog('close');
|
||||
doAjaxCall(url + "keep_original_folder=False", t);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('.btnOpenDialog').click(function(e){
|
||||
e.preventDefault();
|
||||
var parentId = $(this).closest('div').prop('id');
|
||||
fnOpenNormalDialog(parentId);
|
||||
});
|
||||
|
||||
function callback(value) {
|
||||
if (value) {
|
||||
alert("Confirmed");
|
||||
} else {
|
||||
alert("Rejected");
|
||||
}
|
||||
}
|
||||
|
||||
function initThisPage() {
|
||||
$('#manage_albums').click(function() {
|
||||
$('#dialog').dialog();
|
||||
return false;
|
||||
});
|
||||
$('#delete_empty_artists').click(function() {
|
||||
$('#emptyartistdialog').dialog();
|
||||
return false;
|
||||
});
|
||||
ManagePage.init = function() {
|
||||
// Initialize jQuery UI Tabs
|
||||
jQuery( "#tabs" ).tabs();
|
||||
initActions();
|
||||
|
||||
// Initialize jQuery UI Dialogs
|
||||
$('#dialog-manage-albums').dialog({
|
||||
autoOpen: false,
|
||||
modal: true,
|
||||
width: 450,
|
||||
height: 'auto',
|
||||
title: "Choose Album Filter"
|
||||
});
|
||||
|
||||
$('#dialog-empty-artists').dialog({
|
||||
autoOpen: false,
|
||||
modal: true,
|
||||
width: 500,
|
||||
height: 'auto',
|
||||
title: "Confirm Artist Deletion"
|
||||
});
|
||||
|
||||
// Post-process confirmation dialog
|
||||
$('#dialog-post-process').dialog({
|
||||
autoOpen: false,
|
||||
resizable: false,
|
||||
modal: true,
|
||||
height: 170,
|
||||
width: 400,
|
||||
title: "Keep original folder(s)?",
|
||||
buttons: {
|
||||
"Yes": function () {
|
||||
var $dialog = $(this);
|
||||
$dialog.dialog('close');
|
||||
// Retrieve parameters stored on the trigger element
|
||||
var $trigger = $dialog.data('triggerElement');
|
||||
var url = $trigger.data('ajax-url') + "&keep_original_folder=True";
|
||||
var successMsg = $trigger.data('success') || "Post-Processor is being loaded";
|
||||
var errorMsg = $trigger.data('error') || "Error during Post-Processing";
|
||||
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(url, $trigger, 'tabs', successMsg, errorMsg);
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot perform post-process action.");
|
||||
}
|
||||
},
|
||||
"No": function () {
|
||||
var $dialog = $(this);
|
||||
$dialog.dialog('close');
|
||||
// Retrieve parameters stored on the trigger element
|
||||
var $trigger = $dialog.data('triggerElement');
|
||||
var url = $trigger.data('ajax-url') + "&keep_original_folder=False";
|
||||
var successMsg = $trigger.data('success') || "Post-Processor is being loaded";
|
||||
var errorMsg = $trigger.data('error') || "Error during Post-Processing";
|
||||
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(url, $trigger, 'tabs', successMsg, errorMsg);
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot perform post-process action.");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Event delegation for opening dialogs
|
||||
$(document).on('click', '.open-dialog-trigger', function(e) {
|
||||
e.preventDefault();
|
||||
var dialogId = $(this).data('dialog-id');
|
||||
var $dialog = $('#' + dialogId);
|
||||
|
||||
// For post-process dialogs, build the base URL and store it
|
||||
if (dialogId === 'dialog-post-process') {
|
||||
var url = "forcePostProcess?";
|
||||
var inputId = $(this).data('input-id');
|
||||
var inputParam = $(this).data('input-param');
|
||||
if (inputId && inputParam) {
|
||||
var inputValue = $('#' + inputId).val();
|
||||
if (inputValue) {
|
||||
url += inputParam + "=" + encodeURIComponent(inputValue) + "&";
|
||||
}
|
||||
}
|
||||
// Store the base URL and the triggering element on the dialog for use in buttons
|
||||
$dialog.data('ajax-url', url);
|
||||
$dialog.data('triggerElement', $(this));
|
||||
}
|
||||
$dialog.dialog('open');
|
||||
});
|
||||
|
||||
// Event delegation for general AJAX buttons (Save Changes, Delete Empty Artists)
|
||||
$(document).on('click', '.ajax-button', function(e) {
|
||||
e.preventDefault();
|
||||
var $this = $(this);
|
||||
var action = $this.data('action'); // e.g., musicScan, deleteEmptyArtists
|
||||
var successMsg = $this.data('success');
|
||||
var errorMsg = $this.data('error');
|
||||
var formId = $this.closest('form').attr('id');
|
||||
var url = '/' + action;
|
||||
var data = {};
|
||||
|
||||
// For form submissions, serialize the form data
|
||||
if (formId) {
|
||||
var formData = $('#' + formId).serializeArray();
|
||||
$.each(formData, function() {
|
||||
if (data[this.name]) {
|
||||
if (!data[this.name].push) {
|
||||
data[this.name] = [data[this.name]];
|
||||
}
|
||||
data[this.name].push(this.value || '');
|
||||
} else {
|
||||
data[this.name] = this.value || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add specific data attributes from the button if present
|
||||
$.each($this.data(), function(key, value) {
|
||||
// Exclude 'action', 'success', 'error', 'scan' (handled specifically)
|
||||
if (key !== 'action' && key !== 'success' && key !== 'error' && key !== 'scan') {
|
||||
data[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Special handling for 'scan' parameter for musicScan
|
||||
if ($this.data('scan') === 1) {
|
||||
data['scan'] = 1;
|
||||
}
|
||||
|
||||
// Special handling for 'reset' parameter for importLastFM
|
||||
if ($this.data('reset') === 'username') {
|
||||
url += '?username='; // Append empty username to clear it
|
||||
data = {}; // Clear other data if resetting
|
||||
}
|
||||
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
// Assuming doAjaxCall takes URL, trigger, context, successMsg, errorMsg, and data
|
||||
doAjaxCall(url, $this, 'tabs', successMsg, errorMsg, data);
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot perform AJAX button action.");
|
||||
}
|
||||
|
||||
// If it's a delete artists action, close the dialog
|
||||
if (action === 'deleteEmptyArtists') {
|
||||
$('#dialog-empty-artists').dialog('close');
|
||||
// Optionally, refresh the list of empty artists if it's dynamic
|
||||
}
|
||||
});
|
||||
|
||||
// Event delegation for general AJAX links (Force Actions, Force Legacy, Reset Last.fm)
|
||||
$(document).on('click', '.ajax-link', function(e) {
|
||||
e.preventDefault();
|
||||
var $this = $(this);
|
||||
var action = $this.data('action');
|
||||
var successMsg = $this.data('success');
|
||||
var errorMsg = $this.data('error');
|
||||
var url = '/' + action; // Base URL
|
||||
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(url, $this, null, successMsg, errorMsg); // No specific context needed like 'tabs' here unless doAjaxCall uses it.
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot perform AJAX link action.");
|
||||
}
|
||||
});
|
||||
|
||||
// Call initActions if it's a global function that needs to run
|
||||
if (typeof initActions === 'function') {
|
||||
initActions();
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
initThisPage();
|
||||
ManagePage.init();
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<%inherit file="base.html" />
|
||||
<%!
|
||||
from headphones import db
|
||||
import headphones
|
||||
# Removed direct DB imports/interactions here, as data should be pre-fetched server-side.
|
||||
# from headphones import db
|
||||
import headphones # Still needed for headphones.LOSSY_MEDIA_FORMATS if used
|
||||
%>
|
||||
|
||||
<%def name="headerIncludes()">
|
||||
@@ -17,9 +18,10 @@
|
||||
<div id="manageheader" class="title">
|
||||
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Albums</h1>
|
||||
</div>
|
||||
<form action="markAlbums" method="get" id="markAlbums">
|
||||
<form action="markAlbums" method="get" id="markAlbumsForm"> <%-- Renamed ID to avoid conflict with markalbum div --%>
|
||||
<div id="markalbum">Mark selected albums as
|
||||
<select name="action" onChange="doAjaxCall('markAlbums',$(this),'table',true);" data-error="You didn't select any albums">
|
||||
<%-- Replaced inline onChange with a class and data attributes for JS handling --%>
|
||||
<select name="action" id="markAlbumActionSelect">
|
||||
<option disabled="disabled" selected="selected">Choose...</option>
|
||||
<option value="Wanted">Wanted</option>
|
||||
<option value="WantedNew">Wanted (new only)</option>
|
||||
@@ -28,25 +30,27 @@
|
||||
<option value="Ignored">Ignored</option>
|
||||
<option value="Downloaded">Downloaded</option>
|
||||
</select>
|
||||
<input type="hidden" value="Go">
|
||||
<input type="hidden" value="Go"> <%-- This hidden input might be redundant if data is sent via AJAX --%>
|
||||
</div>
|
||||
<table class="display" id="album_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="select"><input type="checkbox" onClick="toggle(this)" /></th>
|
||||
<th id="albumname">Album</th>
|
||||
<th id="artistname">Artist</th>
|
||||
<th id="reldate">Date</th>
|
||||
<th id="type">Type</th>
|
||||
<th id="status">Status</th>
|
||||
<th id="have">Have</th>
|
||||
<th id="bitrate">Bitrate</th>
|
||||
<th id="albumformat">Format</th>
|
||||
<th class="select-all-checkbox"><input type="checkbox" id="toggleAllAlbums" /></th> <%-- Added ID for easier targeting --%>
|
||||
<th class="column-albumname">Album</th>
|
||||
<th class="column-artistname">Artist</th>
|
||||
<th class="column-reldate">Date</th>
|
||||
<th class="column-type">Type</th>
|
||||
<th class="column-status">Status</th>
|
||||
<th class="column-have">Have</th>
|
||||
<th class="column-bitrate">Bitrate</th>
|
||||
<th class="column-albumformat">Format</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
%for album in albums:
|
||||
<%
|
||||
# Assuming these values are now pre-calculated and available in the 'album' dict
|
||||
# Removed all in-template database queries for performance.
|
||||
if album['Status'] == 'Skipped':
|
||||
grade = 'Z'
|
||||
elif album['Status'] == 'Wanted':
|
||||
@@ -58,45 +62,30 @@
|
||||
else:
|
||||
grade = 'A'
|
||||
|
||||
myDB = db.DBConnection()
|
||||
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE AlbumID=?', [album['AlbumID']]))
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE AlbumID=? AND Location IS NOT NULL', [album['AlbumID']])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND AlbumTitle LIKE ? AND Matched = "Failed"', [album['ArtistName'], album['AlbumTitle']]))
|
||||
|
||||
try:
|
||||
percent = (havetracks*100.0)/totaltracks
|
||||
if percent > 100:
|
||||
percent = 100
|
||||
except (ZeroDivisionError, TypeError):
|
||||
percent = 0
|
||||
totaltracks = '?'
|
||||
|
||||
avgbitrate = myDB.action("SELECT AVG(BitRate) FROM tracks WHERE AlbumID=?", [album['AlbumID']]).fetchone()[0]
|
||||
if avgbitrate:
|
||||
bitrate = str(int(avgbitrate)/1000) + ' kbps'
|
||||
else:
|
||||
bitrate = ''
|
||||
|
||||
albumformatcount = myDB.action("SELECT COUNT(DISTINCT Format) FROM tracks WHERE AlbumID=?", [album['AlbumID']]).fetchone()[0]
|
||||
if albumformatcount == 1:
|
||||
albumformat = myDB.action("SELECT DISTINCT Format FROM tracks WHERE AlbumID=?", [album['AlbumID']]).fetchone()[0]
|
||||
elif albumformatcount > 1:
|
||||
albumformat = 'Mixed'
|
||||
else:
|
||||
albumformat = ''
|
||||
|
||||
lossy_formats = [str.upper(fmt) for fmt in headphones.LOSSY_MEDIA_FORMATS]
|
||||
|
||||
# Use the pre-calculated values from the album object
|
||||
totaltracks_display = album.get('TotalTracks', '?')
|
||||
havetracks_display = album.get('HaveTracks', 0)
|
||||
percent_display = album.get('PercentOwned', 0)
|
||||
bitrate_display = album.get('BitrateDisplay', '') # e.g., '192 kbps'
|
||||
albumformat_display = album.get('AlbumFormat', '') # e.g., 'MP3', 'FLAC', 'Mixed'
|
||||
%>
|
||||
<tr class="grade${grade}">
|
||||
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></td>
|
||||
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||
<td id="artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
||||
<td id="reldate">${album['ReleaseDate']}</td>
|
||||
<td id="type">${album['Type']}</td>
|
||||
<td id="status">${album['Status']}</td>
|
||||
<td id="have"><span title="${percent}"><span><div class="progress-container"><div style="width:${percent}%"><div class="havetracks">${havetracks}/${totaltracks}</div></div></div></td>
|
||||
<td id="bitrate">${bitrate}</td>
|
||||
<td id="albumformat">${albumformat}</td>
|
||||
<td class="select-album-checkbox"><input type="checkbox" name="albumid_${album['AlbumID']}" value="${album['AlbumID']}" class="album-checkbox" /></td> <%-- Unique name and class for individual checkboxes --%>
|
||||
<td class="column-albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||
<td class="column-artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
||||
<td class="column-reldate">${album['ReleaseDate']}</td>
|
||||
<td class="column-type">${album['Type']}</td>
|
||||
<td class="column-status">${album['Status']}</td>
|
||||
<td class="column-have">
|
||||
<span title="${percent_display}"></span> <%-- Using percent_display for title attribute --%>
|
||||
<div class="progress-container" role="progressbar" aria-valuenow="${percent_display | int}" aria-valuemin="0" aria-valuemax="100">
|
||||
<div style="width:${percent_display}%">
|
||||
<div class="havetracks">${havetracks_display}/${totaltracks_display}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="column-bitrate">${bitrate_display}</td>
|
||||
<td class="column-albumformat">${albumformat_display}</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</tbody>
|
||||
@@ -106,28 +95,75 @@
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
<style>
|
||||
/* Styles for progress bar, similar to index.html for consistency */
|
||||
.progress-container {
|
||||
background-color: #eee;
|
||||
border-radius: 5px;
|
||||
height: 15px; /* Adjust height as needed */
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%; /* Or a fixed width if preferred */
|
||||
}
|
||||
|
||||
.progress-container > div {
|
||||
background-color: #4CAF50; /* Green color for progress */
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 10px; /* Smaller font for percentage */
|
||||
line-height: 15px; /* Vertically align text */
|
||||
}
|
||||
|
||||
.havetracks {
|
||||
padding: 0 5px; /* Add some padding around the text */
|
||||
}
|
||||
|
||||
/* Styles for album status grades */
|
||||
.gradeZ { /* Skipped */
|
||||
background-color: #f2dede; /* Light red/pink */
|
||||
}
|
||||
.gradeX { /* Wanted */
|
||||
background-color: #d9edf7; /* Light blue */
|
||||
}
|
||||
.gradeI { /* Ignored */
|
||||
background-color: #fcf8e3; /* Light yellow */
|
||||
}
|
||||
.gradeC { /* Snatched */
|
||||
background-color: #dff0d8; /* Light green */
|
||||
}
|
||||
.gradeA { /* Downloaded/Processed (default) */
|
||||
/* No specific background or subtle light grey */
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
<script>
|
||||
function initThisPage() {
|
||||
// Encapsulate page-specific logic
|
||||
var ManageAlbumsPage = ManageAlbumsPage || {};
|
||||
|
||||
ManageAlbumsPage.initDataTable = function() {
|
||||
$('#album_table').dataTable({
|
||||
"bDestroy": true,
|
||||
"aoColumns": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ "sType": "title-numeric"},
|
||||
null,
|
||||
null
|
||||
null, // Checkbox column (not sortable)
|
||||
null, // Album Name
|
||||
null, // Artist Name
|
||||
null, // Date
|
||||
null, // Type
|
||||
null, // Status
|
||||
{ "sType": "title-numeric"}, // Have (uses title for numeric sort)
|
||||
null, // Bitrate
|
||||
null // Format
|
||||
],
|
||||
"aoColumnDefs": [
|
||||
{ 'bSortable': false, 'aTargets': [ 0 ] }
|
||||
{ 'bSortable': false, 'aTargets': [ 0 ] } // Disable sorting for checkbox column
|
||||
],
|
||||
"oLanguage": {
|
||||
"sLengthMenu":"Show _MENU_ albums per page",
|
||||
@@ -136,19 +172,83 @@
|
||||
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
|
||||
"sInfoFiltered":"(filtered from _MAX_ total albums)",
|
||||
"sSearch": ""},
|
||||
"bPaginate": false,
|
||||
"aaSorting": [[5, 'desc']],
|
||||
"bPaginate": false, // All data loaded on one page
|
||||
"aaSorting": [[5, 'desc']], // Default sort by Status descending
|
||||
"fnDrawCallback": function (o) {
|
||||
// Jump to top of page
|
||||
$('html,body').scrollTop(0);
|
||||
}
|
||||
|
||||
});
|
||||
resetFilters("albums");
|
||||
}
|
||||
};
|
||||
|
||||
ManageAlbumsPage.initActions = function() {
|
||||
// Event listener for the "Mark selected albums as" dropdown
|
||||
$('#markAlbumActionSelect').on('change', function() {
|
||||
var $this = $(this);
|
||||
var actionValue = $this.val();
|
||||
var selectedAlbumIds = [];
|
||||
|
||||
// Get all checked album checkboxes
|
||||
$('.album-checkbox:checked').each(function() {
|
||||
selectedAlbumIds.push($(this).val());
|
||||
});
|
||||
|
||||
if (selectedAlbumIds.length === 0) {
|
||||
// Display error if no albums are selected
|
||||
if (typeof showMessage === 'function') { // Assuming showMessage is a global utility
|
||||
showMessage($this.data('error') || "You didn't select any albums", 'error');
|
||||
} else {
|
||||
alert($this.data('error') || "You didn't select any albums");
|
||||
}
|
||||
// Reset select box to default
|
||||
$this.val($('option:first', $this).val());
|
||||
return; // Stop execution
|
||||
}
|
||||
|
||||
// Construct URL with selected album IDs and action
|
||||
var url = 'markAlbums?action=' + encodeURIComponent(actionValue);
|
||||
$.each(selectedAlbumIds, function(index, id) {
|
||||
url += '&albumid=' + encodeURIComponent(id); // Use a consistent param name if backend expects multiple
|
||||
});
|
||||
|
||||
// Perform AJAX call
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(url, $this, 'table', 'Albums marked as ' + actionValue + ' successfully!', 'Error marking albums.');
|
||||
// After successful action, you might want to redraw the table or update rows
|
||||
// $('#album_table').dataTable().fnDraw();
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot mark albums asynchronously.");
|
||||
}
|
||||
// Reset select box to default after action
|
||||
$this.val($('option:first', $this).val());
|
||||
});
|
||||
|
||||
// Event listener for the "toggle all" checkbox
|
||||
$('#toggleAllAlbums').on('click', function() {
|
||||
$('.album-checkbox').prop('checked', this.checked);
|
||||
});
|
||||
|
||||
// Optionally, reset individual checkboxes if header checkbox is unchecked (and vice-versa if needed)
|
||||
$('.album-checkbox').on('click', function() {
|
||||
if (!this.checked) {
|
||||
$('#toggleAllAlbums').prop('checked', false);
|
||||
} else {
|
||||
// If all are checked, check the header checkbox
|
||||
if ($('.album-checkbox:checked').length === $('.album-checkbox').length) {
|
||||
$('#toggleAllAlbums').prop('checked', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
initThisPage();
|
||||
ManageAlbumsPage.initDataTable();
|
||||
ManageAlbumsPage.initActions();
|
||||
|
||||
// Assuming resetFilters is a global function from common.js
|
||||
if (typeof resetFilters === 'function') {
|
||||
resetFilters("albums");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<%inherit file="base.html" />
|
||||
|
||||
|
||||
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
|
||||
@@ -15,9 +13,10 @@
|
||||
<div id="manageheader" class="title">
|
||||
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Artists</h1>
|
||||
</div>
|
||||
<form action="markArtists" method="get" id="markArtists">
|
||||
<div id="markalbum">
|
||||
<select name="action" onChange="doAjaxCall('markArtists',$(this),'table',true);" data-error="You didn't select any artists">
|
||||
<form action="markArtists" method="get" id="markArtistsForm"> <%-- Renamed ID to avoid conflict with markalbum div --%>
|
||||
<div id="markartists_controls"> <%-- More descriptive ID for the controls div --%>
|
||||
<%-- Replaced inline onChange with an ID for JS handling --%>
|
||||
<select name="action" id="markArtistActionSelect">
|
||||
<option disabled="disabled" selected="selected">Choose...</option>
|
||||
<option value="pause">Pause</option>
|
||||
<option value="resume">Resume</option>
|
||||
@@ -25,17 +24,17 @@
|
||||
<option value="delete">Delete</option>
|
||||
</select>
|
||||
selected artists
|
||||
<input type="hidden" value="Go">
|
||||
<input type="hidden" value="Go"> <%-- This hidden input might be redundant if data is sent via AJAX --%>
|
||||
</div>
|
||||
<table class="display" id="artist_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="select"><input type="checkbox" onClick="toggle(this)" /></th>
|
||||
<th id="albumart"></th>
|
||||
<th id="name">Artist Name</th>
|
||||
<th id="status">Status</th>
|
||||
<th id="album">Latest Album</th>
|
||||
<th id="lastupdated">Last Updated</th>
|
||||
<th class="select-all-checkbox"><input type="checkbox" id="toggleAllArtists" /></th> <%-- Added ID for easier targeting --%>
|
||||
<th class="column-albumart"></th> <%-- Changed ID to class for consistency and uniqueness --%>
|
||||
<th class="column-name">Artist Name</th>
|
||||
<th class="column-status">Status</th>
|
||||
<th class="column-album">Latest Album</th>
|
||||
<th class="column-lastupdated">Last Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -48,6 +47,7 @@
|
||||
else:
|
||||
grade = 'Z'
|
||||
|
||||
# Python logic for display formatting is fine here, as it's not doing DB queries.
|
||||
if artist['ReleaseDate'] and artist['LatestAlbum']:
|
||||
releasedate = artist['ReleaseDate']
|
||||
albumdisplay = '<i>%s</i> (%s)' % (artist['LatestAlbum'], artist['ReleaseDate'])
|
||||
@@ -65,12 +65,17 @@
|
||||
|
||||
%>
|
||||
<tr class="grade${grade}">
|
||||
<td id="select"><input type="checkbox" name="${artist['ArtistID']}" class="checkbox" /></td>
|
||||
<td id="albumart"><div id="artistImg"><img class="albumArt" id="${artist['ArtistID']}" src="artwork/thumbs/artist/${artist['ArtistID']}" height="50" width="50"></div></td>
|
||||
<td id="name"><span title="${artist['ArtistSortName']}"></span><a href="artistPage?ArtistID=${artist['ArtistID']}">${artist['ArtistName']}</a></td>
|
||||
<td id="status">${artist['Status']}</td>
|
||||
<td id="album"><span title="${releasedate}"></span><a href="albumPage?AlbumID=${artist['AlbumID']}">${albumdisplay}</a></td>
|
||||
<td id="lastupdated">${lastupdated}</td>
|
||||
<td class="select-artist-checkbox"><input type="checkbox" name="artistid_${artist['ArtistID']}" value="${artist['ArtistID']}" class="artist-checkbox" /></td> <%-- Unique name and class for individual checkboxes --%>
|
||||
<td class="column-albumart">
|
||||
<div class="artistImg-container">
|
||||
<%-- Using data-src for lazy loading with jquery.unveil.min.js and native loading="lazy" --%>
|
||||
<img class="albumArt-thumb" alt="Album art for ${artist['ArtistName']}" data-src="artwork/thumbs/artist/${artist['ArtistID']}" loading="lazy" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="column-name"><span title="${artist['ArtistSortName']}"></span><a href="artistPage?ArtistID=${artist['ArtistID']}">${artist['ArtistName']}</a></td>
|
||||
<td class="column-status">${artist['Status']}</td>
|
||||
<td class="column-album"><span title="${releasedate}"></span><a href="albumPage?AlbumID=${artist['AlbumID']}">${albumdisplay}</a></td>
|
||||
<td class="column-lastupdated">${lastupdated}</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</tbody>
|
||||
@@ -80,37 +85,142 @@
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
<style>
|
||||
/* Basic CSS for album art thumbnail, consistent with index.html */
|
||||
.albumArt-thumb {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
object-fit: cover; /* Ensures image covers the area without distortion */
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Styles for artist status grades */
|
||||
.gradeX { /* Paused */
|
||||
background-color: #f2dede; /* Light red/pink */
|
||||
}
|
||||
.gradeC { /* Loading */
|
||||
background-color: #dff0d8; /* Light green */
|
||||
}
|
||||
.gradeZ { /* Others (e.g., Active) */
|
||||
/* No specific background or subtle light grey */
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
<script src="js/libs/jquery.unveil.min.js"></script> <%-- Added for lazy loading --%>
|
||||
<script>
|
||||
function initThisPage() {
|
||||
// Encapsulate page-specific logic
|
||||
var ManageArtistsPage = ManageArtistsPage || {};
|
||||
|
||||
ManageArtistsPage.initDataTable = function() {
|
||||
$('#artist_table').dataTable({
|
||||
"bDestroy": true,
|
||||
"aoColumns": [
|
||||
null,
|
||||
null,
|
||||
{ "sType": "title-string"},
|
||||
null,
|
||||
{ "sType": "title-string"},
|
||||
null
|
||||
null, // Checkbox column (not sortable)
|
||||
null, // Album art (not sortable by default)
|
||||
{ "sType": "title-string"}, // Artist Name (uses title for sort)
|
||||
null, // Status
|
||||
{ "sType": "title-string"}, // Latest Album (uses title for sort)
|
||||
null // Last Updated
|
||||
],
|
||||
"aoColumnDefs": [
|
||||
{ 'bSortable': false, 'aTargets': [ 0, 1 ] } // Disable sorting for checkbox and album art columns
|
||||
],
|
||||
"oLanguage": {
|
||||
"sSearch" : "",
|
||||
"sEmptyTable": " "
|
||||
},
|
||||
"bStateSave": true,
|
||||
"bPaginate": false
|
||||
"bStateSave": true, // Retain table state across page loads
|
||||
"bPaginate": false, // All data loaded on one page
|
||||
"fnDrawCallback": function (o) {
|
||||
// Re-unveil images after each draw for lazy loading
|
||||
$("img.albumArt-thumb").unveil();
|
||||
}
|
||||
});
|
||||
resetFilters("artists");
|
||||
}
|
||||
};
|
||||
|
||||
ManageArtistsPage.initActions = function() {
|
||||
// Event listener for the "Mark selected artists" dropdown
|
||||
$('#markArtistActionSelect').on('change', function() {
|
||||
var $this = $(this);
|
||||
var actionValue = $this.val();
|
||||
var selectedArtistIds = [];
|
||||
|
||||
// Get all checked artist checkboxes
|
||||
$('.artist-checkbox:checked').each(function() {
|
||||
selectedArtistIds.push($(this).val());
|
||||
});
|
||||
|
||||
if (selectedArtistIds.length === 0) {
|
||||
// Display error if no artists are selected
|
||||
if (typeof showMessage === 'function') { // Assuming showMessage is a global utility
|
||||
showMessage($this.data('error') || "You didn't select any artists", 'error');
|
||||
} else {
|
||||
alert($this.data('error') || "You didn't select any artists");
|
||||
}
|
||||
// Reset select box to default
|
||||
$this.val($('option:first', $this).val());
|
||||
return; // Stop execution
|
||||
}
|
||||
|
||||
// Construct URL with selected artist IDs and action
|
||||
var url = 'markArtists?action=' + encodeURIComponent(actionValue);
|
||||
$.each(selectedArtistIds, function(index, id) {
|
||||
url += '&artistid=' + encodeURIComponent(id); // Use a consistent param name if backend expects multiple
|
||||
});
|
||||
|
||||
// Perform AJAX call
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(url, $this, 'table', 'Artists marked as ' + actionValue + ' successfully!', 'Error marking artists.');
|
||||
// After successful action, you might want to redraw the table or update rows
|
||||
// $('#artist_table').dataTable().fnDraw();
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot mark artists asynchronously.");
|
||||
}
|
||||
// Reset select box to default after action
|
||||
$this.val($('option:first', $this).val());
|
||||
});
|
||||
|
||||
// Event listener for the "toggle all" checkbox
|
||||
$('#toggleAllArtists').on('click', function() {
|
||||
$('.artist-checkbox').prop('checked', this.checked);
|
||||
});
|
||||
|
||||
// Optionally, update "toggle all" checkbox based on individual checkboxes
|
||||
$(document).on('click', '.artist-checkbox', function() {
|
||||
if (!this.checked) {
|
||||
$('#toggleAllArtists').prop('checked', false);
|
||||
} else {
|
||||
// If all are checked, check the header checkbox
|
||||
if ($('.artist-checkbox:checked').length === $('.artist-checkbox').length) {
|
||||
$('#toggleAllArtists').prop('checked', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
initThisPage();
|
||||
ManageArtistsPage.initDataTable();
|
||||
ManageArtistsPage.initActions();
|
||||
|
||||
// Assuming resetFilters is a global function from common.js
|
||||
if (typeof resetFilters === 'function') {
|
||||
resetFilters("artists");
|
||||
}
|
||||
});
|
||||
$(window).load(function(){
|
||||
initFancybox();
|
||||
|
||||
// Call initFancybox if it's a global function that needs to run on window load
|
||||
$(window).on('load', function(){
|
||||
if (typeof initFancybox === 'function') {
|
||||
initFancybox();
|
||||
} else {
|
||||
console.warn("initFancybox function is not defined.");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<%inherit file="base.html" />
|
||||
<%!
|
||||
import headphones
|
||||
from headphones import db, helpers
|
||||
myDB = db.DBConnection()
|
||||
# Removed direct DB imports/interactions here, as data should be pre-fetched server-side.
|
||||
# from headphones import db, helpers
|
||||
# myDB = db.DBConnection() # This should not be in the template
|
||||
%>
|
||||
|
||||
<%def name="headerIncludes()">
|
||||
@@ -20,73 +21,82 @@
|
||||
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Manually Matched Albums</h1>
|
||||
</div>
|
||||
|
||||
<table class="display" id="artist_table">
|
||||
<table class="display" id="manual_album_table"> <%-- Changed ID for clarity --%>
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="artist">Local Artist</th>
|
||||
<th id="album">Local Album</th>
|
||||
<th id="status">Previous Action</th>
|
||||
<th class="column-artist">Local Artist</th> <%-- Changed ID to class --%>
|
||||
<th class="column-album">Local Album</th> <%-- Changed ID to class --%>
|
||||
<th class="column-status">Previous Action</th> <%-- Changed ID to class --%>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% count_albums=0 %>
|
||||
<% count_albums=0 %> <%-- Keeping count_albums for potential unique element IDs if still absolutely necessary, though aiming for generic approach --%>
|
||||
%for album in manualalbums:
|
||||
<tr class="gradeZ">
|
||||
<%
|
||||
# These replacements should ideally be done server-side before passing to template,
|
||||
# or in JS using encodeURIComponent, but kept here for minimal change.
|
||||
old_artist_clean = album['ArtistName'].replace('&','%26').replace('+', '%2B').replace("'","%27")
|
||||
old_album_clean = album['AlbumTitle'].replace('&','%26').replace('+', '%2B').replace("'","%27")
|
||||
%>
|
||||
<td id="artist">${album['ArtistName']}<BR>
|
||||
<button id="reset_artist${count_albums}" onClick="reset_Artist(this.id)">(<-) Reset Artist</button>
|
||||
<div id="reset_artist_dialog${count_albums}" title="Reset Artist" style="display:none">
|
||||
<table>
|
||||
<tr><td>Are you sure you want to reset Local Artist: ${album['ArtistName']} to unmatched?</td></tr>
|
||||
<tr><td align="right"><BR>
|
||||
%if album['AlbumStatus'] == "Ignored":
|
||||
<button href="javascript:void(0)" onclick="doAjaxCall('markManual?action=unignoreArtist&existing_artist=${old_artist_clean}', $(this), 'page');" data-success="Successfully reset ${album['ArtistName']} to unmatched">Reset Artist</button>
|
||||
%elif album['AlbumStatus'] == "Matched":
|
||||
<button href="javascript:void(0)" onclick="doAjaxCall('markManual?action=unmatchArtist&existing_artist=${old_artist_clean}', $(this), 'page');" data-success="Successfully restored ${album['ArtistName']} to unmatched">Reset Artist</button>
|
||||
%endif
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<td class="column-artist">
|
||||
${album['ArtistName']}<BR>
|
||||
<%-- Use a common class and data attributes to pass info to generic dialog --%>
|
||||
<button type="button" class="reset-button"
|
||||
data-reset-type="artist"
|
||||
data-artist-name="${album['ArtistName']}"
|
||||
data-album-status="${album['AlbumStatus']}"
|
||||
data-old-artist-clean="${old_artist_clean}">
|
||||
(<-) Reset Artist
|
||||
</button>
|
||||
</td>
|
||||
<td id="album">${album['AlbumTitle']}<BR>
|
||||
<button id="reset_album${count_albums}" onClick="reset_Album(this.id)">(<-) Reset Album</button>
|
||||
<div id="reset_album_dialog${count_albums}" title="Reset Album" style="display:none">
|
||||
<table>
|
||||
<tr><td>Are you sure you want to reset Local Album: ${album['AlbumTitle']} to unmatched?</td></tr>
|
||||
<tr><td align="right"><BR>
|
||||
%if album['AlbumStatus'] == "Ignored":
|
||||
<button href="javascript:void(0)" onclick="doAjaxCall('markManual?action=unignoreAlbum&existing_artist=${old_artist_clean}&existing_album=${old_album_clean}', $(this), 'page');" data-success="Successfully reset ${album['AlbumTitle']} to unmatched">Reset Album</button>
|
||||
%elif album['AlbumStatus'] == "Matched":
|
||||
<button href="javascript:void(0)" onclick="doAjaxCall('markManual?action=unmatchAlbum&existing_artist=${old_artist_clean}&existing_album=${old_album_clean}', $(this), 'page');" data-success="Successfully reset ${album['AlbumTitle']} to unmatched">Reset Album</button>
|
||||
%endif
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<td class="column-album">
|
||||
${album['AlbumTitle']}<BR>
|
||||
<%-- Use a common class and data attributes to pass info to generic dialog --%>
|
||||
<button type="button" class="reset-button"
|
||||
data-reset-type="album"
|
||||
data-artist-name="${album['ArtistName']}"
|
||||
data-album-title="${album['AlbumTitle']}"
|
||||
data-album-status="${album['AlbumStatus']}"
|
||||
data-old-artist-clean="${old_artist_clean}"
|
||||
data-old-album-clean="${old_album_clean}">
|
||||
(<-) Reset Album
|
||||
</button>
|
||||
</td>
|
||||
<td id="status">${album['AlbumStatus']}
|
||||
|
||||
<td class="column-status">
|
||||
${album['AlbumStatus']}
|
||||
</td>
|
||||
</tr>
|
||||
<% count_albums+=1 %>
|
||||
%endfor
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<%-- Generic Reset Confirmation Dialog (only one on the page) --%>
|
||||
<div id="reset_dialog" title="Reset Confirmation" style="display:none">
|
||||
<p class="dialog-message"></p>
|
||||
<p class="dialog-actions" align="right"><BR>
|
||||
<button type="button" class="confirm-reset-button"></button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#artist_table').dataTable({
|
||||
// Encapsulate page-specific logic
|
||||
var ManageManualPage = ManageManualPage || {};
|
||||
|
||||
ManageManualPage.initDataTable = function() {
|
||||
$('#manual_album_table').dataTable({ <%-- Use the updated ID --%>
|
||||
"bStateSave": true,
|
||||
"bPaginate": true,
|
||||
"oLanguage": {
|
||||
@@ -96,28 +106,98 @@
|
||||
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
|
||||
"sInfoFiltered":"(filtered from _MAX_ total albums)",
|
||||
"sEmptyTable": " ",
|
||||
},
|
||||
},
|
||||
"sPaginationType": "full_numbers",
|
||||
"fnDrawCallback": function (o) {
|
||||
// Jump to top of page
|
||||
$('html,body').scrollTop(0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
initActions();
|
||||
ManageManualPage.initDialogs = function() {
|
||||
// Initialize the generic reset dialog once
|
||||
$('#reset_dialog').dialog({
|
||||
autoOpen: false,
|
||||
modal: true,
|
||||
resizable: false,
|
||||
width: 500,
|
||||
height: 'auto',
|
||||
buttons: {
|
||||
"Cancel": function() {
|
||||
$(this).dialog('close');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ManageManualPage.initActions = function() {
|
||||
// Event delegation for all reset buttons
|
||||
$(document).on('click', '.reset-button', function() {
|
||||
var $button = $(this);
|
||||
var resetType = $button.data('reset-type'); // 'artist' or 'album'
|
||||
var artistName = $button.data('artist-name');
|
||||
var albumTitle = $button.data('album-title');
|
||||
var albumStatus = $button.data('album-status');
|
||||
var oldArtistClean = $button.data('old-artist-clean');
|
||||
var oldAlbumClean = $button.data('old-album-clean');
|
||||
|
||||
var $dialog = $('#reset_dialog');
|
||||
var $dialogMessage = $dialog.find('.dialog-message');
|
||||
var $confirmButton = $dialog.find('.confirm-reset-button');
|
||||
|
||||
var message = "";
|
||||
var actionUrl = "";
|
||||
var successMessage = "";
|
||||
|
||||
if (resetType === 'artist') {
|
||||
message = "Are you sure you want to reset Local Artist: " + artistName + " to unmatched?";
|
||||
if (albumStatus === "Ignored") {
|
||||
actionUrl = 'markManual?action=unignoreArtist&existing_artist=' + oldArtistClean;
|
||||
successMessage = "Successfully reset " + artistName + " to unmatched";
|
||||
} else if (albumStatus === "Matched") {
|
||||
actionUrl = 'markManual?action=unmatchArtist&existing_artist=' + oldArtistClean;
|
||||
successMessage = "Successfully restored " + artistName + " to unmatched";
|
||||
}
|
||||
$confirmButton.text('Reset Artist');
|
||||
} else if (resetType === 'album') {
|
||||
message = "Are you sure you want to reset Local Album: " + albumTitle + " to unmatched?";
|
||||
if (albumStatus === "Ignored") {
|
||||
actionUrl = 'markManual?action=unignoreAlbum&existing_artist=' + oldArtistClean + '&existing_album=' + oldAlbumClean;
|
||||
successMessage = "Successfully reset " + albumTitle + " to unmatched";
|
||||
} else if (albumStatus === "Matched") {
|
||||
actionUrl = 'markManual?action=unmatchAlbum&existing_artist=' + oldArtistClean + '&existing_album=' + oldAlbumClean;
|
||||
successMessage = "Successfully reset " + albumTitle + " to unmatched";
|
||||
}
|
||||
$confirmButton.text('Reset Album');
|
||||
}
|
||||
|
||||
$dialogMessage.text(message);
|
||||
|
||||
// Unbind previous click handler and bind new one
|
||||
$confirmButton.off('click').on('click', function() {
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(actionUrl, $button, 'page', successMessage);
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot perform reset action.");
|
||||
}
|
||||
$dialog.dialog('close');
|
||||
});
|
||||
|
||||
$dialog.dialog('open');
|
||||
});
|
||||
|
||||
// Assuming initActions from common.js is called globally
|
||||
if (typeof initActions === 'function') {
|
||||
initActions();
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
ManageManualPage.initDataTable();
|
||||
ManageManualPage.initDialogs();
|
||||
ManageManualPage.initActions();
|
||||
});
|
||||
|
||||
function reset_Artist(clicked_id) {
|
||||
n=clicked_id.replace("reset_artist","");
|
||||
$("#reset_artist_dialog"+n).dialog();
|
||||
return false;
|
||||
}
|
||||
|
||||
function reset_Album(clicked_id) {
|
||||
n=clicked_id.replace("reset_album","");
|
||||
$("#reset_album_dialog"+n).dialog();
|
||||
return false;
|
||||
}
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a id="menu_link_scan" onclick="doAjaxCall('musicScan?path=${headphones.CONFIG.MUSIC_DIR}&redirect=manageNew',$(this))" data-success="Music library is getting scanned">Scan Music Library</a>
|
||||
<%-- Changed inline onclick to a class and data attributes for JS handling --%>
|
||||
<a id="menu_link_scan" class="ajax-link" data-action="musicScan" data-path="${headphones.CONFIG.MUSIC_DIR}" data-redirect="manageNew" data-success="Music library is getting scanned" href="#">Scan Music Library</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="manage" class="back">« Back to manage overview</a>
|
||||
@@ -18,26 +19,28 @@
|
||||
<div id="manageheader" class="title">
|
||||
<h1 class="clearfix"><i class="fa fa-music"></i> Manage New Artists</h1>
|
||||
</div>
|
||||
<form action="addArtists" method="get">
|
||||
<div id="markalbum">
|
||||
<select name="action">
|
||||
<form action="addArtists" method="get" id="addArtistsForm"> <%-- Added ID to the form --%>
|
||||
<div id="new_artists_controls"> <%-- Unique and descriptive ID --%>
|
||||
<select name="action" id="newArtistActionSelect">
|
||||
<option value="add">(+) ADD Selected Artists</option>
|
||||
<option value="ignore">(-) IGNORE Selected Artists</option>
|
||||
</select>
|
||||
<input type="submit" value="Go">
|
||||
<%-- Changed input type="submit" to button with class for AJAX handling --%>
|
||||
<button type="button" id="submitNewArtistAction" class="ajax-submit-button" data-form-id="addArtistsForm" data-action="addArtists" data-error="You didn't select any artists">Go</button>
|
||||
</div>
|
||||
<table class="display" id="artist_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="select"><input type="checkbox" onClick="toggle(this)" /></th>
|
||||
<th id="name">Artist Name</th>
|
||||
<th class="select-all-checkbox"><input type="checkbox" id="toggleAllNewArtists" /></th> <%-- Added ID for easier targeting --%>
|
||||
<th class="column-name">Artist Name</th> <%-- Changed ID to class --%>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
%for artist in newartists:
|
||||
<tr class="gradeZ">
|
||||
<td id="select"><input type="checkbox" name="${artist['ArtistName']}" class="checkbox" /></td>
|
||||
<td id="name">${artist['ArtistName']}</a></td>
|
||||
<%-- Changed name attribute to a consistent 'artist_ids' and value to ArtistID for robust processing --%>
|
||||
<td class="select-artist-checkbox"><input type="checkbox" name="artist_ids" value="${artist['ArtistID']}" class="new-artist-checkbox" /></td>
|
||||
<td class="column-name">${artist['ArtistName']}</a></td>
|
||||
</tr>
|
||||
%endfor
|
||||
</tbody>
|
||||
@@ -47,22 +50,111 @@
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Encapsulate page-specific logic
|
||||
var ManageNewArtistsPage = ManageNewArtistsPage || {};
|
||||
|
||||
ManageNewArtistsPage.initDataTable = function() {
|
||||
$('#artist_table').dataTable({
|
||||
"aaSorting": [[1, 'asc']],
|
||||
"aaSorting": [[1, 'asc']], // Sort by Artist Name ascending
|
||||
"bStateSave": false,
|
||||
"bPaginate": false,
|
||||
"oLanguage": {
|
||||
"sSearch" : ""},
|
||||
"sSearch" : "",
|
||||
"sEmptyTable": "No new artist information available"
|
||||
},
|
||||
"fnDrawCallback": function (o) {
|
||||
$('html,body').scrollTop(0); // Jump to top of page on draw
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
initActions();
|
||||
ManageNewArtistsPage.initActions = function() {
|
||||
// Event listener for the "Scan Music Library" link in the header
|
||||
$(document).on('click', '#menu_link_scan', function(e) {
|
||||
e.preventDefault();
|
||||
var $this = $(this);
|
||||
var path = $this.data('path');
|
||||
var redirect = $this.data('redirect');
|
||||
var successMsg = $this.data('success');
|
||||
var url = $this.data('action') + '?path=' + encodeURIComponent(path) + '&redirect=' + encodeURIComponent(redirect);
|
||||
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(url, $this, null, successMsg); // 'null' for context if not affecting a specific tab/table
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot scan music library.");
|
||||
}
|
||||
});
|
||||
|
||||
// Event listener for the "Go" button to add/ignore artists
|
||||
$('#submitNewArtistAction').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var $this = $(this);
|
||||
var action = $('#newArtistActionSelect').val();
|
||||
var selectedArtistIds = [];
|
||||
|
||||
// Get all checked new artist checkboxes
|
||||
$('.new-artist-checkbox:checked').each(function() {
|
||||
selectedArtistIds.push($(this).val());
|
||||
});
|
||||
|
||||
if (selectedArtistIds.length === 0) {
|
||||
if (typeof showMessage === 'function') {
|
||||
showMessage($this.data('error') || "You didn't select any artists", 'error');
|
||||
} else {
|
||||
alert($this.data('error') || "You didn't select any artists");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var url = $this.data('action') + '?action=' + encodeURIComponent(action);
|
||||
$.each(selectedArtistIds, function(index, id) {
|
||||
url += '&artist_ids=' + encodeURIComponent(id); // Append selected artist IDs
|
||||
});
|
||||
|
||||
var successMsg = 'Artists successfully ' + (action === 'add' ? 'added' : 'ignored');
|
||||
var errorMsg = 'Error performing action on artists.';
|
||||
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(url, $this, 'table', successMsg, errorMsg);
|
||||
// Optionally, refresh the table or remove processed rows after successful AJAX call
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot add/ignore artists.");
|
||||
}
|
||||
});
|
||||
|
||||
// Event listener for the "toggle all" checkbox
|
||||
$('#toggleAllNewArtists').on('click', function() {
|
||||
$('.new-artist-checkbox').prop('checked', this.checked);
|
||||
});
|
||||
|
||||
// Update "toggle all" checkbox based on individual checkboxes
|
||||
$(document).on('click', '.new-artist-checkbox', function() {
|
||||
if (!this.checked) {
|
||||
$('#toggleAllNewArtists').prop('checked', false);
|
||||
} else {
|
||||
if ($('.new-artist-checkbox:checked').length === $('.new-artist-checkbox').length) {
|
||||
$('#toggleAllNewArtists').prop('checked', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Assuming initActions from common.js is called globally
|
||||
if (typeof initActions === 'function') {
|
||||
initActions();
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
ManageNewArtistsPage.initDataTable();
|
||||
ManageNewArtistsPage.initActions();
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<%inherit file="base.html" />
|
||||
<%!
|
||||
import headphones
|
||||
import json
|
||||
from headphones import db, helpers
|
||||
myDB = db.DBConnection()
|
||||
artist_json = {}
|
||||
counter = 0
|
||||
artist_list = myDB.action("SELECT ArtistName from artists ORDER BY ArtistName COLLATE NOCASE")
|
||||
for artist in artist_list:
|
||||
artist_json[counter] = artist['ArtistName']
|
||||
counter+=1
|
||||
json_artists = json.dumps(artist_json)
|
||||
# Removed direct DB imports and queries from template.
|
||||
# These operations (fetching artists and creating json_artists)
|
||||
# should be performed in the Python view/controller and passed
|
||||
# to the template as part of its context.
|
||||
# import json
|
||||
# from headphones import db, helpers
|
||||
# myDB = db.DBConnection()
|
||||
# artist_json = {}
|
||||
# counter = 0
|
||||
# artist_list = myDB.action("SELECT ArtistName from artists ORDER BY ArtistName COLLATE NOCASE")
|
||||
# for artist in artist_list:
|
||||
# artist_json[counter] = artist['ArtistName']
|
||||
# counter+=1
|
||||
# json_artists = json.dumps(artist_json)
|
||||
%>
|
||||
|
||||
<%def name="headerIncludes()">
|
||||
@@ -29,97 +33,109 @@
|
||||
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Unmatched Albums</h1>
|
||||
</div>
|
||||
|
||||
<table class="display" id="artist_table">
|
||||
<table class="display" id="unmatched_album_table"> <%-- Changed ID for clarity --%>
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="artist">Local Artist</th>
|
||||
<th id="album">Local Album</th>
|
||||
<th class="column-artist">Local Artist</th>
|
||||
<th class="column-album">Local Album</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% count_albums=0 %>
|
||||
<% count_albums=0 %> <%-- Still useful for unique IDs if needed, but aiming for generic dialogs --%>
|
||||
%for album in unmatchedalbums:
|
||||
<tr class="gradeZ">
|
||||
<%
|
||||
old_artist_clean = album['ArtistName'].replace('&','%26').replace("'","%27")
|
||||
old_album_clean = album['AlbumTitle'].replace('&','%26').replace("'","%27")
|
||||
old_artist_js = album['ArtistName'].replace("'","\\'").replace('"','\\"')
|
||||
old_album_js = album['AlbumTitle'].replace("'","\\'").replace('"','\\"')
|
||||
# Pre-escape for direct use in data-attributes where JS string is needed.
|
||||
# URL encoding will happen in JS with encodeURIComponent.
|
||||
old_artist_js_str = album['ArtistName'].replace("'","\\'").replace('"','"')
|
||||
old_album_js_str = album['AlbumTitle'].replace("'","\\'").replace('"','"')
|
||||
%>
|
||||
<td id="artist">${album['ArtistName']}<BR>
|
||||
<button id="ignore_artists${count_albums}" onClick="ignore_Artist(this.id)">(-) Ignore Artist</button>
|
||||
<div id="ignore_artist_dialog${count_albums}" title="Ignore Artist" style="display:none">
|
||||
<table>
|
||||
<tr><td>Are you sure you want to ignore Local Artist: ${album['ArtistName']} from future matching?</td></tr>
|
||||
<tr><td align="right"><BR>
|
||||
<button href="javascript:void(0)" onclick="doAjaxCall('markUnmatched?action=ignoreArtist&existing_artist=${old_artist_clean}', $(this), 'page');" data-success="Successfully ignored ${album['ArtistName']} from future matching">Ignore Artist</button>
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button id="match_artists${count_albums}" onClick="load_Artist(this.id)">(->) Match Artist</button>
|
||||
<div id="artist_dialog${count_albums}" title="Match Artist" style="display:none">
|
||||
<table width=400>
|
||||
<tr><td>Local Artist:</td><td>${album['ArtistName']}</td></tr>
|
||||
<tr><td>Match Artist</td><td>
|
||||
<select id="artist_options${count_albums}" name="new_artist">
|
||||
</select>
|
||||
</td></tr>
|
||||
<tr><td></td><td align="right"><BR>
|
||||
<button href="javascript:void(0)" onclick="artist_matcher(${count_albums}, '${old_artist_js}')">Match Artist</button>
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<td class="column-artist">
|
||||
${album['ArtistName']}<BR>
|
||||
<%-- Data attributes to pass context to JS --%>
|
||||
<button type="button" class="action-button ignore-artist-button"
|
||||
data-artist-name="${album['ArtistName']}"
|
||||
data-old-artist-js="${old_artist_js_str}">
|
||||
(-) Ignore Artist
|
||||
</button>
|
||||
|
||||
<button type="button" class="action-button match-artist-button"
|
||||
data-artist-name="${album['ArtistName']}"
|
||||
data-old-artist-js="${old_artist_js_str}">
|
||||
(->) Match Artist
|
||||
</button>
|
||||
</td>
|
||||
<td id="album">${album['AlbumTitle']}<BR>
|
||||
<button id="ignore_albums${count_albums}" onClick="ignore_Album(this.id)">(-) Ignore Album</button>
|
||||
<div id="ignore_album_dialog${count_albums}" title="Ignore Album" style="display:none">
|
||||
<table>
|
||||
<tr><td>Are you sure you want to ignore Local Album: ${album['AlbumTitle']} from future matching?</td></tr>
|
||||
<tr><td align="right"><BR>
|
||||
<button href="javascript:void(0)" onclick="doAjaxCall('markUnmatched?action=ignoreAlbum&existing_artist=${old_artist_clean}&existing_album=${old_album_clean}', $(this), 'page');" data-success="Successfully ignored ${album['AlbumTitle']} from future matching">Ignore Album</button>
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button id="match_albums${count_albums}" onClick="load_AlbumArtist(this.id)">(->) Match Album</button>
|
||||
<div id="album_dialog${count_albums}" title="Match Album" style="display:none">
|
||||
<table width=650>
|
||||
<tr><td>Local Artist:</td><td>${album['ArtistName']}</td></tr>
|
||||
<tr><td>Local Album:</td><td>${album['AlbumTitle']}</td></tr>
|
||||
<tr><td>Match Artist</td><td>
|
||||
<select id="album_artist_options${count_albums}" name="new_artist">
|
||||
</select>
|
||||
</td></tr>
|
||||
<tr><td>Match Album</td><td>
|
||||
<select id="album_options${count_albums}" name="new_album">
|
||||
</select>
|
||||
</td></tr>
|
||||
<tr><td></td><td align="right"><BR>
|
||||
<button href="javascript:void(0)" onclick="album_matcher(${count_albums}, '${old_artist_js}', '${old_album_js}')">Match Album</button>
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<td class="column-album">
|
||||
${album['AlbumTitle']}<BR>
|
||||
<button type="button" class="action-button ignore-album-button"
|
||||
data-artist-name="${album['ArtistName']}"
|
||||
data-album-title="${album['AlbumTitle']}"
|
||||
data-old-artist-js="${old_artist_js_str}"
|
||||
data-old-album-js="${old_album_js_str}">
|
||||
(-) Ignore Album
|
||||
</button>
|
||||
|
||||
<button type="button" class="action-button match-album-button"
|
||||
data-artist-name="${album['ArtistName']}"
|
||||
data-album-title="${album['AlbumTitle']}"
|
||||
data-old-artist-js="${old_artist_js_str}"
|
||||
data-old-album-js="${old_album_js_str}">
|
||||
(->) Match Album
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<% count_albums+=1 %>
|
||||
%endfor
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<%-- Generic Ignore Confirmation Dialog --%>
|
||||
<div id="ignore_dialog" title="Confirm Ignore" style="display:none">
|
||||
<p class="dialog-message"></p>
|
||||
<p class="dialog-actions" align="right"><BR>
|
||||
<button type="button" class="confirm-ignore-button"></button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%-- Generic Match Dialog --%>
|
||||
<div id="match_dialog" title="Match Album" style="display:none">
|
||||
<p><strong>Local Artist:</strong> <span id="local-artist-display"></span></p>
|
||||
<p><strong>Local Album:</strong> <span id="local-album-display"></span></p>
|
||||
|
||||
<div id="artist-match-section">
|
||||
<p>Match Artist:</p>
|
||||
<select id="match-artist-options" name="new_artist"></select>
|
||||
</div>
|
||||
|
||||
<div id="album-match-section">
|
||||
<p>Match Album:</p>
|
||||
<select id="match-album-options" name="new_album"></select>
|
||||
</div>
|
||||
|
||||
<p class="dialog-actions" align="right"><BR>
|
||||
<button type="button" class="confirm-match-button"></button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#artist_table').dataTable({
|
||||
// Encapsulate page-specific logic
|
||||
var ManageUnmatchedPage = ManageUnmatchedPage || {};
|
||||
|
||||
ManageUnmatchedPage.jsonArtists = ${json_artists | n, unicode}; // Assuming json_artists is passed from backend
|
||||
|
||||
ManageUnmatchedPage.initDataTable = function() {
|
||||
$('#unmatched_album_table').dataTable({ <%-- Use the updated ID --%>
|
||||
"bStateSave": true,
|
||||
"bPaginate": true,
|
||||
"oLanguage": {
|
||||
@@ -129,133 +145,203 @@
|
||||
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
|
||||
"sInfoFiltered":"(filtered from _MAX_ total albums)",
|
||||
"sEmptyTable": " ",
|
||||
},
|
||||
},
|
||||
"sPaginationType": "full_numbers",
|
||||
"fnDrawCallback": function (o) {
|
||||
// Jump to top of page
|
||||
$('html,body').scrollTop(0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
initActions();
|
||||
ManageUnmatchedPage.initDialogs = function() {
|
||||
// Initialize the generic ignore dialog
|
||||
$('#ignore_dialog').dialog({
|
||||
autoOpen: false,
|
||||
modal: true,
|
||||
resizable: false,
|
||||
width: 500,
|
||||
height: 'auto',
|
||||
buttons: {
|
||||
"Cancel": function() {
|
||||
$(this).dialog('close');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize the generic match dialog
|
||||
$('#match_dialog').dialog({
|
||||
autoOpen: false,
|
||||
modal: true,
|
||||
resizable: false,
|
||||
width: 700,
|
||||
height: 'auto',
|
||||
buttons: {
|
||||
"Cancel": function() {
|
||||
$(this).dialog('close');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ManageUnmatchedPage.initActions = function() {
|
||||
// --- Ignore Artist/Album Logic ---
|
||||
$(document).on('click', '.ignore-artist-button, .ignore-album-button', function() {
|
||||
var $button = $(this);
|
||||
var isArtistIgnore = $button.hasClass('ignore-artist-button');
|
||||
var artistName = $button.data('artist-name');
|
||||
var albumTitle = $button.data('album-title');
|
||||
var oldArtistJs = $button.data('old-artist-js'); // JS escaped, needs URI encoding for URL
|
||||
var oldAlbumJs = $button.data('old-album-js'); // JS escaped, needs URI encoding for URL
|
||||
|
||||
var $dialog = $('#ignore_dialog');
|
||||
var $dialogMessage = $dialog.find('.dialog-message');
|
||||
var $confirmButton = $dialog.find('.confirm-ignore-button');
|
||||
|
||||
var message = "";
|
||||
var actionUrl = "";
|
||||
var successMessage = "";
|
||||
|
||||
if (isArtistIgnore) {
|
||||
message = "Are you sure you want to ignore Local Artist: " + artistName + " from future matching?";
|
||||
actionUrl = 'markUnmatched?action=ignoreArtist&existing_artist=' + encodeURIComponent(oldArtistJs);
|
||||
successMessage = "Successfully ignored " + artistName + " from future matching";
|
||||
$confirmButton.text('Ignore Artist');
|
||||
} else { // ignore-album-button
|
||||
message = "Are you sure you want to ignore Local Album: " + albumTitle + " from future matching?";
|
||||
actionUrl = 'markUnmatched?action=ignoreAlbum&existing_artist=' + encodeURIComponent(oldArtistJs) + '&existing_album=' + encodeURIComponent(oldAlbumJs);
|
||||
successMessage = "Successfully ignored " + albumTitle + " from future matching";
|
||||
$confirmButton.text('Ignore Album');
|
||||
}
|
||||
|
||||
$dialogMessage.text(message);
|
||||
|
||||
$confirmButton.off('click').on('click', function() {
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(actionUrl, $button, 'page', successMessage);
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot perform ignore action.");
|
||||
}
|
||||
$dialog.dialog('close');
|
||||
});
|
||||
|
||||
$dialog.dialog('open');
|
||||
});
|
||||
|
||||
// --- Match Artist/Album Logic ---
|
||||
$(document).on('click', '.match-artist-button, .match-album-button', function() {
|
||||
var $button = $(this);
|
||||
var isArtistMatch = $button.hasClass('match-artist-button');
|
||||
var artistName = $button.data('artist-name');
|
||||
var albumTitle = $button.data('album-title');
|
||||
var oldArtistJs = $button.data('old-artist-js');
|
||||
var oldAlbumJs = $button.data('old-album-js');
|
||||
|
||||
var $dialog = $('#match_dialog');
|
||||
var $localArtistDisplay = $('#local-artist-display');
|
||||
var $localAlbumDisplay = $('#local-album-display');
|
||||
var $artistMatchSection = $('#artist-match-section');
|
||||
var $albumMatchSection = $('#album-match-section');
|
||||
var $matchArtistOptions = $('#match-artist-options');
|
||||
var $matchAlbumOptions = $('#match-album-options');
|
||||
var $confirmButton = $dialog.find('.confirm-match-button');
|
||||
|
||||
// Reset dialog state
|
||||
$localArtistDisplay.text(artistName);
|
||||
$matchArtistOptions.empty();
|
||||
$matchAlbumOptions.empty();
|
||||
$confirmButton.off('click'); // Clear previous click handlers
|
||||
|
||||
// Populate artist options
|
||||
$.each(ManageUnmatchedPage.jsonArtists, function(key, value) {
|
||||
$matchArtistOptions.append($("<option/>", {
|
||||
value: value,
|
||||
text: value
|
||||
}));
|
||||
});
|
||||
|
||||
if (isArtistMatch) {
|
||||
$localAlbumDisplay.closest('p').hide(); // Hide local album row
|
||||
$albumMatchSection.hide(); // Hide match album section
|
||||
$localArtistDisplay.text(artistName);
|
||||
$artistMatchSection.show(); // Show match artist section
|
||||
$confirmButton.text('Match Artist');
|
||||
|
||||
$confirmButton.on('click', function() {
|
||||
var newArtist = $matchArtistOptions.val();
|
||||
var actionUrl = 'markUnmatched?action=matchArtist&existing_artist=' + encodeURIComponent(oldArtistJs) + '&new_artist=' + encodeURIComponent(newArtist);
|
||||
var successMessage = 'Successfully matched ' + artistName + ' with ' + newArtist;
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(actionUrl, $button, 'page', successMessage);
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot match artist.");
|
||||
}
|
||||
$dialog.dialog('close');
|
||||
});
|
||||
|
||||
} else { // match-album-button
|
||||
$localAlbumDisplay.closest('p').show(); // Show local album row
|
||||
$albumMatchSection.show(); // Show match album section
|
||||
$localAlbumDisplay.text(albumTitle);
|
||||
$artistMatchSection.show(); // Show match artist section (for selecting matched album's artist)
|
||||
$confirmButton.text('Match Album');
|
||||
|
||||
// Load albums for selected artist initially
|
||||
ManageUnmatchedPage.loadAlbumsForArtist($matchArtistOptions.val(), $matchAlbumOptions);
|
||||
|
||||
// Handle artist selection change
|
||||
$matchArtistOptions.off('change').on('change', function() {
|
||||
ManageUnmatchedPage.loadAlbumsForArtist($(this).val(), $matchAlbumOptions);
|
||||
});
|
||||
|
||||
$confirmButton.on('click', function() {
|
||||
var newArtist = $matchArtistOptions.val();
|
||||
var newAlbum = $matchAlbumOptions.val();
|
||||
var actionUrl = 'markUnmatched?action=matchAlbum&existing_artist=' + encodeURIComponent(oldArtistJs) + '&new_artist=' + encodeURIComponent(newArtist) + '&existing_album=' + encodeURIComponent(oldAlbumJs) + '&new_album=' + encodeURIComponent(newAlbum);
|
||||
var successMessage = 'Successfully matched ' + albumTitle + ' with ' + newAlbum + ' by ' + newArtist;
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(actionUrl, $button, 'page', successMessage);
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot match album.");
|
||||
}
|
||||
$dialog.dialog('close');
|
||||
});
|
||||
}
|
||||
$dialog.dialog('open');
|
||||
});
|
||||
|
||||
// Helper function to load albums for a given artist
|
||||
ManageUnmatchedPage.loadAlbumsForArtist = function(artistName, $targetSelect) {
|
||||
$targetSelect.empty().append($("<option/>", { value: "", text: "Loading albums..." })); // Add loading message
|
||||
var cleanArtistName = encodeURIComponent(artistName);
|
||||
$.getJSON("getAlbumsByArtist_json?artist=" + cleanArtistName, function(data) {
|
||||
$targetSelect.empty();
|
||||
if (Object.keys(data).length > 0) {
|
||||
$.each(data, function(key, value) {
|
||||
$targetSelect.append($("<option/>", {
|
||||
value: value,
|
||||
text: value
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
$targetSelect.append($("<option/>", { value: "", text: "No albums found" }));
|
||||
}
|
||||
}).fail(function() {
|
||||
$targetSelect.empty().append($("<option/>", { value: "", text: "Error loading albums" }));
|
||||
});
|
||||
};
|
||||
|
||||
// Assuming initActions from common.js is called globally
|
||||
if (typeof initActions === 'function') {
|
||||
initActions();
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
ManageUnmatchedPage.initDataTable();
|
||||
ManageUnmatchedPage.initDialogs();
|
||||
ManageUnmatchedPage.initActions();
|
||||
});
|
||||
|
||||
function ignore_Artist(clicked_id) {
|
||||
n=clicked_id.replace("ignore_artists","");
|
||||
$("#ignore_artist_dialog"+n).dialog();
|
||||
return false;
|
||||
}
|
||||
|
||||
function ignore_Album(clicked_id) {
|
||||
n=clicked_id.replace("ignore_albums","");
|
||||
$("#ignore_album_dialog"+n).dialog();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function load_Artist(clicked_id) {
|
||||
n=clicked_id.replace("match_artists","");
|
||||
var d = $("#artist_dialog"+n).dialog();
|
||||
d.dialog("option", "width", 450);
|
||||
d.dialog("option", "position", "center");
|
||||
$('#artist_options'+n).html('');
|
||||
$.each(${json_artists}, function(key, value) {
|
||||
$('#artist_options'+n).append($("<option/>", {
|
||||
value: value,
|
||||
text: value
|
||||
}));
|
||||
});
|
||||
change_artist(n)
|
||||
return false;
|
||||
}
|
||||
|
||||
function change_artist(n) {
|
||||
selected_artist = $("#artist_options"+n).find("option:selected").text();
|
||||
selected_artist_clean = selected_artist.replace('&', '%26').replace('+', '%2B');
|
||||
$("#artist_options"+n).change(function() {
|
||||
selected_artist = $("#artist_options"+n).find("option:selected").text();
|
||||
selected_artist_clean = selected_artist.replace('&', '%26').replace('+', '%2B');
|
||||
});
|
||||
}
|
||||
|
||||
function artist_matcher(n, existing_artist) {
|
||||
var existing_artist = existing_artist.toString();
|
||||
var existing_artist_clean = existing_artist.replace('&', '%26').replace('+', '%2B');
|
||||
$('#match_artists'+n).attr('data-success', 'Successfully matched '+existing_artist+' with '+selected_artist);
|
||||
doAjaxCall('markUnmatched?action=matchArtist&existing_artist='+existing_artist_clean+'&new_artist='+selected_artist_clean, $('#match_artists'+n), 'page');
|
||||
}
|
||||
|
||||
|
||||
function load_AlbumArtist(clicked_id) {
|
||||
n=clicked_id.replace("match_albums","");
|
||||
var d = $("#album_dialog"+n).dialog();
|
||||
d.dialog("option", "width", 700);
|
||||
d.dialog("option", "position", "center");
|
||||
$('#album_artist_options'+n).html('');
|
||||
$.each(${json_artists}, function(key, value) {
|
||||
$('#album_artist_options'+n).append($("<option/>", {
|
||||
value: value,
|
||||
text: value
|
||||
}));
|
||||
});
|
||||
load_json_albums(n)
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function load_json_albums(n) {
|
||||
selected_album_artist = $("#album_artist_options"+n).find("option:selected").text();
|
||||
selected_album_artist_clean = selected_album_artist.replace('&', '%26').replace('+', '%2B');
|
||||
$('#album_options'+n).html('')
|
||||
$.getJSON("getAlbumsByArtist_json?artist="+selected_album_artist_clean, function( data ) {
|
||||
$.each( data, function( key, value ) {
|
||||
$('#album_options'+n).append($("<option/>", {
|
||||
value: value,
|
||||
text: value
|
||||
}));
|
||||
});
|
||||
selected_album= $("#album_options"+n).find("option:selected").text();
|
||||
selected_album_clean = selected_album.replace('&', '%26').replace('+', '%2B');
|
||||
});
|
||||
change_json_albums(n)
|
||||
change_album(n)
|
||||
}
|
||||
|
||||
|
||||
function change_json_albums(n) {
|
||||
$("#album_artist_options"+n).change(function(){
|
||||
selected_album_artist = $("#album_artist_options"+n).find("option:selected").text();
|
||||
selected_album_artist_clean = selected_album_artist.replace('&', '%26').replace('+', '%2B');
|
||||
$('#album_options'+n).html('')
|
||||
$.getJSON("getAlbumsByArtist_json?artist="+selected_album_artist_clean, function( data ) {
|
||||
$.each( data, function( key, value ) {
|
||||
$('#album_options'+n).append($("<option/>", {
|
||||
value: value,
|
||||
text: value
|
||||
}));
|
||||
});
|
||||
selected_album= $("#album_options"+n).find("option:selected").text();
|
||||
selected_album_clean = selected_album.replace('&', '%26').replace('+', '%2B');
|
||||
});
|
||||
change_album(n)
|
||||
});
|
||||
}
|
||||
|
||||
function change_album(n) {
|
||||
$("#album_options"+n).change(function() {
|
||||
selected_album= $("#album_options"+n).find("option:selected").text();
|
||||
selected_album_clean = selected_album.replace('&', '%26').replace('+', '%2B');
|
||||
});
|
||||
}
|
||||
|
||||
function album_matcher(n, existing_artist, existing_album) {
|
||||
var existing_artist = existing_artist.toString();
|
||||
var existing_artist_clean = existing_artist.replace('&', '%26').replace('+', '%2B');
|
||||
var existing_album = existing_album.toString();
|
||||
var existing_album_clean = existing_album.replace('&', '%26').replace('+', '%2B');
|
||||
$('#match_albums'+n).attr('data-success', 'Successfully matched '+existing_album+' with '+selected_album);
|
||||
doAjaxCall('markUnmatched?action=matchAlbum&existing_artist='+existing_artist_clean+'&new_artist='+selected_album_artist_clean+'&existing_album='+existing_album_clean+'&new_album='+selected_album_clean, $('#match_albums'+n), 'page');
|
||||
}
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
@@ -8,24 +8,24 @@
|
||||
<table class="display" id="searchresults_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="albumart"></th>
|
||||
<th class="column-albumart"></th> <%-- Changed ID to class --%>
|
||||
%if type == 'album':
|
||||
<th id="albumname">Album Name</th>
|
||||
<th id="artistnamesmall">Artist Name</th>
|
||||
<th id="format">Format</th>
|
||||
<th id="tracks">Tracks</th>
|
||||
<th id="reldate">Date</th>
|
||||
<th id="scoresmall">Score</th>
|
||||
<th id="mbrelid" style="display:none;"</th>
|
||||
<th class="column-albumname">Album Name</th>
|
||||
<th class="column-artistname-small">Artist Name</th>
|
||||
<th class="column-format">Format</th>
|
||||
<th class="column-tracks">Tracks</th>
|
||||
<th class="column-reldate">Date</th>
|
||||
<th class="column-score-small">Score</th>
|
||||
<th class="column-mbrelid" style="display:none;"</th> <%-- Move display:none to CSS if always hidden --%>
|
||||
%elif type == 'artist':
|
||||
<th id="artistname">Artist Name</th>
|
||||
<th id="score">Score</th>
|
||||
<th class="column-artistname">Artist Name</th>
|
||||
<th class="column-score">Score</th>
|
||||
%else:
|
||||
<th id="seriesname">Series Name</th>
|
||||
<th id="type">Type</th>
|
||||
<th id="score">Score</th>
|
||||
<th class="column-seriesname">Series Name</th>
|
||||
<th class="column-type">Type</th>
|
||||
<th class="column-score">Score</th>
|
||||
%endif
|
||||
<th id="mb"></th>
|
||||
<th class="column-mb"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -39,34 +39,55 @@
|
||||
|
||||
if type == 'album':
|
||||
albuminfo = 'Type: ' + result['rgtype'] + ', Country: ' + result['country']
|
||||
# Constructing CAA URL for fallback, will be used as data-src for unveiling
|
||||
caa_group_url = "http://coverartarchive.org/release-group/%s/front-250.jpg" %result['rgid']
|
||||
# MusicBrainz album art URL (assuming it's a direct link to the image)
|
||||
# If result['image'] provides a direct image URL, use it. Otherwise, rely on CAA or generic.
|
||||
# For this modernization, let's assume getAlbumArtURL is available or build a generic one.
|
||||
# For now, will prioritize getImageLinks.
|
||||
%>
|
||||
<tr class="grade${grade}">
|
||||
%if type == 'album':
|
||||
<td id="albumart" style=" text-align: center; vertical-align: middle;"><div id="artistImg"><img title="${result['albumid']}" class="albumArt" height="50" width="50" onerror="tryCCA(this, '${caa_group_url}')"></div></td>
|
||||
<td class="column-albumart album-art-cell"> <%-- Changed ID to class --%>
|
||||
<div class="artwork-container">
|
||||
<%-- Using data-src for lazy loading. onerror will handle the tryCCA fallback. --%>
|
||||
<img title="${result['albumid']}" class="albumArt search-result-albumart"
|
||||
data-src="" <%-- Will be populated by getImageLinks --%>
|
||||
onerror="tryCCA(this, '${caa_group_url}', 'album')"
|
||||
alt="Cover art for ${result['title']}" loading="lazy">
|
||||
</div>
|
||||
</td>
|
||||
%elif type == 'artist':
|
||||
<td id="albumart"><div id="artistImg"><img title="${result['id']}" class="albumArt" height="50" width="50"></div></td>
|
||||
<td class="column-albumart artist-art-cell"> <%-- Changed ID to class --%>
|
||||
<div class="artwork-container">
|
||||
<%-- Assuming artists might also have an image, or it will be a generic icon. --%>
|
||||
<img title="${result['id']}" class="albumArt search-result-artistart"
|
||||
data-src="" <%-- Will be populated by getImageLinks --%>
|
||||
onerror="this.onerror=null;this.src='interfaces/default/images/no-cover-art.png';"
|
||||
alt="Image for ${result['uniquename']}" loading="lazy">
|
||||
</div>
|
||||
</td>
|
||||
%else:
|
||||
<td id="albumart"></td>
|
||||
<td class="column-albumart series-art-cell"></td> <%-- No artwork for series --%>
|
||||
%endif
|
||||
%if type == 'album':
|
||||
<td id="albumname"><a href="addReleaseById?rid=${result['albumid']}&rgid=${result['rgid']}" title="${albuminfo}">${result['title']}</a></td>
|
||||
<td id="artistnamesmall"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
||||
<td id="format">${result['formats']}</td>
|
||||
<td id="tracks">${result['tracks']}</td>
|
||||
<td id="reldate">${result['date']}</td>
|
||||
<td id="score"><a href="${result['albumurl']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
||||
<td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['albumurl']}"><img src="interfaces/default/images/MusicBrainz_Album_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td>
|
||||
<td id="mbrelid" style="display:none;">${result['albumid']}</td>
|
||||
<td class="column-albumname"><a href="addReleaseById?rid=${result['albumid']}&rgid=${result['rgid']}" title="${albuminfo}">${result['title']}</a></td>
|
||||
<td class="column-artistname-small"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
||||
<td class="column-format">${result['formats']}</td>
|
||||
<td class="column-tracks">${result['tracks']}</td>
|
||||
<td class="column-reldate">${result['date']}</td>
|
||||
<td class="column-score"><a href="${result['albumurl']}" title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
||||
<td class="column-musicbrainz musicbrainz-link-cell"><a href="${result['albumurl']}" target="_blank" rel="noopener noreferrer"><img src="interfaces/default/images/MusicBrainz_Album_Icon.png" title="View on MusicBrainz" height="20" width="20" alt="MusicBrainz Link"></a></td>
|
||||
<td class="column-mbrelid" style="display:none;">${result['albumid']}</td> <%-- Move display:none to CSS if always hidden --%>
|
||||
%elif type == 'artist':
|
||||
<td id="artistname"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
||||
<td id="score"><a href="${result['url']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
||||
<td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['url']}"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td>
|
||||
<td class="column-artistname"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
||||
<td class="column-score"><a href="${result['url']}" title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
||||
<td class="column-musicbrainz musicbrainz-link-cell"><a href="${result['url']}" target="_blank" rel="noopener noreferrer"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20" alt="MusicBrainz Link"></a></td>
|
||||
%else:
|
||||
<td id="seriesname"><a href="addSeries?seriesid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
||||
<td id="type">${result['type']}</td>
|
||||
<td id="score"><a href="${result['url']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
||||
<td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['url']}"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td>
|
||||
<td class="column-seriesname"><a href="addSeries?seriesid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
||||
<td class="column-type">${result['type']}</td>
|
||||
<td class="column-score"><a href="${result['url']}" title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
||||
<td class="column-musicbrainz musicbrainz-link-cell"><a href="${result['url']}" target="_blank" rel="noopener noreferrer"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20" alt="MusicBrainz Link"></a></td>
|
||||
%endif
|
||||
</tr>
|
||||
%endfor
|
||||
@@ -77,37 +98,77 @@
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
<style>
|
||||
/* Basic CSS for album art thumbnail, consistent with other pages */
|
||||
.search-result-albumart, .search-result-artistart {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
object-fit: cover; /* Ensures image covers the area without distortion */
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* MusicBrainz icon styling */
|
||||
.musicbrainz-link-cell img {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Score bar styling (if not already in data_table.css) */
|
||||
.bar {
|
||||
width: 100px; /* Or a fixed width */
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
height: 18px; /* Adjust as needed */
|
||||
line-height: 18px;
|
||||
text-align: right;
|
||||
border-radius: 3px;
|
||||
overflow: hidden; /* Hide overflow if score > 100px */
|
||||
}
|
||||
.score {
|
||||
height: 100%;
|
||||
background-color: #5cb85c; /* Green for score */
|
||||
color: #fff;
|
||||
padding-right: 5px;
|
||||
font-size: 0.8em;
|
||||
box-sizing: border-box; /* Include padding in width calculation */
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script src="js/libs/jquery.unveil.min.js"></script>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
function tryCCA(element, url) {
|
||||
// Global function to try Cover Art Archive or generic fallback
|
||||
function tryCCA(element, url, type) {
|
||||
element.onerror = function() {
|
||||
element.onerror = null;
|
||||
element.src = "interfaces/default/images/no-cover-art.png";
|
||||
element.onerror = null; // Prevent infinite loops
|
||||
element.src = "interfaces/default/images/no-cover-art.png"; // Fallback generic image
|
||||
if (type === 'artist') {
|
||||
// Specific fallback for artists if different
|
||||
// element.src = "interfaces/default/images/no-artist-art.png";
|
||||
}
|
||||
};
|
||||
element.src = url;
|
||||
if (url) {
|
||||
element.src = url; // Try the provided URL (e.g., CAA)
|
||||
} else {
|
||||
element.src = "interfaces/default/images/no-cover-art.png"; // Fallback if no specific URL provided
|
||||
}
|
||||
}
|
||||
function getArt() {
|
||||
$("table#searchresults_table tr td#albumart img").each(function(){
|
||||
var id = $(this).attr('title');
|
||||
var image = $(this);
|
||||
|
||||
if (!image.hasClass('done')) {
|
||||
image.addClass('done');
|
||||
getImageLinks(image, id, "${type}", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
function initThisPage() {
|
||||
// Encapsulate page-specific logic
|
||||
var SearchResultsPage = SearchResultsPage || {};
|
||||
|
||||
SearchResultsPage.initDataTable = function() {
|
||||
$('#searchresults_table').dataTable({
|
||||
"bDestroy": true,
|
||||
"aoColumnDefs": [
|
||||
{ 'bSortable': false, 'aTargets': [ 0 ] }
|
||||
{ 'bSortable': false, 'aTargets': [ 0, 7 ] } // Disable sorting for albumart (0) and MusicBrainz icon (7) columns
|
||||
],
|
||||
"oLanguage": {
|
||||
"sLengthMenu":"Show _MENU_ results per page",
|
||||
@@ -118,36 +179,62 @@
|
||||
"sSearch" : ""},
|
||||
"iDisplayLength": 25,
|
||||
"sPaginationType": "full_numbers",
|
||||
"aaSorting": [],
|
||||
"aaSorting": [], // No initial sorting specified, DataTables default
|
||||
"fnDrawCallback": function (o) {
|
||||
// Jump to top of page
|
||||
$('html,body').scrollTop(0);
|
||||
// Re-unveil images after each draw for lazy loading
|
||||
$("img.search-result-albumart, img.search-result-artistart").unveil();
|
||||
// Re-populate art for new rows
|
||||
SearchResultsPage.getArt();
|
||||
}
|
||||
});
|
||||
$('#searchresults_table').on("draw.dt", function () {
|
||||
getArt();
|
||||
$("img").unveil();
|
||||
};
|
||||
|
||||
// Custom function to get image links and apply them to data-src
|
||||
SearchResultsPage.getArt = function() {
|
||||
// Target all relevant image elements that are not yet "done"
|
||||
$("img.search-result-albumart:not(.done), img.search-result-artistart:not(.done)").each(function(){
|
||||
var $image = $(this);
|
||||
var id = $image.attr('title'); // Use title as ID for getImageLinks
|
||||
|
||||
// Add 'done' class immediately to prevent re-processing
|
||||
$image.addClass('done');
|
||||
|
||||
// Assuming getImageLinks is a global function that fetches the actual image URL
|
||||
// and sets it to the data-src attribute.
|
||||
// If getImageLinks directly sets the src, unveil() will still work, but
|
||||
// this setup aims for data-src first.
|
||||
if (typeof getImageLinks === 'function') {
|
||||
// getImageLinks should ideally return a promise or accept a callback
|
||||
// and then set $image.attr('data-src', actualImageUrl);
|
||||
// For now, assuming it handles it internally or synchronously.
|
||||
getImageLinks($image, id, "${type}", true); // Assuming this sets data-src or src
|
||||
} else {
|
||||
console.warn("getImageLinks function is not defined. Image loading might be impacted.");
|
||||
// Fallback to a generic image if getImageLinks is not available and no data-src is set.
|
||||
// This might be handled by onerror, but adding here for explicit safety.
|
||||
if (!$image.attr('data-src') && !$image.attr('src')) {
|
||||
$image.attr('src', "interfaces/default/images/no-cover-art.png");
|
||||
}
|
||||
}
|
||||
});
|
||||
getArt();
|
||||
resetFilters("album");
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function(){
|
||||
initFancybox();
|
||||
initThisPage();
|
||||
initFancybox(); // Assuming initFancybox is a global function
|
||||
SearchResultsPage.initDataTable();
|
||||
SearchResultsPage.getArt(); // Initial call to load images on page load
|
||||
|
||||
// This part sets search parameters based on backend data.
|
||||
// Assuming searchbar and its inputs exist globally.
|
||||
$("#searchbar input[name=name]").val(${name | json.dumps});
|
||||
$("#searchbar select[name=type]").val(${type | json.dumps});
|
||||
|
||||
// Assuming resetFilters is a global function from common.js
|
||||
if (typeof resetFilters === 'function') {
|
||||
resetFilters("album"); // Or adjust based on 'type' if needed
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
<%!
|
||||
# Abuse JSON module for escaping JavaScript
|
||||
import json
|
||||
%>
|
||||
$(document).ready(function() {
|
||||
// Search parameter
|
||||
$("#searchbar input[name=name]").val(${name | json.dumps});
|
||||
|
||||
// Album or artist
|
||||
$("#searchbar select[name=type]").val(${type | json.dumps});
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
@@ -1,13 +1,45 @@
|
||||
<%inherit file="base.html"/>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<meta http-equiv="refresh" content="${timer};url=index">
|
||||
<style>
|
||||
/* Basic styling for the shutdown message */
|
||||
#shutdown-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh; /* Full viewport height */
|
||||
text-align: center;
|
||||
background-color: #f8f8f8; /* Light background */
|
||||
color: #333;
|
||||
font-family: sans-serif; /* A common, readable font */
|
||||
}
|
||||
|
||||
#shutdown-message h1 {
|
||||
font-size: 2.5em; /* Larger, more prominent text */
|
||||
color: #555;
|
||||
}
|
||||
|
||||
#shutdown-message i {
|
||||
margin-right: 15px; /* Space between icon and text */
|
||||
color: #4CAF50; /* A green color for success/active states */
|
||||
}
|
||||
|
||||
/* If it's truly shutting down or restarting, maybe a different color */
|
||||
#shutdown-message.restarting i {
|
||||
color: #ff9800; /* Orange for restart */
|
||||
}
|
||||
#shutdown-message.shutting-down i {
|
||||
color: #f44336; /* Red for shutdown */
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="body()">
|
||||
<div class="table_wrapper">
|
||||
<div id="shutdown">
|
||||
<h1><i class="fa fa-refresh fa-spin"></i> Headphones is ${message}</h1>
|
||||
<div class="table_wrapper" id="shutdown-container"> <%-- Added a container for centering --%>
|
||||
<div id="shutdown-message" class="${'restarting' if message == 'restarting' else 'shutting-down'}"> <%-- Added class for conditional styling --%>
|
||||
<h1><i class="fa fa-refresh fa-spin"></i> Headphones is ${message}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
@@ -3,7 +3,10 @@
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a href="javascript:void(0)" id="menu_link_scan" onclick="doAjaxCall('forceSearch',$(this))" data-success="Checking for wanted albums successful" data-error="Error checking wanted albums"><i class="fa fa-search"></i> Force Check</a>
|
||||
<%-- Changed inline onclick to a class and data attributes for JS handling --%>
|
||||
<a href="#" id="menu_link_force_check" class="ajax-link" data-action="forceSearch" data-success="Checking for wanted albums successful" data-error="Error checking wanted albums">
|
||||
<i class="fa fa-search"></i> Force Check
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
@@ -13,38 +16,44 @@
|
||||
<div id="paddingheader">
|
||||
<h1 class="clearfix"><i class="fa fa-heart"></i> Wanted Albums</h1>
|
||||
</div>
|
||||
<form action="markAlbums" method="get" id="markAlbums">
|
||||
<div id="markalbum" style="top:0;">
|
||||
<form action="markAlbums" method="get" id="markAlbumsForm"> <%-- Added ID to the form --%>
|
||||
<div id="mark_albums_controls"> <%-- Unique and descriptive ID, moved inline style to CSS --%>
|
||||
Mark selected albums as
|
||||
<select name="action" onChange="doAjaxCall('markAlbums',$(this),'table',true);" data-error="You didn't select any albums">
|
||||
<select name="action" id="markAlbumActionSelect"> <%-- Added ID --%>
|
||||
<option disabled="disabled" selected="selected">Choose...</option>
|
||||
<option value="Skipped">Skipped</option>
|
||||
<option value="Ignored">Ignored</option>
|
||||
<option value="Downloaded">Downloaded</option>
|
||||
</select>
|
||||
<input type="hidden" value="Go">
|
||||
<%-- Replaced hidden input with a visible button if desired, or handle submit via JS --%>
|
||||
<%-- For now, keep the JS-driven approach as close as possible to original logic --%>
|
||||
</div>
|
||||
<div class="table_wrapper" id="wanted_table_wrapper" >
|
||||
<table class="display" id="wanted_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="select"><input type="checkbox" onClick="toggle(this)" /></th>
|
||||
<th id="albumart"></th>
|
||||
<th id="artistname">Artist</th>
|
||||
<th id="albumname">Album Name</th>
|
||||
<th id="reldate">Release Date</th>
|
||||
<th id="type">Type</th>
|
||||
<th class="select-all-checkbox"><input type="checkbox" id="toggleAllWanted" /></th> <%-- Added ID for easier targeting --%>
|
||||
<th class="column-albumart"></th>
|
||||
<th class="column-artistname">Artist</th>
|
||||
<th class="column-albumname">Album Name</th>
|
||||
<th class="column-reldate">Release Date</th>
|
||||
<th class="column-type">Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
%for album in wanted:
|
||||
<tr class="gradeZ">
|
||||
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></th>
|
||||
<td id="albumart"><img title="${album['AlbumID']}" height="64" width="64" src="interfaces/default/images/no-cover-art.png" data-src="artwork/thumbs/album/${album['AlbumID']}"></td>
|
||||
<td id="artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
||||
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||
<td id="reldate">${album['ReleaseDate']}</td>
|
||||
<td id="type">${album['Type']}</td>
|
||||
<td class="select-checkbox"><input type="checkbox" name="album_ids" value="${album['AlbumID']}" class="wanted-album-checkbox" /></td> <%-- Changed name to album_ids, added class --%>
|
||||
<td class="column-albumart">
|
||||
<img title="${album['AlbumID']}" height="64" width="64"
|
||||
data-src="artwork/thumbs/album/${album['AlbumID']}"
|
||||
src="interfaces/default/images/no-cover-art.png" <%-- Fallback src --%>
|
||||
alt="Cover art for ${album['AlbumTitle']}" loading="lazy">
|
||||
</td>
|
||||
<td class="column-artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
||||
<td class="column-albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||
<td class="column-reldate">${album['ReleaseDate']}</td>
|
||||
<td class="column-type">${album['Type']}</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</tbody>
|
||||
@@ -56,26 +65,31 @@
|
||||
<h1 class="clearfix"><i class="fa fa-calendar"></i> Upcoming Albums</h1>
|
||||
</div>
|
||||
<div class="table_wrapper">
|
||||
<table class="display_no_select" id="upcoming_table">
|
||||
<table class="display_no_select" id="upcoming_table"> <%-- No changes here, but ensuring it gets DataTables init --%>
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="albumart"></th>
|
||||
<th id="artistname">Artist</th>
|
||||
<th id="albumname">Album Name</th>
|
||||
<th id="reldate">Release Date</th>
|
||||
<th id="type">Type</th>
|
||||
<th id="status">Status</th>
|
||||
<th class="column-albumart"></th>
|
||||
<th class="column-artistname">Artist</th>
|
||||
<th class="column-albumname">Album Name</th>
|
||||
<th class="column-reldate">Release Date</th>
|
||||
<th class="column-type">Type</th>
|
||||
<th class="column-status">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
%for album in upcoming:
|
||||
<tr class="gradeZ">
|
||||
<td id="albumart"><img title="${album['AlbumID']}" height="64" width="64" data-src="artwork/thumbs/album/${album['AlbumID']}"></td>
|
||||
<td id="artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
||||
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||
<td id="reldate">${album['ReleaseDate']}</td>
|
||||
<td id="type">${album['Type']}</td>
|
||||
<td id="status">${album['Status']}</td>
|
||||
<td class="column-albumart">
|
||||
<img title="${album['AlbumID']}" height="64" width="64"
|
||||
data-src="artwork/thumbs/album/${album['AlbumID']}"
|
||||
src="interfaces/default/images/no-cover-art.png" <%-- Fallback src --%>
|
||||
alt="Cover art for ${album['AlbumTitle']}" loading="lazy">
|
||||
</td>
|
||||
<td class="column-artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
||||
<td class="column-albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||
<td class="column-reldate">${album['ReleaseDate']}</td>
|
||||
<td class="column-type">${album['Type']}</td>
|
||||
<td class="column-status">${album['Status']}</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</tbody>
|
||||
@@ -84,29 +98,154 @@
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
<style>
|
||||
#mark_albums_controls {
|
||||
position: relative; /* Adjust as per original 'top:0;' intent */
|
||||
margin-bottom: 15px; /* Add some space below controls */
|
||||
}
|
||||
/* Style for album art thumbnails */
|
||||
.column-albumart img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: cover; /* Ensures image fills the space without distortion */
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
||||
<script src="js/libs/jquery.unveil.min.js"></script>
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
<script>
|
||||
function initThisPage() {
|
||||
$("img").unveil();
|
||||
// Encapsulate page-specific logic
|
||||
var UpcomingPage = UpcomingPage || {};
|
||||
|
||||
UpcomingPage.initDataTables = function() {
|
||||
// Initialize Wanted Albums table
|
||||
$('#wanted_table').dataTable({
|
||||
"oLanguage": {
|
||||
"sEmptyTable": " "
|
||||
"sEmptyTable": "No wanted albums found" // More descriptive empty table message
|
||||
},
|
||||
"bDestroy": true,
|
||||
"bFilter": false,
|
||||
"bInfo": false,
|
||||
"bPaginate": false
|
||||
"bPaginate": false,
|
||||
"aoColumnDefs": [
|
||||
{ 'bSortable': false, 'aTargets': [ 0, 1 ] } // Disable sorting for checkbox and album art columns
|
||||
],
|
||||
"fnDrawCallback": function (o) {
|
||||
// Re-unveil images after each draw for lazy loading
|
||||
$("img[data-src]").unveil();
|
||||
}
|
||||
});
|
||||
resetFilters("artists");
|
||||
initActions();
|
||||
}
|
||||
|
||||
// Initialize Upcoming Albums table
|
||||
$('#upcoming_table').dataTable({
|
||||
"oLanguage": {
|
||||
"sEmptyTable": "No upcoming albums found" // More descriptive empty table message
|
||||
},
|
||||
"bDestroy": true,
|
||||
"bFilter": false,
|
||||
"bInfo": false,
|
||||
"bPaginate": false,
|
||||
"aoColumnDefs": [
|
||||
{ 'bSortable': false, 'aTargets': [ 0 ] } // Disable sorting for album art column
|
||||
],
|
||||
"aaSorting": [[3, 'asc']], // Sort by Release Date ascending by default
|
||||
"fnDrawCallback": function (o) {
|
||||
// Re-unveil images after each draw for lazy loading
|
||||
$("img[data-src]").unveil();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
UpcomingPage.initActions = function() {
|
||||
// Event listener for "Force Check" link
|
||||
$('#menu_link_force_check').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var $this = $(this);
|
||||
var actionUrl = $this.data('action'); // 'forceSearch'
|
||||
var successMsg = $this.data('success');
|
||||
var errorMsg = $this.data('error');
|
||||
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(actionUrl, $this, 'page', successMsg, errorMsg);
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot force check.");
|
||||
}
|
||||
});
|
||||
|
||||
// Event listener for "Mark selected albums as" dropdown
|
||||
$('#markAlbumActionSelect').on('change', function() {
|
||||
var $this = $(this);
|
||||
var action = $this.val();
|
||||
var selectedAlbumIds = [];
|
||||
|
||||
$('.wanted-album-checkbox:checked').each(function() {
|
||||
selectedAlbumIds.push($(this).val());
|
||||
});
|
||||
|
||||
if (selectedAlbumIds.length === 0) {
|
||||
if (typeof showMessage === 'function') {
|
||||
showMessage($this.data('error') || "You didn't select any albums", 'error');
|
||||
} else {
|
||||
alert($this.data('error') || "You didn't select any albums");
|
||||
}
|
||||
// Reset dropdown to "Choose..."
|
||||
$this.val('Choose...');
|
||||
return;
|
||||
}
|
||||
|
||||
var url = $('#markAlbumsForm').attr('action') + '?action=' + encodeURIComponent(action);
|
||||
$.each(selectedAlbumIds, function(index, id) {
|
||||
url += '&album_ids=' + encodeURIComponent(id); // Append selected album IDs
|
||||
});
|
||||
|
||||
var successMsg = 'Albums successfully marked as ' + action;
|
||||
var errorMsg = 'Error marking albums.';
|
||||
|
||||
if (typeof doAjaxCall === 'function') {
|
||||
doAjaxCall(url, $this, 'table', successMsg, errorMsg);
|
||||
// Reset dropdown after action
|
||||
$this.val('Choose...');
|
||||
} else {
|
||||
console.error("doAjaxCall function is not defined. Cannot mark albums.");
|
||||
}
|
||||
});
|
||||
|
||||
// Event listener for the "toggle all" checkbox in Wanted Albums table
|
||||
$('#toggleAllWanted').on('click', function() {
|
||||
$('.wanted-album-checkbox').prop('checked', this.checked);
|
||||
});
|
||||
|
||||
// Update "toggle all" checkbox based on individual checkboxes
|
||||
$(document).on('click', '.wanted-album-checkbox', function() {
|
||||
if (!this.checked) {
|
||||
$('#toggleAllWanted').prop('checked', false);
|
||||
} else {
|
||||
if ($('.wanted-album-checkbox:checked').length === $('.wanted-album-checkbox').length) {
|
||||
$('#toggleAllWanted').prop('checked', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Assuming resetFilters and initActions are global functions from common.js
|
||||
if (typeof resetFilters === 'function') {
|
||||
resetFilters("artists"); // Or adjust based on context
|
||||
}
|
||||
if (typeof initActions === 'function') {
|
||||
initActions();
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
initThisPage();
|
||||
UpcomingPage.initDataTables();
|
||||
UpcomingPage.initActions();
|
||||
// Initial unveil call for images
|
||||
$("img[data-src]").unveil();
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
@@ -86,8 +86,8 @@ CURRENT_VERSION = None
|
||||
LATEST_VERSION = None
|
||||
COMMITS_BEHIND = None
|
||||
|
||||
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma"]
|
||||
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff"]
|
||||
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma", "opus"]
|
||||
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff", "aif"]
|
||||
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
|
||||
|
||||
MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
|
||||
@@ -218,7 +218,7 @@ def daemonize():
|
||||
pid = os.fork() # @UndefinedVariable - only available in UNIX
|
||||
if pid != 0:
|
||||
sys.exit(0)
|
||||
except OSError, e:
|
||||
except OSError as e:
|
||||
raise RuntimeError("1st fork failed: %s [%d]", e.strerror, e.errno)
|
||||
|
||||
os.setsid()
|
||||
@@ -232,10 +232,10 @@ def daemonize():
|
||||
pid = os.fork() # @UndefinedVariable - only available in UNIX
|
||||
if pid != 0:
|
||||
sys.exit(0)
|
||||
except OSError, e:
|
||||
except OSError as e:
|
||||
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())
|
||||
|
||||
si = open('/dev/null', "r")
|
||||
@@ -251,7 +251,7 @@ def daemonize():
|
||||
|
||||
if CREATEPID:
|
||||
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)
|
||||
|
||||
|
||||
@@ -306,9 +306,10 @@ def initialize_scheduler():
|
||||
minutes=minutes)
|
||||
|
||||
# Remove Torrent + data if Post Processed and finished Seeding
|
||||
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
||||
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
||||
minutes=minutes)
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER != 0:
|
||||
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
||||
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
||||
minutes=minutes)
|
||||
|
||||
# Start scheduler
|
||||
if start_jobs and len(SCHED.get_jobs()):
|
||||
@@ -359,6 +360,8 @@ def sig_handler(signum=None, frame=None):
|
||||
|
||||
|
||||
def dbcheck():
|
||||
logger.debug("SQLite Version: %s", sqlite3.sqlite_version)
|
||||
logger.debug("DB-API Version: %s", sqlite3.version)
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
@@ -376,7 +379,7 @@ def dbcheck():
|
||||
c.execute(
|
||||
'CREATE TABLE IF NOT EXISTS alltracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)')
|
||||
c.execute(
|
||||
'CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT, Kind TEXT)')
|
||||
'CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT, Kind TEXT, TorrentHash TEXT)')
|
||||
# Matched is a temporary value used to see if there was a match found in
|
||||
# alltracks
|
||||
c.execute(
|
||||
@@ -416,6 +419,33 @@ def dbcheck():
|
||||
'CREATE INDEX IF NOT EXISTS tracks_Location ON tracks(Location ASC)')
|
||||
c.execute(
|
||||
'CREATE INDEX IF NOT EXISTS alltracks_Location ON alltracks(Location ASC)')
|
||||
c.execute(
|
||||
'CREATE INDEX IF NOT EXISTS tracks_artistid ON tracks(ArtistID ASC)')
|
||||
|
||||
# Speed up album page
|
||||
c.execute('CREATE INDEX IF NOT EXISTS allalbums_albumid ON allalbums(AlbumID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS alltracks_albumid ON alltracks(AlbumID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS releases_albumid ON releases(ReleaseGroupID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS descriptions_albumid ON descriptions(ReleaseGroupID ASC)')
|
||||
|
||||
# Speed up artist deletion
|
||||
c.execute('CREATE INDEX IF NOT EXISTS allalbums_artistid ON allalbums(ArtistID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS alltracks_artistid ON alltracks(ArtistID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS descriptions_artistid ON descriptions(ArtistID ASC)')
|
||||
|
||||
# Speed up Artist refresh hybrid release
|
||||
c.execute('CREATE INDEX IF NOT EXISTS albums_releaseid ON albums(ReleaseID ASC)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS tracks_releaseid ON tracks(ReleaseID ASC)')
|
||||
|
||||
# Speed up scanning and track matching
|
||||
c.execute('CREATE INDEX IF NOT EXISTS artist_artistname ON artists(ArtistName COLLATE NOCASE ASC)')
|
||||
|
||||
# General speed up
|
||||
c.execute('CREATE INDEX IF NOT EXISTS artist_artistsortname ON artists(ArtistSortName COLLATE NOCASE ASC)')
|
||||
|
||||
c.execute(
|
||||
"""CREATE INDEX IF NOT EXISTS have_matched_artist_album ON have(Matched ASC, ArtistName COLLATE NOCASE ASC, AlbumTitle COLLATE NOCASE ASC)""")
|
||||
c.execute('DROP INDEX IF EXISTS have_matched')
|
||||
|
||||
try:
|
||||
c.execute('SELECT IncludeExtras from artists')
|
||||
@@ -613,6 +643,20 @@ def dbcheck():
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE artists ADD COLUMN MetaCritic TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT TorrentHash from snatched')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE snatched ADD COLUMN TorrentHash TEXT')
|
||||
c.execute('UPDATE snatched SET TorrentHash = FolderName WHERE Status LIKE "Seed_%"')
|
||||
|
||||
# One off script to set CleanName to lower case
|
||||
clean_name_mixed = c.execute('SELECT CleanName FROM have ORDER BY Date Desc').fetchone()
|
||||
if clean_name_mixed and clean_name_mixed[0] != clean_name_mixed[0].lower():
|
||||
logger.info("Updating track clean name, this could take some time...")
|
||||
c.execute('UPDATE tracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||
c.execute('UPDATE alltracks SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||
c.execute('UPDATE have SET CleanName = LOWER(CleanName) WHERE LOWER(CleanName) != CleanName')
|
||||
|
||||
conn.commit()
|
||||
c.close()
|
||||
|
||||
|
||||
@@ -13,16 +13,196 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from headphones import request, db, logger
|
||||
import struct
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from io import BytesIO
|
||||
|
||||
import headphones
|
||||
from headphones import db, request, logger
|
||||
|
||||
|
||||
def getAlbumArt(albumid):
|
||||
myDB = db.DBConnection()
|
||||
asin = myDB.action(
|
||||
'SELECT AlbumASIN from albums WHERE AlbumID=?', [albumid]).fetchone()[0]
|
||||
|
||||
if asin:
|
||||
return 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % asin
|
||||
artwork_path = None
|
||||
artwork = None
|
||||
|
||||
# CAA
|
||||
logger.info("Searching for artwork at CAA")
|
||||
#artwork_path = 'https://coverartarchive.org/release-group/%s/front' % albumid
|
||||
artwork_path = 'https://coverartarchive.org/release-group/%s' % albumid
|
||||
|
||||
data = request.request_json(artwork_path, timeout=20, whitelist_status_code=404)
|
||||
|
||||
image_url = None
|
||||
if data:
|
||||
for item in data.get("images", []):
|
||||
try:
|
||||
if "Front" not in item["types"]:
|
||||
continue
|
||||
|
||||
# Use desired size
|
||||
image_url = item["image"]
|
||||
if headphones.CONFIG.ALBUM_ART_MAX_WIDTH:
|
||||
if isinstance(item.get("thumbnails"), dict):
|
||||
image_url = item["thumbnails"].get(
|
||||
headphones.CONFIG.ALBUM_ART_MAX_WIDTH, image_url
|
||||
)
|
||||
break
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if image_url:
|
||||
artwork = getartwork(image_url)
|
||||
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
|
||||
}
|
||||
headers = {"User-Agent": "Headphones"}
|
||||
r = request.request_response(
|
||||
url,
|
||||
params=params,
|
||||
timeout=20,
|
||||
stream=True,
|
||||
whitelist_status_code=404,
|
||||
headers=headers
|
||||
)
|
||||
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):
|
||||
@@ -34,7 +214,7 @@ def getCachedArt(albumid):
|
||||
if not artwork_path:
|
||||
return
|
||||
|
||||
if artwork_path.startswith('http://'):
|
||||
if artwork_path.startswith("http"):
|
||||
artwork = request.request_content(artwork_path, timeout=20)
|
||||
|
||||
if not artwork:
|
||||
|
||||
@@ -85,8 +85,8 @@ def switch(AlbumID, ReleaseID):
|
||||
|
||||
# Update have track counts on index
|
||||
totaltracks = len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")',
|
||||
[newalbumdata['ArtistID']]))
|
||||
'SELECT TrackTitle from tracks AS tr INNER JOIN albums AS al ON al.AlbumID = tr.AlbumID WHERE al.ArtistID=? '
|
||||
'AND al.Status != "Ignored"', [newalbumdata['ArtistID']]))
|
||||
havetracks = len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
||||
[newalbumdata['ArtistID']]))
|
||||
|
||||
@@ -82,11 +82,11 @@ class Api(object):
|
||||
def fetchData(self):
|
||||
|
||||
if self.data == 'OK':
|
||||
logger.info('Recieved API command: %s', self.cmd)
|
||||
logger.info('Received API command: %s', self.cmd)
|
||||
methodToCall = getattr(self, "_" + self.cmd)
|
||||
methodToCall(**self.kwargs)
|
||||
if 'callback' not in self.kwargs:
|
||||
if isinstance(self.data, basestring):
|
||||
if isinstance(self.data, str):
|
||||
return self.data
|
||||
else:
|
||||
return json.dumps(self.data)
|
||||
@@ -106,7 +106,7 @@ class Api(object):
|
||||
rows_as_dic = []
|
||||
|
||||
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)
|
||||
|
||||
return rows_as_dic
|
||||
@@ -474,17 +474,17 @@ class Api(object):
|
||||
# Handle situations where the torrent url contains arguments that are
|
||||
# parsed
|
||||
if kwargs:
|
||||
import urllib
|
||||
import urllib2
|
||||
url = urllib2.quote(
|
||||
url, safe=":?/=&") + '&' + urllib.urlencode(kwargs)
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import urllib.request, urllib.error, urllib.parse
|
||||
url = urllib.parse.quote(
|
||||
url, safe=":?/=&") + '&' + urllib.parse.urlencode(kwargs)
|
||||
|
||||
try:
|
||||
result = [(title, int(size), url, provider, kind)]
|
||||
except ValueError:
|
||||
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)
|
||||
|
||||
if data and bestqual:
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# This file is part of Headphones.
|
||||
#
|
||||
# Headphones is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Headphones is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
import headphones
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
from headphones import logger, helpers, metadata, request
|
||||
from headphones.common import USER_AGENT
|
||||
from headphones.types import Result
|
||||
|
||||
from mediafile import MediaFile, UnreadableFileError
|
||||
from bs4 import BeautifulSoup
|
||||
from bs4 import FeatureNotFound
|
||||
|
||||
|
||||
def search(album, albumlength=None, page=1, resultlist=None):
|
||||
dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ',
|
||||
'"': '', ',': '', '*': '', '.': '', ':': ''}
|
||||
if resultlist is None:
|
||||
resultlist = []
|
||||
|
||||
cleanalbum = helpers.latinToAscii(
|
||||
helpers.replace_all(album['AlbumTitle'], dic)
|
||||
).strip()
|
||||
cleanartist = helpers.latinToAscii(
|
||||
helpers.replace_all(album['ArtistName'], dic)
|
||||
).strip()
|
||||
|
||||
headers = {'User-Agent': USER_AGENT}
|
||||
params = {
|
||||
"page": page,
|
||||
"q": cleanalbum,
|
||||
}
|
||||
logger.info("Looking up https://bandcamp.com/search with {}".format(
|
||||
params))
|
||||
content = request.request_content(
|
||||
url='https://bandcamp.com/search',
|
||||
params=params,
|
||||
headers=headers
|
||||
).decode('utf8')
|
||||
try:
|
||||
soup = BeautifulSoup(content, "html5lib")
|
||||
except FeatureNotFound:
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
|
||||
for item in soup.find_all("li", class_="searchresult"):
|
||||
type = item.find('div', class_='itemtype').text.strip().lower()
|
||||
if type == "album":
|
||||
data = parse_album(item)
|
||||
|
||||
cleanartist_found = helpers.latinToAscii(data['artist'])
|
||||
cleanalbum_found = helpers.latinToAscii(data['album'])
|
||||
|
||||
logger.debug(u"{} - {}".format(data['album'], cleanalbum_found))
|
||||
|
||||
logger.debug("Comparing {} to {}".format(
|
||||
cleanalbum, cleanalbum_found))
|
||||
if (cleanartist.lower() == cleanartist_found.lower() and
|
||||
cleanalbum.lower() == cleanalbum_found.lower()):
|
||||
resultlist.append(Result(
|
||||
data['title'], data['size'], data['url'],
|
||||
'bandcamp', 'bandcamp', True))
|
||||
else:
|
||||
continue
|
||||
|
||||
if(soup.find('a', class_='next')):
|
||||
page += 1
|
||||
logger.debug("Calling next page ({})".format(page))
|
||||
search(album, albumlength=albumlength,
|
||||
page=page, resultlist=resultlist)
|
||||
|
||||
return resultlist
|
||||
|
||||
|
||||
def download(album, bestqual):
|
||||
html = request.request_content(url=bestqual.url).decode('utf-8')
|
||||
trackinfo = []
|
||||
try:
|
||||
trackinfo = json.loads(
|
||||
re.search(r"trackinfo":(\[.*?\]),", html)
|
||||
.group(1)
|
||||
.replace('"', '"'))
|
||||
except ValueError as e:
|
||||
logger.warn("Couldn't load json: {}".format(e))
|
||||
|
||||
directory = os.path.join(
|
||||
headphones.CONFIG.BANDCAMP_DIR,
|
||||
u'{} - {}'.format(
|
||||
album['ArtistName'].replace('/', '_'),
|
||||
album['AlbumTitle'].replace('/', '_')))
|
||||
directory = helpers.latinToAscii(directory)
|
||||
|
||||
if not os.path.exists(directory):
|
||||
try:
|
||||
os.makedirs(directory)
|
||||
except Exception as e:
|
||||
logger.warn("Could not create directory ({})".format(e))
|
||||
|
||||
index = 1
|
||||
for track in trackinfo:
|
||||
filename = helpers.replace_illegal_chars(
|
||||
u'{:02d} - {}.mp3'.format(index, track['title']))
|
||||
fullname = os.path.join(directory.encode('utf-8'),
|
||||
filename.encode('utf-8'))
|
||||
logger.debug("Downloading to {}".format(fullname))
|
||||
|
||||
if 'file' in track and track['file'] != None and 'mp3-128' in track['file']:
|
||||
content = request.request_content(track['file']['mp3-128'])
|
||||
open(fullname, 'wb').write(content)
|
||||
try:
|
||||
f = MediaFile(fullname)
|
||||
date, year = metadata._date_year(album)
|
||||
f.update({
|
||||
'artist': album['ArtistName'].encode('utf-8'),
|
||||
'album': album['AlbumTitle'].encode('utf-8'),
|
||||
'title': track['title'].encode('utf-8'),
|
||||
'track': track['track_num'],
|
||||
'tracktotal': len(trackinfo),
|
||||
'year': year,
|
||||
})
|
||||
f.save()
|
||||
except UnreadableFileError as ex:
|
||||
logger.warn("MediaFile couldn't parse: %s (%s)",
|
||||
fullname,
|
||||
str(ex))
|
||||
|
||||
index += 1
|
||||
|
||||
return directory
|
||||
|
||||
|
||||
def parse_album(item):
|
||||
album = item.find('div', class_='heading').text.strip()
|
||||
artist = item.find('div', class_='subhead').text.strip().replace("by ", "")
|
||||
released = item.find('div', class_='released').text.strip().replace(
|
||||
"released ", "")
|
||||
year = re.search(r"(\d{4})", released).group(1)
|
||||
|
||||
url = item.find('div', class_='heading').find('a')['href'].split("?")[0]
|
||||
|
||||
length = item.find('div', class_='length').text.strip()
|
||||
tracks, minutes = length.split(",")
|
||||
tracks = tracks.replace(" tracks", "").replace(" track", "").strip()
|
||||
minutes = minutes.replace(" minutes", "").strip()
|
||||
# bandcamp offers mp3 128b with should be 960KB/minute
|
||||
size = int(minutes) * 983040
|
||||
|
||||
data = {"title": u'{} - {} [{}]'.format(artist, album, year),
|
||||
"artist": artist, "album": album,
|
||||
"url": url, "size": size}
|
||||
|
||||
return data
|
||||
@@ -14,11 +14,16 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
import headphones
|
||||
from headphones import db, helpers, logger, lastfm, request
|
||||
from headphones import db, helpers, logger, lastfm, request, mb
|
||||
|
||||
LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
|
||||
LASTFM_API_KEY = "8d983789c771afaeb7412ac358d4bad0"
|
||||
|
||||
FANART_URL = 'https://webservice.fanart.tv/v3/music/'
|
||||
FANART_PROJECT_KEY = '22b73c9603eba09d0c855f2d2bdba31c'
|
||||
FANART_CLIENT_KEY = '919b389a18a3f0b2c916090022ab3c7a'
|
||||
|
||||
|
||||
class Cache(object):
|
||||
@@ -96,6 +101,11 @@ class Cache(object):
|
||||
def _is_current(self, filename=None, date=None):
|
||||
|
||||
if filename:
|
||||
|
||||
# 2019 last.fm no longer allows access to artist images, for now we'll keep the cached artist image if it exists and get new from fanart.tv
|
||||
if self.id_type == 'artist' and 'fanart' not in filename:
|
||||
return True
|
||||
|
||||
base_filename = os.path.basename(filename)
|
||||
date = base_filename.split('.')[1]
|
||||
|
||||
@@ -211,20 +221,34 @@ class Cache(object):
|
||||
if ArtistID:
|
||||
|
||||
self.id_type = 'artist'
|
||||
data = lastfm.request_lastfm("artist.getinfo", mbid=ArtistID, api_key=LASTFM_API_KEY)
|
||||
|
||||
# 2019 last.fm no longer allows access to artist images, try fanart.tv instead
|
||||
image_url = None
|
||||
thumb_url = None
|
||||
data = request.request_json(FANART_URL + ArtistID, whitelist_status_code=404,
|
||||
headers={'api-key': FANART_PROJECT_KEY, 'client-key': FANART_CLIENT_KEY})
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
try:
|
||||
image_url = data['artist']['image'][-1]['#text']
|
||||
except (KeyError, IndexError):
|
||||
logger.debug('No artist image found')
|
||||
image_url = None
|
||||
if data.get('artistthumb'):
|
||||
image_url = data['artistthumb'][0]['url']
|
||||
elif data.get('artistbackground'):
|
||||
image_url = data['artistbackground'][0]['url']
|
||||
# elif data.get('hdmusiclogo'):
|
||||
# image_url = data['hdmusiclogo'][0]['url']
|
||||
|
||||
thumb_url = self._get_thumb_url(data)
|
||||
if not thumb_url:
|
||||
logger.debug('No artist thumbnail image found')
|
||||
# fallback to 1st album cover if none of the above
|
||||
elif 'albums' in data:
|
||||
for mbid, art in 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:
|
||||
|
||||
@@ -283,6 +307,7 @@ class Cache(object):
|
||||
"""
|
||||
|
||||
myDB = db.DBConnection()
|
||||
fanart = False
|
||||
|
||||
# Since lastfm uses release ids rather than release group ids for albums, we have to do a artist + album search for albums
|
||||
# Exception is when adding albums manually, then we should use release id
|
||||
@@ -290,6 +315,14 @@ class Cache(object):
|
||||
|
||||
data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=LASTFM_API_KEY)
|
||||
|
||||
# Try with name if not found
|
||||
if not data:
|
||||
dbartist = myDB.action('SELECT ArtistName, Type FROM artists WHERE ArtistID=?', [self.id]).fetchone()
|
||||
if dbartist:
|
||||
data = lastfm.request_lastfm("artist.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(dbartist['ArtistName']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
@@ -303,30 +336,70 @@ class Cache(object):
|
||||
except KeyError:
|
||||
logger.debug('No artist bio found')
|
||||
self.info_content = None
|
||||
try:
|
||||
image_url = data['artist']['image'][-1]['#text']
|
||||
except KeyError:
|
||||
logger.debug('No artist image found')
|
||||
image_url = None
|
||||
|
||||
thumb_url = self._get_thumb_url(data)
|
||||
if not thumb_url:
|
||||
logger.debug('No artist thumbnail image found')
|
||||
# 2019 last.fm no longer allows access to artist images, try fanart.tv instead
|
||||
image_url = None
|
||||
thumb_url = None
|
||||
data = request.request_json(FANART_URL + self.id, whitelist_status_code=404,
|
||||
headers={'api-key': FANART_PROJECT_KEY, 'client-key': FANART_CLIENT_KEY})
|
||||
|
||||
if data.get('artistthumb'):
|
||||
image_url = data['artistthumb'][0]['url']
|
||||
elif data.get('artistbackground'):
|
||||
image_url = data['artistbackground'][0]['url']
|
||||
# elif data.get('hdmusiclogo'):
|
||||
# image_url = data['hdmusiclogo'][0]['url']
|
||||
|
||||
# fallback to 1st album cover if none of the above
|
||||
elif 'albums' in data:
|
||||
for mbid, art in 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:
|
||||
dbalbum = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?',
|
||||
'SELECT ArtistName, AlbumTitle, ReleaseID, Type FROM albums WHERE AlbumID=?',
|
||||
[self.id]).fetchone()
|
||||
if dbalbum['ReleaseID'] != self.id:
|
||||
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'],
|
||||
api_key=LASTFM_API_KEY)
|
||||
if not data:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'],
|
||||
data = lastfm.request_lastfm("album.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
|
||||
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
else:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
|
||||
if dbalbum['Type'] != "part of":
|
||||
data = lastfm.request_lastfm("album.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(dbalbum['ArtistName']),
|
||||
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
else:
|
||||
|
||||
# Series, use actual artist for the release-group
|
||||
artist = mb.getArtistForReleaseGroup(self.id)
|
||||
if artist:
|
||||
data = lastfm.request_lastfm("album.getinfo",
|
||||
artist=helpers.clean_musicbrainz_name(artist),
|
||||
album=helpers.clean_musicbrainz_name(dbalbum['AlbumTitle']),
|
||||
api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
@@ -406,7 +479,11 @@ class Cache(object):
|
||||
|
||||
ext = os.path.splitext(image_url)[1]
|
||||
|
||||
artwork_path = os.path.join(self.path_to_art_cache,
|
||||
if fanart:
|
||||
artwork_path = os.path.join(self.path_to_art_cache,
|
||||
self.id + '_fanart_' + '.' + helpers.today() + ext)
|
||||
else:
|
||||
artwork_path = os.path.join(self.path_to_art_cache,
|
||||
self.id + '.' + helpers.today() + ext)
|
||||
try:
|
||||
with open(artwork_path, 'wb') as f:
|
||||
@@ -422,7 +499,9 @@ class Cache(object):
|
||||
# as it's missing/outdated.
|
||||
if thumb_url and self.query_type in ['thumb', 'artwork'] and not (
|
||||
self.thumb_files and self._is_current(self.thumb_files[0])):
|
||||
artwork = request.request_content(thumb_url, timeout=20)
|
||||
|
||||
if not (self.query_type == 'artwork' and 'fanart' in thumb_url and artwork):
|
||||
artwork = request.request_content(thumb_url, timeout=20)
|
||||
|
||||
if artwork:
|
||||
# Make sure the artwork dir exists:
|
||||
@@ -445,11 +524,41 @@ class Cache(object):
|
||||
|
||||
ext = os.path.splitext(image_url)[1]
|
||||
|
||||
thumb_path = os.path.join(self.path_to_art_cache,
|
||||
'T_' + self.id + '.' + helpers.today() + ext)
|
||||
if fanart:
|
||||
thumb_path = os.path.join(self.path_to_art_cache,
|
||||
'T_' + self.id + '_fanart_' + '.' + helpers.today() + ext)
|
||||
else:
|
||||
thumb_path = os.path.join(self.path_to_art_cache,
|
||||
'T_' + self.id + '.' + helpers.today() + ext)
|
||||
try:
|
||||
with open(thumb_path, 'wb') as f:
|
||||
f.write(artwork)
|
||||
if self.id_type != 'artist':
|
||||
with open(thumb_path, 'wb') as f:
|
||||
f.write(artwork)
|
||||
else:
|
||||
|
||||
# 2019 last.fm no longer allows access to artist images, use the fanart.tv image to create a thumb
|
||||
artwork_thumb = None
|
||||
if 'fanart' in thumb_url:
|
||||
# Create thumb using image resizing service
|
||||
url = "https://images.weserv.nl"
|
||||
params = {
|
||||
"url": thumb_url,
|
||||
"w": 300
|
||||
}
|
||||
headers = {"User-Agent": "Headphones"}
|
||||
artwork_thumb = request.request_content(
|
||||
url,
|
||||
params=params,
|
||||
timeout=20,
|
||||
whitelist_status_code=404,
|
||||
headers=headers
|
||||
)
|
||||
if artwork_thumb:
|
||||
with open(thumb_path, 'wb') as f:
|
||||
f.write(artwork_thumb)
|
||||
else:
|
||||
with open(thumb_path, 'wb') as f:
|
||||
f.write(artwork)
|
||||
|
||||
os.chmod(thumb_path, int(headphones.CONFIG.FILE_PERMISSIONS, 8))
|
||||
except (OSError, IOError) as e:
|
||||
@@ -465,7 +574,7 @@ def getArtwork(ArtistID=None, AlbumID=None):
|
||||
if not artwork_path:
|
||||
return None
|
||||
|
||||
if artwork_path.startswith('http://'):
|
||||
if artwork_path.startswith(('http://', 'https://')):
|
||||
return artwork_path
|
||||
else:
|
||||
artwork_file = os.path.basename(artwork_path)
|
||||
@@ -479,7 +588,7 @@ def getThumb(ArtistID=None, AlbumID=None):
|
||||
if not artwork_path:
|
||||
return None
|
||||
|
||||
if artwork_path.startswith('http://'):
|
||||
if artwork_path.startswith(('http://', 'https://')):
|
||||
return artwork_path
|
||||
else:
|
||||
thumbnail_file = os.path.basename(artwork_path)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class AuthURLOpener(HeadphonesURLopener):
|
||||
self.numTries = 0
|
||||
|
||||
# call the base class
|
||||
urllib.FancyURLopener.__init__(self)
|
||||
urllib.request.FancyURLopener.__init__(self)
|
||||
|
||||
def prompt_user_passwd(self, host, realm):
|
||||
"""
|
||||
|
||||
@@ -24,6 +24,7 @@ import operator
|
||||
import os
|
||||
import re
|
||||
from headphones import version
|
||||
from functools import reduce
|
||||
|
||||
|
||||
# Identify Our Application
|
||||
@@ -74,7 +75,7 @@ class Quality:
|
||||
@staticmethod
|
||||
def _getStatusStrings(status):
|
||||
toReturn = {}
|
||||
for x in Quality.qualityStrings.keys():
|
||||
for x in list(Quality.qualityStrings.keys()):
|
||||
toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status] + " (" + \
|
||||
Quality.qualityStrings[x] + ")"
|
||||
return toReturn
|
||||
@@ -93,7 +94,7 @@ class Quality:
|
||||
def splitQuality(quality):
|
||||
anyQualities = []
|
||||
bestQualities = []
|
||||
for curQual in Quality.qualityStrings.keys():
|
||||
for curQual in list(Quality.qualityStrings.keys()):
|
||||
if curQual & quality:
|
||||
anyQualities.append(curQual)
|
||||
if curQual << 16 & quality:
|
||||
@@ -101,36 +102,6 @@ class Quality:
|
||||
|
||||
return (anyQualities, bestQualities)
|
||||
|
||||
@staticmethod
|
||||
def nameQuality(name):
|
||||
|
||||
def checkName(list, func):
|
||||
return func([re.search(x, name, re.I) for x in list])
|
||||
|
||||
name = os.path.basename(name)
|
||||
|
||||
# if we have our exact text then assume we put it there
|
||||
for x in Quality.qualityStrings:
|
||||
if x == Quality.UNKNOWN:
|
||||
continue
|
||||
|
||||
regex = '\W' + Quality.qualityStrings[x].replace(' ', '\W') + '\W'
|
||||
regex_match = re.search(regex, name, re.I)
|
||||
if regex_match:
|
||||
return x
|
||||
|
||||
# TODO: fix quality checking here
|
||||
if checkName(["mp3", "192"], any) and not checkName(["flac"], all):
|
||||
return Quality.B192
|
||||
elif checkName(["mp3", "256"], any) and not checkName(["flac"], all):
|
||||
return Quality.B256
|
||||
elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all):
|
||||
return Quality.VBR
|
||||
elif checkName(["mp3", "320"], any) and not checkName(["flac"], all):
|
||||
return Quality.B320
|
||||
else:
|
||||
return Quality.UNKNOWN
|
||||
|
||||
@staticmethod
|
||||
def assumeQuality(name):
|
||||
if name.lower().endswith(".mp3"):
|
||||
@@ -151,28 +122,21 @@ class Quality:
|
||||
@staticmethod
|
||||
def splitCompositeStatus(status):
|
||||
"""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:
|
||||
return (status - x * 100, x)
|
||||
|
||||
return (Quality.NONE, status)
|
||||
|
||||
@staticmethod
|
||||
def statusFromName(name, assume=True):
|
||||
quality = Quality.nameQuality(name)
|
||||
if assume and quality == Quality.UNKNOWN:
|
||||
quality = Quality.assumeQuality(name)
|
||||
return Quality.compositeStatus(DOWNLOADED, quality)
|
||||
|
||||
DOWNLOADED = None
|
||||
SNATCHED = None
|
||||
SNATCHED_PROPER = None
|
||||
|
||||
|
||||
Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()]
|
||||
Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, 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 list(Quality.qualityStrings.keys())]
|
||||
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], [])
|
||||
LOSSLESS = Quality.combineQualities([Quality.FLAC], [])
|
||||
|
||||
@@ -2,15 +2,16 @@ import itertools
|
||||
|
||||
import os
|
||||
import re
|
||||
import ast
|
||||
from configparser import ConfigParser
|
||||
import headphones.logger
|
||||
from configobj import ConfigObj
|
||||
|
||||
|
||||
def bool_int(value):
|
||||
"""
|
||||
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'):
|
||||
value = 0
|
||||
return int(bool(value))
|
||||
@@ -34,11 +35,18 @@ _CONFIG_DEFINITIONS = {
|
||||
'ADD_ALBUM_ART': (int, 'General', 0),
|
||||
'ADVANCEDENCODER': (str, 'General', ''),
|
||||
'ALBUM_ART_FORMAT': (str, 'General', 'folder'),
|
||||
'ALBUM_ART_MIN_WIDTH': (str, 'General', ''),
|
||||
'ALBUM_ART_MAX_WIDTH': (str, 'General', ''),
|
||||
# This is used in importer.py to determine how complete an album needs to
|
||||
# be - to be considered "downloaded". Percentage from 0-100
|
||||
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
|
||||
'API_ENABLED': (int, 'General', 0),
|
||||
'API_KEY': (str, 'General', ''),
|
||||
'ORPHEUS': (int, 'Orpheus.network', 0),
|
||||
'ORPHEUS_PASSWORD': (str, 'Orpheus.network', ''),
|
||||
'ORPHEUS_RATIO': (str, 'Orpheus.network', ''),
|
||||
'ORPHEUS_USERNAME': (str, 'Orpheus.network', ''),
|
||||
'ORPHEUS_URL': (str, 'Orpheus.network', 'https://orpheus.network'),
|
||||
'AUTOWANT_ALL': (int, 'General', 0),
|
||||
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
|
||||
'AUTOWANT_UPCOMING': (int, 'General', 1),
|
||||
@@ -72,6 +80,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'DELUGE_PASSWORD': (str, 'Deluge', ''),
|
||||
'DELUGE_LABEL': (str, 'Deluge', ''),
|
||||
'DELUGE_DONE_DIRECTORY': (str, 'Deluge', ''),
|
||||
'DELUGE_DOWNLOAD_DIRECTORY': (str, 'Deluge', ''),
|
||||
'DELUGE_PAUSED': (int, 'Deluge', 0),
|
||||
'DESTINATION_DIR': (str, 'General', ''),
|
||||
'DETECT_BITRATE': (int, 'General', 0),
|
||||
@@ -138,16 +147,19 @@ _CONFIG_DEFINITIONS = {
|
||||
'IGNORED_FILES': (list, 'Advanced', []), # path
|
||||
'INCLUDE_EXTRAS': (int, 'General', 0),
|
||||
'INTERFACE': (str, 'General', 'default'),
|
||||
'JOIN_APIKEY': (str, 'Join', ''),
|
||||
'JOIN_DEVICEID': (str, 'Join', ''),
|
||||
'JOIN_ENABLED': (int, 'Join', 0),
|
||||
'JOIN_ONSNATCH': (int, 'Join', 0),
|
||||
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
||||
'KAT': (int, 'Kat', 0),
|
||||
'KAT_PROXY_URL': (str, 'Kat', ''),
|
||||
'KAT_RATIO': (str, 'Kat', ''),
|
||||
'KEEP_NFO': (int, 'General', 0),
|
||||
'KEEP_TORRENT_FILES': (int, 'General', 0),
|
||||
'KEEP_TORRENT_FILES_DIR': (path, 'General', ''),
|
||||
'LASTFM_USERNAME': (str, 'General', ''),
|
||||
'LASTFM_APIKEY': (str, 'General', ''),
|
||||
'LAUNCH_BROWSER': (int, 'General', 1),
|
||||
'LIBRARYSCAN': (int, 'General', 1),
|
||||
'LIBRARYSCAN_INTERVAL': (int, 'General', 300),
|
||||
'LIBRARYSCAN_INTERVAL': (int, 'General', 24),
|
||||
'LMS_ENABLED': (int, 'LMS', 0),
|
||||
'LMS_HOST': (str, 'LMS', ''),
|
||||
'LOG_DIR': (path, 'General', ''),
|
||||
@@ -155,8 +167,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'LOSSLESS_BITRATE_TO': (int, 'General', 0),
|
||||
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
|
||||
'MB_IGNORE_AGE': (int, 'General', 365),
|
||||
'MININOVA': (int, 'Mininova', 0),
|
||||
'MININOVA_RATIO': (str, 'Mininova', ''),
|
||||
'MB_IGNORE_AGE_MISSING': (int, 'General', 0),
|
||||
'MIRROR': (str, 'General', 'musicbrainz.org'),
|
||||
'MOVE_FILES': (int, 'General', 0),
|
||||
'MPC_ENABLED': (bool_int, 'MPC', False),
|
||||
@@ -192,9 +203,6 @@ _CONFIG_DEFINITIONS = {
|
||||
'PIRATEBAY': (int, 'Piratebay', 0),
|
||||
'PIRATEBAY_PROXY_URL': (str, 'Piratebay', ''),
|
||||
'PIRATEBAY_RATIO': (str, 'Piratebay', ''),
|
||||
'OLDPIRATEBAY': (int, 'Old Piratebay', 0),
|
||||
'OLDPIRATEBAY_URL': (str, 'Old Piratebay', ''),
|
||||
'OLDPIRATEBAY_RATIO': (str, 'Old Piratebay', ''),
|
||||
'PLEX_CLIENT_HOST': (str, 'Plex', ''),
|
||||
'PLEX_ENABLED': (int, 'Plex', 0),
|
||||
'PLEX_NOTIFY': (int, 'Plex', 0),
|
||||
@@ -226,7 +234,12 @@ _CONFIG_DEFINITIONS = {
|
||||
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
||||
'PUSHOVER_ONSNATCH': (int, 'Pushover', 0),
|
||||
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
||||
'QBITTORRENT_HOST': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_LABEL': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_PASSWORD': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_USERNAME': (str, 'QBitTorrent', ''),
|
||||
'RENAME_FILES': (int, 'General', 0),
|
||||
'RENAME_SINGLE_DISC_IGNORE': (int, 'General', 0),
|
||||
'RENAME_UNPROCESSED': (bool_int, 'General', 1),
|
||||
'RENAME_FROZEN': (bool_int, 'General', 1),
|
||||
'REPLACE_EXISTING_FOLDERS': (int, 'General', 0),
|
||||
@@ -236,6 +249,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
|
||||
'RUTRACKER_RATIO': (str, 'Rutracker', ''),
|
||||
'RUTRACKER_USER': (str, 'Rutracker', ''),
|
||||
'RUTRACKER_COOKIE': (str, 'Rutracker', ''),
|
||||
'SAB_APIKEY': (str, 'SABnzbd', ''),
|
||||
'SAB_CATEGORY': (str, 'SABnzbd', ''),
|
||||
'SAB_HOST': (str, 'SABnzbd', ''),
|
||||
@@ -243,13 +257,21 @@ _CONFIG_DEFINITIONS = {
|
||||
'SAB_USERNAME': (str, 'SABnzbd', ''),
|
||||
'SAMPLINGFREQUENCY': (int, 'General', 44100),
|
||||
'SEARCH_INTERVAL': (int, 'General', 1440),
|
||||
'SLACK_ENABLED': (int, 'Slack', 0),
|
||||
'SLACK_URL': (str, 'Slack', ''),
|
||||
'SLACK_CHANNEL': (str, 'Slack', ''),
|
||||
'SLACK_EMOJI': (str, 'Slack', ''),
|
||||
'SLACK_ONSNATCH': (int, 'Slack', 0),
|
||||
'SOFT_CHROOT': (path, 'General', ''),
|
||||
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
|
||||
'SONGKICK_ENABLED': (int, 'Songkick', 1),
|
||||
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
|
||||
'SONGKICK_LOCATION': (str, 'Songkick', ''),
|
||||
'STRIKE': (int, 'Strike', 0),
|
||||
'STRIKE_RATIO': (str, 'Strike', ''),
|
||||
'SOULSEEK_API_URL': (str, 'Soulseek', ''),
|
||||
'SOULSEEK_API_KEY': (str, 'Soulseek', ''),
|
||||
'SOULSEEK_DOWNLOAD_DIR': (str, 'Soulseek', ''),
|
||||
'SOULSEEK_INCOMPLETE_DOWNLOAD_DIR': (str, 'Soulseek', ''),
|
||||
'SOULSEEK': (int, 'Soulseek', 0),
|
||||
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
|
||||
'SUBSONIC_HOST': (str, 'Subsonic', ''),
|
||||
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
|
||||
@@ -266,6 +288,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'TORZNAB_APIKEY': (str, 'Torznab', ''),
|
||||
'TORZNAB_ENABLED': (int, 'Torznab', 1),
|
||||
'TORZNAB_HOST': (str, 'Torznab', ''),
|
||||
'TORZNAB_RATIO': (str, 'Torznab', ''),
|
||||
'TRANSMISSION_HOST': (str, 'Transmission', ''),
|
||||
'TRANSMISSION_PASSWORD': (str, 'Transmission', ''),
|
||||
'TRANSMISSION_USERNAME': (str, 'Transmission', ''),
|
||||
@@ -282,21 +305,21 @@ _CONFIG_DEFINITIONS = {
|
||||
'UTORRENT_USERNAME': (str, 'uTorrent', ''),
|
||||
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
|
||||
'WAIT_UNTIL_RELEASE_DATE': (int, 'General', 0),
|
||||
'WAFFLES': (int, 'Waffles', 0),
|
||||
'WAFFLES_PASSKEY': (str, 'Waffles', ''),
|
||||
'WAFFLES_RATIO': (str, 'Waffles', ''),
|
||||
'WAFFLES_UID': (str, 'Waffles', ''),
|
||||
'WHATCD': (int, 'What.cd', 0),
|
||||
'WHATCD_PASSWORD': (str, 'What.cd', ''),
|
||||
'WHATCD_RATIO': (str, 'What.cd', ''),
|
||||
'WHATCD_USERNAME': (str, 'What.cd', ''),
|
||||
'REDACTED': (int, 'Redacted', 0),
|
||||
'REDACTED_APIKEY': (str, 'Redacted', ''),
|
||||
'REDACTED_USERNAME': (str, 'Redacted', ''),
|
||||
'REDACTED_PASSWORD': (str, 'Redacted', ''),
|
||||
'REDACTED_RATIO': (str, 'Redacted', ''),
|
||||
'REDACTED_USE_FLTOKEN': (int, 'Redacted', 0),
|
||||
'XBMC_ENABLED': (int, 'XBMC', 0),
|
||||
'XBMC_HOST': (str, 'XBMC', ''),
|
||||
'XBMC_NOTIFY': (int, 'XBMC', 0),
|
||||
'XBMC_PASSWORD': (str, 'XBMC', ''),
|
||||
'XBMC_UPDATE': (int, 'XBMC', 0),
|
||||
'XBMC_USERNAME': (str, 'XBMC', ''),
|
||||
'XLDPROFILE': (str, 'General', '')
|
||||
'XLDPROFILE': (str, 'General', ''),
|
||||
'BANDCAMP': (int, 'General', 0),
|
||||
'BANDCAMP_DIR': (path, 'General', '')
|
||||
}
|
||||
|
||||
|
||||
@@ -308,8 +331,9 @@ class Config(object):
|
||||
def __init__(self, config_file):
|
||||
""" Initialize the config with values from a file """
|
||||
self._config_file = config_file
|
||||
self._config = ConfigObj(self._config_file, encoding='utf-8')
|
||||
for key in _CONFIG_DEFINITIONS.keys():
|
||||
self._config = ConfigParser(interpolation=None)
|
||||
self._config.read(self._config_file)
|
||||
for key in list(_CONFIG_DEFINITIONS.keys()):
|
||||
self.check_setting(key)
|
||||
self.ENCODER_MULTICORE_COUNT = max(0, self.ENCODER_MULTICORE_COUNT)
|
||||
self._upgrade()
|
||||
@@ -326,7 +350,7 @@ class Config(object):
|
||||
|
||||
def check_section(self, section):
|
||||
""" 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] = {}
|
||||
return True
|
||||
else:
|
||||
@@ -336,28 +360,38 @@ class Config(object):
|
||||
""" Cast any value in the config to the right type or use the default """
|
||||
key, definition_type, section, ini_key, default = self._define(key)
|
||||
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:
|
||||
my_val = definition_type(self._config[section][ini_key])
|
||||
# ConfigParser interprets quotes 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 = 'some_val'
|
||||
if type(my_val) in [str, path]:
|
||||
my_val = my_val.strip('"').strip("'")
|
||||
except Exception:
|
||||
my_val = definition_type(default)
|
||||
self._config[section][ini_key] = my_val
|
||||
my_val = default
|
||||
self._config[section][ini_key] = str(my_val)
|
||||
return my_val
|
||||
|
||||
def write(self):
|
||||
""" Make a copy of the stored config and write it to the configured file """
|
||||
new_config = ConfigObj(encoding="UTF-8")
|
||||
new_config.filename = self._config_file
|
||||
new_config = ConfigParser(interpolation=None)
|
||||
|
||||
# first copy over everything from the old config, even if it is not
|
||||
# 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:
|
||||
new_config[key] = {}
|
||||
for subkey, value in subkeys.items():
|
||||
for subkey, value in list(subkeys.items()):
|
||||
new_config[key][subkey] = value
|
||||
|
||||
# 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)
|
||||
self.check_setting(key)
|
||||
if section not in new_config:
|
||||
@@ -368,14 +402,15 @@ class Config(object):
|
||||
headphones.logger.info("Writing configuration to file")
|
||||
|
||||
try:
|
||||
new_config.write()
|
||||
with open(self._config_file, 'w') as configfile:
|
||||
new_config.write(configfile)
|
||||
except IOError as e:
|
||||
headphones.logger.error("Error writing configuration file: %s", e)
|
||||
|
||||
def get_extra_newznabs(self):
|
||||
""" Return the extra newznab tuples """
|
||||
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)])
|
||||
)
|
||||
return extra_newznabs
|
||||
@@ -394,8 +429,8 @@ class Config(object):
|
||||
def get_extra_torznabs(self):
|
||||
""" Return the extra torznab tuples """
|
||||
extra_torznabs = list(
|
||||
itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 3)
|
||||
for i in range(3)])
|
||||
zip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 4)
|
||||
for i in range(4)])
|
||||
)
|
||||
return extra_torznabs
|
||||
|
||||
@@ -430,20 +465,21 @@ class Config(object):
|
||||
return value
|
||||
else:
|
||||
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]
|
||||
|
||||
def process_kwargs(self, kwargs):
|
||||
"""
|
||||
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)
|
||||
self._config[section][ini_key] = definition_type(value)
|
||||
self._config[section][ini_key] = str(value)
|
||||
|
||||
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':
|
||||
# Update the config to use direct path to the encoder rather than the encoder folder
|
||||
@@ -466,4 +502,17 @@ class Config(object):
|
||||
if self.CONFIG_VERSION == '5':
|
||||
if self.OPEN_MAGNET_LINKS:
|
||||
self.MAGNET_LINKS = 2
|
||||
self.CONFIG_VERSION = '5'
|
||||
|
||||
# Add Seed Ratio to Torznabs
|
||||
if self.EXTRA_TORZNABS:
|
||||
extra_torznabs = list(
|
||||
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'
|
||||
|
||||